import { Observable } from "./observable.js"; import { GetDOMTextContent, IsNavigatorAvailable, IsWindowObjectExist } from "./domManagement.js"; import { Logger } from "./logger.js"; import { DeepCopier } from "./deepCopier.js"; import { PrecisionDate } from "./precisionDate.js"; import { _WarnImport } from "./devTools.js"; import { WebRequest } from "./webRequest.js"; import { EngineStore } from "../Engines/engineStore.js"; import { FileToolsOptions, DecodeBase64UrlToBinary, IsBase64DataUrl, LoadFile as FileToolsLoadFile, LoadImage as FileToolLoadImage, ReadFile as FileToolsReadFile, SetCorsBehavior, } from "./fileTools.js"; import { TimingTools } from "./timingTools.js"; import { InstantiationTools } from "./instantiationTools.js"; import { RandomGUID } from "./guid.js"; import { IsExponentOfTwo, Mix } from "./tools.functions.js"; /** * Class containing a set of static utilities functions */ export class Tools { /** * Gets or sets the base URL to use to load assets */ static get BaseUrl() { return FileToolsOptions.BaseUrl; } static set BaseUrl(value) { FileToolsOptions.BaseUrl = value; } /** * This function checks whether a URL is absolute or not. * It will also detect data and blob URLs * @param url the url to check * @returns is the url absolute or relative */ static IsAbsoluteUrl(url) { // See https://stackoverflow.com/a/38979205. // URL is protocol-relative (= absolute) if (url.indexOf("//") === 0) { return true; } // URL has no protocol (= relative) if (url.indexOf("://") === -1) { return false; } // URL does not contain a dot, i.e. no TLD (= relative, possibly REST) if (url.indexOf(".") === -1) { return false; } // URL does not contain a single slash (= relative) if (url.indexOf("/") === -1) { return false; } // The first colon comes after the first slash (= relative) if (url.indexOf(":") > url.indexOf("/")) { return false; } // Protocol is defined before first dot (= absolute) if (url.indexOf("://") < url.indexOf(".")) { return true; } if (url.indexOf("data:") === 0 || url.indexOf("blob:") === 0) { return true; } // Anything else must be relative return false; } /** * Sets the base URL to use to load scripts */ static set ScriptBaseUrl(value) { FileToolsOptions.ScriptBaseUrl = value; } static get ScriptBaseUrl() { return FileToolsOptions.ScriptBaseUrl; } /** * Sets a preprocessing function to run on a source URL before importing it * Note that this function will execute AFTER the base URL is appended to the URL */ static set ScriptPreprocessUrl(func) { FileToolsOptions.ScriptPreprocessUrl = func; } static get ScriptPreprocessUrl() { return FileToolsOptions.ScriptPreprocessUrl; } /** * Gets or sets the retry strategy to apply when an error happens while loading an asset */ static get DefaultRetryStrategy() { return FileToolsOptions.DefaultRetryStrategy; } static set DefaultRetryStrategy(strategy) { FileToolsOptions.DefaultRetryStrategy = strategy; } /** * Default behavior for cors in the application. * It can be a string if the expected behavior is identical in the entire app. * Or a callback to be able to set it per url or on a group of them (in case of Video source for instance) */ static get CorsBehavior() { return FileToolsOptions.CorsBehavior; } static set CorsBehavior(value) { FileToolsOptions.CorsBehavior = value; } /** * Gets or sets a global variable indicating if fallback texture must be used when a texture cannot be loaded * @ignorenaming */ static get UseFallbackTexture() { return EngineStore.UseFallbackTexture; } static set UseFallbackTexture(value) { EngineStore.UseFallbackTexture = value; } /** * Use this object to register external classes like custom textures or material * to allow the loaders to instantiate them */ static get RegisteredExternalClasses() { return InstantiationTools.RegisteredExternalClasses; } static set RegisteredExternalClasses(classes) { InstantiationTools.RegisteredExternalClasses = classes; } /** * Texture content used if a texture cannot loaded * @ignorenaming */ // eslint-disable-next-line @typescript-eslint/naming-convention static get fallbackTexture() { return EngineStore.FallbackTexture; } // eslint-disable-next-line @typescript-eslint/naming-convention static set fallbackTexture(value) { EngineStore.FallbackTexture = value; } /** * Read the content of a byte array at a specified coordinates (taking in account wrapping) * @param u defines the coordinate on X axis * @param v defines the coordinate on Y axis * @param width defines the width of the source data * @param height defines the height of the source data * @param pixels defines the source byte array * @param color defines the output color */ static FetchToRef(u, v, width, height, pixels, color) { const wrappedU = (Math.abs(u) * width) % width | 0; const wrappedV = (Math.abs(v) * height) % height | 0; const position = (wrappedU + wrappedV * width) * 4; color.r = pixels[position] / 255; color.g = pixels[position + 1] / 255; color.b = pixels[position + 2] / 255; color.a = pixels[position + 3] / 255; } /** * Interpolates between a and b via alpha * @param a The lower value (returned when alpha = 0) * @param b The upper value (returned when alpha = 1) * @param alpha The interpolation-factor * @returns The mixed value */ static Mix(a, b, alpha) { return 0; } /** * Tries to instantiate a new object from a given class name * @param className defines the class name to instantiate * @returns the new object or null if the system was not able to do the instantiation */ static Instantiate(className) { return InstantiationTools.Instantiate(className); } /** * Polyfill for setImmediate * @param action defines the action to execute after the current execution block */ static SetImmediate(action) { TimingTools.SetImmediate(action); } /** * Function indicating if a number is an exponent of 2 * @param value defines the value to test * @returns true if the value is an exponent of 2 */ static IsExponentOfTwo(value) { return true; } /** * Returns the nearest 32-bit single precision float representation of a Number * @param value A Number. If the parameter is of a different type, it will get converted * to a number or to NaN if it cannot be converted * @returns number */ static FloatRound(value) { return Math.fround(value); } /** * Extracts the filename from a path * @param path defines the path to use * @returns the filename */ static GetFilename(path) { const index = path.lastIndexOf("/"); if (index < 0) { return path; } return path.substring(index + 1); } /** * Extracts the "folder" part of a path (everything before the filename). * @param uri The URI to extract the info from * @param returnUnchangedIfNoSlash Do not touch the URI if no slashes are present * @returns The "folder" part of the path */ static GetFolderPath(uri, returnUnchangedIfNoSlash = false) { const index = uri.lastIndexOf("/"); if (index < 0) { if (returnUnchangedIfNoSlash) { return uri; } return ""; } return uri.substring(0, index + 1); } /** * Convert an angle in radians to degrees * @param angle defines the angle to convert * @returns the angle in degrees */ static ToDegrees(angle) { return (angle * 180) / Math.PI; } /** * Convert an angle in degrees to radians * @param angle defines the angle to convert * @returns the angle in radians */ static ToRadians(angle) { return (angle * Math.PI) / 180; } /** * Smooth angle changes (kind of low-pass filter), in particular for device orientation "shaking" * Use trigonometric functions to avoid discontinuity (0/360, -180/180) * @param previousAngle defines last angle value, in degrees * @param newAngle defines new angle value, in degrees * @param smoothFactor defines smoothing sensitivity; min 0: no smoothing, max 1: new data ignored * @returns the angle in degrees */ static SmoothAngleChange(previousAngle, newAngle, smoothFactor = 0.9) { const previousAngleRad = this.ToRadians(previousAngle); const newAngleRad = this.ToRadians(newAngle); return this.ToDegrees(Math.atan2((1 - smoothFactor) * Math.sin(newAngleRad) + smoothFactor * Math.sin(previousAngleRad), (1 - smoothFactor) * Math.cos(newAngleRad) + smoothFactor * Math.cos(previousAngleRad))); } /** * Returns an array if obj is not an array * @param obj defines the object to evaluate as an array * @param allowsNullUndefined defines a boolean indicating if obj is allowed to be null or undefined * @returns either obj directly if obj is an array or a new array containing obj */ static MakeArray(obj, allowsNullUndefined) { if (allowsNullUndefined !== true && (obj === undefined || obj == null)) { return null; } return Array.isArray(obj) ? obj : [obj]; } /** * Gets the pointer prefix to use * @param engine defines the engine we are finding the prefix for * @returns "pointer" if touch is enabled. Else returns "mouse" */ static GetPointerPrefix(engine) { let eventPrefix = "pointer"; // Check if pointer events are supported if (IsWindowObjectExist() && !window.PointerEvent) { eventPrefix = "mouse"; } // Special Fallback MacOS Safari... if (engine._badDesktopOS && !engine._badOS && // And not ipad pros who claim to be macs... !(document && "ontouchend" in document)) { eventPrefix = "mouse"; } return eventPrefix; } /** * Sets the cors behavior on a dom element. This will add the required Tools.CorsBehavior to the element. * @param url define the url we are trying * @param element define the dom element where to configure the cors policy * @param element.crossOrigin */ static SetCorsBehavior(url, element) { SetCorsBehavior(url, element); } /** * Sets the referrerPolicy behavior on a dom element. * @param referrerPolicy define the referrer policy to use * @param element define the dom element where to configure the referrer policy * @param element.referrerPolicy */ static SetReferrerPolicyBehavior(referrerPolicy, element) { element.referrerPolicy = referrerPolicy; } // External files /** * Removes unwanted characters from an url * @param url defines the url to clean * @returns the cleaned url */ static CleanUrl(url) { url = url.replace(/#/gm, "%23"); return url; } /** * Gets or sets a function used to pre-process url before using them to load assets */ static get PreprocessUrl() { return FileToolsOptions.PreprocessUrl; } static set PreprocessUrl(processor) { FileToolsOptions.PreprocessUrl = processor; } /** * Loads an image as an HTMLImageElement. * @param input url string, ArrayBuffer, or Blob to load * @param onLoad callback called when the image successfully loads * @param onError callback called when the image fails to load * @param offlineProvider offline provider for caching * @param mimeType optional mime type * @param imageBitmapOptions optional the options to use when creating an ImageBitmap * @returns the HTMLImageElement of the loaded image */ static LoadImage(input, onLoad, onError, offlineProvider, mimeType, imageBitmapOptions) { return FileToolLoadImage(input, onLoad, onError, offlineProvider, mimeType, imageBitmapOptions); } /** * Loads a file from a url * @param url url string, ArrayBuffer, or Blob to load * @param onSuccess callback called when the file successfully loads * @param onProgress callback called while file is loading (if the server supports this mode) * @param offlineProvider defines the offline provider for caching * @param useArrayBuffer defines a boolean indicating that date must be returned as ArrayBuffer * @param onError callback called when the file fails to load * @returns a file request object */ static LoadFile(url, onSuccess, onProgress, offlineProvider, useArrayBuffer, onError) { return FileToolsLoadFile(url, onSuccess, onProgress, offlineProvider, useArrayBuffer, onError); } /** * Loads a file from a url * @param url the file url to load * @param useArrayBuffer defines a boolean indicating that date must be returned as ArrayBuffer * @returns a promise containing an ArrayBuffer corresponding to the loaded file */ static LoadFileAsync(url, useArrayBuffer = true) { return new Promise((resolve, reject) => { FileToolsLoadFile(url, (data) => { resolve(data); }, undefined, undefined, useArrayBuffer, (request, exception) => { reject(exception); }); }); } /** * Get a script URL including preprocessing * @param scriptUrl the script Url to process * @param forceAbsoluteUrl force the script to be an absolute url (adding the current base url if necessary) * @returns a modified URL to use */ static GetBabylonScriptURL(scriptUrl, forceAbsoluteUrl) { if (!scriptUrl) { return ""; } // if the base URL was set, and the script Url is an absolute path change the default path if (Tools.ScriptBaseUrl && scriptUrl.startsWith(Tools._DefaultCdnUrl)) { // change the default host, which is https://cdn.babylonjs.com with the one defined // make sure no trailing slash is present const baseUrl = Tools.ScriptBaseUrl[Tools.ScriptBaseUrl.length - 1] === "/" ? Tools.ScriptBaseUrl.substring(0, Tools.ScriptBaseUrl.length - 1) : Tools.ScriptBaseUrl; scriptUrl = scriptUrl.replace(Tools._DefaultCdnUrl, baseUrl); } // run the preprocessor scriptUrl = Tools.ScriptPreprocessUrl(scriptUrl); if (forceAbsoluteUrl) { scriptUrl = Tools.GetAbsoluteUrl(scriptUrl); } return scriptUrl; } /** * This function is used internally by babylon components to load a script (identified by an url). When the url returns, the * content of this file is added into a new script element, attached to the DOM (body element) * @param scriptUrl defines the url of the script to load * @param onSuccess defines the callback called when the script is loaded * @param onError defines the callback to call if an error occurs * @param scriptId defines the id of the script element */ static LoadBabylonScript(scriptUrl, onSuccess, onError, scriptId) { scriptUrl = Tools.GetBabylonScriptURL(scriptUrl); Tools.LoadScript(scriptUrl, onSuccess, onError); } /** * Load an asynchronous script (identified by an url). When the url returns, the * content of this file is added into a new script element, attached to the DOM (body element) * @param scriptUrl defines the url of the script to laod * @returns a promise request object */ static LoadBabylonScriptAsync(scriptUrl) { scriptUrl = Tools.GetBabylonScriptURL(scriptUrl); return Tools.LoadScriptAsync(scriptUrl); } /** * This function is used internally by babylon components to load a script (identified by an url). When the url returns, the * content of this file is added into a new script element, attached to the DOM (body element) * @param scriptUrl defines the url of the script to load * @param onSuccess defines the callback called when the script is loaded * @param onError defines the callback to call if an error occurs * @param scriptId defines the id of the script element */ static LoadScript(scriptUrl, onSuccess, onError, scriptId) { if (typeof importScripts === "function") { try { importScripts(scriptUrl); onSuccess(); } catch (e) { onError?.(`Unable to load script '${scriptUrl}' in worker`, e); } return; } else if (!IsWindowObjectExist()) { onError?.(`Cannot load script '${scriptUrl}' outside of a window or a worker`); return; } const head = document.getElementsByTagName("head")[0]; const script = document.createElement("script"); script.setAttribute("type", "text/javascript"); script.setAttribute("src", scriptUrl); if (scriptId) { script.id = scriptId; } script.onload = () => { if (onSuccess) { onSuccess(); } }; script.onerror = (e) => { if (onError) { onError(`Unable to load script '${scriptUrl}'`, e); } }; head.appendChild(script); } /** * Load an asynchronous script (identified by an url). When the url returns, the * content of this file is added into a new script element, attached to the DOM (body element) * @param scriptUrl defines the url of the script to load * @param scriptId defines the id of the script element * @returns a promise request object */ static LoadScriptAsync(scriptUrl, scriptId) { return new Promise((resolve, reject) => { this.LoadScript(scriptUrl, () => { resolve(); }, (message, exception) => { reject(exception || new Error(message)); }, scriptId); }); } /** * Loads a file from a blob * @param fileToLoad defines the blob to use * @param callback defines the callback to call when data is loaded * @param progressCallback defines the callback to call during loading process * @returns a file request object */ static ReadFileAsDataURL(fileToLoad, callback, progressCallback) { const reader = new FileReader(); const request = { onCompleteObservable: new Observable(), abort: () => reader.abort(), }; reader.onloadend = () => { request.onCompleteObservable.notifyObservers(request); }; reader.onload = (e) => { //target doesn't have result from ts 1.3 callback(e.target["result"]); }; reader.onprogress = progressCallback; reader.readAsDataURL(fileToLoad); return request; } /** * Reads a file from a File object * @param file defines the file to load * @param onSuccess defines the callback to call when data is loaded * @param onProgress defines the callback to call during loading process * @param useArrayBuffer defines a boolean indicating that data must be returned as an ArrayBuffer * @param onError defines the callback to call when an error occurs * @returns a file request object */ static ReadFile(file, onSuccess, onProgress, useArrayBuffer, onError) { return FileToolsReadFile(file, onSuccess, onProgress, useArrayBuffer, onError); } /** * Creates a data url from a given string content * @param content defines the content to convert * @returns the new data url link */ static FileAsURL(content) { const fileBlob = new Blob([content]); const url = window.URL; const link = url.createObjectURL(fileBlob); return link; } /** * Format the given number to a specific decimal format * @param value defines the number to format * @param decimals defines the number of decimals to use * @returns the formatted string */ static Format(value, decimals = 2) { return value.toFixed(decimals); } /** * Tries to copy an object by duplicating every property * @param source defines the source object * @param destination defines the target object * @param doNotCopyList defines a list of properties to avoid * @param mustCopyList defines a list of properties to copy (even if they start with _) */ static DeepCopy(source, destination, doNotCopyList, mustCopyList) { DeepCopier.DeepCopy(source, destination, doNotCopyList, mustCopyList); } /** * Gets a boolean indicating if the given object has no own property * @param obj defines the object to test * @returns true if object has no own property */ static IsEmpty(obj) { for (const i in obj) { if (Object.prototype.hasOwnProperty.call(obj, i)) { return false; } } return true; } /** * Function used to register events at window level * @param windowElement defines the Window object to use * @param events defines the events to register */ static RegisterTopRootEvents(windowElement, events) { for (let index = 0; index < events.length; index++) { const event = events[index]; windowElement.addEventListener(event.name, event.handler, false); try { if (window.parent) { window.parent.addEventListener(event.name, event.handler, false); } } catch (e) { // Silently fails... } } } /** * Function used to unregister events from window level * @param windowElement defines the Window object to use * @param events defines the events to unregister */ static UnregisterTopRootEvents(windowElement, events) { for (let index = 0; index < events.length; index++) { const event = events[index]; windowElement.removeEventListener(event.name, event.handler); try { if (windowElement.parent) { windowElement.parent.removeEventListener(event.name, event.handler); } } catch (e) { // Silently fails... } } } /** * Dumps the current bound framebuffer * @param width defines the rendering width * @param height defines the rendering height * @param engine defines the hosting engine * @param successCallback defines the callback triggered once the data are available * @param mimeType defines the mime type of the result * @param fileName defines the filename to download. If present, the result will automatically be downloaded * @param quality The quality of the image if lossy mimeType is used (e.g. image/jpeg, image/webp). See {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob | HTMLCanvasElement.toBlob()}'s `quality` parameter. * @returns a void promise */ static async DumpFramebuffer(width, height, engine, successCallback, mimeType = "image/png", fileName, quality) { throw _WarnImport("DumpTools"); } /** * Dumps an array buffer * @param width defines the rendering width * @param height defines the rendering height * @param data the data array * @param successCallback defines the callback triggered once the data are available * @param mimeType defines the mime type of the result * @param fileName defines the filename to download. If present, the result will automatically be downloaded * @param invertY true to invert the picture in the Y dimension * @param toArrayBuffer true to convert the data to an ArrayBuffer (encoded as `mimeType`) instead of a base64 string * @param quality The quality of the image if lossy mimeType is used (e.g. image/jpeg, image/webp). See {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob | HTMLCanvasElement.toBlob()}'s `quality` parameter. */ static DumpData(width, height, data, successCallback, mimeType = "image/png", fileName, invertY = false, toArrayBuffer = false, quality) { throw _WarnImport("DumpTools"); } // eslint-disable-next-line jsdoc/require-returns-check /** * Dumps an array buffer * @param width defines the rendering width * @param height defines the rendering height * @param data the data array * @param mimeType defines the mime type of the result * @param fileName defines the filename to download. If present, the result will automatically be downloaded * @param invertY true to invert the picture in the Y dimension * @param toArrayBuffer true to convert the data to an ArrayBuffer (encoded as `mimeType`) instead of a base64 string * @param quality The quality of the image if lossy mimeType is used (e.g. image/jpeg, image/webp). See {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob | HTMLCanvasElement.toBlob()}'s `quality` parameter. * @returns a promise that resolve to the final data */ static DumpDataAsync(width, height, data, mimeType = "image/png", fileName, invertY = false, toArrayBuffer = false, quality) { throw _WarnImport("DumpTools"); } static _IsOffScreenCanvas(canvas) { return canvas.convertToBlob !== undefined; } /** * Converts the canvas data to blob. * This acts as a polyfill for browsers not supporting the to blob function. * @param canvas Defines the canvas to extract the data from (can be an offscreen canvas) * @param successCallback Defines the callback triggered once the data are available * @param mimeType Defines the mime type of the result * @param quality The quality of the image if lossy mimeType is used (e.g. image/jpeg, image/webp). See {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob | HTMLCanvasElement.toBlob()}'s `quality` parameter. */ static ToBlob(canvas, successCallback, mimeType = "image/png", quality) { // We need HTMLCanvasElement.toBlob for HD screenshots if (!Tools._IsOffScreenCanvas(canvas) && !canvas.toBlob) { // low performance polyfill based on toDataURL (https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob) canvas.toBlob = function (callback, type, quality) { setTimeout(() => { const binStr = atob(this.toDataURL(type, quality).split(",")[1]), len = binStr.length, arr = new Uint8Array(len); for (let i = 0; i < len; i++) { arr[i] = binStr.charCodeAt(i); } callback(new Blob([arr])); }); }; } if (Tools._IsOffScreenCanvas(canvas)) { canvas .convertToBlob({ type: mimeType, quality, }) .then((blob) => successCallback(blob)); } else { canvas.toBlob(function (blob) { successCallback(blob); }, mimeType, quality); } } /** * Download a Blob object * @param blob the Blob object * @param fileName the file name to download */ static DownloadBlob(blob, fileName) { //Creating a link if the browser have the download attribute on the a tag, to automatically start download generated image. if ("download" in document.createElement("a")) { if (!fileName) { const date = new Date(); const stringDate = (date.getFullYear() + "-" + (date.getMonth() + 1)).slice(2) + "-" + date.getDate() + "_" + date.getHours() + "-" + ("0" + date.getMinutes()).slice(-2); fileName = "screenshot_" + stringDate + ".png"; } Tools.Download(blob, fileName); } else { if (blob && typeof URL !== "undefined") { const url = URL.createObjectURL(blob); const newWindow = window.open(""); if (!newWindow) { return; } const img = newWindow.document.createElement("img"); img.onload = function () { // no longer need to read the blob so it's revoked URL.revokeObjectURL(url); }; img.src = url; newWindow.document.body.appendChild(img); } } } /** * Encodes the canvas data to base 64, or automatically downloads the result if `fileName` is defined. * @param canvas The canvas to get the data from, which can be an offscreen canvas. * @param successCallback The callback which is triggered once the data is available. If `fileName` is defined, the callback will be invoked after the download occurs, and the `data` argument will be an empty string. * @param mimeType The mime type of the result. * @param fileName The name of the file to download. If defined, the result will automatically be downloaded. If not defined, and `successCallback` is also not defined, the result will automatically be downloaded with an auto-generated file name. * @param quality The quality of the image if lossy mimeType is used (e.g. image/jpeg, image/webp). See {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob | HTMLCanvasElement.toBlob()}'s `quality` parameter. */ static EncodeScreenshotCanvasData(canvas, successCallback, mimeType = "image/png", fileName, quality) { if (typeof fileName === "string" || !successCallback) { this.ToBlob(canvas, function (blob) { if (blob) { Tools.DownloadBlob(blob, fileName); } if (successCallback) { successCallback(""); } }, mimeType, quality); } else if (successCallback) { if (Tools._IsOffScreenCanvas(canvas)) { canvas .convertToBlob({ type: mimeType, quality, }) .then((blob) => { const reader = new FileReader(); reader.readAsDataURL(blob); reader.onloadend = () => { const base64data = reader.result; successCallback(base64data); }; }); return; } const base64Image = canvas.toDataURL(mimeType, quality); successCallback(base64Image); } } /** * Downloads a blob in the browser * @param blob defines the blob to download * @param fileName defines the name of the downloaded file */ static Download(blob, fileName) { if (typeof URL === "undefined") { return; } const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); document.body.appendChild(a); a.style.display = "none"; a.href = url; a.download = fileName; a.addEventListener("click", () => { if (a.parentElement) { a.parentElement.removeChild(a); } }); a.click(); window.URL.revokeObjectURL(url); } /** * Will return the right value of the noPreventDefault variable * Needed to keep backwards compatibility to the old API. * * @param args arguments passed to the attachControl function * @returns the correct value for noPreventDefault */ static BackCompatCameraNoPreventDefault(args) { // is it used correctly? if (typeof args[0] === "boolean") { return args[0]; } else if (typeof args[1] === "boolean") { return args[1]; } return false; } /** * Captures a screenshot of the current rendering * @see https://doc.babylonjs.com/features/featuresDeepDive/scene/renderToPNG * @param engine defines the rendering engine * @param camera defines the source camera * @param size This parameter can be set to a single number or to an object with the * following (optional) properties: precision, width, height. If a single number is passed, * it will be used for both width and height. If an object is passed, the screenshot size * will be derived from the parameters. The precision property is a multiplier allowing * rendering at a higher or lower resolution * @param successCallback defines the callback receives a single parameter which contains the * screenshot as a string of base64-encoded characters. This string can be assigned to the * src parameter of an to display it * @param mimeType defines the MIME type of the screenshot image (default: image/png). * Check your browser for supported MIME types * @param forceDownload force the system to download the image even if a successCallback is provided * @param quality The quality of the image if lossy mimeType is used (e.g. image/jpeg, image/webp). See {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob | HTMLCanvasElement.toBlob()}'s `quality` parameter. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars static CreateScreenshot(engine, camera, size, successCallback, mimeType = "image/png", forceDownload = false, quality) { throw _WarnImport("ScreenshotTools"); } // eslint-disable-next-line jsdoc/require-returns-check /** * Captures a screenshot of the current rendering * @see https://doc.babylonjs.com/features/featuresDeepDive/scene/renderToPNG * @param engine defines the rendering engine * @param camera defines the source camera * @param size This parameter can be set to a single number or to an object with the * following (optional) properties: precision, width, height. If a single number is passed, * it will be used for both width and height. If an object is passed, the screenshot size * will be derived from the parameters. The precision property is a multiplier allowing * rendering at a higher or lower resolution * @param mimeType defines the MIME type of the screenshot image (default: image/png). * Check your browser for supported MIME types * @param quality The quality of the image if lossy mimeType is used (e.g. image/jpeg, image/webp). See {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob | HTMLCanvasElement.toBlob()}'s `quality` parameter. * @returns screenshot as a string of base64-encoded characters. This string can be assigned * to the src parameter of an to display it */ // eslint-disable-next-line @typescript-eslint/no-unused-vars static CreateScreenshotAsync(engine, camera, size, mimeType = "image/png", quality) { throw _WarnImport("ScreenshotTools"); } /** * Generates an image screenshot from the specified camera. * @see https://doc.babylonjs.com/features/featuresDeepDive/scene/renderToPNG * @param engine The engine to use for rendering * @param camera The camera to use for rendering * @param size This parameter can be set to a single number or to an object with the * following (optional) properties: precision, width, height. If a single number is passed, * it will be used for both width and height. If an object is passed, the screenshot size * will be derived from the parameters. The precision property is a multiplier allowing * rendering at a higher or lower resolution * @param successCallback The callback receives a single parameter which contains the * screenshot as a string of base64-encoded characters. This string can be assigned to the * src parameter of an to display it * @param mimeType The MIME type of the screenshot image (default: image/png). * Check your browser for supported MIME types * @param samples Texture samples (default: 1) * @param antialiasing Whether antialiasing should be turned on or not (default: false) * @param fileName A name for for the downloaded file. * @param renderSprites Whether the sprites should be rendered or not (default: false) * @param enableStencilBuffer Whether the stencil buffer should be enabled or not (default: false) * @param useLayerMask if the camera's layer mask should be used to filter what should be rendered (default: true) * @param quality The quality of the image if lossy mimeType is used (e.g. image/jpeg, image/webp). See {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob | HTMLCanvasElement.toBlob()}'s `quality` parameter. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars static CreateScreenshotUsingRenderTarget(engine, camera, size, successCallback, mimeType = "image/png", samples = 1, antialiasing = false, fileName, renderSprites = false, enableStencilBuffer = false, useLayerMask = true, quality) { throw _WarnImport("ScreenshotTools"); } // eslint-disable-next-line jsdoc/require-returns-check /** * Generates an image screenshot from the specified camera. * @see https://doc.babylonjs.com/features/featuresDeepDive/scene/renderToPNG * @param engine The engine to use for rendering * @param camera The camera to use for rendering * @param size This parameter can be set to a single number or to an object with the * following (optional) properties: precision, width, height. If a single number is passed, * it will be used for both width and height. If an object is passed, the screenshot size * will be derived from the parameters. The precision property is a multiplier allowing * rendering at a higher or lower resolution * @param mimeType The MIME type of the screenshot image (default: image/png). * Check your browser for supported MIME types * @param samples Texture samples (default: 1) * @param antialiasing Whether antialiasing should be turned on or not (default: false) * @param fileName A name for for the downloaded file. * @returns screenshot as a string of base64-encoded characters. This string can be assigned * @param renderSprites Whether the sprites should be rendered or not (default: false) * @param enableStencilBuffer Whether the stencil buffer should be enabled or not (default: false) * @param useLayerMask if the camera's layer mask should be used to filter what should be rendered (default: true) * @param quality The quality of the image if lossy mimeType is used (e.g. image/jpeg, image/webp). See {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob | HTMLCanvasElement.toBlob()}'s `quality` parameter. * to the src parameter of an to display it */ // eslint-disable-next-line @typescript-eslint/no-unused-vars static CreateScreenshotUsingRenderTargetAsync(engine, camera, size, mimeType = "image/png", samples = 1, antialiasing = false, fileName, renderSprites = false, enableStencilBuffer = false, useLayerMask = true, quality) { throw _WarnImport("ScreenshotTools"); } /** * Implementation from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#answer-2117523 * Be aware Math.random() could cause collisions, but: * "All but 6 of the 128 bits of the ID are randomly generated, which means that for any two ids, there's a 1 in 2^^122 (or 5.3x10^^36) chance they'll collide" * @returns a pseudo random id */ static RandomId() { return RandomGUID(); } /** * Test if the given uri is a base64 string * @deprecated Please use FileTools.IsBase64DataUrl instead. * @param uri The uri to test * @returns True if the uri is a base64 string or false otherwise */ static IsBase64(uri) { return IsBase64DataUrl(uri); } /** * Decode the given base64 uri. * @deprecated Please use FileTools.DecodeBase64UrlToBinary instead. * @param uri The uri to decode * @returns The decoded base64 data. */ static DecodeBase64(uri) { return DecodeBase64UrlToBinary(uri); } /** * Gets a value indicating the number of loading errors * @ignorenaming */ // eslint-disable-next-line @typescript-eslint/naming-convention static get errorsCount() { return Logger.errorsCount; } /** * Log a message to the console * @param message defines the message to log */ static Log(message) { Logger.Log(message); } /** * Write a warning message to the console * @param message defines the message to log */ static Warn(message) { Logger.Warn(message); } /** * Write an error message to the console * @param message defines the message to log */ static Error(message) { Logger.Error(message); } /** * Gets current log cache (list of logs) */ static get LogCache() { return Logger.LogCache; } /** * Clears the log cache */ static ClearLogCache() { Logger.ClearLogCache(); } /** * Sets the current log level (MessageLogLevel / WarningLogLevel / ErrorLogLevel) */ static set LogLevels(level) { Logger.LogLevels = level; } /** * Sets the current performance log level */ static set PerformanceLogLevel(level) { if ((level & Tools.PerformanceUserMarkLogLevel) === Tools.PerformanceUserMarkLogLevel) { Tools.StartPerformanceCounter = Tools._StartUserMark; Tools.EndPerformanceCounter = Tools._EndUserMark; return; } if ((level & Tools.PerformanceConsoleLogLevel) === Tools.PerformanceConsoleLogLevel) { Tools.StartPerformanceCounter = Tools._StartPerformanceConsole; Tools.EndPerformanceCounter = Tools._EndPerformanceConsole; return; } Tools.StartPerformanceCounter = Tools._StartPerformanceCounterDisabled; Tools.EndPerformanceCounter = Tools._EndPerformanceCounterDisabled; } // eslint-disable-next-line @typescript-eslint/no-unused-vars static _StartPerformanceCounterDisabled(counterName, condition) { } // eslint-disable-next-line @typescript-eslint/no-unused-vars static _EndPerformanceCounterDisabled(counterName, condition) { } static _StartUserMark(counterName, condition = true) { if (!Tools._Performance) { if (!IsWindowObjectExist()) { return; } Tools._Performance = window.performance; } if (!condition || !Tools._Performance.mark) { return; } Tools._Performance.mark(counterName + "-Begin"); } static _EndUserMark(counterName, condition = true) { if (!condition || !Tools._Performance.mark) { return; } Tools._Performance.mark(counterName + "-End"); Tools._Performance.measure(counterName, counterName + "-Begin", counterName + "-End"); } static _StartPerformanceConsole(counterName, condition = true) { if (!condition) { return; } Tools._StartUserMark(counterName, condition); if (console.time) { console.time(counterName); } } static _EndPerformanceConsole(counterName, condition = true) { if (!condition) { return; } Tools._EndUserMark(counterName, condition); console.timeEnd(counterName); } /** * Gets either window.performance.now() if supported or Date.now() else */ static get Now() { return PrecisionDate.Now; } /** * This method will return the name of the class used to create the instance of the given object. * It will works only on Javascript basic data types (number, string, ...) and instance of class declared with the @className decorator. * @param object the object to get the class name from * @param isType defines if the object is actually a type * @returns the name of the class, will be "object" for a custom data type not using the @className decorator */ static GetClassName(object, isType = false) { let name = null; if (!isType && object.getClassName) { name = object.getClassName(); } else { if (object instanceof Object) { const classObj = isType ? object : Object.getPrototypeOf(object); name = classObj.constructor["__bjsclassName__"]; } if (!name) { name = typeof object; } } return name; } /** * Gets the first element of an array satisfying a given predicate * @param array defines the array to browse * @param predicate defines the predicate to use * @returns null if not found or the element */ static First(array, predicate) { for (const el of array) { if (predicate(el)) { return el; } } return null; } /** * This method will return the name of the full name of the class, including its owning module (if any). * It will works only on Javascript basic data types (number, string, ...) and instance of class declared with the @className decorator or implementing a method getClassName():string (in which case the module won't be specified). * @param object the object to get the class name from * @param isType defines if the object is actually a type * @returns a string that can have two forms: "moduleName.className" if module was specified when the class' Name was registered or "className" if there was not module specified. * @ignorenaming */ // eslint-disable-next-line @typescript-eslint/naming-convention static getFullClassName(object, isType = false) { let className = null; let moduleName = null; if (!isType && object.getClassName) { className = object.getClassName(); } else { if (object instanceof Object) { const classObj = isType ? object : Object.getPrototypeOf(object); className = classObj.constructor["__bjsclassName__"]; moduleName = classObj.constructor["__bjsmoduleName__"]; } if (!className) { className = typeof object; } } if (!className) { return null; } return (moduleName != null ? moduleName + "." : "") + className; } /** * Returns a promise that resolves after the given amount of time. * @param delay Number of milliseconds to delay * @returns Promise that resolves after the given amount of time */ static DelayAsync(delay) { return new Promise((resolve) => { setTimeout(() => { resolve(); }, delay); }); } /** * Utility function to detect if the current user agent is Safari * @returns whether or not the current user agent is safari */ static IsSafari() { if (!IsNavigatorAvailable()) { return false; } return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); } } /** * Enable/Disable Custom HTTP Request Headers globally. * default = false * @see CustomRequestHeaders */ Tools.UseCustomRequestHeaders = false; /** * Custom HTTP Request Headers to be sent with XMLHttpRequests * i.e. when loading files, where the server/service expects an Authorization header */ Tools.CustomRequestHeaders = WebRequest.CustomRequestHeaders; /** * Extracts text content from a DOM element hierarchy * Back Compat only, please use GetDOMTextContent instead. */ Tools.GetDOMTextContent = GetDOMTextContent; /** * @internal */ Tools._DefaultCdnUrl = "https://cdn.babylonjs.com"; // eslint-disable-next-line jsdoc/require-returns-check, jsdoc/require-param /** * @returns the absolute URL of a given (relative) url */ Tools.GetAbsoluteUrl = typeof document === "object" ? (url) => { const a = document.createElement("a"); a.href = url; return a.href; } : typeof URL === "function" && typeof location === "object" ? (url) => new URL(url, location.origin).href : () => { throw new Error("Unable to get absolute URL. Override BABYLON.Tools.GetAbsoluteUrl to a custom implementation for the current context."); }; // Logs /** * No log */ Tools.NoneLogLevel = Logger.NoneLogLevel; /** * Only message logs */ Tools.MessageLogLevel = Logger.MessageLogLevel; /** * Only warning logs */ Tools.WarningLogLevel = Logger.WarningLogLevel; /** * Only error logs */ Tools.ErrorLogLevel = Logger.ErrorLogLevel; /** * All logs */ Tools.AllLogLevel = Logger.AllLogLevel; /** * Checks if the window object exists * Back Compat only, please use IsWindowObjectExist instead. */ Tools.IsWindowObjectExist = IsWindowObjectExist; // Performances /** * No performance log */ Tools.PerformanceNoneLogLevel = 0; /** * Use user marks to log performance */ Tools.PerformanceUserMarkLogLevel = 1; /** * Log performance to the console */ Tools.PerformanceConsoleLogLevel = 2; /** * Starts a performance counter */ Tools.StartPerformanceCounter = Tools._StartPerformanceCounterDisabled; /** * Ends a specific performance counter */ Tools.EndPerformanceCounter = Tools._EndPerformanceCounterDisabled; /** * Use this className as a decorator on a given class definition to add it a name and optionally its module. * You can then use the Tools.getClassName(obj) on an instance to retrieve its class name. * This method is the only way to get it done in all cases, even if the .js file declaring the class is minified * @param name The name of the class, case should be preserved * @param module The name of the Module hosting the class, optional, but strongly recommended to specify if possible. Case should be preserved. * @returns a decorator function to apply on the class definition. */ export function className(name, module) { return (target) => { target["__bjsclassName__"] = name; target["__bjsmoduleName__"] = module != null ? module : null; }; } /** * An implementation of a loop for asynchronous functions. */ export class AsyncLoop { /** * Constructor. * @param iterations the number of iterations. * @param func the function to run each iteration * @param successCallback the callback that will be called upon successful execution * @param offset starting offset. */ constructor( /** * Defines the number of iterations for the loop */ iterations, func, successCallback, offset = 0) { this.iterations = iterations; this.index = offset - 1; this._done = false; this._fn = func; this._successCallback = successCallback; } /** * Execute the next iteration. Must be called after the last iteration was finished. */ executeNext() { if (!this._done) { if (this.index + 1 < this.iterations) { ++this.index; this._fn(this); } else { this.breakLoop(); } } } /** * Break the loop and run the success callback. */ breakLoop() { this._done = true; this._successCallback(); } /** * Create and run an async loop. * @param iterations the number of iterations. * @param fn the function to run each iteration * @param successCallback the callback that will be called upon successful execution * @param offset starting offset. * @returns the created async loop object */ static Run(iterations, fn, successCallback, offset = 0) { const loop = new AsyncLoop(iterations, fn, successCallback, offset); loop.executeNext(); return loop; } /** * A for-loop that will run a given number of iterations synchronous and the rest async. * @param iterations total number of iterations * @param syncedIterations number of synchronous iterations in each async iteration. * @param fn the function to call each iteration. * @param callback a success call back that will be called when iterating stops. * @param breakFunction a break condition (optional) * @param timeout timeout settings for the setTimeout function. default - 0. * @returns the created async loop object */ static SyncAsyncForLoop(iterations, syncedIterations, fn, callback, breakFunction, timeout = 0) { return AsyncLoop.Run(Math.ceil(iterations / syncedIterations), (loop) => { if (breakFunction && breakFunction()) { loop.breakLoop(); } else { setTimeout(() => { for (let i = 0; i < syncedIterations; ++i) { const iteration = loop.index * syncedIterations + i; if (iteration >= iterations) { break; } fn(iteration); if (breakFunction && breakFunction()) { loop.breakLoop(); break; } } loop.executeNext(); }, timeout); } }, callback); } } Tools.Mix = Mix; Tools.IsExponentOfTwo = IsExponentOfTwo; // Will only be define if Tools is imported freeing up some space when only engine is required EngineStore.FallbackTexture = "data:image/jpg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/4QBmRXhpZgAATU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAAExAAIAAAAQAAAATgAAAAAAAABgAAAAAQAAAGAAAAABcGFpbnQubmV0IDQuMC41AP/bAEMABAIDAwMCBAMDAwQEBAQFCQYFBQUFCwgIBgkNCw0NDQsMDA4QFBEODxMPDAwSGBITFRYXFxcOERkbGRYaFBYXFv/bAEMBBAQEBQUFCgYGChYPDA8WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFv/AABEIAQABAAMBIgACEQEDEQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/APH6KKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FCiiigD6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++gooooA+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gUKKKKAPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76CiiigD5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BQooooA+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/voKKKKAPl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FCiiigD6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++gooooA+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gUKKKKAPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76CiiigD5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BQooooA+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/voKKKKAPl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FCiiigD6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++gooooA+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gUKKKKAPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76Pl+iiivuj+BT6gooor4U/vo+X6KKK+6P4FPqCiiivhT++j5fooor7o/gU+oKKKK+FP76P//Z"; //# sourceMappingURL=tools.js.map