From 817bc6cd4360906a84f28ab6bbeffe0856f79f19 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 21 Aug 2019 09:50:23 +1200 Subject: [PATCH 01/68] Assets API base functionality JSDoc --- .../src/BaseAssetScriptingInterface.h | 39 +++--- .../src/AssetScriptingInterface.h | 118 +++++++++++++----- 2 files changed, 112 insertions(+), 45 deletions(-) diff --git a/libraries/networking/src/BaseAssetScriptingInterface.h b/libraries/networking/src/BaseAssetScriptingInterface.h index 497f627421..ffe1586c71 100644 --- a/libraries/networking/src/BaseAssetScriptingInterface.h +++ b/libraries/networking/src/BaseAssetScriptingInterface.h @@ -33,51 +33,62 @@ public: public slots: /**jsdoc + * Checks whether a string is a valid path. Note: A valid path must start with a "/". * @function Assets.isValidPath - * @param {string} input - * @returns {boolean} + * @param {string} path - The path to check. + * @returns {boolean} true if the path is a valid path, false if it isn't. */ bool isValidPath(QString input) { return AssetUtils::isValidPath(input); } /**jsdoc + * Checks whether a string is a valid path and filename. Note: A valid path and filename must start with a "/" + * but must not end with a "/". * @function Assets.isValidFilePath - * @param {string} input - * @returns {boolean} + * @param {string} path - The path to check. + * @returns {boolean} true if the path is a valid file path, false if it isn't. */ bool isValidFilePath(QString input) { return AssetUtils::isValidFilePath(input); } /**jsdoc + * Gets the normalized ATP URL for a path or hash: ensures that it has "atp:/" at the start. * @function Assets.getATPUrl - * @param {string} input - * @returns {string} + * @param {string} url - The URL to normalize. + * @returns {string} The normalized ATP URL. */ QUrl getATPUrl(QString input) { return AssetUtils::getATPUrl(input); } /**jsdoc + * Gets the SHA256 hexadecimal hash portion of an asset server URL. * @function Assets.extractAssetHash - * @param {string} input - * @returns {string} + * @param {string} url - The URL to get the SHA256 hexadecimal hash from. + * @returns {string} The SHA256 hexadecimal hash portion of the URL if present and valid, "" otherwise. */ QString extractAssetHash(QString input) { return AssetUtils::extractAssetHash(input); } /**jsdoc + * Checks whether a string is a valid SHA256 hexadecimal hash, i.e., 64 hexadecimal characters. * @function Assets.isValidHash - * @param {string} input - * @returns {boolean} + * @param {string} hash - The hash to check. + * @returns {boolean} true if the hash is a valid SHA256 hexadecimal string, false if it isn't. */ bool isValidHash(QString input) { return AssetUtils::isValidHash(input); } /**jsdoc + * Calculates the SHA256 hash of given data. * @function Assets.hashData - * @param {} data - * @returns {object} + * @param {string|ArrayBuffer} data - The data to calculate the hash of. + * @returns {ArrayBuffere} The SHA256 hash of the data. */ QByteArray hashData(const QByteArray& data) { return AssetUtils::hashData(data); } /**jsdoc + * Calculates the SHA256 hash of given data, in hexadecimal format. * @function Assets.hashDataHex - * @param {} data - * @returns {string} + * @param {string|ArrayBuffer} data - The data to calculate the hash of. + * @returns {string} The SHA256 hash of the data, in hexadecimal format. + * @example Calculate the hash of some text. + * var text = "Hello world!"; + * print("Hash: " + Assets.hashDataHex(text)); */ QString hashDataHex(const QByteArray& data) { return hashData(data).toHex(); } diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index 07d681ca88..29aec094cf 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -25,7 +25,9 @@ #include /**jsdoc - * The Assets API allows you to communicate with the Asset Browser. + * The Assets API provides facilities for interacting with the domain's asset server. Assets are stored in the + * asset server in files with SHA256 names. These files are mapped to user-friendly URLs of the format: + * atp:/path/filename. * @namespace Assets * * @hifi-interface @@ -41,77 +43,131 @@ public: AssetScriptingInterface(QObject* parent = nullptr); /**jsdoc - * Upload content to the connected domain's asset server. + * Uploads content to the asset server, storing it in a SHA256-named file. + *

Note: The asset server destroys any unmapped SHA256-named file at server restart. Use {@link Assets.setMapping} to + * set a path-to-hash mapping for the new file.

* @function Assets.uploadData - * @static - * @param data {string} content to upload - * @param callback {Assets~uploadDataCallback} called when upload is complete + * @param {string} data - The content to upload. + * @param {Assets~uploadDataCallback} callback - The function to call upon completion. + * @example Store a string in the asset server. + * Assets.uploadData("Hello world!", function (url, hash) { + * print("URL: " + url); // atp:0a1b...9g + * Assets.setMapping("/assetsExamples/helloWorld.txt", hash, function (error) { + * if (error) { + * print("ERROR: Could not set mapping!"); + * return; + * } + * }); + * }); */ /**jsdoc - * Called when uploadData is complete + * Called when an {@link Assets.uploadData} call is complete. * @callback Assets~uploadDataCallback - * @param {string} url - * @param {string} hash + * @param {string} url - The raw URL of the file that the content is stored in, with atp: as the scheme and + * the SHA256 hash as the filename (with no extension). + * @param {string} hash - The SHA256 hash of the content. */ Q_INVOKABLE void uploadData(QString data, QScriptValue callback); /**jsdoc - * Download data from the connected domain's asset server. + * Downloads content from the asset server, form a SHA256-named file. * @function Assets.downloadData - * @param url {string} URL of asset to download, must be ATP scheme URL. - * @param callback {Assets~downloadDataCallback} + * @param {string} url - The raw URL of asset to download: atp: followed by the assets's SHA256 hash. + * @param {Assets~downloadDataCallback} callback - The function to call upon completion. + * @example Store and retrieve a string from the asset server. + * var assetURL; + * + * // Store the string. + * Assets.uploadData("Hello world!", function (url, hash) { + * assetURL = url; + * print("url: " + assetURL); // atp:a0g89... + * Assets.setMapping("/assetsExamples/hellowWorld.txt", hash, function (error) { + * if (error) { + * print("ERROR: Could not set mapping!"); + * return; + * } + * }); + * }); + * + * // Retrieve the string. + * Script.setTimeout(function () { + * Assets.downloadData(assetURL, function (data, error) { + * print("Downloaded data: " + data); + * print("Error: " + JSON.stringify(error)); + * }); + * }, 1000); */ /**jsdoc - * Called when downloadData is complete + * Called when an {@link Assets.downloadData} call is complete. * @callback Assets~downloadDataCallback - * @param data {string} content that was downloaded + * @param {string} data - The content that was downloaded. + * @param {Assets.DownloadDataError} error - The success or failure of the download. */ - Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete); + /**jsdoc + * The success or failure of an {@link Assets.downloadData} call. + * @typedef {object} Assets.DownloadDataError + * @property {string} errorMessage - "" if the download was successful, otherwise a description of the error. + */ + Q_INVOKABLE void downloadData(QString url, QScriptValue callback); /**jsdoc - * Sets up a path to hash mapping within the connected domain's asset server + * Sets a path-to-hash mapping within the asset server. * @function Assets.setMapping - * @param path {string} - * @param hash {string} - * @param callback {Assets~setMappingCallback} + * @param {string} path - A user-friendly path for the file in the asset server, without leading "atp:". + * @param {string} hash - The hash in the asset server. + * @param {Assets~setMappingCallback} callback - The function to call upon completion. */ /**jsdoc - * Called when setMapping is complete + * Called when an {@link Assets.setMapping} call is complete. * @callback Assets~setMappingCallback - * @param {string} error + * @param {string} error - null if the path-to-hash mapping was set, otherwise a description of the error. */ Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback); /**jsdoc - * Look up a path to hash mapping within the connected domain's asset server + * Gets the hash for a path within the asset server. The hash is for the unbaked or baked version of the + * asset, according to the asset server setting for the particular path. * @function Assets.getMapping - * @param path {string} - * @param callback {Assets~getMappingCallback} + * @param {string} path - The path to a file in the asset server to get the hash of. + * @param {Assets~getMappingCallback} callback - The function to call upon completion. + * @example Report the hash of an asset server item. + * var assetPath = Window.browseAssets(); + * if (assetPath) { + * var mapping = Assets.getMapping(assetPath, function (error, hash) { + * print("Asset: " + assetPath); + * print("- hash: " + hash); + * print("- error: " + error); + * }); + * } */ /**jsdoc - * Called when getMapping is complete. + * Called when an {@link Assets.getMapping} call is complete. * @callback Assets~getMappingCallback - * @param assetID {string} hash value if found, else an empty string - * @param error {string} error description if the path could not be resolved; otherwise a null value. + * @param {string} error - null if the path was found, otherwise a description of the error. + * @param {string} hash - The hash value if the path was found, "" if it wasn't. */ Q_INVOKABLE void getMapping(QString path, QScriptValue callback); /**jsdoc + * Sets whether or not to bake an asset in the asset server. * @function Assets.setBakingEnabled - * @param path {string} - * @param enabled {boolean} - * @param callback {} + * @param {string} path - The path to a file in the asset server. + * @param {boolean} enabled - true to enable baking of the asset, false to disable. + * @param {Assets~setBakingEnabledCallback} callback - The function to call upon completion. */ /**jsdoc - * Called when setBakingEnabled is complete. + * Called when an {@link Assets.setBakingEnabled} call is complete. * @callback Assets~setBakingEnabledCallback + * @param {string} error - null if baking was successfully enabled or disabled, otherwise a description of the + * error. */ + // Note: Second callback parameter not documented because it's always {}. Q_INVOKABLE void setBakingEnabled(QString path, bool enabled, QScriptValue callback); #if (PR_BUILD || DEV_BUILD) /** * This function is purely for development purposes, and not meant for use in a - * production context. It is not a public-facing API, so it should not contain jsdoc. + * production context. It is not a public-facing API, so it should not have JSDoc. */ Q_INVOKABLE void sendFakedHandshake(); #endif From 5f3081784c7a43c5297915c4829a05f0db55346f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 21 Aug 2019 12:36:37 +1200 Subject: [PATCH 02/68] Assets API methods that include mappings JSDoc --- .../src/BaseAssetScriptingInterface.h | 15 + .../src/AssetScriptingInterface.cpp | 15 - .../src/AssetScriptingInterface.h | 263 ++++++++++++++---- 3 files changed, 217 insertions(+), 76 deletions(-) diff --git a/libraries/networking/src/BaseAssetScriptingInterface.h b/libraries/networking/src/BaseAssetScriptingInterface.h index ffe1586c71..e2af073941 100644 --- a/libraries/networking/src/BaseAssetScriptingInterface.h +++ b/libraries/networking/src/BaseAssetScriptingInterface.h @@ -24,6 +24,21 @@ class BaseAssetScriptingInterface : public QObject { Q_OBJECT public: + + /**jsdoc + *

Types of response that {@link Assets.getAsset} and {@link Assets.loadFromCache} may provide.

+ * + * + * + * + * + * + * + * + * + *
ValueDescription
"text"UTF-8 decoded string value.
"arraybuffer"A binary ArrayBuffer object.
"json"A parsed JSON object.
+ * @typedef {string} Assets.ResponseType + */ const QStringList RESPONSE_TYPES{ "text", "arraybuffer", "json" }; using Promise = MiniPromise::Promise; QSharedPointer assetClient(); diff --git a/libraries/script-engine/src/AssetScriptingInterface.cpp b/libraries/script-engine/src/AssetScriptingInterface.cpp index e389bd8446..fda66e0886 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.cpp +++ b/libraries/script-engine/src/AssetScriptingInterface.cpp @@ -215,21 +215,6 @@ void AssetScriptingInterface::deleteAsset(QScriptValue options, QScriptValue sco jsVerify(false, "TODO: deleteAsset API"); } -/**jsdoc - * @typedef {string} Assets.GetOptions.ResponseType - *

Available responseType values for use with @{link Assets.getAsset} and @{link Assets.loadFromCache} configuration option.

- * - * - * - * - * - * - * - * - * - *
responseTypetypeof response value
"text"contents returned as utf-8 decoded String value
"arraybuffer"contents as a binary ArrayBuffer object
"json"contents as a parsed JSON object
- */ - void AssetScriptingInterface::getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { JS_VERIFY(options.isObject() || options.isString(), "expected request options Object or URL as first parameter"); diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index 29aec094cf..31fd52f4c8 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -173,94 +173,235 @@ public: #endif /**jsdoc - * Request Asset data from the ATP Server - * @function Assets.getAsset - * @param {URL|Assets.GetOptions} options An atp: style URL, hash, or relative mapped path; or an {@link Assets.GetOptions} object with request parameters - * @param {Assets~getAssetCallback} scope A scope callback function to receive (error, results) values - * @param {function} [callback=undefined] + * Details of a callback function. + * @typedef {object} Assets.CallbackDetails + * @property {object} scope - The scope that the callback function is defined in. This object is bound to + * this when the function is called. + * @property {Assets~putAssetCallback|Assets~getAssetCallback|Assets~resolveAssetCallback} callback - The function to + * call upon completion. May be an inline function or a function identifier. If a function identifier, it must be a + * member of scope. */ /**jsdoc - * A set of properties that can be passed to {@link Assets.getAsset}. + * Source and download options for {@link Assets.getAsset}. * @typedef {object} Assets.GetOptions - * @property {string} [url] an "atp:" style URL, hash, or relative mapped path to fetch - * @property {string} [responseType=text] the desired reponse type (text | arraybuffer | json) - * @property {boolean} [decompress=false] whether to attempt gunzip decompression on the fetched data - * See: {@link Assets.putAsset} and its .compress=true option + * @property {string} url - The mapped path or hash to download. May have a leading "atp:". + * @property {Assets.ResponseType} [responseType="text"] - The desired result type. + * @property {boolean} [decompress=false] - true to gunzip decompress the downloaded data. Synonym: + * compressed. */ - /**jsdoc - * Called when Assets.getAsset is complete. + * Called when an {@link Assets.getAsset} call is complete. * @callback Assets~getAssetCallback - * @param {string} error - contains error message or null value if no error occured fetching the asset - * @param {Asset~getAssetResult} result - result object containing, on success containing asset metadata and contents + * @param {string} error - null if the content was downloaded, otherwise a description of the error. + * @param {Assets.GetResult} result - Information on and the content downloaded. */ - /**jsdoc - * Result value returned by {@link Assets.getAsset}. - * @typedef {object} Assets~getAssetResult - * @property {string} [url] the resolved "atp:" style URL for the fetched asset - * @property {string} [hash] the resolved hash for the fetched asset - * @property {string|ArrayBuffer|Object} [response] response data (possibly converted per .responseType value) - * @property {string} [responseType] response type (text | arraybuffer | json) - * @property {string} [contentType] detected asset mime-type (autodetected) - * @property {number} [byteLength] response data size in bytes - * @property {number} [decompressed] flag indicating whether data was decompressed + * Result value returned by {@link Assets.getAsset}. + * @typedef {object} Assets.GetResult + * @property {number} [byteLength] - The number of bytes in the downloaded response. + * @property {boolean} cached - + * @property {string} [contentType] - Automatically detected MIME type of the content. + * @property {boolean} [decompressed] - true if the content was decompressed, false if it wasn't. + * @property {string} [hash] - The hash for the downloaded asset. + * @property {string} [hashURL] - The ATP URL of the hash file. + * @property {string} [path] - The path for the asset, if a path was requested. Otherwise, undefined. + * @property {string|ArrayBuffer|object} [response] - The downloaded content. + * @property {Assets.ResponseType} [responseType] - The type of the downloaded content. + * @property {string} [url] - The URL of the asset requested: the path with leading "atp:" if a path was + * requested, otherwise the requested URL. + * @property {boolean} [wasRedirected] - true if the downloaded data is the baked version of the asset, + * false if it isn't baked. + */ + /**jsdoc + * Downloads content from the asset server. + * @function Assets.getAsset + * @param {Assets.GetOptions|string} source - What to download and download options. If a string, the mapped path or hash + * to download, optionally including a leading "atp:". + * @param {Assets~getAssetCallback} callback - The function to call upon completion. May be a function identifier or an + * inline function. + * @example Retrieve a string from the asset server. + * Assets.getAsset( + * { + * url: "/assetsExamples/helloWorld.txt", + * responseType: "text" + * }, + * function (error, result) { + * if (error) { + * print("ERROR: Data not downloaded"); + * } else { + * print("Data: " + result.response); + * } + * } + * ); + */ + /**jsdoc + * Downloads content from the asset server. + * @function Assets.getAsset + * @param {Assets.GetOptions|string} source - What to download and download options. If a string, the mapped path or hash + * to download, optionally including a leading "atp:". + * @param {object} scope - The scope that the callback function is defined in. This object is bound to + * this when the function is called. + * @param {Assets~getAssetCallback} callback - The function to call upon completion. May be an inline function, a function + * identifier, or the name of a function in a string. If the name of a function or a function identifier, it must be + * a member of scope. + */ + /**jsdoc + * Downloads content from the asset server. + * @function Assets.getAsset + * @param {Assets.GetOptions|string} source - What to download and download options. If a string, the mapped path or hash + * to download, optionally including a leading "atp:". + * @param {Assets.CallbackDetails} callbackDetails - Details of the function to call upon completion. */ - Q_INVOKABLE void getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * Upload Asset data to the ATP Server - * @function Assets.putAsset - * @param {Assets.PutOptions} options A PutOptions object with upload parameters - * @param {Assets~putAssetCallback} scope[callback] A scoped callback function invoked with (error, results) - * @param {function} [callback=undefined] - */ - - /**jsdoc - * A set of properties that can be passed to {@link Assets.putAsset}. + * Content and upload options for {@link Assets.putAsset}. * @typedef {object} Assets.PutOptions - * @property {ArrayBuffer|string} [data] byte buffer or string value representing the new asset's content - * @property {string} [path=null] ATP path mapping to automatically create (upon successful upload to hash) - * @property {boolean} [compress=false] whether to gzip compress data before uploading + * @property {string|ArrayBuffer} data - The content to upload. + * @property {string} [path] - A user-friendly path for the file in the asset server. May have a leading + * "atp:". + *

Note: The asset server destroys any unmapped SHA256-named file at server restart. Either set the mapping path + * with this property or use {@link Assets.setMapping} to set a path-to-hash mapping for the uploaded file.

+ * @property {boolean} [compress=false] - true to gzip compress the content for upload and storage, + * false to upload and store the data without gzip compression. Synonym: compressed. */ - /**jsdoc - * Called when Assets.putAsset is complete. - * @callback Assets~puttAssetCallback - * @param {string} error - contains error message (or null value if no error occured while uploading/mapping the new asset) - * @param {Asset~putAssetResult} result - result object containing error or result status of asset upload + * Called when an {@link Assets.putAsset} call is complete. + * @callback Assets~putAssetCallback + * @param {string} error - null if the content was uploaded and any path-to-hash mapping set, otherwise a + * description of the error. + * @param {Assets.PutResult} result - Information on the content uploaded. */ - /**jsdoc * Result value returned by {@link Assets.putAsset}. - * @typedef {object} Assets~putAssetResult - * @property {string} [url] the resolved "atp:" style URL for the uploaded asset (based on .path if specified, otherwise on the resulting ATP hash) - * @property {string} [path] the uploaded asset's resulting ATP path (or undefined if no path mapping was assigned) - * @property {string} [hash] the uploaded asset's resulting ATP hash - * @property {boolean} [compressed] flag indicating whether the data was compressed before upload - * @property {number} [byteLength] flag indicating final byte size of the data uploaded to the ATP server + * @typedef {object} Assets.PutResult + * @property {number} [byteLength] - The number of bytes in the hash file stored on the asset server. + * @property {boolean} [compressed] - true if the content stored is gzip compressed. + * @property {string} [contentType] - "application/gzip" if the content stored is gzip compressed. + * @property {string} [hash] - The SHA256 hash of the content. + * @property {string} [url] - The atp: URL of the content: using the path if specified, otherwise the hash. + * @property {string} [path] - The uploaded content's mapped path, if specified. + */ + /**jsdoc + * Uploads content to the assert server and sets a path-to-hash mapping. + * @function Assets.putAsset + * @param {Assets.PutOptions|string} options - The content to upload and upload options. If a string, the value of the + * string is uploaded but a path-to-hash mapping is not set. + * @param {Assets~putAssetCallback} callback - The function to call upon completion. May be an inline function or a + * function identifier. + * @example Store a string in the asset server. + * Assets.putAsset( + * { + * data: "Hello world!", + * path: "/assetsExamples/helloWorld.txt" + * }, + * function (error, result) { + * if (error) { + * print("ERROR: Data not uploaded or mapping not set"); + * } else { + * print("URL: " + result.url); // atp:/assetsExamples/helloWorld.txt + * } + * } + * ); + */ + /**jsdoc + * Uploads content to the assert server and sets a path-to-hash mapping. + * @function Assets.putAsset + * @param {Assets.PutOptions|string} options - The content to upload and upload options. If a string, the value of the + * string is uploaded but a path-to-hash mapping is not set. + * @param {object} scope - The scope that the callback function is defined in. This object is bound to + * this when the function is called. + * @param {Assets~getAssetCallback} callback - The function to call upon completion. May be an inline function, a function + * identifier, or the name of a function in a string. If the name of a function or a function identifier, it must be + * a member of scope. + */ + /**jsdoc + * Uploads content to the assert server and sets a path-to-hash mapping. + * @function Assets.putAsset + * @param {Assets.PutOptions|string} options - The content to upload and upload options. If a string, the value of the + * string is uploaded but a path-to-hash mapping is not set. + * @param {Assets.CallbackDetails} callbackDetails - Details of the function to call upon completion. */ - Q_INVOKABLE void putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.deleteAsset - * @param {} options - * @param {} scope - * @param {} [callback = ""] + * Called when an {@link Assets.deleteAsset} call is complete. + *

Not implemented: This type is not implemented yet.

+ * @callback Assets~deleteAssetCallback + * @param {string} error - null if the content was deleted, otherwise a description of the error. + * @param {Assets.DeleteResult} result - Information on the content deleted. */ - - Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); - /**jsdoc - * @function Assets.resolveAsset - * @param {} options - * @param {} scope - * @param {} [callback = ""] + * Deletes content from the asset server. + *

Not implemented: This method is not implemented yet.

+ * @function Assets.deleteAsset + * @param {Assets.DeleteOptions} options - The content to delete and delete options. + * @param {object} scope - he scope that the callback function is defined in. + * @param {Assets~deleteAssetCallback} callback - The function to call upon completion. */ + Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + /**jsdoc + * Source options for {@link Assets.resolveAsset}. + * @typedef {object} Assets.ResolveOptions + * @property {string} url - The hash or path to resolve. May have a leading "atp:". + */ + /**jsdoc + * Called when an {@link Assets.resolveAsset} call is complete. + * @callback Assets~resolveAssetCallback + * @param {string} error - null if the asset hash or path was resolved, otherwise a description of the error. + * @param {Assets.ResolveResult} result - Information on the hash or path resolved. + */ + /**jsdoc + * Result value returned by {@link Assets.resolveAsset}. + *

Note: If resolving a hash, a file of that hash need not be present on the asset server for the hash to resolve.

+ * @typedef {object} Assets.ResolveResult + * @property {string} [hash] - The hash of the asset. + * @property {string} [hashURL] - The url of the asset's hash file, with leading atp:. + * @property {string} [path] - The path to the asset. + * @property {string} [url] - The URL of the asset. + * @property {boolean} [wasRedirected] - true if the resolved data is for the baked version of the asset, + * false if it isn't. + */ + /**jsdoc + * Resolves and returns information on a hash or a path in the asset server. + * @function Assets.resolveAsset + * @param {string|Assets.ResolveOptions} source - The hash or path to resolve if a string, otherwise an object specifying + * what to resolve. If a string, it may have a leading "atp:". + * @param {Assets~resolveAssetCallback} callback - The function to call upon completion. May be a function identifier or + * an inline function. + * @example Get the hash and URL for a path. + * Assets.resolveAsset( + * "/assetsExamples/helloWorld.txt", + * function (error, result) { + * if (error) { + * print("ERROR: " + error); + * } else { + * print("Hash: " + result.hash); + * print("URL: " + result.url); + * } + * } + * ); + */ + /**jsdoc + * Resolves and returns information on a hash or a path in the asset server. + * @function Assets.resolveAsset + * @param {string|Assets.ResolveOptions} source - The hash or path to resolve if a string, otherwise an object specifying + * what to resolve. If a string, it may have a leading "atp:". + * @param {object} scope - The scope that the callback function is defined in. This object is bound to + * this when the function is called. + * @param {Assets~resolveAssetCallback} callback - The function to call upon completion. May be an inline function, a + * function identifier, or the name of a function in a string. If the name of a function or a function identifier, it + * must be a member of scope. + */ + /**jsdoc + * Resolves and returns information on a hash or a path in the asset server. + * @function Assets.resolveAsset + * @param {string|Assets.ResolveOptions} source - The hash or path to resolve if a string, otherwise an object specifying + * what to resolve. If a string, it may have a leading "atp:". + * @param {Assets.CallbackDetails} callbackDetails - Details of the function to call upon completion. + */ Q_INVOKABLE void resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc From 11e72c638bd6449711e5358cb30bc649652f8432 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 22 Aug 2019 09:47:25 +1200 Subject: [PATCH 03/68] Assets API compression methods JSDoc --- .../src/BaseAssetScriptingInterface.h | 3 +- .../src/AssetScriptingInterface.h | 105 +++++++++++++++--- 2 files changed, 94 insertions(+), 14 deletions(-) diff --git a/libraries/networking/src/BaseAssetScriptingInterface.h b/libraries/networking/src/BaseAssetScriptingInterface.h index e2af073941..f15b9acac3 100644 --- a/libraries/networking/src/BaseAssetScriptingInterface.h +++ b/libraries/networking/src/BaseAssetScriptingInterface.h @@ -26,7 +26,8 @@ class BaseAssetScriptingInterface : public QObject { public: /**jsdoc - *

Types of response that {@link Assets.getAsset} and {@link Assets.loadFromCache} may provide.

+ *

Types of response that {@link Assets.getAsset}, {@link Assets.loadFromCache}, or {@link Assets.decompressData} may + * provide.

* * * diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index 31fd52f4c8..a4363fd19a 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -177,9 +177,10 @@ public: * @typedef {object} Assets.CallbackDetails * @property {object} scope - The scope that the callback function is defined in. This object is bound to * this when the function is called. - * @property {Assets~putAssetCallback|Assets~getAssetCallback|Assets~resolveAssetCallback} callback - The function to - * call upon completion. May be an inline function or a function identifier. If a function identifier, it must be a - * member of scope. + * @property {Assets~putAssetCallback|Assets~getAssetCallback|Assets~resolveAssetCallback|Assets~compressDataCallback + * |Assets~decompressDataCallback} + * callback - The function to call upon completion. May be an inline function or a function identifier. If a function + * identifier, it must be a member of scope. */ /**jsdoc @@ -405,21 +406,99 @@ public: Q_INVOKABLE void resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.decompressData - * @param {} options - * @param {} scope - * @param {} [callback = ""] + * Content and decompression options for {@link Assets.decompressData}. + * @typedef {object} Assets.DecompressOptions + * @property {ArrayBuffer} data - The data to decompress. + * @property {Assets.ResponseType} [responseType=text] - The type of data to return. + */ + /**jsdoc + * Called when an {@link Assets.decompressData} call is complete. + * @callback Assets~decompressDataCallback + * @param {string} error - null if the data was successfully compressed, otherwise a description of the error. + * @param {Assets.DecompressResult} result - Information on and the decompressed data. + */ + /**jsdoc + * Result value returned by {@link Assets.decompressData}. + * @typedef {object} Assets.DecompressResult + * @property {number} [byteLength] - The number of bytes in the decompressed data. + * @property {string} [contentType] - The MIME type of the decompressed data. + * @property {boolean} [decompressed] - true if the data is decompressed. + * @property {string|ArrayBuffer|object} [response] - The decompressed data. + * @property {Assets.ResponseType} [responseType] - The type of the response. + */ + /**jsdoc + * Decompresses data in memory using gunzip. + * @function Assets.decompressData + * @param {Assets.DecompressOptions} source - What to decompress and decompression options. + * @param {Assets~decompressDataCallback} callback - The function to call upon completion. May be a function identifier or + * an inline function. + */ + /**jsdoc + * Decompresses data in memory using gunzip. + * @function Assets.decompressData + * @param {Assets.DecompressOptions} source - What to decompress and decompression options. + * @param {object} scope - The scope that the callback function is defined in. This object is bound to + * this when the function is called. + * @param {Assets~decompressDataCallback} callback - The function to call upon completion. May be an inline function, a + * function identifier, or the name of a function in a string. If the name of a function or a function identifier, it + * must be a member of scope. + */ + /**jsdoc + * Decompresses data in memory using gunzip. + * @function Assets.decompressData + * @param {Assets.DecompressOptions} source - What to decompress and decompression options. + * @param {Assets.CallbackDetails} callbackDetails - Details of the function to call upon completion. */ - Q_INVOKABLE void decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.compressData - * @param {} options - * @param {} scope - * @param {} [callback = ""] + * Content and compression options for {@link Assets.compressData}. + * @typedef {object} Assets.CompressOptions + * @property {ArrayBuffer|string} data - The data to compress. + * @property {number} level - The compression level, range -19. -1 means + * use the default gzip compression level, 0 means no compression, and 9 means maximum + * compression. + */ + /**jsdoc + * Called when an {@link Assets.compressData} call is complete. + * @callback Assets~compressDataCallback + * @param {string} error - null if the data was successfully compressed, otherwise a description of the error. + * @param {Assets.CompressResult} result - Information on and the compressed data. + */ + /**jsdoc + * Result value returned by {@link Assets.compressData}. + * @typedef {object} Assets.CompressResult + * @property {boolean} [compressed] - true if the data is compressed. + * @property {number} [byteLength] - The number of bytes in the compressed data. + * @property {string} [contentType] - The MIME type of the compressed data, i.e., "application/gzip". + * @property {ArrayBuffer} [data] - The compressed data. + */ + /**jsdoc + * Compresses data in memory using gzip. + * @function Assets.compressData + * @param {Assets.CompressOptions|ArrayBuffer|string} source - What to compress and compression options. If an ArrayBuffer + * or a string, the data to compress. + * @param {Assets~compressDataCallback} callback - The function to call upon completion. May be a function identifier or an + * inline function. + */ + /**jsdoc + * Compresses data in memory using gzip. + * @function Assets.compressData + * @param {Assets.CompressOptions|ArrayBuffer|string} source - What to compress and compression options. If an ArrayBuffer + * or a string, the data to compress. + * @param {object} scope - The scope that the callback function is defined in. This object is bound to + * this when the function is called. + * @param {Assets~compressDataCallback} callback - The function to call upon completion. May be an inline function, a + * function identifier, or the name of a function in a string. If the name of a function or a function identifier, it + * must be a member of scope. + */ + /**jsdoc + * Compresses data in memory using gzip. + * @function Assets.compressData + * @param {Assets.CompressOptions|ArrayBuffer|string} source - What to compress and compressopn options. If an ArrayBuffer + * or a string, the data to compress. + * @param {Assets.CallbackDetails} callbackDetails - Details of the function to call upon completion. */ - Q_INVOKABLE void compressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc From 942347158a482e1366fc34f5e3f058a722f25797 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 22 Aug 2019 10:16:56 +1200 Subject: [PATCH 04/68] Collapse some function signatures' JSDoc --- .../src/AssetScriptingInterface.h | 54 ++++--------------- 1 file changed, 10 insertions(+), 44 deletions(-) diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index a4363fd19a..bb98f32131 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -219,8 +219,8 @@ public: * @function Assets.getAsset * @param {Assets.GetOptions|string} source - What to download and download options. If a string, the mapped path or hash * to download, optionally including a leading "atp:". - * @param {Assets~getAssetCallback} callback - The function to call upon completion. May be a function identifier or an - * inline function. + * @param {Assets.CallbackDetails|Assets~getAssetCallback} callback - The function to call upon completion. May be a + * function identifier or an inline function. * @example * Assets.getAsset( * { @@ -247,13 +247,6 @@ public: * identifier, or the name of a function in a string. If the name of a function or a function identifier, it must be * a member of scope. */ - /**jsdoc - * Downloads content from the asset server. - * @function Assets.getAsset - * @param {Assets.GetOptions|string} source - What to download and download options. If a string, the mapped path or hash - * to download, optionally including a leading "atp:". - * @param {Assets.CallbackDetails} callbackDetails - Details of the function to call upon completion. - */ Q_INVOKABLE void getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc @@ -289,8 +282,8 @@ public: * @function Assets.putAsset * @param {Assets.PutOptions|string} options - The content to upload and upload options. If a string, the value of the * string is uploaded but a path-to-hash mapping is not set. - * @param {Assets~putAssetCallback} callback - The function to call upon completion. May be an inline function or a - * function identifier. + * @param {Assets.CallbackDetails|Assets~putAssetCallback} callback - The function to call upon completion. May be an + * inline function or a function identifier. * @example * Assets.putAsset( * { @@ -317,13 +310,6 @@ public: * identifier, or the name of a function in a string. If the name of a function or a function identifier, it must be * a member of scope. */ - /**jsdoc - * Uploads content to the assert server and sets a path-to-hash mapping. - * @function Assets.putAsset - * @param {Assets.PutOptions|string} options - The content to upload and upload options. If a string, the value of the - * string is uploaded but a path-to-hash mapping is not set. - * @param {Assets.CallbackDetails} callbackDetails - Details of the function to call upon completion. - */ Q_INVOKABLE void putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc @@ -370,8 +356,8 @@ public: * @function Assets.resolveAsset * @param {string|Assets.ResolveOptions} source - The hash or path to resolve if a string, otherwise an object specifying * what to resolve. If a string, it may have a leading "atp:". - * @param {Assets~resolveAssetCallback} callback - The function to call upon completion. May be a function identifier or - * an inline function. + * @param {Assets.CallbackDetails|Assets~resolveAssetCallback} callback - The function to call upon completion. May be a + * function identifier or an inline function. * @example * Assets.resolveAsset( * "/assetsExamples/helloWorld.txt", @@ -396,13 +382,6 @@ public: * function identifier, or the name of a function in a string. If the name of a function or a function identifier, it * must be a member of scope. */ - /**jsdoc - * Resolves and returns information on a hash or a path in the asset server. - * @function Assets.resolveAsset - * @param {string|Assets.ResolveOptions} source - The hash or path to resolve if a string, otherwise an object specifying - * what to resolve. If a string, it may have a leading "atp:". - * @param {Assets.CallbackDetails} callbackDetails - Details of the function to call upon completion. - */ Q_INVOKABLE void resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc @@ -430,8 +409,8 @@ public: * Decompresses data in memory using gunzip. * @function Assets.decompressData * @param {Assets.DecompressOptions} source - What to decompress and decompression options. - * @param {Assets~decompressDataCallback} callback - The function to call upon completion. May be a function identifier or - * an inline function. + * @param {Assets.CallbackDetails|Assets~decompressDataCallback} callback - The function to call upon completion. May be a + * function identifier or an inline function. */ /**jsdoc * Decompresses data in memory using gunzip. @@ -443,12 +422,6 @@ public: * function identifier, or the name of a function in a string. If the name of a function or a function identifier, it * must be a member of scope. */ - /**jsdoc - * Decompresses data in memory using gunzip. - * @function Assets.decompressData - * @param {Assets.DecompressOptions} source - What to decompress and decompression options. - * @param {Assets.CallbackDetails} callbackDetails - Details of the function to call upon completion. - */ Q_INVOKABLE void decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc @@ -478,8 +451,8 @@ public: * @function Assets.compressData * @param {Assets.CompressOptions|ArrayBuffer|string} source - What to compress and compression options. If an ArrayBuffer * or a string, the data to compress. - * @param {Assets~compressDataCallback} callback - The function to call upon completion. May be a function identifier or an - * inline function. + * @param {Assets.CallbackDetails|Assets~compressDataCallback} callback - The function to call upon completion. May be a + * function identifier or an inline function. */ /**jsdoc * Compresses data in memory using gzip. @@ -492,13 +465,6 @@ public: * function identifier, or the name of a function in a string. If the name of a function or a function identifier, it * must be a member of scope. */ - /**jsdoc - * Compresses data in memory using gzip. - * @function Assets.compressData - * @param {Assets.CompressOptions|ArrayBuffer|string} source - What to compress and compressopn options. If an ArrayBuffer - * or a string, the data to compress. - * @param {Assets.CallbackDetails} callbackDetails - Details of the function to call upon completion. - */ Q_INVOKABLE void compressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc From 2ad1d7a2a281558c130599a9d8ddc834ac27594e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 22 Aug 2019 10:27:17 +1200 Subject: [PATCH 05/68] Fix typos --- libraries/networking/src/BaseAssetScriptingInterface.h | 4 ++-- libraries/script-engine/src/AssetScriptingInterface.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/networking/src/BaseAssetScriptingInterface.h b/libraries/networking/src/BaseAssetScriptingInterface.h index f15b9acac3..ac2c211343 100644 --- a/libraries/networking/src/BaseAssetScriptingInterface.h +++ b/libraries/networking/src/BaseAssetScriptingInterface.h @@ -66,7 +66,7 @@ public slots: bool isValidFilePath(QString input) { return AssetUtils::isValidFilePath(input); } /**jsdoc - * Gets the normalized ATP URL for a path or hash: ensures that it has "atp:/" at the start. + * Gets the normalized ATP URL for a path or hash: ensures that it has "atp:" at the start. * @function Assets.getATPUrl * @param {string} url - The URL to normalize. * @returns {string} The normalized ATP URL. @@ -93,7 +93,7 @@ public slots: * Calculates the SHA256 hash of given data. * @function Assets.hashData * @param {string|ArrayBuffer} data - The data to calculate the hash of. - * @returns {ArrayBuffere} The SHA256 hash of the data. + * @returns {ArrayBuffer} The SHA256 hash of the data. */ QByteArray hashData(const QByteArray& data) { return AssetUtils::hashData(data); } diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index bb98f32131..a93114275a 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -324,7 +324,7 @@ public: *

Not implemented: This method is not implemented yet.

* @function Assets.deleteAsset * @param {Assets.DeleteOptions} options - The content to delete and delete options. - * @param {object} scope - he scope that the callback function is defined in. + * @param {object} scope - The scope that the callback function is defined in. * @param {Assets~deleteAssetCallback} callback - The function to call upon completion. */ Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); From 8820d1d76bc0b088101576df23c3756b383bb562 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 4 Sep 2019 13:48:06 +1200 Subject: [PATCH 06/68] Assets API cache methods --- libraries/networking/src/AssetClient.cpp | 39 ++++ .../src/BaseAssetScriptingInterface.cpp | 11 ++ .../src/AssetScriptingInterface.cpp | 17 ++ .../src/AssetScriptingInterface.h | 179 +++++++++++++++--- 4 files changed, 216 insertions(+), 30 deletions(-) diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index b9a3e6f61e..0ba21e93cd 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -85,6 +85,13 @@ namespace { const QString& CACHE_ERROR_MESSAGE{ "AssetClient::Error: %1 %2" }; } +/**jsdoc + * Cache status value returned by {@link Assets.getCacheStatus}. + * @typedef {object} Assets.GetCacheStatusResult + * @property {string} cacheDirectory - The path of the cache directory. + * @property {number} cacheSize - The current cache size, in bytes. + * @property {number} maximumCacheSize - The maximum cache size, in bytes. + */ MiniPromise::Promise AssetClient::cacheInfoRequestAsync(MiniPromise::Promise deferred) { if (!deferred) { deferred = makePromise(__FUNCTION__); // create on caller's thread @@ -106,6 +113,20 @@ MiniPromise::Promise AssetClient::cacheInfoRequestAsync(MiniPromise::Promise def return deferred; } +/**jsdoc + * Information on an asset in the cache. Value returned by {@link Assets.queryCacheMeta} and included in the data returned by + * {@link Assets.loadFromCache}. + * @typedef {object} Assets.CacheItemMetaData + * @property {object} [attributes] - The attributes that are stored with this cache item. Not used. + * @property {Date} [expirationDate] - The date and time when the meta data expires. An invalid date means "never expires". + * @property {boolean} isValid - true if the item specified in the URL is in the cache, false if + * it isn't. + * @property {Date} [lastModified] - The date and time when the meta data was last modified. + * @property {object} [rawHeaders] - The raw headers that are set in the meta data. Not used. + * @property {boolean} [saveToDisk] - true if the cache item is allowed to be store on disk, + * false if it isn't. + * @property {string} [url|metaDataURL] - The ATP URL of the cached item. + */ MiniPromise::Promise AssetClient::queryCacheMetaAsync(const QUrl& url, MiniPromise::Promise deferred) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "queryCacheMetaAsync", Q_ARG(const QUrl&, url), Q_ARG(MiniPromise::Promise, deferred)); @@ -202,6 +223,24 @@ namespace { } } +/**jsdoc + * Last-modified and expiry times for a cache item. + * @typedef {object} Assets.SaveToCacheHeaders + * @property {string} [last-modified] - The date and time the cache value was last modified, in the format: + * "ddd, dd MMM yyyy HH:mm:ss". The default value is the current date and time. + * @property {string} [expires] - The date and time the cache value expires, in the format: + * "ddd, dd MMM yyyy HH:mm:ss". The default value is an invalid date, representing "never expires". + */ +/**jsdoc + * Information on saving asset data to the cache with {@link Assets.saveToCache}. + * @typedef {object} Assets.SaveToCacheResult + * @property {number} [byteLength] - The size of the cached data, in bytes. + * @property {Date} [expirationDate] - The date and time that the cache item expires. An invalid date means "never expires". + * @property {Date} [lastModified] - The date and time that the cache item was last modified. + * @property {string} [metaDataURL] - The URL associated with the cache item. + * @property {boolean} [success] - true if the save to cache request was successful. + * @property {string} [url] - The URL associated with the cache item. + */ MiniPromise::Promise AssetClient::saveToCacheAsync(const QUrl& url, const QByteArray& data, const QVariantMap& headers, MiniPromise::Promise deferred) { if (!deferred) { deferred = makePromise(__FUNCTION__); // create on caller's thread diff --git a/libraries/networking/src/BaseAssetScriptingInterface.cpp b/libraries/networking/src/BaseAssetScriptingInterface.cpp index b231339e51..2a98dbf3c3 100644 --- a/libraries/networking/src/BaseAssetScriptingInterface.cpp +++ b/libraries/networking/src/BaseAssetScriptingInterface.cpp @@ -68,6 +68,17 @@ Promise BaseAssetScriptingInterface::queryCacheMeta(const QUrl& url) { return assetClient()->queryCacheMetaAsync(url, makePromise(__FUNCTION__)); } +/**jsdoc + * Data and information returned by {@link Assets.loadFromCache}. + * @typedef {object} Assets.LoadFromCacheResult + * @property {number} [byteLength] - The number of bytes in the retrieved data. + * @property {string} [contentType] - The automatically detected MIME type of the content. + * @property {ArrayBuffer} data - The data bytes. + * @property {Assets.CacheItemMetaData} metadata - Information on the cache item. + * @property {string|object|ArrayBuffer} [response] - The content of the response. + * @property {Assets.ResponseType} responseType - The type of the content in response. + * @property {string} url - The URL of the cache item. + */ Promise BaseAssetScriptingInterface::loadFromCache(const QUrl& url, bool decompress, const QString& responseType) { QVariantMap metaData = { { "_type", "cache" }, diff --git a/libraries/script-engine/src/AssetScriptingInterface.cpp b/libraries/script-engine/src/AssetScriptingInterface.cpp index fda66e0886..c440aab830 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.cpp +++ b/libraries/script-engine/src/AssetScriptingInterface.cpp @@ -362,6 +362,15 @@ void AssetScriptingInterface::queryCacheMeta(QScriptValue options, QScriptValue jsPromiseReady(Parent::queryCacheMeta(url), scope, callback); } +/**jsdoc + * Source and retrieval options for {@link Assets.loadFromCache}. + * @typedef {object} Assets.LoadFromCacheOptions + * @property {string} url - The URL of the asset to load from cache. Must start with "atp:" or + * "cache:". + * @property {Assets.ResponseType} [responseType=text] - The desired result type. + * @property {boolean} [decompress=false] - true to gunzip decompress the cached data. Synonym: + * compressed. + */ void AssetScriptingInterface::loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback) { QString url, responseType; bool decompress = false; @@ -396,6 +405,14 @@ bool AssetScriptingInterface::canWriteCacheValue(const QUrl& url) { return true; } +/**jsdoc + * The data to save to the cache and cache options for {@link Assets.saveToCache}. + * @typedef {object} Assets.SaveToCacheOptions + * @property {string|ArrayBuffer} data - The data to save to the cache. + * @property {Assets.SaveToCacheHeaders} [headers] - The last-modified and expiry times for the cache item. + * @property {string} [url] - The URL to associate with the cache item. Must start with "atp:" or + * "cache:". If not specified, the URL is "atp:" followed by the SHA256 hash of the content. + */ void AssetScriptingInterface::saveToCache(QScriptValue options, QScriptValue scope, QScriptValue callback) { JS_VERIFY(options.isObject(), QString("expected options object as first parameter not: %1").arg(options.toVariant().typeName())); diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index a93114275a..8a0ec27817 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -468,66 +468,185 @@ public: Q_INVOKABLE void compressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc + * Initializes the local cache if it isn't already initialized. * @function Assets.initializeCache - * @returns {boolean} + * @returns {boolean} true if the local cache is initialized, false if it isn't. */ - Q_INVOKABLE bool initializeCache() { return Parent::initializeCache(); } /**jsdoc + * Checks whether the script can write to the local cache. * @function Assets.canWriteCacheValue - * @param {string} url - * @returns {boolean} + * @param {string} url - Not used. + * @returns {boolean} true if the script is an Interface, avatar, or assignment client script, + * false if the script is a client entity or server entity script. + * @example
+ * print("Can write to cache: " + Assets.canWriteCacheValue(null)); */ - Q_INVOKABLE bool canWriteCacheValue(const QUrl& url); /**jsdoc - * @function Assets.getCacheStatus - * @param {} scope - * @param {} [callback=undefined] + * Called when a {@link Assets.getCacheStatus} call is complete. + * @callback Assets~getCacheStatusCallback + * @param {string} error - null if the cache status was retrieved without error, otherwise a description of + * the error. + * @param {Assets.GetCacheStatusResult} status - Details of the current cache status. + */ + /**jsdoc + * Gets the current cache status. + * @function Assets.getCacheStatus + * @param {object|Assets.CallbackDetails|Assets~getCacheStatusCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~getCacheStatusCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example + * Assets.getCacheStatus(function (error, status) { + * print("Cache status"); + * print("- Error: " + error); + * print("- Status: " + JSON.stringify(status)); + * }); */ - Q_INVOKABLE void getCacheStatus(QScriptValue scope, QScriptValue callback = QScriptValue()) { jsPromiseReady(Parent::getCacheStatus(), scope, callback); } /**jsdoc - * @function Assets.queryCacheMeta - * @param {} options - * @param {} scope - * @param {} [callback=undefined] + * Called when {@link Assets.queryCacheMeta} is complete. + * @callback Assets~queryCacheMetaCallback + * @param {string} error - null if the URL has a valid cache entry, otherwise a description of the error. + * @param {Assets.CacheItemMetaData} queryResult - Information on an asset in the cache. + */ + /**jsdoc + * Gets information about the status of an asset in the cache. + * @function Assets.queryCacheMeta + * @param {string|object} path - The URL of the cached asset to get information on. If an object then path.url + * is used. Must start with "atp:" or "cache:". + * @param {object|Assets.CallbackDetails|Assets~queryCacheMetaCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~queryCacheMetaCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example + * Assets.queryCacheMeta( + * "cache:/cacheExample/helloCache.txt", + * function (error, result) { + * if (error) { + * print("Error: " + error); + * } else { + * print("Success:"); + * print("- URL: " + result.url); + * print("- isValid: " + result.isValid); + * print("- saveToDisk: " + result.saveToDisk); + * print("- expirationDate: " + result.expirationDate); + * } + * } + * ); */ - Q_INVOKABLE void queryCacheMeta(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.loadFromCache - * @param {} options - * @param {} scope - * @param {} [callback=undefined] + * Called when an {@link Assets.loadFromCache} call is complete. + * @callback Assets~loadFromCacheCallback + * @param {string} error - null if the cache item was successfully retrieved, otherwise a description of the + * error. + * @param {Assets.LoadFromCacheResult} result - Information on and the retrieved data. + */ + /**jsdoc + * Retrieves data from the cache directly, without downloading it. + * @function Assets.loadFromCache + * @param {string|Assets.LoadFromCacheOptions} options - The URL of the asset to load from the cache if a string, otherwise + * an object specifying the asset to load from the cache and load options. The URL must start with "atp:" + * or "cache:". + * @param {object|Assets.CallbackDetails|Assets~loadFromCacheCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~loadFromCacheCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example + * Assets.loadFromCache( + * "cache:/cacheExample/helloCache.txt", + * function (error, result) { + * if (error) { + * print("Error: " + error); + * } else { + * print("Success:"); + * print("- Response: " + result.response); + * print("- Content type: " + result.contentType); + * print("- Number of bytes: " + result.byteLength); + * print("- Bytes: " + [].slice.call(new Uint8Array(result.data), 0, result.byteLength)); + * print("- URL: " + result.url); + * } + * } + * ); */ - Q_INVOKABLE void loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.saveToCache - * @param {} options - * @param {} scope - * @param {} [callback=undefined] + * Called when an {@link Assets.saveToCache} call is complete. + * @callback Assets~saveToCacheCallback + * @param {string} error - null if the asset data was successfully saved to the cache, otherwise a description + * of the error. + * @param {Assets.SaveToCacheResult} result - Information on the cached data. + */ + /**jsdoc + * Saves asset data to the cache directly, without downloading it from a URL. + *

Note: Can only be used in Interface, avatar, and assignment client scripts.

+ * @function Assets.saveToCache + * @param {Assets.SaveToCacheOptions} options - The data to save to the cache and cache options. + * @param {object|Assets.CallbackDetails|Assets~saveToCacheCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~saveToCacheCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example + * Assets.saveToCache( + * { + * url: "cache:/cacheExample/helloCache.txt", + * data: "Hello cache" + * }, + * function (error, result) { + * if (error) { + * print("Error: " + error); + * } else { + * print("Success:"); + * print("- Bytes: " + result.byteLength); + * print("- URL: " + result.url); + * } + * } + * ); */ - Q_INVOKABLE void saveToCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc + * Saves asset data to the cache directly, without downloading it from a URL. + *

Note: Can only be used in Interface, avatar, and assignment client scripts.

* @function Assets.saveToCache - * @param {} url - * @param {} data - * @param {} metadata - * @param {} scope - * @param {} [callback=undefined] + * @param {string} url - The URL to associate with the cache item. Must start with "atp:" or + * "cache:". + * @param {string|ArrayBuffer} data - The data to save to the cache. + * @param {Assets.SaveToCacheHeaders} headers - The last-modified and expiry times for the cache item. + * @param {object|Assets.CallbackDetails|Assets~saveToCacheCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~saveToCacheCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

*/ - Q_INVOKABLE void saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& metadata, QScriptValue scope, QScriptValue callback = QScriptValue()); protected: From 9457aed631b3dfdefe9deb8b216d151f91fe581e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 4 Sep 2019 15:43:18 +1200 Subject: [PATCH 07/68] Tidying and polishing Assets JSDoc --- libraries/networking/src/AssetClient.cpp | 6 +- .../src/BaseAssetScriptingInterface.h | 4 +- .../src/AssetScriptingInterface.cpp | 114 ++++++- .../src/AssetScriptingInterface.h | 288 ++++++------------ 4 files changed, 202 insertions(+), 210 deletions(-) diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 0ba21e93cd..44f42caec2 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -226,10 +226,10 @@ namespace { /**jsdoc * Last-modified and expiry times for a cache item. * @typedef {object} Assets.SaveToCacheHeaders - * @property {string} [last-modified] - The date and time the cache value was last modified, in the format: - * "ddd, dd MMM yyyy HH:mm:ss". The default value is the current date and time. - * @property {string} [expires] - The date and time the cache value expires, in the format: + * @property {string} [expires] - The date and time the cache value expires, in the format: * "ddd, dd MMM yyyy HH:mm:ss". The default value is an invalid date, representing "never expires". + * @property {string} [last-modified] - The date and time the cache value was last modified, in the format: + * "ddd, dd MMM yyyy HH:mm:ss". The default value is the current date and time. */ /**jsdoc * Information on saving asset data to the cache with {@link Assets.saveToCache}. diff --git a/libraries/networking/src/BaseAssetScriptingInterface.h b/libraries/networking/src/BaseAssetScriptingInterface.h index ac2c211343..7d118e1979 100644 --- a/libraries/networking/src/BaseAssetScriptingInterface.h +++ b/libraries/networking/src/BaseAssetScriptingInterface.h @@ -26,16 +26,16 @@ class BaseAssetScriptingInterface : public QObject { public: /**jsdoc - *

Types of response that {@link Assets.getAsset}, {@link Assets.loadFromCache}, or {@link Assets.decompressData} may + *

Types of response that {@link Assets.decompressData}, {@link Assets.getAsset}, or {@link Assets.loadFromCache} may * provide.

*
ValueDescription
Retrieve a string from the asset server.Store a string in the asset server.Get the hash and URL for a path.Report whether the script can write to the cache.Report the cache status.Report details of a string store in the cache.Retrieve a string from the cache.Save a string in the cache.
* * * * - * * * + * * *
ValueDescription
"text"UTF-8 decoded string value.
"arraybuffer"A binary ArrayBuffer object.
"json"A parsed JSON object.
"text"UTF-8 decoded string value.
* @typedef {string} Assets.ResponseType diff --git a/libraries/script-engine/src/AssetScriptingInterface.cpp b/libraries/script-engine/src/AssetScriptingInterface.cpp index c440aab830..db5776663c 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.cpp +++ b/libraries/script-engine/src/AssetScriptingInterface.cpp @@ -81,6 +81,11 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu setMappingRequest->start(); } +/**jsdoc + * The success or failure of an {@link Assets.downloadData} call. + * @typedef {object} Assets.DownloadDataError + * @property {string} errorMessage - "" if the download was successful, otherwise a description of the error. + */ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) { // FIXME: historically this API method failed silently when given a non-atp prefixed // urlString (or if the AssetRequest failed). @@ -215,6 +220,32 @@ void AssetScriptingInterface::deleteAsset(QScriptValue options, QScriptValue sco jsVerify(false, "TODO: deleteAsset API"); } +/**jsdoc + * Source and download options for {@link Assets.getAsset}. + * @typedef {object} Assets.GetOptions + * @property {boolean} [decompress=false] - true to gunzip decompress the downloaded data. Synonym: + * compressed. + * @property {Assets.ResponseType} [responseType="text"] - The desired result type. + * @property {string} url - The mapped path or hash to download. May have a leading "atp:". + */ +/**jsdoc + * Result value returned by {@link Assets.getAsset}. + * @typedef {object} Assets.GetResult + * @property {number} [byteLength] - The number of bytes in the downloaded content in response. + * @property {boolean} cached - true if the item was retrieved from the cache, false if it was + * downloaded. + * @property {string} [contentType] - The automatically detected MIME type of the content. + * @property {boolean} [decompressed] - true if the content was decompressed, false if it wasn't. + * @property {string} [hash] - The hash for the downloaded asset. + * @property {string} [hashURL] - The ATP URL of the hash file. + * @property {string} [path] - The path for the asset, if a path was requested. Otherwise, undefined. + * @property {string|object|ArrayBuffer} [response] - The downloaded content. + * @property {Assets.ResponseType} [responseType] - The type of the downloaded content in response. + * @property {string} [url] - The URL of the asset requested: the path with leading "atp:" if a path was + * requested, otherwise the requested URL. + * @property {boolean} [wasRedirected] - true if the downloaded data is the baked version of the asset, + * false if it isn't baked. + */ void AssetScriptingInterface::getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { JS_VERIFY(options.isObject() || options.isString(), "expected request options Object or URL as first parameter"); @@ -262,6 +293,22 @@ void AssetScriptingInterface::getAsset(QScriptValue options, QScriptValue scope, } } +/**jsdoc + * Source options for {@link Assets.resolveAsset}. + * @typedef {object} Assets.ResolveOptions + * @property {string} url - The hash or path to resolve. May have a leading "atp:". + */ +/**jsdoc + * Result value returned by {@link Assets.resolveAsset}. + *

Note: If resolving a hash, a file of that hash need not be present on the asset server for the hash to resolve.

+ * @typedef {object} Assets.ResolveResult + * @property {string} [hash] - The hash of the asset. + * @property {string} [hashURL] - The url of the asset's hash file, with leading atp:. + * @property {string} [path] - The path to the asset. + * @property {string} [url] - The URL of the asset. + * @property {boolean} [wasRedirected] - true if the resolved data is for the baked version of the asset, + * false if it isn't. + */ void AssetScriptingInterface::resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { const QString& URL{ "url" }; @@ -274,6 +321,21 @@ void AssetScriptingInterface::resolveAsset(QScriptValue options, QScriptValue sc jsPromiseReady(getAssetInfo(asset), scope, callback); } +/**jsdoc + * Content and decompression options for {@link Assets.decompressData}. + * @typedef {object} Assets.DecompressOptions + * @property {ArrayBuffer} data - The data to decompress. + * @property {Assets.ResponseType} [responseType=text] - The type of decompressed data to return. + */ +/**jsdoc + * Result value returned by {@link Assets.decompressData}. + * @typedef {object} Assets.DecompressResult + * @property {number} [byteLength] - The number of bytes in the decompressed data. + * @property {string} [contentType] - The MIME type of the decompressed data. + * @property {boolean} [decompressed] - true if the data is decompressed. + * @property {string|object|ArrayBuffer} [response] - The decompressed data. + * @property {Assets.ResponseType} [responseType] - The type of the decompressed data in response. + */ void AssetScriptingInterface::decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback) { auto data = options.property("data"); QByteArray dataByteArray = qscriptvalue_cast(data); @@ -298,6 +360,23 @@ namespace { const int32_t DEFAULT_GZIP_COMPRESSION_LEVEL = -1; const int32_t MAX_GZIP_COMPRESSION_LEVEL = 9; } + +/**jsdoc + * Content and compression options for {@link Assets.compressData}. + * @typedef {object} Assets.CompressOptions + * @property {string|ArrayBuffer} data - The data to compress. + * @property {number} level - The compression level, range -19. -1 means + * use the default gzip compression level, 0 means no compression, and 9 means maximum + * compression. + */ +/**jsdoc + * Result value returned by {@link Assets.compressData}. + * @typedef {object} Assets.CompressResult + * @property {number} [byteLength] - The number of bytes in the compressed data. + * @property {boolean} [compressed] - true if the data is compressed. + * @property {string} [contentType] - The MIME type of the compressed data, i.e., "application/gzip". + * @property {ArrayBuffer} [data] - The compressed data. + */ void AssetScriptingInterface::compressData(QScriptValue options, QScriptValue scope, QScriptValue callback) { auto data = options.property("data").isValid() ? options.property("data") : options; QByteArray dataByteArray = data.isString() ? data.toString().toUtf8() : qscriptvalue_cast(data); @@ -306,6 +385,27 @@ void AssetScriptingInterface::compressData(QScriptValue options, QScriptValue sc jsPromiseReady(compressBytes(dataByteArray, level), scope, callback); } +/**jsdoc + * Content and upload options for {@link Assets.putAsset}. + * @typedef {object} Assets.PutOptions + * @property {boolean} [compress=false] - true to gzip compress the content for upload and storage, + * false to upload and store the data without gzip compression. Synonym: compressed. + * @property {string|ArrayBuffer} data - The content to upload. + * @property {string} [path] - A user-friendly path for the file in the asset server. May have a leading + * "atp:". IF not specified, no path-to-hash mapping is set. + *

Note: The asset server destroys any unmapped SHA256-named file at server restart. Either set the mapping path + * with this property or use {@link Assets.setMapping} to set a path-to-hash mapping for the uploaded file.

+ */ +/**jsdoc + * Result value returned by {@link Assets.putAsset}. + * @typedef {object} Assets.PutResult + * @property {number} [byteLength] - The number of bytes in the hash file stored on the asset server. + * @property {boolean} [compressed] - true if the content stored is gzip compressed. + * @property {string} [contentType] - "application/gzip" if the content stored is gzip compressed. + * @property {string} [hash] - The SHA256 hash of the content. + * @property {string} [url] - The atp: URL of the content: using the path if specified, otherwise the hash. + * @property {string} [path] - The uploaded content's mapped path, if specified. + */ void AssetScriptingInterface::putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { auto compress = options.property("compress").toBool() || options.property("compressed").toBool(); auto data = options.isObject() ? options.property("data") : options; @@ -356,6 +456,12 @@ void AssetScriptingInterface::putAsset(QScriptValue options, QScriptValue scope, } } +/**jsdoc + * Source for {@link Assets.queryCacheMeta}. + * @typedef {object} Assets.QueryCacheMetaOptions + * @property {string} url - The URL of the cached asset to get information on. Must start with "atp:" or + * "cache:". + */ void AssetScriptingInterface::queryCacheMeta(QScriptValue options, QScriptValue scope, QScriptValue callback) { QString url = options.isString() ? options.toString() : options.property("url").toString(); JS_VERIFY(QUrl(url).isValid(), QString("Invalid URL '%1'").arg(url)); @@ -365,11 +471,11 @@ void AssetScriptingInterface::queryCacheMeta(QScriptValue options, QScriptValue /**jsdoc * Source and retrieval options for {@link Assets.loadFromCache}. * @typedef {object} Assets.LoadFromCacheOptions - * @property {string} url - The URL of the asset to load from cache. Must start with "atp:" or - * "cache:". - * @property {Assets.ResponseType} [responseType=text] - The desired result type. - * @property {boolean} [decompress=false] - true to gunzip decompress the cached data. Synonym: + * @property {boolean} [decompress=false] - true to gunzip decompress the cached data. Synonym: * compressed. + * @property {Assets.ResponseType} [responseType=text] - The desired result type. + * @property {string} url - The URL of the asset to load from cache. Must start with "atp:" or + * "cache:". */ void AssetScriptingInterface::loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback) { QString url, responseType; diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index 8a0ec27817..c4c7940a80 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -25,9 +25,15 @@ #include /**jsdoc - * The Assets API provides facilities for interacting with the domain's asset server. Assets are stored in the - * asset server in files with SHA256 names. These files are mapped to user-friendly URLs of the format: - * atp:/path/filename. + * The Assets API provides facilities for interacting with the domain's asset server and the client cache. + * cache. + *

Assets are stored in the asset server in files with SHA256 names. These files are mapped to user-friendly URLs of the + * format: atp:/path/filename. The assets may optionally be baked, in which case a request for the original + * unbaked version of the asset is automatically redirected to the baked version. The asset data may optionally be stored as + * compressed.

+ *

The client cache can be access directly, using "atp:" "cache:" URLs. Interface, avatar, and + * assignment client scripts can write to the cache. All script types can read from the cache.

+ * * @namespace Assets * * @hifi-interface @@ -69,6 +75,12 @@ public: */ Q_INVOKABLE void uploadData(QString data, QScriptValue callback); + /**jsdoc + * Called when an {@link Assets.downloadData} call is complete. + * @callback Assets~downloadDataCallback + * @param {string} data - The content that was downloaded. + * @param {Assets.DownloadDataError} error - The success or failure of the download. + */ /**jsdoc * Downloads content from the asset server, form a SHA256-named file. * @function Assets.downloadData @@ -97,19 +109,13 @@ public: * }); * }, 1000); */ - /**jsdoc - * Called when an {@link Assets.downloadData} call is complete. - * @callback Assets~downloadDataCallback - * @param {string} data - The content that was downloaded. - * @param {Assets.DownloadDataError} error - The success or failure of the download. - */ - /**jsdoc - * The success or failure of an {@link Assets.downloadData} call. - * @typedef {object} Assets.DownloadDataError - * @property {string} errorMessage - "" if the download was successful, otherwise a description of the error. - */ Q_INVOKABLE void downloadData(QString url, QScriptValue callback); + /**jsdoc + * Called when an {@link Assets.setMapping} call is complete. + * @callback Assets~setMappingCallback + * @param {string} error - null if the path-to-hash mapping was set, otherwise a description of the error. + */ /**jsdoc * Sets a path-to-hash mapping within the asset server. * @function Assets.setMapping @@ -117,13 +123,14 @@ public: * @param {string} hash - The hash in the asset server. * @param {Assets~setMappingCallback} callback - The function to call upon completion. */ - /**jsdoc - * Called when an {@link Assets.setMapping} call is complete. - * @callback Assets~setMappingCallback - * @param {string} error - null if the path-to-hash mapping was set, otherwise a description of the error. - */ Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback); + /**jsdoc + * Called when an {@link Assets.getMapping} call is complete. + * @callback Assets~getMappingCallback + * @param {string} error - null if the path was found, otherwise a description of the error. + * @param {string} hash - The hash value if the path was found, "" if it wasn't. + */ /**jsdoc * Gets the hash for a path within the asset server. The hash is for the unbaked or baked version of the * asset, according to the asset server setting for the particular path. @@ -140,14 +147,14 @@ public: * }); * } */ - /**jsdoc - * Called when an {@link Assets.getMapping} call is complete. - * @callback Assets~getMappingCallback - * @param {string} error - null if the path was found, otherwise a description of the error. - * @param {string} hash - The hash value if the path was found, "" if it wasn't. - */ Q_INVOKABLE void getMapping(QString path, QScriptValue callback); + /**jsdoc + * Called when an {@link Assets.setBakingEnabled} call is complete. + * @callback Assets~setBakingEnabledCallback + * @param {string} error - null if baking was successfully enabled or disabled, otherwise a description of the + * error. + */ /**jsdoc * Sets whether or not to bake an asset in the asset server. * @function Assets.setBakingEnabled @@ -155,12 +162,6 @@ public: * @param {boolean} enabled - true to enable baking of the asset, false to disable. * @param {Assets~setBakingEnabledCallback} callback - The function to call upon completion. */ - /**jsdoc - * Called when an {@link Assets.setBakingEnabled} call is complete. - * @callback Assets~setBakingEnabledCallback - * @param {string} error - null if baking was successfully enabled or disabled, otherwise a description of the - * error. - */ // Note: Second callback parameter not documented because it's always {}. Q_INVOKABLE void setBakingEnabled(QString path, bool enabled, QScriptValue callback); @@ -177,50 +178,32 @@ public: * @typedef {object} Assets.CallbackDetails * @property {object} scope - The scope that the callback function is defined in. This object is bound to * this when the function is called. - * @property {Assets~putAssetCallback|Assets~getAssetCallback|Assets~resolveAssetCallback|Assets~compressDataCallback - * |Assets~decompressDataCallback} + * @property {Assets~compressDataCallback|Assets~decompressDataCallback|Assets~getAssetCallback + * |Assets~getCacheStatusCallback|Assets~loadFromCacheCallback|Assets~putAssetCallback|Assets~queryCacheMetaCallback + * |Assets~resolveAssetCallback|Assets~saveToCacheCallback} * callback - The function to call upon completion. May be an inline function or a function identifier. If a function * identifier, it must be a member of scope. */ - /**jsdoc - * Source and download options for {@link Assets.getAsset}. - * @typedef {object} Assets.GetOptions - * @property {string} url - The mapped path or hash to download. May have a leading "atp:". - * @property {Assets.ResponseType} [responseType="text"] - The desired result type. - * @property {boolean} [decompress=false] - true to gunzip decompress the downloaded data. Synonym: - * compressed. - */ /**jsdoc * Called when an {@link Assets.getAsset} call is complete. * @callback Assets~getAssetCallback * @param {string} error - null if the content was downloaded, otherwise a description of the error. * @param {Assets.GetResult} result - Information on and the content downloaded. */ - /**jsdoc - * Result value returned by {@link Assets.getAsset}. - * @typedef {object} Assets.GetResult - * @property {number} [byteLength] - The number of bytes in the downloaded response. - * @property {boolean} cached - - * @property {string} [contentType] - Automatically detected MIME type of the content. - * @property {boolean} [decompressed] - true if the content was decompressed, false if it wasn't. - * @property {string} [hash] - The hash for the downloaded asset. - * @property {string} [hashURL] - The ATP URL of the hash file. - * @property {string} [path] - The path for the asset, if a path was requested. Otherwise, undefined. - * @property {string|ArrayBuffer|object} [response] - The downloaded content. - * @property {Assets.ResponseType} [responseType] - The type of the downloaded content. - * @property {string} [url] - The URL of the asset requested: the path with leading "atp:" if a path was - * requested, otherwise the requested URL. - * @property {boolean} [wasRedirected] - true if the downloaded data is the baked version of the asset, - * false if it isn't baked. - */ /**jsdoc * Downloads content from the asset server. * @function Assets.getAsset - * @param {Assets.GetOptions|string} source - What to download and download options. If a string, the mapped path or hash + * @param {string|Assets.GetOptions} source - What to download and download options. If a string, the mapped path or hash * to download, optionally including a leading "atp:". - * @param {Assets.CallbackDetails|Assets~getAssetCallback} callback - The function to call upon completion. May be a - * function identifier or an inline function. + * @param {object|Assets.CallbackDetails|Assets~getAssetCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~getAssetCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

* @example Retrieve a string from the asset server. * Assets.getAsset( * { @@ -236,30 +219,8 @@ public: * } * ); */ - /**jsdoc - * Downloads content from the asset server. - * @function Assets.getAsset - * @param {Assets.GetOptions|string} source - What to download and download options. If a string, the mapped path or hash - * to download, optionally including a leading "atp:". - * @param {object} scope - The scope that the callback function is defined in. This object is bound to - * this when the function is called. - * @param {Assets~getAssetCallback} callback - The function to call upon completion. May be an inline function, a function - * identifier, or the name of a function in a string. If the name of a function or a function identifier, it must be - * a member of scope. - */ Q_INVOKABLE void getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); - /**jsdoc - * Content and upload options for {@link Assets.putAsset}. - * @typedef {object} Assets.PutOptions - * @property {string|ArrayBuffer} data - The content to upload. - * @property {string} [path] - A user-friendly path for the file in the asset server. May have a leading - * "atp:". - *

Note: The asset server destroys any unmapped SHA256-named file at server restart. Either set the mapping path - * with this property or use {@link Assets.setMapping} to set a path-to-hash mapping for the uploaded file.

- * @property {boolean} [compress=false] - true to gzip compress the content for upload and storage, - * false to upload and store the data without gzip compression. Synonym: compressed. - */ /**jsdoc * Called when an {@link Assets.putAsset} call is complete. * @callback Assets~putAssetCallback @@ -267,23 +228,19 @@ public: * description of the error. * @param {Assets.PutResult} result - Information on the content uploaded. */ - /**jsdoc - * Result value returned by {@link Assets.putAsset}. - * @typedef {object} Assets.PutResult - * @property {number} [byteLength] - The number of bytes in the hash file stored on the asset server. - * @property {boolean} [compressed] - true if the content stored is gzip compressed. - * @property {string} [contentType] - "application/gzip" if the content stored is gzip compressed. - * @property {string} [hash] - The SHA256 hash of the content. - * @property {string} [url] - The atp: URL of the content: using the path if specified, otherwise the hash. - * @property {string} [path] - The uploaded content's mapped path, if specified. - */ /**jsdoc * Uploads content to the assert server and sets a path-to-hash mapping. * @function Assets.putAsset - * @param {Assets.PutOptions|string} options - The content to upload and upload options. If a string, the value of the + * @param {string|Assets.PutOptions} options - The content to upload and upload options. If a string, the value of the * string is uploaded but a path-to-hash mapping is not set. - * @param {Assets.CallbackDetails|Assets~putAssetCallback} callback - The function to call upon completion. May be an - * inline function or a function identifier. + * @param {object|Assets.CallbackDetails|Assets~putAssetCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~putAssetCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

* @example Store a string in the asset server. * Assets.putAsset( * { @@ -299,17 +256,6 @@ public: * } * ); */ - /**jsdoc - * Uploads content to the assert server and sets a path-to-hash mapping. - * @function Assets.putAsset - * @param {Assets.PutOptions|string} options - The content to upload and upload options. If a string, the value of the - * string is uploaded but a path-to-hash mapping is not set. - * @param {object} scope - The scope that the callback function is defined in. This object is bound to - * this when the function is called. - * @param {Assets~getAssetCallback} callback - The function to call upon completion. May be an inline function, a function - * identifier, or the name of a function in a string. If the name of a function or a function identifier, it must be - * a member of scope. - */ Q_INVOKABLE void putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc @@ -329,35 +275,25 @@ public: */ Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); - /**jsdoc - * Source options for {@link Assets.resolveAsset}. - * @typedef {object} Assets.ResolveOptions - * @property {string} url - The hash or path to resolve. May have a leading "atp:". - */ /**jsdoc * Called when an {@link Assets.resolveAsset} call is complete. * @callback Assets~resolveAssetCallback * @param {string} error - null if the asset hash or path was resolved, otherwise a description of the error. * @param {Assets.ResolveResult} result - Information on the hash or path resolved. */ - /**jsdoc - * Result value returned by {@link Assets.resolveAsset}. - *

Note: If resolving a hash, a file of that hash need not be present on the asset server for the hash to resolve.

- * @typedef {object} Assets.ResolveResult - * @property {string} [hash] - The hash of the asset. - * @property {string} [hashURL] - The url of the asset's hash file, with leading atp:. - * @property {string} [path] - The path to the asset. - * @property {string} [url] - The URL of the asset. - * @property {boolean} [wasRedirected] - true if the resolved data is for the baked version of the asset, - * false if it isn't. - */ /**jsdoc * Resolves and returns information on a hash or a path in the asset server. * @function Assets.resolveAsset * @param {string|Assets.ResolveOptions} source - The hash or path to resolve if a string, otherwise an object specifying * what to resolve. If a string, it may have a leading "atp:". - * @param {Assets.CallbackDetails|Assets~resolveAssetCallback} callback - The function to call upon completion. May be a - * function identifier or an inline function. + * @param {object|Assets.CallbackDetails|Assets~resolveAssetCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~resolveAssetCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

* @example Get the hash and URL for a path. * Assets.resolveAsset( * "/assetsExamples/helloWorld.txt", @@ -371,111 +307,60 @@ public: * } * ); */ - /**jsdoc - * Resolves and returns information on a hash or a path in the asset server. - * @function Assets.resolveAsset - * @param {string|Assets.ResolveOptions} source - The hash or path to resolve if a string, otherwise an object specifying - * what to resolve. If a string, it may have a leading "atp:". - * @param {object} scope - The scope that the callback function is defined in. This object is bound to - * this when the function is called. - * @param {Assets~resolveAssetCallback} callback - The function to call upon completion. May be an inline function, a - * function identifier, or the name of a function in a string. If the name of a function or a function identifier, it - * must be a member of scope. - */ Q_INVOKABLE void resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); - /**jsdoc - * Content and decompression options for {@link Assets.decompressData}. - * @typedef {object} Assets.DecompressOptions - * @property {ArrayBuffer} data - The data to decompress. - * @property {Assets.ResponseType} [responseType=text] - The type of data to return. - */ /**jsdoc * Called when an {@link Assets.decompressData} call is complete. * @callback Assets~decompressDataCallback * @param {string} error - null if the data was successfully compressed, otherwise a description of the error. * @param {Assets.DecompressResult} result - Information on and the decompressed data. */ - /**jsdoc - * Result value returned by {@link Assets.decompressData}. - * @typedef {object} Assets.DecompressResult - * @property {number} [byteLength] - The number of bytes in the decompressed data. - * @property {string} [contentType] - The MIME type of the decompressed data. - * @property {boolean} [decompressed] - true if the data is decompressed. - * @property {string|ArrayBuffer|object} [response] - The decompressed data. - * @property {Assets.ResponseType} [responseType] - The type of the response. - */ /**jsdoc * Decompresses data in memory using gunzip. * @function Assets.decompressData * @param {Assets.DecompressOptions} source - What to decompress and decompression options. - * @param {Assets.CallbackDetails|Assets~decompressDataCallback} callback - The function to call upon completion. May be a - * function identifier or an inline function. - */ - /**jsdoc - * Decompresses data in memory using gunzip. - * @function Assets.decompressData - * @param {Assets.DecompressOptions} source - What to decompress and decompression options. - * @param {object} scope - The scope that the callback function is defined in. This object is bound to - * this when the function is called. - * @param {Assets~decompressDataCallback} callback - The function to call upon completion. May be an inline function, a - * function identifier, or the name of a function in a string. If the name of a function or a function identifier, it - * must be a member of scope. + * @param {object|Assets.CallbackDetails|Assets~decompressDataCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~decompressDataCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

*/ Q_INVOKABLE void decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); - /**jsdoc - * Content and compression options for {@link Assets.compressData}. - * @typedef {object} Assets.CompressOptions - * @property {ArrayBuffer|string} data - The data to compress. - * @property {number} level - The compression level, range -19. -1 means - * use the default gzip compression level, 0 means no compression, and 9 means maximum - * compression. - */ /**jsdoc * Called when an {@link Assets.compressData} call is complete. * @callback Assets~compressDataCallback * @param {string} error - null if the data was successfully compressed, otherwise a description of the error. * @param {Assets.CompressResult} result - Information on and the compressed data. */ - /**jsdoc - * Result value returned by {@link Assets.compressData}. - * @typedef {object} Assets.CompressResult - * @property {boolean} [compressed] - true if the data is compressed. - * @property {number} [byteLength] - The number of bytes in the compressed data. - * @property {string} [contentType] - The MIME type of the compressed data, i.e., "application/gzip". - * @property {ArrayBuffer} [data] - The compressed data. - */ /**jsdoc * Compresses data in memory using gzip. * @function Assets.compressData - * @param {Assets.CompressOptions|ArrayBuffer|string} source - What to compress and compression options. If an ArrayBuffer - * or a string, the data to compress. - * @param {Assets.CallbackDetails|Assets~compressDataCallback} callback - The function to call upon completion. May be a - * function identifier or an inline function. - */ - /**jsdoc - * Compresses data in memory using gzip. - * @function Assets.compressData - * @param {Assets.CompressOptions|ArrayBuffer|string} source - What to compress and compression options. If an ArrayBuffer - * or a string, the data to compress. - * @param {object} scope - The scope that the callback function is defined in. This object is bound to - * this when the function is called. - * @param {Assets~compressDataCallback} callback - The function to call upon completion. May be an inline function, a - * function identifier, or the name of a function in a string. If the name of a function or a function identifier, it - * must be a member of scope. + * @param {string|ArrayBuffer|Assets.CompressOptions} source - What to compress and compression options. If a string or + * ArrayBuffer, the data to compress. + * @param {object|Assets.CallbackDetails|Assets~compressDataCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~compressDataCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

*/ Q_INVOKABLE void compressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * Initializes the local cache if it isn't already initialized. + * Initializes the cache if it isn't already initialized. * @function Assets.initializeCache - * @returns {boolean} true if the local cache is initialized, false if it isn't. + * @returns {boolean} true if the cache is initialized, false if it isn't. */ Q_INVOKABLE bool initializeCache() { return Parent::initializeCache(); } /**jsdoc - * Checks whether the script can write to the local cache. + * Checks whether the script can write to the cache. * @function Assets.canWriteCacheValue * @param {string} url - Not used. * @returns {boolean} true if the script is an Interface, avatar, or assignment client script, @@ -490,7 +375,7 @@ public: * @callback Assets~getCacheStatusCallback * @param {string} error - null if the cache status was retrieved without error, otherwise a description of * the error. - * @param {Assets.GetCacheStatusResult} status - Details of the current cache status. + * @param {Assets.GetCacheStatusResult} result - Details of the current cache status. */ /**jsdoc * Gets the current cache status. @@ -518,13 +403,14 @@ public: * Called when {@link Assets.queryCacheMeta} is complete. * @callback Assets~queryCacheMetaCallback * @param {string} error - null if the URL has a valid cache entry, otherwise a description of the error. - * @param {Assets.CacheItemMetaData} queryResult - Information on an asset in the cache. + * @param {Assets.CacheItemMetaData} result - Information on an asset in the cache. */ /**jsdoc * Gets information about the status of an asset in the cache. * @function Assets.queryCacheMeta - * @param {string|object} path - The URL of the cached asset to get information on. If an object then path.url - * is used. Must start with "atp:" or "cache:". + * @param {string|Assets.QueryCacheMetaOptions} path - The URL of the cached asset to get information on if a string, + * otherwise an object specifying the cached asset to get information on. The URL must start with "atp:" + * or "cache:". * @param {object|Assets.CallbackDetails|Assets~queryCacheMetaCallback} scopeOrCallback - If an object, then the scope that * the callback function is defined in. This object is bound to this when the function is * called. From 06ebba554e0e6fee5f6ba84be6756c36f42116e1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 5 Sep 2019 12:23:12 +1200 Subject: [PATCH 08/68] Doc review --- libraries/script-engine/src/AssetScriptingInterface.h | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index c4c7940a80..325e177964 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -26,12 +26,11 @@ /**jsdoc * The Assets API provides facilities for interacting with the domain's asset server and the client cache. - * cache. *

Assets are stored in the asset server in files with SHA256 names. These files are mapped to user-friendly URLs of the * format: atp:/path/filename. The assets may optionally be baked, in which case a request for the original * unbaked version of the asset is automatically redirected to the baked version. The asset data may optionally be stored as * compressed.

- *

The client cache can be access directly, using "atp:" "cache:" URLs. Interface, avatar, and + *

The client cache can be access directly, using "atp:" or "cache:" URLs. Interface, avatar, and * assignment client scripts can write to the cache. All script types can read from the cache.

* * @namespace Assets @@ -82,7 +81,7 @@ public: * @param {Assets.DownloadDataError} error - The success or failure of the download. */ /**jsdoc - * Downloads content from the asset server, form a SHA256-named file. + * Downloads content from the asset server, from a SHA256-named file. * @function Assets.downloadData * @param {string} url - The raw URL of asset to download: atp: followed by the assets's SHA256 hash. * @param {Assets~downloadDataCallback} callback - The function to call upon completion. @@ -93,7 +92,7 @@ public: * Assets.uploadData("Hello world!", function (url, hash) { * assetURL = url; * print("url: " + assetURL); // atp:a0g89... - * Assets.setMapping("/assetsExamples/hellowWorld.txt", hash, function (error) { + * Assets.setMapping("/assetsExamples/helloWorld.txt", hash, function (error) { * if (error) { * print("ERROR: Could not set mapping!"); * return; @@ -229,7 +228,7 @@ public: * @param {Assets.PutResult} result - Information on the content uploaded. */ /**jsdoc - * Uploads content to the assert server and sets a path-to-hash mapping. + * Uploads content to the asset server and sets a path-to-hash mapping. * @function Assets.putAsset * @param {string|Assets.PutOptions} options - The content to upload and upload options. If a string, the value of the * string is uploaded but a path-to-hash mapping is not set. From 49ab5042268569215c7431d5ded45a91ece263c1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 5 Sep 2019 12:23:56 +1200 Subject: [PATCH 09/68] Make placement of callback JSDoc consistent --- .../script-engine/src/AssetScriptingInterface.h | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index 325e177964..5da3c51a08 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -47,6 +47,13 @@ public: using Parent = BaseAssetScriptingInterface; AssetScriptingInterface(QObject* parent = nullptr); + /**jsdoc + * Called when an {@link Assets.uploadData} call is complete. + * @callback Assets~uploadDataCallback + * @param {string} url - The raw URL of the file that the content is stored in, with atp: as the scheme and + * the SHA256 hash as the filename (with no extension). + * @param {string} hash - The SHA256 hash of the content. + */ /**jsdoc * Uploads content to the asset server, storing it in a SHA256-named file. *

Note: The asset server destroys any unmapped SHA256-named file at server restart. Use {@link Assets.setMapping} to @@ -65,13 +72,6 @@ public: * }); * }); */ - /**jsdoc - * Called when an {@link Assets.uploadData} call is complete. - * @callback Assets~uploadDataCallback - * @param {string} url - The raw URL of the file that the content is stored in, with atp: as the scheme and - * the SHA256 hash as the filename (with no extension). - * @param {string} hash - The SHA256 hash of the content. - */ Q_INVOKABLE void uploadData(QString data, QScriptValue callback); /**jsdoc From 5aef2d674a4d02cccf5c495926d3dbc86f392155 Mon Sep 17 00:00:00 2001 From: ingerjm0 Date: Thu, 5 Sep 2019 10:12:59 -0700 Subject: [PATCH 10/68] DOC-69: Move Supported Script Types section to top of JSDoc pages --- tools/jsdoc/plugins/hifi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index 67dafe5a16..07549530ce 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -125,7 +125,7 @@ exports.handlers = { if (rows.length > 0) { var availableIn = "

Supported Script Types: " + rows.join(" • ") + "

"; - e.doclet.description = (e.doclet.description ? e.doclet.description : "") + availableIn; + e.doclet.description = availableIn + (e.doclet.description ? e.doclet.description : ""); } } From 5ebf141d7374e77dbfb5cb297ecfa7da111ee127 Mon Sep 17 00:00:00 2001 From: ingerjm0 Date: Thu, 5 Sep 2019 10:39:18 -0700 Subject: [PATCH 11/68] DOC-67: Incorrect location of example code for Avatar API --- .../hifi-jsdoc-template/tmpl/container.tmpl | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl index 2a5e863d6d..7dabba0809 100644 --- a/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl @@ -92,35 +92,29 @@ - - - - -

Example 1? 's':'' ?>

- - +

Description

- -

Classes

- -

- - - -

Example 1? 's':'' ?>

+ +

Classes

+ +

+ + + + @@ -245,7 +239,7 @@ - + Date: Thu, 5 Sep 2019 11:08:25 -0700 Subject: [PATCH 12/68] DOC-156: Fix font size issues on non-paragraph text --- tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css b/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css index a33cff15e4..fabdfa35d0 100644 --- a/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css +++ b/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css @@ -38,6 +38,7 @@ body font-weight: 400; color: #000000; letter-spacing: 0.5px; + font-size: 0.95rem; } p { From af7e68786a5ad8583fcdba81e01e7faa4968fcee Mon Sep 17 00:00:00 2001 From: ingerjm0 Date: Thu, 5 Sep 2019 11:29:13 -0700 Subject: [PATCH 13/68] DOC-156 (comment): Fixed spacing on non-paragraph text --- tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css b/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css index fabdfa35d0..391f1c6da4 100644 --- a/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css +++ b/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css @@ -23,7 +23,7 @@ ********************************************************************/ * { - box-sizing: border-box + box-sizing: border-box; } html @@ -39,6 +39,7 @@ body color: #000000; letter-spacing: 0.5px; font-size: 0.95rem; + line-height: 20px; } p { @@ -423,7 +424,6 @@ header { display: block; text-align: center; font-size: 90%; - margin-top: -20px; } .variation { From d8c655a151d1fb07a54a776f6205cd320838fe8c Mon Sep 17 00:00:00 2001 From: ingerjm0 Date: Thu, 5 Sep 2019 11:42:34 -0700 Subject: [PATCH 14/68] Fixed spacing issues in navigation caused by fix to to DOC-156 --- tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css b/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css index 391f1c6da4..c8deaf4b6d 100644 --- a/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css +++ b/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css @@ -44,7 +44,6 @@ body p { font-size: 0.95rem; - line-height: 20px; } section @@ -201,6 +200,7 @@ nav { padding-left: 20px; padding-right: 20px; box-sizing: border-box; + line-height: 12px; } nav::-webkit-scrollbar { From b8b7dbf6819f5e4a2d9c17abc46b94c08242743d Mon Sep 17 00:00:00 2001 From: ingerjm0 Date: Thu, 5 Sep 2019 13:39:06 -0700 Subject: [PATCH 15/68] DOC-148: Increase size of monospace type --- tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css b/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css index c8deaf4b6d..2386f88586 100644 --- a/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css +++ b/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css @@ -129,7 +129,6 @@ table { thead { border-color: #d8e1d9; background:#d8e1d9; - text-align: left; } table tr { @@ -147,6 +146,7 @@ td { article table thead tr th, article table tbody tr td, article table tbody tr td p { font-size: .89rem; line-height: 20px; + text-align: left; } article table thead tr th, article table tbody tr td { @@ -380,12 +380,12 @@ nav > h2 > a { tt, code, kbd, samp { font-family: Consolas, Monaco, 'Andale Mono', monospace; - font-size: 0.9rem; + font-size: 1.05em; } .name, .signature { font-family: Consolas, Monaco, 'Andale Mono', monospace; - font-size: 0.9rem; + font-size: 1.05em; } img { @@ -538,7 +538,7 @@ header { .prettyprint code { - font-size: 0.7rem; + font-size: 0.9em; line-height: 18px; display: block; padding: 4px 12px; From 55bdc7d2a61db8f9fdc24beb6b2b4c46cdd2ce3a Mon Sep 17 00:00:00 2001 From: ingerjm0 Date: Thu, 5 Sep 2019 13:54:55 -0700 Subject: [PATCH 16/68] Made sure ctrlaltdavid's changes made it into my repo --- tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl index 7dabba0809..f9c6c42924 100644 --- a/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl @@ -94,6 +94,10 @@ + +

Example 1? 's':'' ?>

+ +

Description

@@ -184,7 +188,7 @@

Members

- + From 2322a9ea7f42e264f54af732685eed8b784a7f26 Mon Sep 17 00:00:00 2001 From: ingerjm0 Date: Thu, 5 Sep 2019 13:58:59 -0700 Subject: [PATCH 17/68] Making sure ctrlaltdavid's changes made it into my repository --- tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl index f9c6c42924..acbab36d8d 100644 --- a/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl @@ -188,7 +188,7 @@

Members

- + @@ -243,7 +243,7 @@ - + Date: Mon, 9 Sep 2019 17:32:27 -0700 Subject: [PATCH 18/68] Give better error message when draco part of model baking fails --- libraries/baking/src/ModelBaker.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 9d6a368e1c..46da29cf00 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -437,7 +437,7 @@ void ModelBaker::abort() { bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList) { if (dracoMeshBytes.isEmpty()) { - handleError("Failed to finalize the baking of a draco Geometry node"); + handleError("Failed to finalize the baking of a draco Geometry node from model " + _modelURL.toString()); return false; } From 384b7fd6292927cba8f4f4fb712335d8a1aaebf6 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 10 Sep 2019 12:21:36 -0700 Subject: [PATCH 19/68] Fix FBXBaker failing when an unbaked mesh is empty Error early for all model bakers for draco errors --- libraries/baking/src/ModelBaker.cpp | 10 ++++++++-- libraries/baking/src/OBJBaker.h | 2 +- libraries/hfm/src/hfm/HFM.h | 2 +- libraries/model-baker/src/model-baker/Baker.cpp | 13 +++++++++---- libraries/model-baker/src/model-baker/Baker.h | 1 + .../src/model-baker/BuildDracoMeshTask.cpp | 17 +++++++++++------ .../src/model-baker/BuildDracoMeshTask.h | 2 +- 7 files changed, 32 insertions(+), 15 deletions(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 46da29cf00..adf6deeb56 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -246,6 +246,13 @@ void ModelBaker::bakeSourceCopy() { // Begin hfm baking baker.run(); + for (auto error : baker.getDracoErrors()) { + if (error) { + handleError("Failed to finalize the baking of a draco Geometry node from model " + _modelURL.toString()); + return; + } + } + _hfmModel = baker.getHFMModel(); _materialMapping = baker.getMaterialMapping(); dracoMeshes = baker.getDracoMeshes(); @@ -437,8 +444,7 @@ void ModelBaker::abort() { bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList) { if (dracoMeshBytes.isEmpty()) { - handleError("Failed to finalize the baking of a draco Geometry node from model " + _modelURL.toString()); - return false; + handleWarning("Empty mesh detected in model: '" + _modelURL.toString() + "'. It will be included in the baked output."); } FBXNode dracoNode; diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h index 9d0fe53e3c..55adec5786 100644 --- a/libraries/baking/src/OBJBaker.h +++ b/libraries/baking/src/OBJBaker.h @@ -28,7 +28,7 @@ protected: private: void createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& hfmModel, const hifi::ByteArray& dracoMesh); - void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel); + void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel); NodeID nextNodeID() { return _nodeID++; } NodeID _nodeID { 0 }; diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h index 484a10aa3b..888e562bca 100644 --- a/libraries/hfm/src/hfm/HFM.h +++ b/libraries/hfm/src/hfm/HFM.h @@ -56,7 +56,7 @@ const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048; using ShapeVertices = std::vector; // The version of the Draco mesh binary data itself. See also: FBX_DRACO_MESH_VERSION in FBX.h -static const int DRACO_MESH_VERSION = 2; +static const int DRACO_MESH_VERSION = 3; static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000; static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES; diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index c896613df5..47a8db82b8 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -120,7 +120,7 @@ namespace baker { class BakerEngineBuilder { public: using Input = VaryingSet3; - using Output = VaryingSet4, std::vector>>; + using Output = VaryingSet5, std::vector, std::vector>>; using JobModel = Task::ModelIO; void build(JobModel& model, const Varying& input, Varying& output) { const auto& hfmModelIn = input.getN(0); @@ -168,7 +168,8 @@ namespace baker { const auto buildDracoMeshInputs = BuildDracoMeshTask::Input(meshesIn, normalsPerMesh, tangentsPerMesh).asVarying(); const auto buildDracoMeshOutputs = model.addJob("BuildDracoMesh", buildDracoMeshInputs); const auto dracoMeshes = buildDracoMeshOutputs.getN(0); - const auto materialList = buildDracoMeshOutputs.getN(1); + const auto dracoErrors = buildDracoMeshOutputs.getN(1); + const auto materialList = buildDracoMeshOutputs.getN(2); // Parse flow data const auto flowData = model.addJob("ParseFlowData", mapping); @@ -181,7 +182,7 @@ namespace baker { const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices, flowData).asVarying(); const auto hfmModelOut = model.addJob("BuildModel", buildModelInputs); - output = Output(hfmModelOut, materialMapping, dracoMeshes, materialList); + output = Output(hfmModelOut, materialMapping, dracoMeshes, dracoErrors, materialList); } }; @@ -212,7 +213,11 @@ namespace baker { return _engine->getOutput().get().get2(); } - std::vector> Baker::getDracoMaterialLists() const { + std::vector Baker::getDracoErrors() const { return _engine->getOutput().get().get3(); } + + std::vector> Baker::getDracoMaterialLists() const { + return _engine->getOutput().get().get4(); + } }; diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 6f74cb646e..9780484fa4 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -33,6 +33,7 @@ namespace baker { hfm::Model::Pointer getHFMModel() const; MaterialMapping getMaterialMapping() const; const std::vector& getDracoMeshes() const; + std::vector getDracoErrors() const; // This is a ByteArray and not a std::string because the character sequence can contain the null character (particularly for FBX materials) std::vector> getDracoMaterialLists() const; diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp index 25a45cefe5..180664a52a 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -51,7 +51,7 @@ std::vector createMaterialList(const hfm::Mesh& mesh) { return materialList; } -std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::vector& normals, const std::vector& tangents, const std::vector& materialList) { +std::tuple, bool> createDracoMesh(const hfm::Mesh& mesh, const std::vector& normals, const std::vector& tangents, const std::vector& materialList) { Q_ASSERT(normals.size() == 0 || (int)normals.size() == mesh.vertices.size()); Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); @@ -68,7 +68,7 @@ std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::v } if (numTriangles == 0) { - return std::unique_ptr(); + return std::make_tuple(std::unique_ptr(), false); } draco::TriangleSoupMeshBuilder meshBuilder; @@ -184,7 +184,7 @@ std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::v if (!dracoMesh) { qCWarning(model_baker) << "Failed to finalize the baking of a draco Geometry node"; - return std::unique_ptr(); + return std::make_tuple(std::unique_ptr(), true); } // we need to modify unique attribute IDs for custom attributes @@ -201,7 +201,7 @@ std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::v dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX); } - return dracoMesh; + return std::make_tuple(std::move(dracoMesh), false); } #endif // not Q_OS_ANDROID @@ -218,20 +218,25 @@ void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Inp const auto& normalsPerMesh = input.get1(); const auto& tangentsPerMesh = input.get2(); auto& dracoBytesPerMesh = output.edit0(); - auto& materialLists = output.edit1(); + auto& dracoErrorsPerMesh = output.edit1(); + auto& materialLists = output.edit2(); dracoBytesPerMesh.reserve(meshes.size()); + dracoErrorsPerMesh.reserve(meshes.size()); materialLists.reserve(meshes.size()); for (size_t i = 0; i < meshes.size(); i++) { const auto& mesh = meshes[i]; const auto& normals = baker::safeGet(normalsPerMesh, i); const auto& tangents = baker::safeGet(tangentsPerMesh, i); dracoBytesPerMesh.emplace_back(); + dracoErrorsPerMesh.emplace_back(); auto& dracoBytes = dracoBytesPerMesh.back(); + auto& dracoError = dracoErrorsPerMesh.back(); materialLists.push_back(createMaterialList(mesh)); const auto& materialList = materialLists.back(); - auto dracoMesh = createDracoMesh(mesh, normals, tangents, materialList); + std::unique_ptr dracoMesh; + std::tie(dracoMesh, dracoError) = createDracoMesh(mesh, normals, tangents, materialList); if (dracoMesh) { draco::Encoder encoder; diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h index 0e33be3c41..ac9ad648ab 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h @@ -34,7 +34,7 @@ class BuildDracoMeshTask { public: using Config = BuildDracoMeshConfig; using Input = baker::VaryingSet3, baker::NormalsPerMesh, baker::TangentsPerMesh>; - using Output = baker::VaryingSet2, std::vector>>; + using Output = baker::VaryingSet3, std::vector, std::vector>>; using JobModel = baker::Job::ModelIO; void configure(const Config& config); From 65d85138ccb1477ebadc3f17b89b8b1abddd2668 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 10 Sep 2019 16:31:30 -0700 Subject: [PATCH 20/68] TIL that std::vector is a bit field https://howardhinnant.github.io/onvectorbool.html --- libraries/baking/src/ModelBaker.cpp | 9 ++++----- .../model-baker/src/model-baker/BuildDracoMeshTask.cpp | 8 +++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index adf6deeb56..70290fe283 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -246,11 +246,10 @@ void ModelBaker::bakeSourceCopy() { // Begin hfm baking baker.run(); - for (auto error : baker.getDracoErrors()) { - if (error) { - handleError("Failed to finalize the baking of a draco Geometry node from model " + _modelURL.toString()); - return; - } + const auto& errors = baker.getDracoErrors(); + if (std::find(errors.cbegin(), errors.cend(), true) != errors.cend()) { + handleError("Failed to finalize the baking of a draco Geometry node from model " + _modelURL.toString()); + return; } _hfmModel = baker.getHFMModel(); diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp index 180664a52a..9fff570cc0 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -222,21 +222,23 @@ void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Inp auto& materialLists = output.edit2(); dracoBytesPerMesh.reserve(meshes.size()); - dracoErrorsPerMesh.reserve(meshes.size()); + // vector is an exception to the std::vector conventions as it is a bit field + // So a bool reference to an element doesn't work + dracoErrorsPerMesh.resize(meshes.size()); materialLists.reserve(meshes.size()); for (size_t i = 0; i < meshes.size(); i++) { const auto& mesh = meshes[i]; const auto& normals = baker::safeGet(normalsPerMesh, i); const auto& tangents = baker::safeGet(tangentsPerMesh, i); dracoBytesPerMesh.emplace_back(); - dracoErrorsPerMesh.emplace_back(); auto& dracoBytes = dracoBytesPerMesh.back(); - auto& dracoError = dracoErrorsPerMesh.back(); materialLists.push_back(createMaterialList(mesh)); const auto& materialList = materialLists.back(); + bool dracoError; std::unique_ptr dracoMesh; std::tie(dracoMesh, dracoError) = createDracoMesh(mesh, normals, tangents, materialList); + dracoErrorsPerMesh[dracoErrorsPerMesh.size()-1] = dracoError; if (dracoMesh) { draco::Encoder encoder; From b969a9b1e02689ea692d587529462a9f0f6ee972 Mon Sep 17 00:00:00 2001 From: dante ruiz Date: Tue, 10 Sep 2019 16:50:50 -0700 Subject: [PATCH 21/68] fix tablet html loading errors --- .../ui/src/ui/TabletScriptingInterface.cpp | 60 ++++++++++++++++++- .../ui/src/ui/TabletScriptingInterface.h | 4 ++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index c54f63690d..6c57314367 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -795,11 +795,25 @@ void TabletProxy::loadWebScreenOnTop(const QVariant& url) { } void TabletProxy::loadWebScreenOnTop(const QVariant& url, const QString& injectJavaScriptUrl) { + bool localSafeContext = hifi::scripting::isLocalAccessSafeThread(); if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "loadWebScreenOnTop", Q_ARG(QVariant, url), Q_ARG(QString, injectJavaScriptUrl)); + QMetaObject::invokeMethod(this, "loadHTMLSourceImpl", Q_ARG(QVariant, url), Q_ARG(QString, injectJavaScriptUrl), Q_ARG(bool, localSafeContext)); return; } + loadHTMLSourceImpl(url, injectJavaScriptUrl, localSafeContext); +} + + + +void TabletProxy::loadHTMLSourceImpl(const QVariant& url, const QString& injectJavaScriptUrl, bool localSafeContext) { + if (QThread::currentThread() != thread()) { + qCWarning(uiLogging) << __FUNCTION__ << "may not be called directly by scripts"; + return; + + } + + QObject* root = nullptr; if (!_toolbarMode && _qmlTabletRoot) { root = _qmlTabletRoot; @@ -808,22 +822,59 @@ void TabletProxy::loadWebScreenOnTop(const QVariant& url, const QString& injectJ } if (root) { + if (localSafeContext) { + hifi::scripting::setLocalAccessSafeThread(true); + } QMetaObject::invokeMethod(root, "loadQMLOnTop", Q_ARG(const QVariant&, QVariant(WEB_VIEW_SOURCE_URL))); QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); if (_toolbarMode && _desktopWindow) { QMetaObject::invokeMethod(root, "setResizable", Q_ARG(const QVariant&, QVariant(false))); } QMetaObject::invokeMethod(root, "loadWebOnTop", Q_ARG(const QVariant&, QVariant(url)), Q_ARG(const QVariant&, QVariant(injectJavaScriptUrl))); + hifi::scripting::setLocalAccessSafeThread(false); } _state = State::Web; + /*QObject* root = nullptr; + if (!_toolbarMode && _qmlTabletRoot) { + root = _qmlTabletRoot; + } else if (_toolbarMode && _desktopWindow) { + root = _desktopWindow->asQuickItem(); + } + + if (root) { + // BUGZ-1398: tablet access to local HTML files from client scripts + // Here we TEMPORARILY mark the main thread as allowed to load local file content, + // because the thread that originally made the call is so marked. + if (localSafeContext) { + hifi::scripting::setLocalAccessSafeThread(true); + } + QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, path)); + hifi::scripting::setLocalAccessSafeThread(false); + _state = State::QML; + _currentPathLoaded = path; + QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); + if (_toolbarMode && _desktopWindow) { + QMetaObject::invokeMethod(root, "setResizable", Q_ARG(const QVariant&, QVariant(resizable))); + } + + } else { + qCDebug(uiLogging) << "tablet cannot load QML because _qmlTabletRoot is null"; + }*/ } void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaScriptUrl, bool loadOtherBase) { + bool localSafeContext = hifi::scripting::isLocalAccessSafeThread(); if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "gotoWebScreen", Q_ARG(QString, url), Q_ARG(QString, injectedJavaScriptUrl), Q_ARG(bool, loadOtherBase)); + QMetaObject::invokeMethod(this, "loadHTMLSourceImpl", Q_ARG(QString, url), Q_ARG(QString, injectedJavaScriptUrl), Q_ARG(bool, loadOtherBase), Q_ARG(bool, localSafeContext)); return; } + + loadHTMLSourceImpl(url, injectedJavaScriptUrl, loadOtherBase, localSafeContext); +} + +void TabletProxy::loadHTMLSourceImpl(const QString& url, const QString& injectedJavaScriptUrl, bool loadOtherBase, bool localSafeContext) { + QObject* root = nullptr; if (!_toolbarMode && _qmlTabletRoot) { root = _qmlTabletRoot; @@ -832,6 +883,9 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS } if (root) { + if (localSafeContext) { + hifi::scripting::setLocalAccessSafeThread(true); + } if (loadOtherBase) { QMetaObject::invokeMethod(root, "loadTabletWebBase", Q_ARG(const QVariant&, QVariant(url)), Q_ARG(const QVariant&, QVariant(injectedJavaScriptUrl))); } else { @@ -841,6 +895,8 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS if (_toolbarMode && _desktopWindow) { QMetaObject::invokeMethod(root, "setResizable", Q_ARG(const QVariant&, QVariant(false))); } + + hifi::scripting::setLocalAccessSafeThread(false); _state = State::Web; _currentPathLoaded = QVariant(url); } else { diff --git a/libraries/ui/src/ui/TabletScriptingInterface.h b/libraries/ui/src/ui/TabletScriptingInterface.h index ba02ba25b0..9a5ff9efac 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.h +++ b/libraries/ui/src/ui/TabletScriptingInterface.h @@ -298,6 +298,10 @@ public: */ Q_INVOKABLE void loadQMLSourceImpl(const QVariant& path, bool resizable, bool localSafeContext); + Q_INVOKABLE void loadHTMLSourceImpl(const QVariant& url, const QString& injectJavaScriptUrl, bool localSafeContext); + + Q_INVOKABLE void loadHTMLSourceImpl(const QString& url, const QString& injectedJavaScriptUrl, bool loadOtherBase, bool localSafeContext); + // FIXME: This currently relies on a script initializing the tablet (hence the bool denoting success); // it should be initialized internally so it cannot fail From a0ad1f3a68173f012e5f0b4e70a0d3266cde8c7c Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 10 Sep 2019 17:25:35 -0700 Subject: [PATCH 22/68] DEV-444 - OAuth administration improvements --- .../resources/describe-settings.json | 110 +++++++++++- .../resources/web/js/base-settings.js | 23 ++- .../resources/web/js/domain-server.js | 1 + .../resources/web/settings/js/settings.js | 119 +++++++++++- domain-server/src/DomainServer.cpp | 59 +++--- domain-server/src/DomainServer.h | 1 + .../src/DomainServerSettingsManager.cpp | 170 +++++++++++++++++- 7 files changed, 446 insertions(+), 37 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 9cb4c2cab9..b854955953 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 2.3, + "version": 2.4, "settings": [ { "name": "metaverse", @@ -1705,6 +1705,114 @@ } ] }, + { + "name": "oauth", + "label": "OAuth", + "show_on_enable": true, + "settings": [ + { + "name": "enable", + "type": "checkbox", + "default": false, + "hidden": true + }, + { + "name": "admin-users", + "label": "Admin Users", + "type": "table", + "can_add_new_rows": true, + "help": "Any of these users can administer the domain.", + "numbered": false, + "backup": false, + "advanced": false, + "columns": [ + { + "name": "username", + "label": "Username", + "can_set": true + } + ] + }, + { + "name": "admin-roles", + "label": "Admin Roles", + "type": "table", + "can_add_new_rows": true, + "help": "Any user with any of these metaverse roles can administer the domain.", + "numbered": false, + "backup": false, + "advanced": true, + "columns": [ + { + "name": "role", + "label": "Role", + "can_set": true + } + ] + }, + { + "name": "client-id", + "label": "Client ID", + "help": "OAuth client ID.", + "default": "", + "advanced": true, + "backup": false + }, + { + "name": "client-secret", + "label": "Client Secret", + "help": "OAuth client secret.", + "type": "password", + "password_placeholder": "******", + "value-hidden": true, + "advanced": true, + "backup": false + }, + { + "name": "provider", + "label": "Provider", + "help": "OAuth provider URL.", + "default": "https://metaverse.highfidelity.com", + "advanced": true, + "backup": false + }, + { + "name": "hostname", + "label": "Hostname", + "help": "OAuth hostname.", + "default": "", + "advanced": true, + "backup": false + }, + { + "name": "key-passphrase", + "label": "SSL Private Key Passphrase", + "help": "SSL Private Key Passphrase", + "type": "password", + "password_placeholder": "******", + "value-hidden": true, + "advanced": true, + "backup": false + }, + { + "name": "cert-fingerprint", + "type": "hidden", + "readonly": true, + "advanced": true, + "backup": false + }, + { + "name": "cert", + "advanced": true, + "backup": false + }, + { + "name": "key", + "advanced": true, + "backup": false + } + ] + }, { "name": "automatic_content_archives", "label": "Automatic Content Archives", diff --git a/domain-server/resources/web/js/base-settings.js b/domain-server/resources/web/js/base-settings.js index bd96f636a8..295013878c 100644 --- a/domain-server/resources/web/js/base-settings.js +++ b/domain-server/resources/web/js/base-settings.js @@ -2,6 +2,9 @@ var DomainInfo = null; var viewHelpers = { getFormGroup: function(keypath, setting, values, isAdvanced) { + if (setting.hidden) { + return ""; + } form_group = "
" } - - form_group += "" + setting.help + "" + if (setting.help) { + form_group += "" + setting.help + "" + } } } @@ -114,12 +118,17 @@ function reloadSettings(callback) { data.descriptions.push(Settings.extraGroupsAtEnd[endGroupIndex]); } + data.descriptions = data.descriptions.map(function(x) { + x.hidden = x.hidden || (x.show_on_enable && data.values[x.name] && !data.values[x.name].enable); + return x; + }); + $('#panels').html(Settings.panelsTemplate(data)); Settings.data = data; Settings.initialValues = form2js('settings-form', ".", false, cleanupFormValues, true); - Settings.afterReloadActions(); + Settings.afterReloadActions(data); // setup any bootstrap switches $('.toggle-checkbox').bootstrapSwitch(); @@ -129,10 +138,14 @@ function reloadSettings(callback) { Settings.pendingChanges = 0; // call the callback now that settings are loaded - callback(true); + if (callback) { + callback(true); + } }).fail(function() { // call the failure object since settings load faild - callback(false) + if (callback) { + callback(false); + } }); } diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index a8b7267b88..9524b18caf 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -91,6 +91,7 @@ $(document).ready(function(){ // make a JSON request to get the dropdown menus for content and settings // we don't error handle here because the top level menu is still clickable and usables if this fails $.getJSON('/settings-menu-groups.json', function(data){ + function makeGroupDropdownElement(group, base) { var html_id = group.html_id ? group.html_id : group.name; return "
  • " + group.label + "
  • "; diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 08d0550841..fcf7700687 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -18,7 +18,19 @@ $(document).ready(function(){ Settings.extraGroupsAtIndex = Settings.extraDomainGroupsAtIndex; var METAVERSE_URL = URLs.METAVERSE_URL; - Settings.afterReloadActions = function() { + var SSL_PRIVATE_KEY_FILE_ID = 'ssl-private-key-file'; + var SSL_PRIVATE_KEY_CONTENTS_ID = 'key-contents'; + var SSL_PRIVATE_KEY_CONTENTS_NAME = 'oauth.key-contents'; + var SSL_CERT_UPLOAD_ID = 'ssl-cert-button'; + var SSL_CERT_FILE_ID = 'ssl-cert-file'; + var SSL_CERT_FINGERPRINT_ID = 'cert-fingerprint'; + var SSL_CERT_FINGERPRINT_SPAN_ID = 'cert-fingerprint-span-id'; + var SSL_CERT_CONTENTS_ID = 'cert-contents'; + var SSL_CERT_CONTENTS_NAME = 'oauth.cert-contents'; + var SSL_PRIVATE_KEY_PATH = 'oauth.key'; + var SSL_CERT_PATH = 'oauth.cert'; + + Settings.afterReloadActions = function(data) { getMetaverseUrl(function(metaverse_url) { METAVERSE_URL = metaverse_url; @@ -32,6 +44,8 @@ $(document).ready(function(){ setupDomainNetworkingSettings(); // setupDomainLabelSetting(); + setupSettingsOAuth(data); + setupSettingsBackup(); if (domainIDIsSet()) { @@ -124,6 +138,48 @@ $(document).ready(function(){ } } + if (formJSON["oauth"]) { + var private_key = formJSON["oauth"]["key-contents"]; + var cert = formJSON["oauth"]["cert-contents"]; + var oauthErrors = ""; + if (private_key != undefined) { + var pattern = /-+BEGIN PRIVATE KEY-+[A-Za-z0-9+/\n=]*-+END PRIVATE KEY-+/m; + if (!pattern.test(private_key)) { + oauthErrors = "Private key must be in PEM format"; + } + } + if (cert != undefined) { + var pattern = /-+BEGIN CERTIFICATE-+[A-Za-z0-9+/\n=]*-+END CERTIFICATE-+/m; + if (!pattern.test(cert)) { + oauthErrors = "Certificate must be in PEM format"; + } + } + if ($('#oauth.panel').length) { + if (!$('input[name="oauth.client-id"]').val()) { + oauthErrors += "OAuth requires a client Id.
    "; + } + if (!$('input[name="oauth.provider"]').val()) { + oauthErrors += "OAuth requires a provider.
    "; + } + if (!$('input[name="oauth.hostname"]').val()) { + oauthErrors += "OAuth requires a hostname.
    "; + } + if (!$('input[name="' + SSL_PRIVATE_KEY_PATH + '"]').val() && !$('input[name="' + SSL_PRIVATE_KEY_CONTENTS_NAME + '"]').val()) { + oauthErrors += "OAuth requires an SSL Private Key.
    "; + } + if (!$('input[name="' + SSL_CERT_PATH + '"]').val() && !$('input[name="' + SSL_CERT_CONTENTS_NAME + '"]').val()) { + oauthErrors += "OAuth requires an SSL Certificate.
    "; + } + if (!$("table[name='oauth.admin-users'] tr.value-row").length && + !$("table[name='oauth.admin-roles'] tr.value-row").length) { + oauthErrors += "OAuth must have at least one admin user or admin role.
    "; + } + } + if (oauthErrors) { + bootbox.alert({ "message": oauthErrors, "title": "OAuth Configuration Error" }); + return false; + } + } postSettings(formJSON); }; @@ -1035,6 +1091,67 @@ $(document).ready(function(){ }); } + function setupSettingsOAuth(data) { + // construct the HTML needed for the settings backup panel + var html = "
    "; + html += "
    "; + html += ""; + html += ""; + html += ""; + html += "
    "; + html += "
    "; + html += ""; + html += "
    Fingerprint:" + data.values.oauth["cert-fingerprint"] + "
    "; + html += ""; + html += ""; + html += ""; + html += "
    "; + + $('#oauth-advanced').append(html); + + $('#key-path-label').after($('[data-keypath="' + SSL_PRIVATE_KEY_PATH + '"]')); + $('#cert-path-label').after($('[data-keypath="' + SSL_CERT_PATH + '"]')); + $('[name="' + SSL_PRIVATE_KEY_PATH + '"]').val(data.values.oauth.key); + $('[name="' + SSL_CERT_PATH + '"]').val(data.values.oauth.cert); + + $('body').on('change input propertychange', '#' + SSL_PRIVATE_KEY_FILE_ID, function(e){ + var f = e.target.files[0]; + var reader = new FileReader(); + reader.onload = function(e) { + $('#' + SSL_PRIVATE_KEY_CONTENTS_ID).val(reader.result); + $('#' + SSL_PRIVATE_KEY_CONTENTS_ID).attr('data-changed', true); + $('[name="' + SSL_PRIVATE_KEY_PATH + '"]').val(''); + badgeForDifferences($('#' + SSL_PRIVATE_KEY_CONTENTS_ID)); + } + reader.readAsText(f); + }); + $('body').on('change input propertychange', '#' + SSL_CERT_FILE_ID, function(e){ + var f = e.target.files[0]; + var reader = new FileReader(); + reader.onload = function(e) { + $('#' + SSL_CERT_CONTENTS_ID).val(reader.result); + $('#' + SSL_CERT_CONTENTS_ID).attr('data-changed', true); + $('[name="' + SSL_CERT_PATH + '"]').val(''); + $('#' + SSL_CERT_FINGERPRINT_SPAN_ID).text(''); + badgeForDifferences($('#' + SSL_CERT_CONTENTS_ID)); + } + reader.readAsText(f); + }); + + $('body').on('change input propertychange', '[name="' + SSL_PRIVATE_KEY_PATH + '"]', function(e){ + $('#' + SSL_PRIVATE_KEY_FILE_ID).val(''); + $('#' + SSL_PRIVATE_KEY_CONTENTS_ID).val(''); + badgeForDifferences($('[name="' + SSL_PRIVATE_KEY_PATH + '"]').attr('data-changed', true)); + }); + + $('body').on('change input propertychange', '[name="' + SSL_CERT_PATH + '"]', function(e){ + $('#' + SSL_CERT_FILE_ID).val(''); + $('#' + SSL_CERT_CONTENTS_ID).val(''); + $('#' + SSL_CERT_FINGERPRINT_SPAN_ID).text(''); + badgeForDifferences($('[name="' + SSL_CERT_PATH + '"]').attr('data-changed', true)); + }); + } + var RESTORE_SETTINGS_UPLOAD_ID = 'restore-settings-button'; var RESTORE_SETTINGS_FILE_ID = 'restore-settings-file'; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 74ad014b53..7f6c366bc3 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -226,9 +226,10 @@ DomainServer::DomainServer(int argc, char* argv[]) : setupGroupCacheRefresh(); - // if we were given a certificate/private key or oauth credentials they must succeed - if (!(optionallyReadX509KeyAndCertificate() && optionallySetupOAuth())) { - return; + optionallySetupOAuth(); + + if (_oauthEnable) { + _oauthEnable = optionallyReadX509KeyAndCertificate(); } _settingsManager.apiRefreshGroupInformation(); @@ -447,8 +448,9 @@ QUuid DomainServer::getID() { } bool DomainServer::optionallyReadX509KeyAndCertificate() { - const QString X509_CERTIFICATE_OPTION = "cert"; - const QString X509_PRIVATE_KEY_OPTION = "key"; + const QString X509_CERTIFICATE_OPTION = "oauth.cert"; + const QString X509_PRIVATE_KEY_OPTION = "oauth.key"; + const QString X509_PRIVATE_KEY_PASSPHRASE_OPTION = "oauth.key-passphrase"; const QString X509_KEY_PASSPHRASE_ENV = "DOMAIN_SERVER_KEY_PASSPHRASE"; QString certPath = _settingsManager.valueForKeyPath(X509_CERTIFICATE_OPTION).toString(); @@ -459,7 +461,12 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { // this is used for Oauth callbacks when authorizing users against a data server // let's make sure we can load the key and certificate - QString keyPassphraseString = QProcessEnvironment::systemEnvironment().value(X509_KEY_PASSPHRASE_ENV); + QString keyPassphraseEnv = QProcessEnvironment::systemEnvironment().value(X509_KEY_PASSPHRASE_ENV); + QString keyPassphraseString = _settingsManager.valueForKeyPath(X509_PRIVATE_KEY_PASSPHRASE_OPTION).toString(); + + if (!keyPassphraseEnv.isEmpty()) { + keyPassphraseString = keyPassphraseEnv; + } qDebug() << "Reading certificate file at" << certPath << "for HTTPS."; qDebug() << "Reading key file at" << keyPath << "for HTTPS."; @@ -473,16 +480,15 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { QSslCertificate sslCertificate(&certFile); QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, keyPassphraseString.toUtf8()); + if (privateKey.isNull()) { + qCritical() << "SSL Private Key Not Loading. Bad password or key format?"; + } + _httpsManager.reset(new HTTPSManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTPS_PORT, sslCertificate, privateKey, QString(), this)); qDebug() << "TCP server listening for HTTPS connections on" << DOMAIN_SERVER_HTTPS_PORT; } else if (!certPath.isEmpty() || !keyPath.isEmpty()) { - static const QString MISSING_CERT_ERROR_MSG = "Missing certificate or private key. domain-server will now quit."; - static const int MISSING_CERT_ERROR_CODE = 3; - - QMetaObject::invokeMethod(this, "queuedQuit", Qt::QueuedConnection, - Q_ARG(QString, MISSING_CERT_ERROR_MSG), Q_ARG(int, MISSING_CERT_ERROR_CODE)); return false; } @@ -490,10 +496,12 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { } bool DomainServer::optionallySetupOAuth() { - const QString OAUTH_PROVIDER_URL_OPTION = "oauth-provider"; - const QString OAUTH_CLIENT_ID_OPTION = "oauth-client-id"; + const QString OAUTH_ENABLE_OPTION = "oauth.enable"; + const QString OAUTH_PROVIDER_URL_OPTION = "oauth.provider"; + const QString OAUTH_CLIENT_ID_OPTION = "oauth.client-id"; const QString OAUTH_CLIENT_SECRET_ENV = "DOMAIN_SERVER_CLIENT_SECRET"; - const QString REDIRECT_HOSTNAME_OPTION = "hostname"; + const QString OAUTH_CLIENT_SECRET_OPTION = "oauth.client-secret"; + const QString REDIRECT_HOSTNAME_OPTION = "oauth.hostname"; _oauthProviderURL = QUrl(_settingsManager.valueForKeyPath(OAUTH_PROVIDER_URL_OPTION).toString()); @@ -502,22 +510,24 @@ bool DomainServer::optionallySetupOAuth() { _oauthProviderURL = NetworkingConstants::METAVERSE_SERVER_URL(); } + _oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV); + if (_oauthClientSecret.isEmpty()) { + _oauthClientSecret = _settingsManager.valueForKeyPath(OAUTH_CLIENT_SECRET_OPTION).toString(); + } auto accountManager = DependencyManager::get(); accountManager->setAuthURL(_oauthProviderURL); _oauthClientID = _settingsManager.valueForKeyPath(OAUTH_CLIENT_ID_OPTION).toString(); - _oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV); _hostname = _settingsManager.valueForKeyPath(REDIRECT_HOSTNAME_OPTION).toString(); - if (!_oauthClientID.isEmpty()) { + _oauthEnable = _settingsManager.valueForKeyPath(OAUTH_ENABLE_OPTION).toBool(); + + if (_oauthEnable) { if (_oauthProviderURL.isEmpty() || _hostname.isEmpty() || _oauthClientID.isEmpty() || _oauthClientSecret.isEmpty()) { - static const QString MISSING_OAUTH_INFO_MSG = "Missing OAuth provider URL, hostname, client ID, or client secret. domain-server will now quit."; - static const int MISSING_OAUTH_INFO_ERROR_CODE = 4; - QMetaObject::invokeMethod(this, "queuedQuit", Qt::QueuedConnection, - Q_ARG(QString, MISSING_OAUTH_INFO_MSG), Q_ARG(int, MISSING_OAUTH_INFO_ERROR_CODE)); + _oauthEnable = false; return false; } else { qDebug() << "OAuth will be used to identify clients using provider at" << _oauthProviderURL.toString(); @@ -2693,8 +2703,8 @@ void DomainServer::profileRequestFinished() { std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* connection) { static const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie"; - static const QString ADMIN_USERS_CONFIG_KEY = "admin-users"; - static const QString ADMIN_ROLES_CONFIG_KEY = "admin-roles"; + static const QString ADMIN_USERS_CONFIG_KEY = "oauth.admin-users"; + static const QString ADMIN_ROLES_CONFIG_KEY = "oauth.admin-roles"; static const QString BASIC_AUTH_USERNAME_KEY_PATH = "security.http_username"; static const QString BASIC_AUTH_PASSWORD_KEY_PATH = "security.http_password"; const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)"; @@ -2704,12 +2714,11 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c QVariant adminUsersVariant = _settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY); QVariant adminRolesVariant = _settingsManager.valueForKeyPath(ADMIN_ROLES_CONFIG_KEY); - if (!_oauthProviderURL.isEmpty() - && (adminUsersVariant.isValid() || adminRolesVariant.isValid())) { + if (_oauthEnable) { QString cookieString = connection->requestHeader(HTTP_COOKIE_HEADER_KEY); QRegExp cookieUUIDRegex(COOKIE_UUID_REGEX_STRING); - + QUuid cookieUUID; if (cookieString.indexOf(cookieUUIDRegex) != -1) { cookieUUID = cookieUUIDRegex.cap(1); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 02362abd7b..5e8eee53fe 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -236,6 +236,7 @@ private: bool _isUsingDTLS { false }; + bool _oauthEnable { false }; QUrl _oauthProviderURL; QString _oauthClientID; QString _oauthClientSecret; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 17d473f02c..2e6ccf8be2 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -22,7 +22,9 @@ #include #include #include +#include #include +#include #include #include @@ -46,10 +48,14 @@ const QString DESCRIPTION_SETTINGS_KEY = "settings"; const QString SETTING_DEFAULT_KEY = "default"; const QString DESCRIPTION_NAME_KEY = "name"; const QString DESCRIPTION_GROUP_LABEL_KEY = "label"; +const QString DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY = "show_on_enable"; +const QString DESCRIPTION_ENABLE_KEY = "enable"; const QString DESCRIPTION_BACKUP_FLAG_KEY = "backup"; const QString SETTING_DESCRIPTION_TYPE_KEY = "type"; const QString DESCRIPTION_COLUMNS_KEY = "columns"; const QString CONTENT_SETTING_FLAG_KEY = "content_setting"; +static const QString SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY = "domain_settings"; +static const QString SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY = "content_settings"; const QString SETTINGS_VIEWPOINT_KEY = "viewpoint"; @@ -136,6 +142,10 @@ void DomainServerSettingsManager::splitSettingsDescription() { settingsDropdownGroup[DESCRIPTION_GROUP_LABEL_KEY] = groupObject[DESCRIPTION_GROUP_LABEL_KEY]; + if (groupObject.contains(DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY)) { + settingsDropdownGroup[DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY] = groupObject[DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY]; + } + static const QString DESCRIPTION_GROUP_HTML_ID_KEY = "html_id"; if (groupObject.contains(DESCRIPTION_GROUP_HTML_ID_KEY)) { settingsDropdownGroup[DESCRIPTION_GROUP_HTML_ID_KEY] = groupObject[DESCRIPTION_GROUP_HTML_ID_KEY]; @@ -170,9 +180,6 @@ void DomainServerSettingsManager::splitSettingsDescription() { // populate the settings menu groups with what we've collected - static const QString SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY = "domain_settings"; - static const QString SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY = "content_settings"; - _settingsMenuGroups[SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY] = domainSettingsMenuGroups; _settingsMenuGroups[SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY] = contentSettingsMenuGroups; } @@ -448,6 +455,77 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena packPermissions(); } + if (oldVersion < 2.4) { + // migrate oauth settings to their own group + const QString ADMIN_USERS = "admin-users"; + const QString OAUTH_ADMIN_USERS = "oauth.admin-users"; + const QString OAUTH_CLIENT_ID = "oauth.client-id"; + const QString ALT_ADMIN_USERS = "admin.users"; + const QString ADMIN_ROLES = "admin-roles"; + const QString OAUTH_ADMIN_ROLES = "oauth.admin-roles"; + const QString OAUTH_ENABLE = "oauth.enable"; + + QVector > conversionMap = { + {"key", "oauth.key"}, + {"cert", "oauth.cert"}, + {"hostname", "oauth.hostname"}, + {"oauth-client-id", "oauth.client-id"}, + {"oauth-provider", "oauth.provider"} + }; + + for (auto & conversion : conversionMap) { + QVariant* prevValue = _configMap.valueForKeyPath(conversion.first); + if (prevValue) { + auto newValue = _configMap.valueForKeyPath(conversion.second, true); + *newValue = *prevValue; + } + } + + QVariant* client_id = _configMap.valueForKeyPath(OAUTH_CLIENT_ID); + if (client_id) { + QVariant* oauthEnable = _configMap.valueForKeyPath(OAUTH_ENABLE, true); + + *oauthEnable = QVariant(true); + } + + QVariant* oldAdminUsers = _configMap.valueForKeyPath(ADMIN_USERS); + QVariant* newAdminUsers = _configMap.valueForKeyPath(OAUTH_ADMIN_USERS, true); + QVariantList adminUsers(newAdminUsers->toList()); + if (oldAdminUsers) { + QStringList adminUsersList = oldAdminUsers->toStringList(); + for (auto & user : adminUsersList) { + if (!adminUsers.contains(user)) { + adminUsers.append(user); + } + } + } + QVariant* altAdminUsers = _configMap.valueForKeyPath(ALT_ADMIN_USERS); + if (altAdminUsers) { + QStringList adminUsersList = altAdminUsers->toStringList(); + for (auto & user : adminUsersList) { + if (!adminUsers.contains(user)) { + adminUsers.append(user); + } + } + } + + *newAdminUsers = adminUsers; + + QVariant* oldAdminRoles = _configMap.valueForKeyPath(ADMIN_ROLES); + QVariant* newAdminRoles = _configMap.valueForKeyPath(OAUTH_ADMIN_ROLES, true); + QVariantList adminRoles(newAdminRoles->toList()); + if (oldAdminRoles) { + QStringList adminRoleList = oldAdminRoles->toStringList(); + for (auto & role : adminRoleList) { + if (!adminRoles.contains(role)) { + adminRoles.append(role); + } + } + } + + *newAdminRoles = adminRoles; + } + // write the current description version to our settings *versionVariant = _descriptionVersion; @@ -1185,7 +1263,23 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection return true; } else if (url.path() == SETTINGS_MENU_GROUPS_PATH) { - connection->respond(HTTPConnection::StatusCode200, QJsonDocument(_settingsMenuGroups).toJson(), "application/json"); + + QJsonObject settings; + for (auto & key : _settingsMenuGroups.keys()) { + const QJsonArray& settingGroups = _settingsMenuGroups[key].toArray(); + QJsonArray groups; + foreach (const QJsonValue& group, settingGroups) { + QJsonObject groupObject = group.toObject(); + if (!groupObject.contains(DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY) + || (groupObject[DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY].toBool() + && _configMap.valueForKeyPath(groupObject[DESCRIPTION_NAME_KEY].toString() + "." + DESCRIPTION_ENABLE_KEY)->toBool() )) { + groups.append(groupObject); + } + } + settings[key] = groups; + } + + connection->respond(HTTPConnection::StatusCode200, QJsonDocument(settings).toJson(), "application/json"); return true; } else if (url.path() == SETTINGS_BACKUP_PATH) { @@ -1440,12 +1534,35 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt } if (!groupKey.isEmpty() && !groupResponseObject.isEmpty()) { + // set this group's object to the constructed object responseObject[groupKey] = groupResponseObject; } } } + // add 'derived' values used primarily for UI + + const QString X509_CERTIFICATE_OPTION = "oauth.cert"; + + QString certPath = valueForKeyPath(X509_CERTIFICATE_OPTION).toString(); + if (!certPath.isEmpty()) { + // the user wants to use the following cert and key for HTTPS + // this is used for Oauth callbacks when authorizing users against a data server + // let's make sure we can load the key and certificate + + qDebug() << "Reading certificate file at" << certPath << "for HTTPS."; + + QFile certFile(certPath); + certFile.open(QIODevice::ReadOnly); + + QSslCertificate sslCertificate(&certFile); + QString digest = sslCertificate.digest().toHex(':'); + auto groupObject = responseObject["oauth"].toObject(); + groupObject["cert-fingerprint"] = digest; + responseObject["oauth"] = groupObject; + } + return responseObject; } @@ -1551,23 +1668,66 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson return QJsonObject(); } -bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, +bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedSettingsObject, SettingsType settingsType) { // take a write lock since we're about to overwrite settings in the config map QWriteLocker locker(&_settingsLock); + QJsonObject postedObject(postedSettingsObject); + static const QString SECURITY_ROOT_KEY = "security"; static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist"; static const QString BROADCASTING_KEY = "broadcasting"; static const QString WIZARD_KEY = "wizard"; static const QString DESCRIPTION_ROOT_KEY = "descriptors"; + static const QString OAUTH_ROOT_KEY = "oauth"; + static const QString OAUTH_KEY_CONTENTS = "key-contents"; + static const QString OAUTH_CERT_CONTENTS = "cert-contents"; + static const QString OAUTH_CERT_PATH = "cert"; + static const QString OAUTH_KEY_PASSPHRASE = "key-passphrase"; + static const QString OAUTH_KEY_PATH = "key"; auto& settingsVariant = _configMap.getConfig(); bool needRestart = false; auto& filteredDescriptionArray = settingsType == DomainSettings ? _domainSettingsDescription : _contentSettingsDescription; + auto oauthObject = postedObject[OAUTH_ROOT_KEY].toObject(); + if (oauthObject.contains(OAUTH_CERT_CONTENTS)) { + QSslCertificate cert(oauthObject[OAUTH_CERT_CONTENTS].toString().toUtf8()); + if (!cert.isNull()) { + static const QString CERT_FILE_NAME = "certificate.crt"; + auto certPath = PathUtils::getAppDataFilePath(CERT_FILE_NAME); + QFile file(certPath); + if (file.open(QFile::WriteOnly)) { + file.write(cert.toPem()); + file.close(); + } + oauthObject[OAUTH_CERT_PATH] = certPath; + } + oauthObject.remove(OAUTH_CERT_CONTENTS); + } + if (oauthObject.contains(OAUTH_KEY_CONTENTS)) { + QString keyPassphraseString = oauthObject[OAUTH_KEY_PASSPHRASE].toString(); + QSslKey key(oauthObject[OAUTH_KEY_CONTENTS].toString().toUtf8(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, keyPassphraseString.toUtf8()); + if (!key.isNull()) { + static const QString KEY_FILE_NAME = "certificate.key"; + auto keyPath = PathUtils::getAppDataFilePath(KEY_FILE_NAME); + QFile file(keyPath); + if (file.open(QFile::WriteOnly)) { + file.write(key.toPem()); + file.close(); + file.setPermissions(QFile::ReadOwner | QFile::WriteOwner); + } + oauthObject[OAUTH_KEY_PATH] = keyPath; + } + oauthObject.remove(OAUTH_KEY_CONTENTS); + } + + postedObject[OAUTH_ROOT_KEY] = oauthObject; + + qDebug() << postedObject; // Iterate on the setting groups foreach(const QString& rootKey, postedObject.keys()) { const QJsonValue& rootValue = postedObject[rootKey]; From e9eb07d3886606586c6bc0a3a528706a1635b6e9 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 11 Sep 2019 10:20:59 -0700 Subject: [PATCH 23/68] BUGZ-1339: Disable the Emote Indicator animation to prevent it from being 'jittery' --- .../simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml b/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml index 787ccadd62..bbd1d4d735 100644 --- a/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml +++ b/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml @@ -45,7 +45,8 @@ Rectangle { } Behavior on requestedWidth { - enabled: true + enabled: false // Set this to `true` once we have a different windowing system that better supports on-screen widgets + // like the Emote Indicator. SmoothedAnimation { duration: 220 } } From 5ec8b0d589e3a383ca7d83c4f5819091643c043c Mon Sep 17 00:00:00 2001 From: milad Date: Wed, 11 Sep 2019 11:39:19 -0700 Subject: [PATCH 24/68] added environment variable check for crash menu --- interface/src/Menu.cpp | 66 +++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 8c6292681b..7c260e16b9 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -725,45 +725,51 @@ Menu::Menu() { DependencyManager::get().data(), SLOT(setForceCoarsePicking(bool))); // Developer > Crash >>> - MenuWrapper* crashMenu = developerMenu->addMenu("Crash"); + bool result = false; + const QString HIFI_SHOW_SOURCE_DEVELOPER_CRASH_MENU("HIFI_SHOW_SOURCE_DEVELOPER_CRASH_MENU"); + result = QProcessEnvironment::systemEnvironment().contains(HIFI_SHOW_SOURCE_DEVELOPER_CRASH_MENU); + if (result) { + MenuWrapper* crashMenu = developerMenu->addMenu("Crash"); - // Developer > Crash > Display Crash Options - addCheckableActionToQMenuAndActionHash(crashMenu, MenuOption::DisplayCrashOptions, 0, true); + // Developer > Crash > Display Crash Options + addCheckableActionToQMenuAndActionHash(crashMenu, MenuOption::DisplayCrashOptions, 0, true); - addActionToQMenuAndActionHash(crashMenu, MenuOption::DeadlockInterface, 0, qApp, SLOT(deadlockApplication())); - addActionToQMenuAndActionHash(crashMenu, MenuOption::UnresponsiveInterface, 0, qApp, SLOT(unresponsiveApplication())); + addActionToQMenuAndActionHash(crashMenu, MenuOption::DeadlockInterface, 0, qApp, SLOT(deadlockApplication())); + addActionToQMenuAndActionHash(crashMenu, MenuOption::UnresponsiveInterface, 0, qApp, SLOT(unresponsiveApplication())); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunction); - connect(action, &QAction::triggered, qApp, []() { crash::pureVirtualCall(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunctionThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::pureVirtualCall).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunction); + connect(action, &QAction::triggered, qApp, []() { crash::pureVirtualCall(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunctionThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::pureVirtualCall).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFree); - connect(action, &QAction::triggered, qApp, []() { crash::doubleFree(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFreeThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doubleFree).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFree); + connect(action, &QAction::triggered, qApp, []() { crash::doubleFree(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFreeThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doubleFree).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbort); - connect(action, &QAction::triggered, qApp, []() { crash::doAbort(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbortThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doAbort).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbort); + connect(action, &QAction::triggered, qApp, []() { crash::doAbort(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbortThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doAbort).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereference); - connect(action, &QAction::triggered, qApp, []() { crash::nullDeref(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereferenceThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::nullDeref).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereference); + connect(action, &QAction::triggered, qApp, []() { crash::nullDeref(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereferenceThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::nullDeref).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccess); - connect(action, &QAction::triggered, qApp, []() { crash::outOfBoundsVectorCrash(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccessThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::outOfBoundsVectorCrash).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccess); + connect(action, &QAction::triggered, qApp, []() { crash::outOfBoundsVectorCrash(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccessThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::outOfBoundsVectorCrash).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFault); - connect(action, &QAction::triggered, qApp, []() { crash::newFault(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFaultThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::newFault).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFault); + connect(action, &QAction::triggered, qApp, []() { crash::newFault(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFaultThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::newFault).join(); }); - addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOnShutdown, 0, qApp, SLOT(crashOnShutdown())); + addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOnShutdown, 0, qApp, SLOT(crashOnShutdown())); + } + // Developer > Show Statistics addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Stats, 0, true); From f9c5d00efa8ad1dfdc6eb3ee6d15044486b8ceeb Mon Sep 17 00:00:00 2001 From: milad Date: Wed, 11 Sep 2019 12:10:47 -0700 Subject: [PATCH 25/68] remove source from variable name --- interface/src/Menu.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 7c260e16b9..193de2792d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -726,8 +726,8 @@ Menu::Menu() { // Developer > Crash >>> bool result = false; - const QString HIFI_SHOW_SOURCE_DEVELOPER_CRASH_MENU("HIFI_SHOW_SOURCE_DEVELOPER_CRASH_MENU"); - result = QProcessEnvironment::systemEnvironment().contains(HIFI_SHOW_SOURCE_DEVELOPER_CRASH_MENU); + const QString HIFI_SHOW_DEVELOPER_CRASH_MENU("HIFI_SHOW_DEVELOPER_CRASH_MENU"); + result = QProcessEnvironment::systemEnvironment().contains(HIFI_SHOW_DEVELOPER_CRASH_MENU); if (result) { MenuWrapper* crashMenu = developerMenu->addMenu("Crash"); From 3cf56210b239b15dce26e8e69a0a8fa3c3b478c5 Mon Sep 17 00:00:00 2001 From: milad Date: Wed, 11 Sep 2019 12:30:24 -0700 Subject: [PATCH 26/68] modified Audio.qml to handle desktop audio level meter visible --- interface/resources/qml/hifi/audio/Audio.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index d3bf6aca0a..dbfd46ded1 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -266,6 +266,7 @@ Rectangle { labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.warnWhenMuted; + visible: bar.currentIndex === 0 ? false : true; onClicked: { AudioScriptingInterface.warnWhenMuted = checked; checked = Qt.binding(function() { return AudioScriptingInterface.warnWhenMuted; }); // restore binding @@ -277,8 +278,8 @@ Rectangle { id: audioLevelSwitch height: root.switchHeight; switchWidth: root.switchWidth; - anchors.top: warnMutedSwitch.bottom - anchors.topMargin: 24 + anchors.top: bar.currentIndex === 0 ? parent.top : warnMutedSwitch.bottom + anchors.topMargin: bar.currentIndex === 0 ? 0 : 24 anchors.left: parent.left labelTextOn: qsTr("Audio Level Meter"); labelTextSize: 16; From 275350c2fa7f7ba1de0ab07e866f16eb72d905b1 Mon Sep 17 00:00:00 2001 From: milad Date: Wed, 11 Sep 2019 12:38:29 -0700 Subject: [PATCH 27/68] removed sound from the out --- .../simplifiedUI/simplifiedEmote/emojiApp/simplifiedEmoji.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/simplifiedUI/simplifiedEmote/emojiApp/simplifiedEmoji.js b/scripts/simplifiedUI/simplifiedEmote/emojiApp/simplifiedEmoji.js index 1b16843b09..4363f5d899 100644 --- a/scripts/simplifiedUI/simplifiedEmote/emojiApp/simplifiedEmoji.js +++ b/scripts/simplifiedUI/simplifiedEmote/emojiApp/simplifiedEmoji.js @@ -326,9 +326,7 @@ function playPopAnimation() { if (popType === "in") { currentPopScale = MIN_POP_SCALE; } else { - // Start with the pop sound on the out currentPopScale = finalInPopScale ? finalInPopScale : MAX_POP_SCALE; - playSound(emojiDestroySound, DEFAULT_VOLUME, MyAvatar.position, true); } } From d95a803324b687bb1800dd6d31e4c39c67190b36 Mon Sep 17 00:00:00 2001 From: MiladNazeri Date: Wed, 11 Sep 2019 12:44:59 -0700 Subject: [PATCH 28/68] Update interface/resources/qml/hifi/audio/Audio.qml Co-Authored-By: Zach Fox --- interface/resources/qml/hifi/audio/Audio.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index dbfd46ded1..d177094d92 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -266,7 +266,7 @@ Rectangle { labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.warnWhenMuted; - visible: bar.currentIndex === 0 ? false : true; + visible: bar.currentIndex !== 0; onClicked: { AudioScriptingInterface.warnWhenMuted = checked; checked = Qt.binding(function() { return AudioScriptingInterface.warnWhenMuted; }); // restore binding From a9da5b8f16a16e2162cc75cfe3b04563f9033827 Mon Sep 17 00:00:00 2001 From: MiladNazeri Date: Wed, 11 Sep 2019 12:45:18 -0700 Subject: [PATCH 29/68] Update interface/resources/qml/hifi/audio/Audio.qml Co-Authored-By: Zach Fox --- interface/resources/qml/hifi/audio/Audio.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index d177094d92..fccba12a8a 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -278,7 +278,7 @@ Rectangle { id: audioLevelSwitch height: root.switchHeight; switchWidth: root.switchWidth; - anchors.top: bar.currentIndex === 0 ? parent.top : warnMutedSwitch.bottom + anchors.top: warnMutedSwitch.visible ? warnMutedSwitch.bottom : parent.top anchors.topMargin: bar.currentIndex === 0 ? 0 : 24 anchors.left: parent.left labelTextOn: qsTr("Audio Level Meter"); From 4c340746c53060dc7b998c5b346372db24566092 Mon Sep 17 00:00:00 2001 From: milad Date: Wed, 11 Sep 2019 12:50:49 -0700 Subject: [PATCH 30/68] removed references to the emoji destroy sound --- .../resources/sounds/emojiPopSound2.wav | Bin 105984 -> 0 bytes .../emojiApp/simplifiedEmoji.js | 1 - 2 files changed, 1 deletion(-) delete mode 100644 scripts/simplifiedUI/simplifiedEmote/emojiApp/resources/sounds/emojiPopSound2.wav diff --git a/scripts/simplifiedUI/simplifiedEmote/emojiApp/resources/sounds/emojiPopSound2.wav b/scripts/simplifiedUI/simplifiedEmote/emojiApp/resources/sounds/emojiPopSound2.wav deleted file mode 100644 index 3d917997a721e2e758a168a298f1e57c5813aa45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105984 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 Date: Wed, 11 Sep 2019 14:11:43 -0700 Subject: [PATCH 31/68] fix crash and some UI issues --- domain-server/resources/web/settings/js/settings.js | 4 ++-- domain-server/src/DomainServerSettingsManager.cpp | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index fcf7700687..136d5b0ebc 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -145,13 +145,13 @@ $(document).ready(function(){ if (private_key != undefined) { var pattern = /-+BEGIN PRIVATE KEY-+[A-Za-z0-9+/\n=]*-+END PRIVATE KEY-+/m; if (!pattern.test(private_key)) { - oauthErrors = "Private key must be in PEM format"; + oauthErrors += "Private key must be in PEM format
    "; } } if (cert != undefined) { var pattern = /-+BEGIN CERTIFICATE-+[A-Za-z0-9+/\n=]*-+END CERTIFICATE-+/m; if (!pattern.test(cert)) { - oauthErrors = "Certificate must be in PEM format"; + oauthErrors += "Certificate must be in PEM format
    "; } } if ($('#oauth.panel').length) { diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 2e6ccf8be2..cc0f02ecda 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -1270,9 +1270,10 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection QJsonArray groups; foreach (const QJsonValue& group, settingGroups) { QJsonObject groupObject = group.toObject(); + QVariant* enableKey = _configMap.valueForKeyPath(groupObject[DESCRIPTION_NAME_KEY].toString() + "." + DESCRIPTION_ENABLE_KEY); + if (!groupObject.contains(DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY) - || (groupObject[DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY].toBool() - && _configMap.valueForKeyPath(groupObject[DESCRIPTION_NAME_KEY].toString() + "." + DESCRIPTION_ENABLE_KEY)->toBool() )) { + || (groupObject[DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY].toBool() && enableKey && enableKey->toBool() )) { groups.append(groupObject); } } From 639beee6cbdb7b4222aab08b182c62be36eab24f Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 11 Sep 2019 15:21:49 -0700 Subject: [PATCH 32/68] Fix logic for reliable service address-change when new add already used --- libraries/networking/src/udt/Socket.cpp | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 4c01517346..67af69ae8f 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -546,7 +546,6 @@ void Socket::handleStateChanged(QAbstractSocket::SocketState socketState) { void Socket::handleRemoteAddressChange(HifiSockAddr previousAddress, HifiSockAddr currentAddress) { { Lock connectionsLock(_connectionsHashMutex); - _connectionsHash.erase(currentAddress); const auto connectionIter = _connectionsHash.find(previousAddress); if (connectionIter != _connectionsHash.end()) { @@ -554,18 +553,16 @@ void Socket::handleRemoteAddressChange(HifiSockAddr previousAddress, HifiSockAdd _connectionsHash.erase(connectionIter); connection->setDestinationAddress(currentAddress); _connectionsHash[currentAddress] = move(connection); - } - } + connectionsLock.release(); - { - Lock sequenceNumbersLock(_unreliableSequenceNumbersMutex); - _unreliableSequenceNumbers.erase(currentAddress); + Lock sequenceNumbersLock(_unreliableSequenceNumbersMutex); + const auto sequenceNumbersIter = _unreliableSequenceNumbers.find(previousAddress); + if (sequenceNumbersIter != _unreliableSequenceNumbers.end()) { + auto sequenceNumbers = sequenceNumbersIter->second; + _unreliableSequenceNumbers.erase(sequenceNumbersIter); + _unreliableSequenceNumbers[currentAddress] = sequenceNumbers; + } - const auto sequenceNumbersIter = _unreliableSequenceNumbers.find(previousAddress); - if (sequenceNumbersIter != _unreliableSequenceNumbers.end()) { - auto sequenceNumbers = sequenceNumbersIter->second; - _unreliableSequenceNumbers.erase(sequenceNumbersIter); - _unreliableSequenceNumbers[currentAddress] = sequenceNumbers; } } } From fad1296180e6b869177341663af5c806e92cc037 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 12 Sep 2019 11:02:10 +1200 Subject: [PATCH 33/68] Fix up Workload JSDoc stubs --- libraries/task/src/task/Config.h | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/libraries/task/src/task/Config.h b/libraries/task/src/task/Config.h index 8accba9e1f..71d48c9a18 100644 --- a/libraries/task/src/task/Config.h +++ b/libraries/task/src/task/Config.h @@ -90,6 +90,17 @@ public: using Config = JobConfig; }; +/**jsdoc + * @namespace Workload + * + * @hifi-interface + * @hifi-client-entity + * @hifi-avatar + * + * @property {number} cpuRunTime - Read-only. + * @property {boolean} enabled + * @property {number} branch + */ // A default Config is always on; to create an enableable Config, use the ctor JobConfig(bool enabled) class JobConfig : public QObject { Q_OBJECT @@ -139,7 +150,7 @@ public: double getCPURunTime() const { return _msCPURunTime; } /**jsdoc - * @function Render.getConfig + * @function Workload.getConfig * @param {string} name * @returns {object} */ @@ -162,19 +173,19 @@ public: // Describe the node graph data connections of the associated Job/Task /**jsdoc - * @function JobConfig.isTask + * @function Workload.isTask * @returns {boolean} */ Q_INVOKABLE bool isTask() const { return _isTask; } /**jsdoc - * @function JobConfig.isSwitch + * @function Workload.isSwitch * @returns {boolean} */ Q_INVOKABLE bool isSwitch() const { return _isSwitch; } /**jsdoc - * @function JobConfig.getSubConfigs + * @function Workload.getSubConfigs * @returns {object[]} */ Q_INVOKABLE QObjectList getSubConfigs() const { @@ -187,13 +198,13 @@ public: } /**jsdoc - * @function JobConfig.getNumSubs + * @function Workload.getNumSubs * @returns {number} */ Q_INVOKABLE int getNumSubs() const { return getSubConfigs().size(); } /**jsdoc - * @function JobConfig.getSubConfig + * @function Workload.getSubConfig * @param {number} index * @returns {object} */ @@ -214,7 +225,7 @@ public slots: /**jsdoc * @function Workload.load - * @param {object} map + * @param {object} json */ void load(const QJsonObject& val) { qObjectFromJsonValue(val, *this); emit loaded(); } From 9a7de106142fe2f449985374451a4029a0a850d1 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Wed, 11 Sep 2019 16:27:00 -0700 Subject: [PATCH 34/68] set file permissions on the config.json file for the domain server --- domain-server/resources/web/js/domain-server.js | 1 - domain-server/src/DomainServer.cpp | 2 +- domain-server/src/DomainServerSettingsManager.cpp | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index 9524b18caf..a8b7267b88 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -91,7 +91,6 @@ $(document).ready(function(){ // make a JSON request to get the dropdown menus for content and settings // we don't error handle here because the top level menu is still clickable and usables if this fails $.getJSON('/settings-menu-groups.json', function(data){ - function makeGroupDropdownElement(group, base) { var html_id = group.html_id ? group.html_id : group.name; return "
  • " + group.label + "
  • "; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 7f6c366bc3..307a43ee88 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2718,7 +2718,7 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c QString cookieString = connection->requestHeader(HTTP_COOKIE_HEADER_KEY); QRegExp cookieUUIDRegex(COOKIE_UUID_REGEX_STRING); - + QUuid cookieUUID; if (cookieString.indexOf(cookieUUIDRegex) != -1) { cookieUUID = cookieUUIDRegex.cap(1); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index cc0f02ecda..73d78a5c70 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -1535,7 +1535,6 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt } if (!groupKey.isEmpty() && !groupResponseObject.isEmpty()) { - // set this group's object to the constructed object responseObject[groupKey] = groupResponseObject; } @@ -1728,7 +1727,6 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ postedObject[OAUTH_ROOT_KEY] = oauthObject; - qDebug() << postedObject; // Iterate on the setting groups foreach(const QString& rootKey, postedObject.keys()) { const QJsonValue& rootValue = postedObject[rootKey]; @@ -1913,6 +1911,8 @@ void DomainServerSettingsManager::persistToFile() { _configMap.loadConfig(); return; // defend against future code } + + QFile(settingsFilename).setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); } QStringList DomainServerSettingsManager::getAllKnownGroupNames() { From d548afb2771d0f83117cd96ae92454cc58a3efa2 Mon Sep 17 00:00:00 2001 From: jennaingersoll <42979611+jennaingersoll@users.noreply.github.com> Date: Wed, 11 Sep 2019 17:06:35 -0700 Subject: [PATCH 35/68] Removed indent per review --- tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl index acbab36d8d..5c149fa434 100644 --- a/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl @@ -92,7 +92,7 @@ - +

    Example 1? 's':'' ?>

    From 12b980d99dafc779243c3c646247b5c437cba7a6 Mon Sep 17 00:00:00 2001 From: milad Date: Wed, 11 Sep 2019 17:08:35 -0700 Subject: [PATCH 36/68] about to test if this new right click disable is working --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index fd82567a94..0279963259 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -800,7 +800,7 @@ QUuid EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); EntityItemPointer entity; if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { - if (!EntityTree::areEntityClicksCaptured()) { + if (!EntityTree::areEntityClicksCaptured() && event->button() == Qt::MouseButton::LeftButton) { auto properties = entity->getProperties(); QString urlString = properties.getHref(); QUrl url = QUrl(urlString, QUrl::StrictMode); From f7f0483320669d907cf84d618517e97901733416 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Thu, 12 Sep 2019 08:41:35 -0700 Subject: [PATCH 37/68] Don't disable HMD leaning while seated --- interface/src/avatar/MyAvatar.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 4d1c20010c..ae84347d17 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -6257,7 +6257,6 @@ void MyAvatar::beginSit(const glm::vec3& position, const glm::quat& rotation) { _characterController.setSeated(true); setCollisionsEnabled(false); - setHMDLeanRecenterEnabled(false); // Disable movement setSitDriveKeysStatus(false); centerBody(); @@ -6276,7 +6275,6 @@ void MyAvatar::endSit(const glm::vec3& position, const glm::quat& rotation) { clearPinOnJoint(getJointIndex("Hips")); _characterController.setSeated(false); setCollisionsEnabled(true); - setHMDLeanRecenterEnabled(true); centerBody(); slamPosition(position); setWorldOrientation(rotation); From 00f7b75f34f55f3ffddcc67ceff103a9102039ce Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 12 Sep 2019 10:45:35 -0700 Subject: [PATCH 38/68] Fix draco errors being added to wrong part of list --- libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp index 9fff570cc0..12347c30b1 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -238,7 +238,7 @@ void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Inp bool dracoError; std::unique_ptr dracoMesh; std::tie(dracoMesh, dracoError) = createDracoMesh(mesh, normals, tangents, materialList); - dracoErrorsPerMesh[dracoErrorsPerMesh.size()-1] = dracoError; + dracoErrorsPerMesh[i] = dracoError; if (dracoMesh) { draco::Encoder encoder; From 2d68cfa5e7f6a3306d7a7ad64bef2d429fa53dcb Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 12 Sep 2019 12:04:24 -0700 Subject: [PATCH 39/68] Use unique_lock::unlock() instead of unique_lock::release() ... which actually has the opposite effect --- libraries/networking/src/udt/Socket.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 67af69ae8f..20cb30dbd8 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -553,7 +553,7 @@ void Socket::handleRemoteAddressChange(HifiSockAddr previousAddress, HifiSockAdd _connectionsHash.erase(connectionIter); connection->setDestinationAddress(currentAddress); _connectionsHash[currentAddress] = move(connection); - connectionsLock.release(); + connectionsLock.unlock(); Lock sequenceNumbersLock(_unreliableSequenceNumbersMutex); const auto sequenceNumbersIter = _unreliableSequenceNumbers.find(previousAddress); From 3c043309be8ea4dd160e61d038476c949baea2a0 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Thu, 12 Sep 2019 14:03:17 -0700 Subject: [PATCH 40/68] Detect HMD mode properly from rig, in order to enable head IK --- libraries/animation/src/Rig.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 561995cce4..5fe2cb33ff 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1958,8 +1958,7 @@ void Rig::updateReactions(const ControllerParameters& params) { bool isSeated = _state == RigRole::Seated; bool hipsEnabled = params.primaryControllerFlags[PrimaryControllerType_Hips] & (uint8_t)ControllerFlags::Enabled; - bool hipsEstimated = params.primaryControllerFlags[PrimaryControllerType_Hips] & (uint8_t)ControllerFlags::Estimated; - bool hmdMode = hipsEnabled && !hipsEstimated; + bool hmdMode = hipsEnabled; if ((reactionPlaying || isSeated) && !hmdMode) { // TODO: make this smooth. From 4aa85baf75c8894ddb3e25d525392627c1c00bca Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Thu, 12 Sep 2019 14:07:56 -0700 Subject: [PATCH 41/68] Restate disable leaning when seated --- interface/src/avatar/MyAvatar.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index ae84347d17..4d1c20010c 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -6257,6 +6257,7 @@ void MyAvatar::beginSit(const glm::vec3& position, const glm::quat& rotation) { _characterController.setSeated(true); setCollisionsEnabled(false); + setHMDLeanRecenterEnabled(false); // Disable movement setSitDriveKeysStatus(false); centerBody(); @@ -6275,6 +6276,7 @@ void MyAvatar::endSit(const glm::vec3& position, const glm::quat& rotation) { clearPinOnJoint(getJointIndex("Hips")); _characterController.setSeated(false); setCollisionsEnabled(true); + setHMDLeanRecenterEnabled(true); centerBody(); slamPosition(position); setWorldOrientation(rotation); From 5370e67507be6e2309fd51e8200899b0026dbc67 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 12 Sep 2019 14:48:21 -0700 Subject: [PATCH 42/68] Update logo on login popup --- .../resources/images/high-fidelity-banner.svg | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/interface/resources/images/high-fidelity-banner.svg b/interface/resources/images/high-fidelity-banner.svg index d5666be0fa..19127e729c 100644 --- a/interface/resources/images/high-fidelity-banner.svg +++ b/interface/resources/images/high-fidelity-banner.svg @@ -1,17 +1 @@ - - - - - - - - - - - - - - - - - +Artboard 1 \ No newline at end of file From bc119d6c850e62d2fc10f56d2c3440a6772fce5e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 12 Sep 2019 17:02:19 -0700 Subject: [PATCH 43/68] don't expect final kinematic simulation update --- libraries/physics/src/EntityMotionState.cpp | 73 +++++++++++---------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 68c8266e9f..de82dd6ace 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -84,48 +84,51 @@ EntityMotionState::~EntityMotionState() { } void EntityMotionState::updateServerPhysicsVariables() { - if (_ownershipState != EntityMotionState::OwnershipState::LocallyOwned) { - // only slam these values if we are NOT the simulation owner - Transform localTransform; - _entity->getLocalTransformAndVelocities(localTransform, _serverVelocity, _serverAngularVelocity); - _serverPosition = localTransform.getTranslation(); - _serverRotation = localTransform.getRotation(); - _serverAcceleration = _entity->getAcceleration(); - _serverActionData = _entity->getDynamicData(); - _lastStep = ObjectMotionState::getWorldSimulationStep(); - } + // only slam these values if we are NOT the simulation owner + Transform localTransform; + _entity->getLocalTransformAndVelocities(localTransform, _serverVelocity, _serverAngularVelocity); + _serverPosition = localTransform.getTranslation(); + _serverRotation = localTransform.getRotation(); + _serverAcceleration = _entity->getAcceleration(); + _serverActionData = _entity->getDynamicData(); + _lastStep = ObjectMotionState::getWorldSimulationStep(); } void EntityMotionState::handleDeactivation() { - if (_entity->getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES)) { - // Some non-physical event (script-call or network-packet) has modified the entity's transform and/or velocities - // at the last minute before deactivation --> the values stored in _server* and _body are stale. - // We assume the EntityMotionState is the last to know, so we copy from EntityItem and let things sort themselves out. - Transform localTransform; - _entity->getLocalTransformAndVelocities(localTransform, _serverVelocity, _serverAngularVelocity); - _serverPosition = localTransform.getTranslation(); - _serverRotation = localTransform.getRotation(); - _serverAcceleration = _entity->getAcceleration(); - _serverActionData = _entity->getDynamicData(); - _lastStep = ObjectMotionState::getWorldSimulationStep(); - } else { - // copy _server data to entity - Transform localTransform = _entity->getLocalTransform(); - localTransform.setTranslation(_serverPosition); - localTransform.setRotation(_serverRotation); - _entity->setLocalTransformAndVelocities(localTransform, ENTITY_ITEM_ZERO_VEC3, ENTITY_ITEM_ZERO_VEC3); - // and also to RigidBody - btTransform worldTrans; - worldTrans.setOrigin(glmToBullet(_entity->getWorldPosition())); - worldTrans.setRotation(glmToBullet(_entity->getWorldOrientation())); - _body->setWorldTransform(worldTrans); - // no need to update velocities... should already be zero - } + if (_entity->getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES)) { + // Some non-physical event (script-call or network-packet) has modified the entity's transform and/or + // velocities at the last minute before deactivation --> the values stored in _server* and _body are stale. + // We assume the EntityMotionState is the last to know, so we copy from EntityItem to _server* variables + // here but don't clear the flags --> the will body be set straight before next simulation step. + updateServerPhysicsVariables(); + } else if (_body->isStaticOrKinematicObject() && _ownershipState != EntityMotionState::OwnershipState::LocallyOwned) { + // To allow the ESS to move entities around in a kinematic way we had to remove the requirement that + // every moving+simulated entity has an authoritative simulation owner. As a result, we cannot rely + // on a final authoritative update of kinmatic objects prior to deactivation in the local simulation. + // For this case (unowned kinematic objects) we update the _server* variables for good measure but + // leave the entity and body alone. They should have been updated correctly in the last call to + // EntityMotionState::getWorldTransform(). + updateServerPhysicsVariables(); + } else { + // copy _server data to entity + Transform localTransform = _entity->getLocalTransform(); + localTransform.setTranslation(_serverPosition); + localTransform.setRotation(_serverRotation); + _entity->setLocalTransformAndVelocities(localTransform, ENTITY_ITEM_ZERO_VEC3, ENTITY_ITEM_ZERO_VEC3); + // and also to RigidBody + btTransform worldTrans; + worldTrans.setOrigin(glmToBullet(_entity->getWorldPosition())); + worldTrans.setRotation(glmToBullet(_entity->getWorldOrientation())); + _body->setWorldTransform(worldTrans); + // no need to update velocities... should already be zero + } } // virtual void EntityMotionState::handleEasyChanges(uint32_t& flags) { - updateServerPhysicsVariables(); + if (_ownershipState != EntityMotionState::OwnershipState::LocallyOwned) { + updateServerPhysicsVariables(); + } ObjectMotionState::handleEasyChanges(flags); if (flags & Simulation::DIRTY_SIMULATOR_ID) { From 8491a3792385a636298c527669a5ecbaec5ed4b6 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Thu, 12 Sep 2019 17:12:50 -0700 Subject: [PATCH 44/68] fixing WebEntities html --- interface/resources/qml/Web3DSurface.qml | 33 ++++++++++++++----- .../src/RenderableWebEntityItem.cpp | 10 ++++++ libraries/entities/src/WebEntityItem.cpp | 10 ++++++ libraries/entities/src/WebEntityItem.h | 3 ++ .../ui/src/ui/TabletScriptingInterface.cpp | 26 --------------- 5 files changed, 47 insertions(+), 35 deletions(-) diff --git a/interface/resources/qml/Web3DSurface.qml b/interface/resources/qml/Web3DSurface.qml index 32c19daf14..ff574ceaa5 100644 --- a/interface/resources/qml/Web3DSurface.qml +++ b/interface/resources/qml/Web3DSurface.qml @@ -12,20 +12,35 @@ import QtQuick 2.5 import "controls" as Controls -Controls.WebView { +Item { + id: root + anchors.fill: parent + property string url: "" + property string scriptUrl: null - // This is for JS/QML communication, which is unused in a Web3DOverlay, - // but not having this here results in spurious warnings about a - // missing signal - signal sendToScript(var message); + onUrlChanged: { + load(root.url, root.scriptUrl); + } - function onWebEventReceived(event) { - if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") { - ApplicationInterface.addAssetToWorldFromURL(event.slice(18)); + onScriptUrlChanged: { + if (root.item) { + root.item.scriptUrl = root.scriptUrl; + } else { + load(root.url, root.scriptUrl); } } + property var item: null + + function load(url, scriptUrl) { + QmlSurface.load("./controls/WebView.qml", root, function(newItem) { + root.item = newItem + root.item.url = url + root.item.scriptUrl = scriptUrl + }) + } + Component.onCompleted: { - eventBridge.webEventReceived.connect(onWebEventReceived); + load(root.url, root.scriptUrl); } } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index a1d24fe52e..b1feddfd47 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include "EntitiesRendererLogging.h" #include @@ -180,14 +181,23 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene } // This work must be done on the main thread + bool localSafeContext = entity->getLocalSafeContext(); if (!_webSurface) { + if (localSafeContext) { + ::hifi::scripting::setLocalAccessSafeThread(true); + } buildWebSurface(entity, newSourceURL); + ::hifi::scripting::setLocalAccessSafeThread(false); } if (_webSurface) { if (_webSurface->getRootItem()) { if (_contentType == ContentType::HtmlContent && _sourceURL != newSourceURL) { + if (localSafeContext) { + ::hifi::scripting::setLocalAccessSafeThread(true); + } _webSurface->getRootItem()->setProperty(URL_PROPERTY, newSourceURL); + ::hifi::scripting::setLocalAccessSafeThread(false); _sourceURL = newSourceURL; } else if (_contentType != ContentType::HtmlContent) { _sourceURL = newSourceURL; diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp index 186a8fa8b4..a62f599e4c 100644 --- a/libraries/entities/src/WebEntityItem.cpp +++ b/libraries/entities/src/WebEntityItem.cpp @@ -15,6 +15,7 @@ #include #include +#include #include "EntitiesLogging.h" #include "EntityItemProperties.h" @@ -31,6 +32,9 @@ EntityItemPointer WebEntityItem::factory(const EntityItemID& entityID, const Ent } WebEntityItem::WebEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) { + // this initialzation of localSafeContext is reading a thread-local variable and that is depends on + // the ctor being executed on the same thread as the script, assuming it's being create by a script + _localSafeContext = hifi::scripting::isLocalAccessSafeThread(); _type = EntityTypes::Web; } @@ -241,6 +245,12 @@ glm::u8vec3 WebEntityItem::getColor() const { }); } +bool WebEntityItem::getLocalSafeContext() const { + return resultWithReadLock([&] { + return _localSafeContext; + }); +} + void WebEntityItem::setAlpha(float alpha) { withWriteLock([&] { _needsRenderUpdate |= _alpha != alpha; diff --git a/libraries/entities/src/WebEntityItem.h b/libraries/entities/src/WebEntityItem.h index bb1e527712..b61e2b124f 100644 --- a/libraries/entities/src/WebEntityItem.h +++ b/libraries/entities/src/WebEntityItem.h @@ -74,6 +74,8 @@ public: void setScriptURL(const QString& value); QString getScriptURL() const; + bool getLocalSafeContext() const; + static const uint8_t DEFAULT_MAX_FPS; void setMaxFPS(uint8_t value); uint8_t getMaxFPS() const; @@ -98,6 +100,7 @@ protected: uint8_t _maxFPS; WebInputMode _inputMode; bool _showKeyboardFocusHighlight; + bool _localSafeContext { false }; }; #endif // hifi_WebEntityItem_h diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 6c57314367..3465138e00 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -834,32 +834,6 @@ void TabletProxy::loadHTMLSourceImpl(const QVariant& url, const QString& injectJ hifi::scripting::setLocalAccessSafeThread(false); } _state = State::Web; - /*QObject* root = nullptr; - if (!_toolbarMode && _qmlTabletRoot) { - root = _qmlTabletRoot; - } else if (_toolbarMode && _desktopWindow) { - root = _desktopWindow->asQuickItem(); - } - - if (root) { - // BUGZ-1398: tablet access to local HTML files from client scripts - // Here we TEMPORARILY mark the main thread as allowed to load local file content, - // because the thread that originally made the call is so marked. - if (localSafeContext) { - hifi::scripting::setLocalAccessSafeThread(true); - } - QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, path)); - hifi::scripting::setLocalAccessSafeThread(false); - _state = State::QML; - _currentPathLoaded = path; - QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); - if (_toolbarMode && _desktopWindow) { - QMetaObject::invokeMethod(root, "setResizable", Q_ARG(const QVariant&, QVariant(resizable))); - } - - } else { - qCDebug(uiLogging) << "tablet cannot load QML because _qmlTabletRoot is null"; - }*/ } void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaScriptUrl, bool loadOtherBase) { From 60045f4783d11286dd4cf7dc56096d46f54aa0a3 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 16 Sep 2019 17:48:30 -0700 Subject: [PATCH 45/68] Replacing key combination to trigger a frame capture --- interface/src/Application.cpp | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b719f26c68..33fdad913b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4347,14 +4347,14 @@ void Application::keyPressEvent(QKeyEvent* event) { _keyboardMouseDevice->keyReleaseEvent(event); } - bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); + bool isControlOrCommand = event->modifiers().testFlag(Qt::ControlModifier); bool isOption = event->modifiers().testFlag(Qt::AltModifier); switch (event->key()) { case Qt::Key_4: case Qt::Key_5: case Qt::Key_6: case Qt::Key_7: - if (isMeta || isOption) { + if (isControlOrCommand || isOption) { unsigned int index = static_cast(event->key() - Qt::Key_1); auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); if (index < displayPlugins.size()) { @@ -4375,7 +4375,8 @@ void Application::keyPressEvent(QKeyEvent* event) { } bool isShifted = event->modifiers().testFlag(Qt::ShiftModifier); - bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); + bool isControlOrCommand = event->modifiers().testFlag(Qt::ControlModifier); + bool isMetaOrMacControl = event->modifiers().testFlag(Qt::MetaModifier); bool isOption = event->modifiers().testFlag(Qt::AltModifier); switch (event->key()) { case Qt::Key_Enter: @@ -4408,7 +4409,7 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_5: case Qt::Key_6: case Qt::Key_7: - if (isMeta || isOption) { + if (isControlOrCommand || isOption) { unsigned int index = static_cast(event->key() - Qt::Key_1); auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); if (index < displayPlugins.size()) { @@ -4424,7 +4425,7 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_G: - if (isShifted && isMeta && Menu::getInstance() && Menu::getInstance()->getMenu("Developer")->isVisible()) { + if (isShifted && isControlOrCommand && isOption && isMetaOrMacControl) { static const QString HIFI_FRAMES_FOLDER_VAR = "HIFI_FRAMES_FOLDER"; static const QString GPU_FRAME_FOLDER = QProcessEnvironment::systemEnvironment().contains(HIFI_FRAMES_FOLDER_VAR) ? QProcessEnvironment::systemEnvironment().value(HIFI_FRAMES_FOLDER_VAR) @@ -4437,7 +4438,7 @@ void Application::keyPressEvent(QKeyEvent* event) { } break; case Qt::Key_X: - if (isShifted && isMeta) { + if (isShifted && isControlOrCommand) { auto offscreenUi = getOffscreenUI(); offscreenUi->togglePinned(); //offscreenUi->getSurfaceContext()->engine()->clearComponentCache(); @@ -4447,7 +4448,7 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_Y: - if (isShifted && isMeta) { + if (isShifted && isControlOrCommand) { getActiveDisplayPlugin()->cycleDebugOutput(); } break; @@ -4460,16 +4461,16 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_L: - if (isShifted && isMeta) { + if (isShifted && isControlOrCommand) { Menu::getInstance()->triggerOption(MenuOption::Log); - } else if (isMeta) { + } else if (isControlOrCommand) { auto dialogsManager = DependencyManager::get(); dialogsManager->toggleAddressBar(); } break; case Qt::Key_R: - if (isMeta && !event->isAutoRepeat()) { + if (isControlOrCommand && !event->isAutoRepeat()) { DependencyManager::get()->reloadAllScripts(); getOffscreenUI()->clearCache(); } @@ -4480,7 +4481,7 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_M: - if (isMeta) { + if (isControlOrCommand) { auto audioClient = DependencyManager::get(); audioClient->setMuted(!audioClient->isMuted()); QSharedPointer audioScriptingInterface = qSharedPointerDynamicCast(DependencyManager::get()); @@ -4491,13 +4492,13 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_S: - if (isShifted && isMeta && !isOption) { + if (isShifted && isControlOrCommand && !isOption) { Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); } break; case Qt::Key_Apostrophe: { - if (isMeta) { + if (isControlOrCommand) { auto cursor = Cursor::Manager::instance().getCursor(); auto curIcon = cursor->getIcon(); if (curIcon == Cursor::Icon::DEFAULT) { @@ -4524,7 +4525,7 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_Plus: { - if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { + if (isControlOrCommand && event->modifiers().testFlag(Qt::KeypadModifier)) { auto& cursorManager = Cursor::Manager::instance(); cursorManager.setScale(cursorManager.getScale() * 1.1f); } else { @@ -4534,7 +4535,7 @@ void Application::keyPressEvent(QKeyEvent* event) { } case Qt::Key_Minus: { - if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { + if (isControlOrCommand && event->modifiers().testFlag(Qt::KeypadModifier)) { auto& cursorManager = Cursor::Manager::instance(); cursorManager.setScale(cursorManager.getScale() / 1.1f); } else { From af14245d45e4c45f0bfa83ec8f41004e848e208c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 Sep 2019 18:00:45 -0700 Subject: [PATCH 46/68] provide a way to get at _sensorResetMat from outside class --- plugins/openvr/src/OpenVrDisplayPlugin.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index 25427c6dcd..9212a42639 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -76,6 +76,8 @@ public: int visionSqueezePerEye, float visionSqueezeGroundPlaneY, float visionSqueezeSpotlightSize) override; + glm::mat4 getSensorResetMatrix() const { return _sensorResetMat; } + protected: bool internalActivate() override; void internalDeactivate() override; From 64e4ff88e6dee937e9af7af407a6ce0c18c292fa Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 Sep 2019 18:01:12 -0700 Subject: [PATCH 47/68] findClosestApproachOfLines will find where two lines get the closest to each other --- libraries/shared/src/GeometryUtil.cpp | 24 +++++++++++++++++++++++- libraries/shared/src/GeometryUtil.h | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp index b6fca03403..23d2f0f71b 100644 --- a/libraries/shared/src/GeometryUtil.cpp +++ b/libraries/shared/src/GeometryUtil.cpp @@ -547,6 +547,28 @@ bool doLineSegmentsIntersect(glm::vec2 r1p1, glm::vec2 r1p2, glm::vec2 r2p1, glm (d4 == 0 && isOnSegment(r1p1.x, r1p1.y, r1p2.x, r1p2.y, r2p2.x, r2p2.y)); } +bool findClosestApproachOfLines(glm::vec3 p1, glm::vec3 d1, glm::vec3 p2, glm::vec3 d2, + // return values... + float& t1, float& t2) { + // https://math.stackexchange.com/questions/1993953/closest-points-between-two-lines/1993990#1993990 + // https://en.wikipedia.org/wiki/Skew_lines#Nearest_Points + glm::vec3 n1 = glm::cross(d1, glm::cross(d2, d1)); + glm::vec3 n2 = glm::cross(d2, glm::cross(d1, d2)); + + float denom1 = glm::dot(d1, n2); + float denom2 = glm::dot(d2, n1); + + if (denom1 != 0.0f && denom2 != 0.0f) { + t1 = glm::dot((p2 - p1), n2) / denom1; + t2 = glm::dot((p1 - p2), n1) / denom2; + return true; + } else { + t1 = 0.0f; + t2 = 0.0f; + return false; + } +} + bool isOnSegment(float xi, float yi, float xj, float yj, float xk, float yk) { return (xi <= xk || xj <= xk) && (xk <= xi || xk <= xj) && (yi <= yk || yj <= yk) && (yk <= yi || yk <= yj); @@ -1813,4 +1835,4 @@ bool solve_quartic(float a, float b, float c, float d, glm::vec4& roots) { bool computeRealQuarticRoots(float a, float b, float c, float d, float e, glm::vec4& roots) { return solve_quartic(b / a, c / a, d / a, e / a, roots); -} \ No newline at end of file +} diff --git a/libraries/shared/src/GeometryUtil.h b/libraries/shared/src/GeometryUtil.h index 764eeb1500..d786d63980 100644 --- a/libraries/shared/src/GeometryUtil.h +++ b/libraries/shared/src/GeometryUtil.h @@ -150,6 +150,7 @@ int clipTriangleWithPlane(const Triangle& triangle, const Plane& plane, Triangle int clipTriangleWithPlanes(const Triangle& triangle, const Plane* planes, int planeCount, Triangle* clippedTriangles, int maxClippedTriangleCount); bool doLineSegmentsIntersect(glm::vec2 r1p1, glm::vec2 r1p2, glm::vec2 r2p1, glm::vec2 r2p2); +bool findClosestApproachOfLines(glm::vec3 p1, glm::vec3 d1, glm::vec3 p2, glm::vec3 d2, float& t1, float& t2); bool isOnSegment(float xi, float yi, float xj, float yj, float xk, float yk); int computeDirection(float xi, float yi, float xj, float yj, float xk, float yk); From 73e6be9c3705bc53bd36ce584c4307b61b67dd5d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 Sep 2019 18:03:58 -0700 Subject: [PATCH 48/68] allow drawing DebugDraw spheres with a specific size --- libraries/render-utils/src/AnimDebugDraw.cpp | 4 ++-- libraries/shared/src/DebugDraw.cpp | 12 +++++++----- libraries/shared/src/DebugDraw.h | 16 +++++++++++----- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/libraries/render-utils/src/AnimDebugDraw.cpp b/libraries/render-utils/src/AnimDebugDraw.cpp index bf528ee5f0..c4020cb4c4 100644 --- a/libraries/render-utils/src/AnimDebugDraw.cpp +++ b/libraries/render-utils/src/AnimDebugDraw.cpp @@ -393,7 +393,7 @@ void AnimDebugDraw::update() { glm::quat rot = std::get<0>(iter.second); glm::vec3 pos = std::get<1>(iter.second); glm::vec4 color = std::get<2>(iter.second); - const float radius = POSE_RADIUS; + const float radius = std::get<3>(iter.second) * POSE_RADIUS; addBone(AnimPose::identity, AnimPose(glm::vec3(1), rot, pos), radius, color, v); } @@ -402,7 +402,7 @@ void AnimDebugDraw::update() { glm::quat rot = std::get<0>(iter.second); glm::vec3 pos = std::get<1>(iter.second); glm::vec4 color = std::get<2>(iter.second); - const float radius = POSE_RADIUS; + const float radius = std::get<3>(iter.second) * POSE_RADIUS; addBone(myAvatarPose, AnimPose(glm::vec3(1), rot, pos), radius, color, v); } diff --git a/libraries/shared/src/DebugDraw.cpp b/libraries/shared/src/DebugDraw.cpp index 1b2418f7c7..2539a43672 100644 --- a/libraries/shared/src/DebugDraw.cpp +++ b/libraries/shared/src/DebugDraw.cpp @@ -31,9 +31,10 @@ void DebugDraw::drawRay(const glm::vec3& start, const glm::vec3& end, const glm: _rays.push_back(Ray(start, end, color)); } -void DebugDraw::addMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color) { +void DebugDraw::addMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, + const glm::vec4& color, float size) { Lock lock(_mapMutex); - _markers[key] = MarkerInfo(rotation, position, color); + _markers[key] = MarkerInfo(rotation, position, color, size); } void DebugDraw::removeMarker(const QString& key) { @@ -41,9 +42,10 @@ void DebugDraw::removeMarker(const QString& key) { _markers.erase(key); } -void DebugDraw::addMyAvatarMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color) { +void DebugDraw::addMyAvatarMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, + const glm::vec4& color, float size) { Lock lock(_mapMutex); - _myAvatarMarkers[key] = MarkerInfo(rotation, position, color); + _myAvatarMarkers[key] = MarkerInfo(rotation, position, color, size); } void DebugDraw::removeMyAvatarMarker(const QString& key) { @@ -83,4 +85,4 @@ void DebugDraw::drawRays(const std::vector>& lin auto point2 = translation + rotation * line.second; _rays.push_back(Ray(point1, point2, color)); } -} \ No newline at end of file +} diff --git a/libraries/shared/src/DebugDraw.h b/libraries/shared/src/DebugDraw.h index 9e3140ca9b..9db48b759b 100644 --- a/libraries/shared/src/DebugDraw.h +++ b/libraries/shared/src/DebugDraw.h @@ -95,19 +95,22 @@ public: * @param {Quat} rotation - The orientation of the marker in world coordinates. * @param {Vec3} position - The position of the market in world coordinates. * @param {Vec4} color - The color of the marker. + * @param {float} size - A float between 0.0 and 1.0 (10 cm) to control the size of the marker. * @example Briefly draw a debug marker in front of your avatar, in world coordinates. * var MARKER_NAME = "my marker"; * DebugDraw.addMarker( * MARKER_NAME, * Quat.ZERO, * Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5})), - * { red: 255, green: 0, blue: 0 } + * { red: 255, green: 0, blue: 0 }, + * 1.0 * ); * Script.setTimeout(function () { * DebugDraw.removeMarker(MARKER_NAME); * }, 5000); */ - Q_INVOKABLE void addMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color); + Q_INVOKABLE void addMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, + const glm::vec4& color, float size = 1.0f); /**jsdoc * Removes a debug marker that was added in world coordinates. @@ -125,19 +128,22 @@ public: * @param {Quat} rotation - The orientation of the marker in avatar coordinates. * @param {Vec3} position - The position of the market in avatar coordinates. * @param {Vec4} color - color of the marker. + * @param {float} size - A float between 0.0 and 1.0 (10 cm) to control the size of the marker. * @example Briefly draw a debug marker in front of your avatar, in avatar coordinates. * var MARKER_NAME = "My avatar marker"; * DebugDraw.addMyAvatarMarker( * MARKER_NAME, * Quat.ZERO, * { x: 0, y: 0, z: -5 }, - * { red: 255, green: 0, blue: 0 } + * { red: 255, green: 0, blue: 0 }, + * 1.0 * ); * Script.setTimeout(function () { * DebugDraw.removeMyAvatarMarker(MARKER_NAME); * }, 5000); */ - Q_INVOKABLE void addMyAvatarMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color); + Q_INVOKABLE void addMyAvatarMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, + const glm::vec4& color, float size = 1.0f); /**jsdoc * Removes a debug marker that was added in avatar coordinates. @@ -146,7 +152,7 @@ public: */ Q_INVOKABLE void removeMyAvatarMarker(const QString& key); - using MarkerInfo = std::tuple; + using MarkerInfo = std::tuple; using MarkerMap = std::map; using Ray = std::tuple; using Rays = std::vector; From 954aeb5e257315875358d32b375a44d806843996 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 Sep 2019 18:05:38 -0700 Subject: [PATCH 49/68] AxisValue action channels now have a 'valid' flag, like Poses do --- .../controllers/src/controllers/AxisValue.cpp | 9 ++++++--- libraries/controllers/src/controllers/AxisValue.h | 4 ++-- .../controllers/src/controllers/InputDevice.cpp | 4 ++-- .../src/controllers/StandardController.cpp | 4 ++++ .../src/controllers/StandardControls.h | 4 ++++ .../src/controllers/UserInputMapper.cpp | 15 +++++++++++++++ .../controllers/src/controllers/UserInputMapper.h | 8 +++++--- .../controllers/src/controllers/impl/Endpoint.h | 2 +- .../controllers/impl/endpoints/ActionEndpoint.cpp | 2 +- .../controllers/impl/endpoints/ActionEndpoint.h | 2 +- .../impl/endpoints/CompositeEndpoint.cpp | 8 ++++++-- .../controllers/impl/endpoints/InputEndpoint.cpp | 2 +- .../controllers/impl/endpoints/ScriptEndpoint.h | 2 +- .../src/controllers/impl/filters/ClampFilter.h | 2 +- .../impl/filters/ConstrainToIntegerFilter.h | 2 +- .../filters/ConstrainToPositiveIntegerFilter.h | 2 +- .../controllers/impl/filters/DeadZoneFilter.cpp | 2 +- .../controllers/impl/filters/HysteresisFilter.cpp | 3 +-- .../src/controllers/impl/filters/NotFilter.cpp | 2 +- .../src/controllers/impl/filters/PulseFilter.cpp | 2 +- .../src/controllers/impl/filters/ScaleFilter.h | 2 +- 21 files changed, 57 insertions(+), 26 deletions(-) diff --git a/libraries/controllers/src/controllers/AxisValue.cpp b/libraries/controllers/src/controllers/AxisValue.cpp index 4b7913754c..0acbc6dbc7 100644 --- a/libraries/controllers/src/controllers/AxisValue.cpp +++ b/libraries/controllers/src/controllers/AxisValue.cpp @@ -12,10 +12,13 @@ namespace controller { - AxisValue::AxisValue(const float value, const quint64 timestamp) : - value(value), timestamp(timestamp) { } + AxisValue::AxisValue(const float value, const quint64 timestamp, bool valid) : + value(value), timestamp(timestamp), valid(valid) { + } bool AxisValue::operator==(const AxisValue& right) const { - return value == right.value && timestamp == right.timestamp; + return value == right.value && + timestamp == right.timestamp && + valid == right.valid; } } diff --git a/libraries/controllers/src/controllers/AxisValue.h b/libraries/controllers/src/controllers/AxisValue.h index e4bc20f7d2..ec356e1fb3 100644 --- a/libraries/controllers/src/controllers/AxisValue.h +++ b/libraries/controllers/src/controllers/AxisValue.h @@ -21,14 +21,14 @@ namespace controller { float value { 0.0f }; // The value can be timestamped to determine if consecutive identical values should be output (e.g., mouse movement). quint64 timestamp { 0 }; + bool valid { false }; AxisValue() {} - AxisValue(const float value, const quint64 timestamp); + AxisValue(const float value, const quint64 timestamp, bool valid = true); bool operator ==(const AxisValue& right) const; bool operator !=(const AxisValue& right) const { return !(*this == right); } }; - } #endif // hifi_controllers_AxisValue_h diff --git a/libraries/controllers/src/controllers/InputDevice.cpp b/libraries/controllers/src/controllers/InputDevice.cpp index dd430263fa..b28e7cfc82 100644 --- a/libraries/controllers/src/controllers/InputDevice.cpp +++ b/libraries/controllers/src/controllers/InputDevice.cpp @@ -77,13 +77,13 @@ namespace controller { return { getButton(channel), 0 }; case ChannelType::POSE: - return { getPose(channel).valid ? 1.0f : 0.0f, 0 }; + return { getPose(channel).valid ? 1.0f : 0.0f, 0, getPose(channel).valid }; default: break; } - return { 0.0f, 0 }; + return { 0.0f, 0, false }; } AxisValue InputDevice::getValue(const Input& input) const { diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp index ece10ecca3..dbc92cc7e5 100644 --- a/libraries/controllers/src/controllers/StandardController.cpp +++ b/libraries/controllers/src/controllers/StandardController.cpp @@ -354,6 +354,10 @@ Input::NamedVector StandardController::getAvailableInputs() const { makePair(HIPS, "Hips"), makePair(SPINE2, "Spine2"), makePair(HEAD, "Head"), + makePair(LEFT_EYE, "LeftEye"), + makePair(RIGHT_EYE, "RightEye"), + makePair(LEFT_EYE_BLINK, "LeftEyeBlink"), + makePair(RIGHT_EYE_BLINK, "RightEyeBlink"), // Aliases, PlayStation style names makePair(LB, "L1"), diff --git a/libraries/controllers/src/controllers/StandardControls.h b/libraries/controllers/src/controllers/StandardControls.h index f521ab81cf..99d9246264 100644 --- a/libraries/controllers/src/controllers/StandardControls.h +++ b/libraries/controllers/src/controllers/StandardControls.h @@ -90,6 +90,8 @@ namespace controller { // Grips LEFT_GRIP, RIGHT_GRIP, + LEFT_EYE_BLINK, + RIGHT_EYE_BLINK, NUM_STANDARD_AXES, LZ = LT, RZ = RT @@ -174,6 +176,8 @@ namespace controller { TRACKED_OBJECT_13, TRACKED_OBJECT_14, TRACKED_OBJECT_15, + LEFT_EYE, + RIGHT_EYE, NUM_STANDARD_POSES }; diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index 33dc37312e..1eb1a9fa1a 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -256,6 +256,9 @@ void UserInputMapper::update(float deltaTime) { for (auto& channel : _actionStates) { channel = 0.0f; } + for (unsigned int i = 0; i < _actionStatesValid.size(); i++) { + _actionStatesValid[i] = true; + } for (auto& channel : _poseStates) { channel = Pose(); @@ -1233,5 +1236,17 @@ void UserInputMapper::disableMapping(const Mapping::Pointer& mapping) { } } +void UserInputMapper::setActionState(Action action, float value, bool valid) { + _actionStates[toInt(action)] = value; + _actionStatesValid[toInt(action)] = valid; +} + +void UserInputMapper::deltaActionState(Action action, float delta, bool valid) { + _actionStates[toInt(action)] += delta; + bool wasValid = _actionStatesValid[toInt(action)]; + _actionStatesValid[toInt(action)] = wasValid & valid; +} + + } diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index 2b3c947491..cd44f3226c 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -82,13 +82,14 @@ namespace controller { QString getActionName(Action action) const; QString getStandardPoseName(uint16_t pose); float getActionState(Action action) const { return _actionStates[toInt(action)]; } + bool getActionStateValid(Action action) const { return _actionStatesValid[toInt(action)]; } Pose getPoseState(Action action) const; int findAction(const QString& actionName) const; QVector getActionNames() const; Input inputFromAction(Action action) const { return getActionInputs()[toInt(action)].first; } - void setActionState(Action action, float value) { _actionStates[toInt(action)] = value; } - void deltaActionState(Action action, float delta) { _actionStates[toInt(action)] += delta; } + void setActionState(Action action, float value, bool valid = true); + void deltaActionState(Action action, float delta, bool valid = true); void setActionState(Action action, const Pose& value) { _poseStates[toInt(action)] = value; } bool triggerHapticPulse(float strength, float duration, controller::Hand hand); bool triggerHapticPulseOnDevice(uint16 deviceID, float strength, float duration, controller::Hand hand); @@ -146,6 +147,7 @@ namespace controller { std::vector _actionStates = std::vector(toInt(Action::NUM_ACTIONS), 0.0f); std::vector _actionScales = std::vector(toInt(Action::NUM_ACTIONS), 1.0f); std::vector _lastActionStates = std::vector(toInt(Action::NUM_ACTIONS), 0.0f); + std::vector _actionStatesValid = std::vector(toInt(Action::NUM_ACTIONS), false); std::vector _poseStates = std::vector(toInt(Action::NUM_ACTIONS)); std::vector _lastStandardStates = std::vector(); @@ -167,7 +169,7 @@ namespace controller { ConditionalPointer conditionalFor(const QJSValue& endpoint); ConditionalPointer conditionalFor(const QScriptValue& endpoint); ConditionalPointer conditionalFor(const Input& endpoint) const; - + MappingPointer parseMapping(const QJsonValue& json); RoutePointer parseRoute(const QJsonValue& value); EndpointPointer parseDestination(const QJsonValue& value); diff --git a/libraries/controllers/src/controllers/impl/Endpoint.h b/libraries/controllers/src/controllers/impl/Endpoint.h index bcf71f3094..692e427e16 100644 --- a/libraries/controllers/src/controllers/impl/Endpoint.h +++ b/libraries/controllers/src/controllers/impl/Endpoint.h @@ -100,7 +100,7 @@ namespace controller { _currentPose = value; } protected: - AxisValue _currentValue { 0.0f, 0 }; + AxisValue _currentValue { 0.0f, 0, false }; Pose _currentPose {}; }; diff --git a/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp b/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp index 58744c468c..4be23b0c29 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp +++ b/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp @@ -26,7 +26,7 @@ void ActionEndpoint::apply(AxisValue newValue, const Pointer& source) { _currentValue.value += newValue.value; if (_input != Input::INVALID_INPUT) { - userInputMapper->deltaActionState(Action(_input.getChannel()), newValue.value); + userInputMapper->deltaActionState(Action(_input.getChannel()), newValue.value, newValue.valid); } } diff --git a/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.h b/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.h index 94da4663aa..7ab21031a7 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.h +++ b/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.h @@ -32,7 +32,7 @@ public: virtual void reset() override; private: - AxisValue _currentValue { 0.0f, 0 }; + AxisValue _currentValue { 0.0f, 0, false }; Pose _currentPose{}; }; diff --git a/libraries/controllers/src/controllers/impl/endpoints/CompositeEndpoint.cpp b/libraries/controllers/src/controllers/impl/endpoints/CompositeEndpoint.cpp index f54c786a33..82a44bd7ae 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/CompositeEndpoint.cpp +++ b/libraries/controllers/src/controllers/impl/endpoints/CompositeEndpoint.cpp @@ -27,7 +27,9 @@ bool CompositeEndpoint::readable() const { AxisValue CompositeEndpoint::peek() const { auto negative = first->peek(); auto positive = second->peek(); - auto result = AxisValue(positive.value - negative.value, std::max(positive.timestamp, negative.timestamp)); + auto result = AxisValue(positive.value - negative.value, + std::max(positive.timestamp, negative.timestamp), + negative.valid && positive.valid); return result; } @@ -35,7 +37,9 @@ AxisValue CompositeEndpoint::peek() const { AxisValue CompositeEndpoint::value() { auto negative = first->value(); auto positive = second->value(); - auto result = AxisValue(positive.value - negative.value, std::max(positive.timestamp, negative.timestamp)); + auto result = AxisValue(positive.value - negative.value, + std::max(positive.timestamp, negative.timestamp), + negative.valid && positive.valid); return result; } diff --git a/libraries/controllers/src/controllers/impl/endpoints/InputEndpoint.cpp b/libraries/controllers/src/controllers/impl/endpoints/InputEndpoint.cpp index 3755d860b6..db0aed47e1 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/InputEndpoint.cpp +++ b/libraries/controllers/src/controllers/impl/endpoints/InputEndpoint.cpp @@ -16,7 +16,7 @@ using namespace controller; AxisValue InputEndpoint::peek() const { if (isPose()) { - return peekPose().valid ? AxisValue(1.0f, 0) : AxisValue(0.0f, 0); + return peekPose().valid ? AxisValue(1.0f, 0) : AxisValue(0.0f, 0, false); } auto userInputMapper = DependencyManager::get(); auto deviceProxy = userInputMapper->getDevice(_input); diff --git a/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.h b/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.h index e739ab0b01..1aa1746b24 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.h +++ b/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.h @@ -41,7 +41,7 @@ protected: private: QScriptValue _callable; float _lastValueRead { 0.0f }; - AxisValue _lastValueWritten { 0.0f, 0 }; + AxisValue _lastValueWritten { 0.0f, 0, false }; bool _returnPose { false }; Pose _lastPoseRead; diff --git a/libraries/controllers/src/controllers/impl/filters/ClampFilter.h b/libraries/controllers/src/controllers/impl/filters/ClampFilter.h index 04684655c9..7ec8173c2a 100644 --- a/libraries/controllers/src/controllers/impl/filters/ClampFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/ClampFilter.h @@ -19,7 +19,7 @@ class ClampFilter : public Filter { public: ClampFilter(float min = 0.0, float max = 1.0) : _min(min), _max(max) {}; virtual AxisValue apply(AxisValue value) const override { - return { glm::clamp(value.value, _min, _max), value.timestamp }; + return { glm::clamp(value.value, _min, _max), value.timestamp, value.valid }; } virtual Pose apply(Pose value) const override { return value; } diff --git a/libraries/controllers/src/controllers/impl/filters/ConstrainToIntegerFilter.h b/libraries/controllers/src/controllers/impl/filters/ConstrainToIntegerFilter.h index 2cce5f828d..3adcbbd008 100644 --- a/libraries/controllers/src/controllers/impl/filters/ConstrainToIntegerFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/ConstrainToIntegerFilter.h @@ -20,7 +20,7 @@ public: ConstrainToIntegerFilter() = default; virtual AxisValue apply(AxisValue value) const override { - return { glm::sign(value.value), value.timestamp }; + return { glm::sign(value.value), value.timestamp, value.valid }; } virtual Pose apply(Pose value) const override { return value; } diff --git a/libraries/controllers/src/controllers/impl/filters/ConstrainToPositiveIntegerFilter.h b/libraries/controllers/src/controllers/impl/filters/ConstrainToPositiveIntegerFilter.h index 07dd6654f1..5dcc43b3f7 100644 --- a/libraries/controllers/src/controllers/impl/filters/ConstrainToPositiveIntegerFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/ConstrainToPositiveIntegerFilter.h @@ -20,7 +20,7 @@ public: ConstrainToPositiveIntegerFilter() = default; virtual AxisValue apply(AxisValue value) const override { - return { (value.value <= 0.0f) ? 0.0f : 1.0f, value.timestamp }; + return { (value.value <= 0.0f) ? 0.0f : 1.0f, value.timestamp, value.valid }; } virtual Pose apply(Pose value) const override { return value; } diff --git a/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp b/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp index 84d3b9de60..4c396fee70 100644 --- a/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp @@ -18,7 +18,7 @@ AxisValue DeadZoneFilter::apply(AxisValue value) const { if (magnitude < _min) { return { 0.0f, value.timestamp }; } - return { (magnitude - _min) * scale, value.timestamp }; + return { (magnitude - _min) * scale, value.timestamp, value.valid }; } bool DeadZoneFilter::parseParameters(const QJsonValue& parameters) { diff --git a/libraries/controllers/src/controllers/impl/filters/HysteresisFilter.cpp b/libraries/controllers/src/controllers/impl/filters/HysteresisFilter.cpp index 91e59a39b9..b7b573a98c 100644 --- a/libraries/controllers/src/controllers/impl/filters/HysteresisFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/HysteresisFilter.cpp @@ -19,7 +19,6 @@ HysteresisFilter::HysteresisFilter(float min, float max) : _min(min), _max(max) } }; - AxisValue HysteresisFilter::apply(AxisValue value) const { if (_signaled) { if (value.value <= _min) { @@ -30,7 +29,7 @@ AxisValue HysteresisFilter::apply(AxisValue value) const { _signaled = true; } } - return { _signaled ? 1.0f : 0.0f, value.timestamp }; + return { _signaled ? 1.0f : 0.0f, value.timestamp, value.valid }; } bool HysteresisFilter::parseParameters(const QJsonValue& parameters) { diff --git a/libraries/controllers/src/controllers/impl/filters/NotFilter.cpp b/libraries/controllers/src/controllers/impl/filters/NotFilter.cpp index c0396857e5..dd181b7857 100644 --- a/libraries/controllers/src/controllers/impl/filters/NotFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/NotFilter.cpp @@ -6,5 +6,5 @@ NotFilter::NotFilter() { } AxisValue NotFilter::apply(AxisValue value) const { - return { (value.value == 0.0f) ? 1.0f : 0.0f, value.timestamp }; + return { (value.value == 0.0f) ? 1.0f : 0.0f, value.timestamp, value.valid }; } diff --git a/libraries/controllers/src/controllers/impl/filters/PulseFilter.cpp b/libraries/controllers/src/controllers/impl/filters/PulseFilter.cpp index d37eb99ca9..353bf7dca9 100644 --- a/libraries/controllers/src/controllers/impl/filters/PulseFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/PulseFilter.cpp @@ -29,7 +29,7 @@ AxisValue PulseFilter::apply(AxisValue value) const { _lastEmitTime = DEFAULT_LAST_EMIT_TIME; } - return { result, value.timestamp }; + return { result, value.timestamp, value.valid }; } bool PulseFilter::parseParameters(const QJsonValue& parameters) { diff --git a/libraries/controllers/src/controllers/impl/filters/ScaleFilter.h b/libraries/controllers/src/controllers/impl/filters/ScaleFilter.h index 3eb58e7f47..7c146d4e4a 100644 --- a/libraries/controllers/src/controllers/impl/filters/ScaleFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/ScaleFilter.h @@ -23,7 +23,7 @@ public: ScaleFilter(float scale) : _scale(scale) {} virtual AxisValue apply(AxisValue value) const override { - return { value.value * _scale, value.timestamp }; + return { value.value * _scale, value.timestamp, value.valid }; } virtual Pose apply(Pose value) const override { From b64ff70d6cc1a4759576e42b714a0c530133e04d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 Sep 2019 18:07:37 -0700 Subject: [PATCH 50/68] add show-lookat-targets DebugDraw. remove old/unused eyetracker code. --- interface/src/Menu.cpp | 22 ++++--------------- interface/src/Menu.h | 2 ++ .../src/avatars-renderer/Avatar.cpp | 18 +++++++++++++++ .../src/avatars-renderer/Avatar.h | 2 ++ 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 193de2792d..0b1cb789f1 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -534,32 +534,18 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::AutoMuteAudio, 0, false); #endif -#ifdef HAVE_IVIEWHMD - // Developer > Avatar > Eye Tracking - MenuWrapper* eyeTrackingMenu = avatarDebugMenu->addMenu("Eye Tracking"); - addCheckableActionToQMenuAndActionHash(eyeTrackingMenu, MenuOption::SMIEyeTracking, 0, false, - qApp, SLOT(setActiveEyeTracker())); - { - MenuWrapper* calibrateEyeTrackingMenu = eyeTrackingMenu->addMenu("Calibrate"); - addActionToQMenuAndActionHash(calibrateEyeTrackingMenu, MenuOption::OnePointCalibration, 0, - qApp, SLOT(calibrateEyeTracker1Point())); - addActionToQMenuAndActionHash(calibrateEyeTrackingMenu, MenuOption::ThreePointCalibration, 0, - qApp, SLOT(calibrateEyeTracker3Points())); - addActionToQMenuAndActionHash(calibrateEyeTrackingMenu, MenuOption::FivePointCalibration, 0, - qApp, SLOT(calibrateEyeTracker5Points())); - } - addCheckableActionToQMenuAndActionHash(eyeTrackingMenu, MenuOption::SimulateEyeTracking, 0, false, - qApp, SLOT(setActiveEyeTracker())); -#endif - action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AvatarReceiveStats, 0, false); connect(action, &QAction::triggered, [this]{ Avatar::setShowReceiveStats(isOptionChecked(MenuOption::AvatarReceiveStats)); }); action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowBoundingCollisionShapes, 0, false); connect(action, &QAction::triggered, [this]{ Avatar::setShowCollisionShapes(isOptionChecked(MenuOption::ShowBoundingCollisionShapes)); }); action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowMyLookAtVectors, 0, false); connect(action, &QAction::triggered, [this]{ Avatar::setShowMyLookAtVectors(isOptionChecked(MenuOption::ShowMyLookAtVectors)); }); + action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowMyLookAtTarget, 0, false); + connect(action, &QAction::triggered, [this]{ Avatar::setShowMyLookAtTarget(isOptionChecked(MenuOption::ShowMyLookAtTarget)); }); action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowOtherLookAtVectors, 0, false); connect(action, &QAction::triggered, [this]{ Avatar::setShowOtherLookAtVectors(isOptionChecked(MenuOption::ShowOtherLookAtVectors)); }); + action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowOtherLookAtTarget, 0, false); + connect(action, &QAction::triggered, [this]{ Avatar::setShowOtherLookAtTarget(isOptionChecked(MenuOption::ShowOtherLookAtTarget)); }); auto avatarManager = DependencyManager::get(); auto avatar = avatarManager->getMyAvatar(); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 7dff264adc..c299dd8c10 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -188,7 +188,9 @@ namespace MenuOption { const QString ShowBoundingCollisionShapes = "Show Bounding Collision Shapes"; const QString ShowDSConnectTable = "Show Domain Connection Timing"; const QString ShowMyLookAtVectors = "Show My Eye Vectors"; + const QString ShowMyLookAtTarget = "Show My Look-At Target"; const QString ShowOtherLookAtVectors = "Show Other Eye Vectors"; + const QString ShowOtherLookAtTarget = "Show Other Look-At Target"; const QString EnableLookAtSnapping = "Enable LookAt Snapping"; const QString ShowRealtimeEntityStats = "Show Realtime Entity Stats"; const QString SimulateEyeTracking = "Simulate"; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 7f363dd36f..b0f3934278 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -108,11 +108,21 @@ void Avatar::setShowMyLookAtVectors(bool showMine) { showMyLookAtVectors = showMine; } +static bool showMyLookAtTarget = false; +void Avatar::setShowMyLookAtTarget(bool showMine) { + showMyLookAtTarget = showMine; +} + static bool showOtherLookAtVectors = false; void Avatar::setShowOtherLookAtVectors(bool showOthers) { showOtherLookAtVectors = showOthers; } +static bool showOtherLookAtTarget = false; +void Avatar::setShowOtherLookAtTarget(bool showOthers) { + showOtherLookAtTarget = showOthers; +} + static bool showCollisionShapes = false; void Avatar::setShowCollisionShapes(bool render) { showCollisionShapes = render; @@ -711,6 +721,14 @@ void Avatar::updateRenderItem(render::Transaction& transaction) { void Avatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { + if (isMyAvatar() ? showMyLookAtTarget : showOtherLookAtTarget) { + glm::vec3 lookAtTarget = getHead()->getLookAtPosition(); + DebugDraw::getInstance().addMarker(QString("look-at-") + getID().toString(), + glm::quat(), lookAtTarget, glm::vec4(1), 1.0f); + } else { + DebugDraw::getInstance().removeMarker(QString("look-at-") + getID().toString()); + } + if (isMyAvatar() ? showMyLookAtVectors : showOtherLookAtVectors) { const float EYE_RAY_LENGTH = 10.0; const glm::vec4 BLUE(0.0f, 0.0f, _lookAtSnappingEnabled ? 1.0f : 0.25f, 1.0f); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 7bb15ecbf7..b969449d5e 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -140,7 +140,9 @@ public: static void setShowAvatars(bool render); static void setShowReceiveStats(bool receiveStats); static void setShowMyLookAtVectors(bool showMine); + static void setShowMyLookAtTarget(bool showMine); static void setShowOtherLookAtVectors(bool showOthers); + static void setShowOtherLookAtTarget(bool showOthers); static void setShowCollisionShapes(bool render); static void setShowNamesAboveHeads(bool show); From 7fa24efca0b050ca0579b1ccd6fd9780961409f6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 Sep 2019 18:08:28 -0700 Subject: [PATCH 51/68] add a way to query Rig to see if a joint has been overridden by a script --- libraries/animation/src/Rig.cpp | 16 +++++++++++++++- libraries/animation/src/Rig.h | 4 +++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 5fe2cb33ff..19e3878d84 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -715,7 +715,7 @@ void Rig::reset(const HFMModel& hfmModel) { } } -bool Rig::jointStatesEmpty() { +bool Rig::jointStatesEmpty() const { return _internalPoseSet._relativePoses.empty(); } @@ -878,6 +878,20 @@ void Rig::setJointRotation(int index, bool valid, const glm::quat& rotation, flo } } +bool Rig::getIsJointOverridden(int jointIndex) const { + if (QThread::currentThread() == thread()) { + if (isIndexValid(jointIndex)) { + return _internalPoseSet._overrideFlags[jointIndex]; + } + } else { + QReadLocker readLock(&_externalPoseSetLock); + if (jointIndex >= 0 && jointIndex < (int)_externalPoseSet._overrideFlags.size()) { + return _externalPoseSet._overrideFlags[jointIndex]; + } + } + return false; +} + bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, glm::vec3 translation, glm::quat rotation) const { bool success { false }; glm::vec3 originalPosition = position; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 99794fd0a7..a70659b0ae 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -135,7 +135,7 @@ public: void initJointStates(const HFMModel& hfmModel, const glm::mat4& modelOffset); void reset(const HFMModel& hfmModel); - bool jointStatesEmpty(); + bool jointStatesEmpty() const; int getJointStateCount() const; int indexOfJoint(const QString& jointName) const; QString nameOfJoint(int jointIndex) const; @@ -163,6 +163,8 @@ public: void setJointTranslation(int index, bool valid, const glm::vec3& translation, float priority); void setJointRotation(int index, bool valid, const glm::quat& rotation, float priority); + bool getIsJointOverridden(int jointIndex) const; + // if translation and rotation is identity, position will be in rig space bool getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, glm::vec3 translation, glm::quat rotation) const; From 943348ba5171b5c19c27df09911b4be3ed207beb Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 Sep 2019 18:11:04 -0700 Subject: [PATCH 52/68] input action channels for eyes and eyelids and fingers. connect eyelid input actions to blendshapes. --- interface/resources/controllers/standard.json | 9 +++- .../controllers/standard_nomovement.json | 8 +++- interface/resources/controllers/vive.json | 44 +++++++++++++++++++ interface/src/avatar/MyHead.cpp | 37 +++++++++++----- .../controllers/src/controllers/Actions.cpp | 4 ++ .../controllers/src/controllers/Actions.h | 5 +++ 6 files changed, 93 insertions(+), 14 deletions(-) diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 0a5bd12460..195f909942 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -162,7 +162,14 @@ { "from": "Standard.Head", "to": "Actions.Head" }, { "from": "Standard.LeftArm", "to": "Actions.LeftArm" }, { "from": "Standard.RightArm", "to": "Actions.RightArm" }, - + + { "from": "Standard.LeftEye", "to": "Actions.LeftEye" }, + { "from": "Standard.RightEye", "to": "Actions.RightEye" }, + + { "from": "Standard.LeftEyeBlink", "to": "Actions.LeftEyeBlink" }, + { "from": "Standard.RightEyeBlink", "to": "Actions.RightEyeBlink" }, + + { "from": "Standard.TrackedObject00", "to" : "Actions.TrackedObject00" }, { "from": "Standard.TrackedObject01", "to" : "Actions.TrackedObject01" }, { "from": "Standard.TrackedObject02", "to" : "Actions.TrackedObject02" }, diff --git a/interface/resources/controllers/standard_nomovement.json b/interface/resources/controllers/standard_nomovement.json index 015bc33056..602d3bb798 100644 --- a/interface/resources/controllers/standard_nomovement.json +++ b/interface/resources/controllers/standard_nomovement.json @@ -57,7 +57,13 @@ { "from": "Standard.Head", "to": "Actions.Head" }, { "from": "Standard.LeftArm", "to": "Actions.LeftArm" }, { "from": "Standard.RightArm", "to": "Actions.RightArm" }, - + + { "from": "Standard.LeftEye", "to": "Actions.LeftEye" }, + { "from": "Standard.RightEye", "to": "Actions.RightEye" }, + + { "from": "Standard.LeftEyeBlink", "to": "Actions.LeftEyeBlink" }, + { "from": "Standard.RightEyeBlink", "to": "Actions.RightEyeBlink" }, + { "from": "Standard.TrackedObject00", "to" : "Actions.TrackedObject00" }, { "from": "Standard.TrackedObject01", "to" : "Actions.TrackedObject01" }, { "from": "Standard.TrackedObject02", "to" : "Actions.TrackedObject02" }, diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 730e1bcb58..b6fae1dd79 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -54,8 +54,52 @@ { "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" }, { "from": "Vive.LeftHand", "to": "Standard.LeftHand" }, + { "from": "Vive.LeftHandThumb1", "to": "Standard.LeftHandThumb1"}, + { "from": "Vive.LeftHandThumb2", "to": "Standard.LeftHandThumb2"}, + { "from": "Vive.LeftHandThumb3", "to": "Standard.LeftHandThumb3"}, + { "from": "Vive.LeftHandThumb4", "to": "Standard.LeftHandThumb4"}, + { "from": "Vive.LeftHandIndex1", "to": "Standard.LeftHandIndex1"}, + { "from": "Vive.LeftHandIndex2", "to": "Standard.LeftHandIndex2"}, + { "from": "Vive.LeftHandIndex3", "to": "Standard.LeftHandIndex3"}, + { "from": "Vive.LeftHandIndex4", "to": "Standard.LeftHandIndex4"}, + { "from": "Vive.LeftHandMiddle1", "to": "Standard.LeftHandMiddle1"}, + { "from": "Vive.LeftHandMiddle2", "to": "Standard.LeftHandMiddle2"}, + { "from": "Vive.LeftHandMiddle3", "to": "Standard.LeftHandMiddle3"}, + { "from": "Vive.LeftHandMiddle4", "to": "Standard.LeftHandMiddle4"}, + { "from": "Vive.LeftHandRing1", "to": "Standard.LeftHandRing1"}, + { "from": "Vive.LeftHandRing2", "to": "Standard.LeftHandRing2"}, + { "from": "Vive.LeftHandRing3", "to": "Standard.LeftHandRing3"}, + { "from": "Vive.LeftHandRing4", "to": "Standard.LeftHandRing4"}, + { "from": "Vive.LeftHandPinky1", "to": "Standard.LeftHandPinky1"}, + { "from": "Vive.LeftHandPinky2", "to": "Standard.LeftHandPinky2"}, + { "from": "Vive.LeftHandPinky3", "to": "Standard.LeftHandPinky3"}, + { "from": "Vive.LeftHandPinky4", "to": "Standard.LeftHandPinky4"}, { "from": "Vive.RightHand", "to": "Standard.RightHand" }, + { "from": "Vive.RightHandThumb1", "to": "Standard.RightHandThumb1"}, + { "from": "Vive.RightHandThumb2", "to": "Standard.RightHandThumb2"}, + { "from": "Vive.RightHandThumb3", "to": "Standard.RightHandThumb3"}, + { "from": "Vive.RightHandThumb4", "to": "Standard.RightHandThumb4"}, + { "from": "Vive.RightHandIndex1", "to": "Standard.RightHandIndex1"}, + { "from": "Vive.RightHandIndex2", "to": "Standard.RightHandIndex2"}, + { "from": "Vive.RightHandIndex3", "to": "Standard.RightHandIndex3"}, + { "from": "Vive.RightHandIndex4", "to": "Standard.RightHandIndex4"}, + { "from": "Vive.RightHandMiddle1", "to": "Standard.RightHandMiddle1"}, + { "from": "Vive.RightHandMiddle2", "to": "Standard.RightHandMiddle2"}, + { "from": "Vive.RightHandMiddle3", "to": "Standard.RightHandMiddle3"}, + { "from": "Vive.RightHandMiddle4", "to": "Standard.RightHandMiddle4"}, + { "from": "Vive.RightHandRing1", "to": "Standard.RightHandRing1"}, + { "from": "Vive.RightHandRing2", "to": "Standard.RightHandRing2"}, + { "from": "Vive.RightHandRing3", "to": "Standard.RightHandRing3"}, + { "from": "Vive.RightHandRing4", "to": "Standard.RightHandRing4"}, + { "from": "Vive.RightHandPinky1", "to": "Standard.RightHandPinky1"}, + { "from": "Vive.RightHandPinky2", "to": "Standard.RightHandPinky2"}, + { "from": "Vive.RightHandPinky3", "to": "Standard.RightHandPinky3"}, + { "from": "Vive.RightHandPinky4", "to": "Standard.RightHandPinky4"}, { "from": "Vive.Head", "to" : "Standard.Head" }, + { "from": "Vive.LeftEye", "to" : "Standard.LeftEye" }, + { "from": "Vive.RightEye", "to" : "Standard.RightEye" }, + { "from": "Vive.LeftEyeBlink", "to" : "Standard.LeftEyeBlink" }, + { "from": "Vive.RightEyeBlink", "to" : "Standard.RightEyeBlink" }, { "from": "Vive.LeftFoot", "to" : "Standard.LeftFoot", diff --git a/interface/src/avatar/MyHead.cpp b/interface/src/avatar/MyHead.cpp index 9b05a26c76..b5e8bc2171 100644 --- a/interface/src/avatar/MyHead.cpp +++ b/interface/src/avatar/MyHead.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include "devices/DdeFaceTracker.h" #include "Application.h" @@ -46,18 +45,32 @@ void MyHead::simulate(float deltaTime) { auto player = DependencyManager::get(); // Only use face trackers when not playing back a recording. if (!player->isPlaying()) { - auto faceTracker = qApp->getActiveFaceTracker(); - const bool hasActualFaceTrackerConnected = faceTracker && !faceTracker->isMuted(); - _isFaceTrackerConnected = hasActualFaceTrackerConnected || _owningAvatar->getHasScriptedBlendshapes(); - if (_isFaceTrackerConnected) { - if (hasActualFaceTrackerConnected) { - _blendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); - } - } + // auto faceTracker = qApp->getActiveFaceTracker(); + // const bool hasActualFaceTrackerConnected = faceTracker && !faceTracker->isMuted(); + // _isFaceTrackerConnected = hasActualFaceTrackerConnected || _owningAvatar->getHasScriptedBlendshapes(); + // if (_isFaceTrackerConnected) { + // if (hasActualFaceTrackerConnected) { + // _blendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); + // } + // } - auto eyeTracker = DependencyManager::get(); - _isEyeTrackerConnected = eyeTracker->isTracking(); - // if eye tracker is connected we should get the data here. + auto userInputMapper = DependencyManager::get(); + bool eyeLidsTracked = + userInputMapper->getActionStateValid(controller::Action::LEFT_EYE_BLINK) && + userInputMapper->getActionStateValid(controller::Action::RIGHT_EYE_BLINK); + setFaceTrackerConnected(eyeLidsTracked); + if (eyeLidsTracked) { + float leftEyeBlink = userInputMapper->getActionState(controller::Action::LEFT_EYE_BLINK); + float rightEyeBlink = userInputMapper->getActionState(controller::Action::RIGHT_EYE_BLINK); + _blendshapeCoefficients.resize(std::max(_blendshapeCoefficients.size(), 2)); + _blendshapeCoefficients[0] = leftEyeBlink; + _blendshapeCoefficients[1] = rightEyeBlink; + } else { + const float FULLY_OPEN = 0.0f; + _blendshapeCoefficients.resize(std::max(_blendshapeCoefficients.size(), 2)); + _blendshapeCoefficients[0] = FULLY_OPEN; + _blendshapeCoefficients[1] = FULLY_OPEN; + } } Parent::simulate(deltaTime); } diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 9f9d92fed7..40011f2682 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -347,6 +347,10 @@ namespace controller { makePosePair(Action::HIPS, "Hips"), makePosePair(Action::SPINE2, "Spine2"), makePosePair(Action::HEAD, "Head"), + makePosePair(Action::LEFT_EYE, "LeftEye"), + makePosePair(Action::RIGHT_EYE, "RightEye"), + makeAxisPair(Action::LEFT_EYE_BLINK, "LeftEyeBlink"), + makeAxisPair(Action::RIGHT_EYE_BLINK, "RightEyeBlink"), makePosePair(Action::LEFT_HAND_THUMB1, "LeftHandThumb1"), makePosePair(Action::LEFT_HAND_THUMB2, "LeftHandThumb2"), diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index 3e99d8d147..f91d9f2522 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -181,6 +181,11 @@ enum class Action { TRACKED_OBJECT_15, SPRINT, + LEFT_EYE, + RIGHT_EYE, + LEFT_EYE_BLINK, + RIGHT_EYE_BLINK, + NUM_ACTIONS }; From 5c570d28a0cb6db782aa6c75a12a7db734c1e8fd Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 Sep 2019 18:12:19 -0700 Subject: [PATCH 53/68] remove old/unused eyetracker code --- interface/src/Application.h | 7 ---- .../src/avatars-renderer/Head.cpp | 13 ++++--- libraries/avatars/src/AvatarData.cpp | 15 ++++---- libraries/avatars/src/HeadData.cpp | 37 +++++++++++++++++++ libraries/avatars/src/HeadData.h | 30 +++++++-------- 5 files changed, 66 insertions(+), 36 deletions(-) diff --git a/interface/src/Application.h b/interface/src/Application.h index cd867598c0..af2348d1e9 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -437,13 +437,6 @@ public slots: void sendWrongProtocolVersionsSignature(bool checked) { ::sendWrongProtocolVersionsSignature(checked); } #endif -#ifdef HAVE_IVIEWHMD - void setActiveEyeTracker(); - void calibrateEyeTracker1Point(); - void calibrateEyeTracker3Points(); - void calibrateEyeTracker5Points(); -#endif - static void showHelp(); void cycleCamera(); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Head.cpp b/libraries/avatars-renderer/src/avatars-renderer/Head.cpp index a551793ab0..445184f5f8 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Head.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Head.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include "Logging.h" @@ -58,7 +57,7 @@ void Head::simulate(float deltaTime) { _longTermAverageLoudness = glm::mix(_longTermAverageLoudness, _averageLoudness, glm::min(deltaTime / AUDIO_LONG_TERM_AVERAGING_SECS, 1.0f)); } - if (!_isEyeTrackerConnected) { + if (getHasProceduralEyeMovement()) { // Update eye saccades const float AVERAGE_MICROSACCADE_INTERVAL = 1.0f; const float AVERAGE_SACCADE_INTERVAL = 6.0f; @@ -82,6 +81,7 @@ void Head::simulate(float deltaTime) { const float FULLY_OPEN = 0.0f; const float FULLY_CLOSED = 1.0f; if (getHasProceduralBlinkFaceMovement()) { + // handle automatic blinks // Detect transition from talking to not; force blink after that and a delay bool forceBlink = false; const float TALKING_LOUDNESS = 150.0f; @@ -129,7 +129,7 @@ void Head::simulate(float deltaTime) { _leftEyeBlink = FULLY_OPEN; } - // use data to update fake Faceshift blendshape coefficients + // use data to update fake Faceshift blendshape coefficients if (getHasAudioEnabledFaceMovement()) { // Update audio attack data for facial animation (eyebrows and mouth) float audioAttackAveragingRate = (10.0f - deltaTime * NORMAL_HZ) / 10.0f; // --> 0.9 at 60 Hz @@ -152,7 +152,8 @@ void Head::simulate(float deltaTime) { _mouthTime = 0.0f; } - FaceTracker::updateFakeCoefficients(_leftEyeBlink, + FaceTracker::updateFakeCoefficients( + _leftEyeBlink, _rightEyeBlink, _browAudioLift, _audioJawOpen, @@ -162,6 +163,8 @@ void Head::simulate(float deltaTime) { _transientBlendshapeCoefficients); if (getHasProceduralEyeFaceMovement()) { + // This controls two things, the eye brow and the upper eye lid, it is driven by the vertical up/down angle of the + // eyes relative to the head. This is to try to help prevent sleepy eyes/crazy eyes. applyEyelidOffset(getOrientation()); } @@ -292,7 +295,7 @@ glm::quat Head::getFinalOrientationInLocalFrame() const { } // Everyone else's head keeps track of a lookAtPosition that everybody sees the same, and refers to where that head -// is looking in model space -- e.g., at someone's eyeball, or between their eyes, or mouth, etc. Everyon's Interface +// is looking in model space -- e.g., at someone's eyeball, or between their eyes, or mouth, etc. Everyone's Interface // will have the same value for the lookAtPosition of any given head. // // Everyone else's head also keeps track of a correctedLookAtPosition that may be different for the same head within diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c03f9430be..a91154ff15 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -245,9 +245,10 @@ QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail, bool dro } QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, - const QVector& lastSentJointData, - AvatarDataPacket::SendStatus& sendStatus, bool dropFaceTracking, bool distanceAdjust, - glm::vec3 viewerPosition, QVector* sentJointDataOut, int maxDataSize, AvatarDataRate* outboundDataRateOut) const { + const QVector& lastSentJointData, AvatarDataPacket::SendStatus& sendStatus, + bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, + QVector* sentJointDataOut, + int maxDataSize, AvatarDataRate* outboundDataRateOut) const { bool cullSmallChanges = (dataDetail == CullSmallData); bool sendAll = (dataDetail == SendAllData); @@ -532,7 +533,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent setAtBit16(flags, IS_FACE_TRACKER_CONNECTED); } // eye tracker state - if (_headData->_isEyeTrackerConnected) { + if (!_headData->_hasProceduralEyeMovement) { setAtBit16(flags, IS_EYE_TRACKER_CONNECTED); } // referential state @@ -1150,7 +1151,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { + (oneAtBit16(bitItems, HAND_STATE_FINGER_POINTING_BIT) ? IS_FINGER_POINTING_FLAG : 0); auto newFaceTrackerConnected = oneAtBit16(bitItems, IS_FACE_TRACKER_CONNECTED); - auto newEyeTrackerConnected = oneAtBit16(bitItems, IS_EYE_TRACKER_CONNECTED); + auto newHasntProceduralEyeMovement = oneAtBit16(bitItems, IS_EYE_TRACKER_CONNECTED); auto newHasAudioEnabledFaceMovement = oneAtBit16(bitItems, AUDIO_ENABLED_FACE_MOVEMENT); auto newHasProceduralEyeFaceMovement = oneAtBit16(bitItems, PROCEDURAL_EYE_FACE_MOVEMENT); @@ -1161,7 +1162,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { bool keyStateChanged = (_keyState != newKeyState); bool handStateChanged = (_handState != newHandState); bool faceStateChanged = (_headData->_isFaceTrackerConnected != newFaceTrackerConnected); - bool eyeStateChanged = (_headData->_isEyeTrackerConnected != newEyeTrackerConnected); + bool eyeStateChanged = (_headData->_hasProceduralEyeMovement == newHasntProceduralEyeMovement); bool audioEnableFaceMovementChanged = (_headData->getHasAudioEnabledFaceMovement() != newHasAudioEnabledFaceMovement); bool proceduralEyeFaceMovementChanged = (_headData->getHasProceduralEyeFaceMovement() != newHasProceduralEyeFaceMovement); bool proceduralBlinkFaceMovementChanged = (_headData->getHasProceduralBlinkFaceMovement() != newHasProceduralBlinkFaceMovement); @@ -1174,7 +1175,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { _keyState = newKeyState; _handState = newHandState; _headData->_isFaceTrackerConnected = newFaceTrackerConnected; - _headData->_isEyeTrackerConnected = newEyeTrackerConnected; + _headData->setHasProceduralEyeMovement(!newHasntProceduralEyeMovement); _headData->setHasAudioEnabledFaceMovement(newHasAudioEnabledFaceMovement); _headData->setHasProceduralEyeFaceMovement(newHasProceduralEyeFaceMovement); _headData->setHasProceduralBlinkFaceMovement(newHasProceduralBlinkFaceMovement); diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index 19f5efcd16..c86e534929 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -196,3 +196,40 @@ void HeadData::fromJson(const QJsonObject& json) { setHeadOrientation(quatFromJsonValue(json[JSON_AVATAR_HEAD_ROTATION])); } } + +bool HeadData::getHasProceduralEyeFaceMovement() const { + return _hasProceduralEyeFaceMovement; +} + +void HeadData::setHasProceduralEyeFaceMovement(bool hasProceduralEyeFaceMovement) { + _hasProceduralEyeFaceMovement = hasProceduralEyeFaceMovement; +} + +bool HeadData::getHasProceduralBlinkFaceMovement() const { + // return _hasProceduralBlinkFaceMovement; + return _hasProceduralBlinkFaceMovement && !_isFaceTrackerConnected; +} + +void HeadData::setHasProceduralBlinkFaceMovement(bool hasProceduralBlinkFaceMovement) { + _hasProceduralBlinkFaceMovement = hasProceduralBlinkFaceMovement; +} + +bool HeadData::getHasAudioEnabledFaceMovement() const { + return _hasAudioEnabledFaceMovement; +} + +void HeadData::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) { + _hasAudioEnabledFaceMovement = hasAudioEnabledFaceMovement; +} + +bool HeadData::getHasProceduralEyeMovement() const { + return _hasProceduralEyeMovement; +} + +void HeadData::setHasProceduralEyeMovement(bool hasProceduralEyeMovement) { + _hasProceduralEyeMovement = hasProceduralEyeMovement; +} + +void HeadData::setFaceTrackerConnected(bool value) { + _isFaceTrackerConnected = value; +} diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index 6d211da2cd..dc5aaf2595 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -72,23 +72,17 @@ public: } bool lookAtPositionChangedSince(quint64 time) { return _lookAtPositionChanged >= time; } - bool getHasProceduralEyeFaceMovement() const { return _hasProceduralEyeFaceMovement; } + bool getHasProceduralEyeFaceMovement() const; + void setHasProceduralEyeFaceMovement(bool hasProceduralEyeFaceMovement); + bool getHasProceduralBlinkFaceMovement() const; + void setHasProceduralBlinkFaceMovement(bool hasProceduralBlinkFaceMovement); + bool getHasAudioEnabledFaceMovement() const; + void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement); + bool getHasProceduralEyeMovement() const; + void setHasProceduralEyeMovement(bool hasProceduralEyeMovement); - void setHasProceduralEyeFaceMovement(const bool hasProceduralEyeFaceMovement) { - _hasProceduralEyeFaceMovement = hasProceduralEyeFaceMovement; - } - - bool getHasProceduralBlinkFaceMovement() const { return _hasProceduralBlinkFaceMovement; } - - void setHasProceduralBlinkFaceMovement(const bool hasProceduralBlinkFaceMovement) { - _hasProceduralBlinkFaceMovement = hasProceduralBlinkFaceMovement; - } - - bool getHasAudioEnabledFaceMovement() const { return _hasAudioEnabledFaceMovement; } - - void setHasAudioEnabledFaceMovement(const bool hasAudioEnabledFaceMovement) { - _hasAudioEnabledFaceMovement = hasAudioEnabledFaceMovement; - } + void setFaceTrackerConnected(bool value); + bool getFaceTrackerConnected() const { return _isFaceTrackerConnected; } friend class AvatarData; @@ -107,8 +101,10 @@ protected: bool _hasAudioEnabledFaceMovement { true }; bool _hasProceduralBlinkFaceMovement { true }; bool _hasProceduralEyeFaceMovement { true }; + bool _hasProceduralEyeMovement { true }; + bool _isFaceTrackerConnected { false }; - bool _isEyeTrackerConnected { false }; + float _leftEyeBlink { 0.0f }; float _rightEyeBlink { 0.0f }; float _averageLoudness { 0.0f }; From a288c0a52d73a9974283aa7e751885828672cbdd Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 Sep 2019 18:14:27 -0700 Subject: [PATCH 54/68] remove old/unused eyetracker code. --- interface/src/Application.cpp | 143 +--------- interface/src/avatar/MyAvatar.cpp | 244 +++++++++++++++++- interface/src/avatar/MyAvatar.h | 8 + interface/src/avatar/MySkeletonModel.cpp | 13 +- .../src/avatars-renderer/SkeletonModel.cpp | 35 ++- .../src/avatars-renderer/SkeletonModel.h | 3 + 6 files changed, 284 insertions(+), 162 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b719f26c68..a6b903ec09 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -60,6 +60,7 @@ #include #include #include +#include #include #include #include @@ -154,7 +155,6 @@ #include #include #include -#include #include #include #include @@ -878,7 +878,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); #endif - DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -1997,12 +1996,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(ddeTracker.data(), &FaceTracker::muteToggled, this, &Application::faceTrackerMuteToggled); #endif -#ifdef HAVE_IVIEWHMD - auto eyeTracker = DependencyManager::get(); - eyeTracker->init(); - setActiveEyeTracker(); -#endif - // If launched from Steam, let it handle updates const QString HIFI_NO_UPDATER_COMMAND_LINE_KEY = "--no-updater"; bool noUpdater = arguments().indexOf(HIFI_NO_UPDATER_COMMAND_LINE_KEY) != -1; @@ -2746,9 +2739,6 @@ void Application::cleanupBeforeQuit() { // Stop third party processes so that they're not left running in the event of a subsequent shutdown crash. #ifdef HAVE_DDE DependencyManager::get()->setEnabled(false); -#endif -#ifdef HAVE_IVIEWHMD - DependencyManager::get()->setEnabled(false, true); #endif AnimDebugDraw::getInstance().shutdown(); @@ -2823,9 +2813,6 @@ void Application::cleanupBeforeQuit() { #ifdef HAVE_DDE DependencyManager::destroy(); #endif -#ifdef HAVE_IVIEWHMD - DependencyManager::destroy(); -#endif DependencyManager::destroy(); // Must be destroyed before TabletScriptingInterface @@ -2834,7 +2821,7 @@ void Application::cleanupBeforeQuit() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); - + DependencyManager::destroy(); _snapshotSoundInjector = nullptr; @@ -5328,35 +5315,6 @@ void Application::setActiveFaceTracker() const { #endif } -#ifdef HAVE_IVIEWHMD -void Application::setActiveEyeTracker() { - auto eyeTracker = DependencyManager::get(); - if (!eyeTracker->isInitialized()) { - return; - } - - bool isEyeTracking = Menu::getInstance()->isOptionChecked(MenuOption::SMIEyeTracking); - bool isSimulating = Menu::getInstance()->isOptionChecked(MenuOption::SimulateEyeTracking); - eyeTracker->setEnabled(isEyeTracking, isSimulating); - - Menu::getInstance()->getActionForOption(MenuOption::OnePointCalibration)->setEnabled(isEyeTracking && !isSimulating); - Menu::getInstance()->getActionForOption(MenuOption::ThreePointCalibration)->setEnabled(isEyeTracking && !isSimulating); - Menu::getInstance()->getActionForOption(MenuOption::FivePointCalibration)->setEnabled(isEyeTracking && !isSimulating); -} - -void Application::calibrateEyeTracker1Point() { - DependencyManager::get()->calibrate(1); -} - -void Application::calibrateEyeTracker3Points() { - DependencyManager::get()->calibrate(3); -} - -void Application::calibrateEyeTracker5Points() { - DependencyManager::get()->calibrate(5); -} -#endif - bool Application::exportEntities(const QString& filename, const QVector& entityIDs, const glm::vec3* givenOffset) { @@ -5830,8 +5788,8 @@ void Application::pushPostUpdateLambda(void* key, const std::function& f _postUpdateLambdas[key] = func; } -// Called during Application::update immediately before AvatarManager::updateMyAvatar, updating my data that is then sent to everyone. -// (Maybe this code should be moved there?) +// Called during Application::update immediately before AvatarManager::updateMyAvatar, updating my data that is then sent +// to everyone. // The principal result is to call updateLookAtTargetAvatar() and then setLookAtPosition(). // Note that it is called BEFORE we update position or joints based on sensors, etc. void Application::updateMyAvatarLookAtPosition() { @@ -5840,91 +5798,8 @@ void Application::updateMyAvatarLookAtPosition() { PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()"); auto myAvatar = getMyAvatar(); - myAvatar->updateLookAtTargetAvatar(); FaceTracker* faceTracker = getActiveFaceTracker(); - auto eyeTracker = DependencyManager::get(); - - bool isLookingAtSomeone = false; - bool isHMD = qApp->isHMDMode(); - glm::vec3 lookAtSpot; - if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) { - // Look at the point that the user is looking at. - glm::vec3 lookAtPosition = eyeTracker->getLookAtPosition(); - if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - lookAtPosition.x = -lookAtPosition.x; - } - if (isHMD) { - // TODO -- this code is probably wrong, getHeadPose() returns something in sensor frame, not avatar - glm::mat4 headPose = getActiveDisplayPlugin()->getHeadPose(); - glm::quat hmdRotation = glm::quat_cast(headPose); - lookAtSpot = _myCamera.getPosition() + myAvatar->getWorldOrientation() * (hmdRotation * lookAtPosition); - } else { - lookAtSpot = myAvatar->getHead()->getEyePosition() - + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * lookAtPosition); - } - } else { - AvatarSharedPointer lookingAt = myAvatar->getLookAtTargetAvatar().lock(); - bool haveLookAtCandidate = lookingAt && myAvatar.get() != lookingAt.get(); - auto avatar = static_pointer_cast(lookingAt); - bool mutualLookAtSnappingEnabled = avatar && avatar->getLookAtSnappingEnabled() && myAvatar->getLookAtSnappingEnabled(); - if (haveLookAtCandidate && mutualLookAtSnappingEnabled) { - // If I am looking at someone else, look directly at one of their eyes - isLookingAtSomeone = true; - auto lookingAtHead = avatar->getHead(); - - const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; - glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; - glm::vec3 fromLookingAtToMe = glm::normalize(myAvatar->getHead()->getEyePosition() - - lookingAtHead->getEyePosition()); - float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); - - if (faceAngle < MAXIMUM_FACE_ANGLE) { - // Randomly look back and forth between look targets - eyeContactTarget target = Menu::getInstance()->isOptionChecked(MenuOption::FixGaze) ? - LEFT_EYE : myAvatar->getEyeContactTarget(); - switch (target) { - case LEFT_EYE: - lookAtSpot = lookingAtHead->getLeftEyePosition(); - break; - case RIGHT_EYE: - lookAtSpot = lookingAtHead->getRightEyePosition(); - break; - case MOUTH: - lookAtSpot = lookingAtHead->getMouthPosition(); - break; - } - } else { - // Just look at their head (mid point between eyes) - lookAtSpot = lookingAtHead->getEyePosition(); - } - } else { - // I am not looking at anyone else, so just look forward - auto headPose = myAvatar->getControllerPoseInWorldFrame(controller::Action::HEAD); - if (headPose.isValid()) { - lookAtSpot = transformPoint(headPose.getMatrix(), glm::vec3(0.0f, 0.0f, TREE_SCALE)); - } else { - lookAtSpot = myAvatar->getHead()->getEyePosition() + - (myAvatar->getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE)); - } - } - - // Deflect the eyes a bit to match the detected gaze from the face tracker if active. - if (faceTracker && !faceTracker->isMuted()) { - float eyePitch = faceTracker->getEstimatedEyePitch(); - float eyeYaw = faceTracker->getEstimatedEyeYaw(); - const float GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT = 0.1f; - glm::vec3 origin = myAvatar->getHead()->getEyePosition(); - float deflection = faceTracker->getEyeDeflection(); - if (isLookingAtSomeone) { - deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT; - } - lookAtSpot = origin + _myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3( - eyePitch * deflection, eyeYaw * deflection, 0.0f))) * - glm::inverse(_myCamera.getOrientation()) * (lookAtSpot - origin); - } - } - - myAvatar->getHead()->setLookAtPosition(lookAtSpot); + myAvatar->updateLookAtPosition(faceTracker, _myCamera); } void Application::updateThreads(float deltaTime) { @@ -6496,7 +6371,10 @@ void Application::update(float deltaTime) { controller::Action::LEFT_UP_LEG, controller::Action::RIGHT_UP_LEG, controller::Action::LEFT_TOE_BASE, - controller::Action::RIGHT_TOE_BASE + controller::Action::RIGHT_TOE_BASE, + controller::Action::LEFT_EYE, + controller::Action::RIGHT_EYE + }; // copy controller poses from userInputMapper to myAvatar. @@ -7171,8 +7049,7 @@ void Application::resetSensors(bool andReload) { #ifdef HAVE_DDE DependencyManager::get()->reset(); #endif - - DependencyManager::get()->reset(); + _overlayConductor.centerUI(); getActiveDisplayPlugin()->resetSensors(); getMyAvatar()->reset(true, andReload); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 4d1c20010c..a4000c5233 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -772,6 +772,18 @@ void MyAvatar::update(float deltaTime) { emit energyChanged(currentEnergy); updateEyeContactTarget(deltaTime); + + // if we're getting eye rotations from a tracker, disable observer-side procedural eye motions + auto userInputMapper = DependencyManager::get(); + bool eyesTracked = + userInputMapper->getPoseState(controller::Action::LEFT_EYE).valid && + userInputMapper->getPoseState(controller::Action::RIGHT_EYE).valid; + + int leftEyeJointIndex = getJointIndex("LeftEye"); + int rightEyeJointIndex = getJointIndex("RightEye"); + bool eyesAreOverridden = getIsJointOverridden(leftEyeJointIndex) || getIsJointOverridden(rightEyeJointIndex); + + _headData->setHasProceduralEyeMovement(!(eyesTracked || eyesAreOverridden)); } void MyAvatar::updateEyeContactTarget(float deltaTime) { @@ -1454,8 +1466,50 @@ void MyAvatar::setEnableDebugDrawHandControllers(bool isEnabled) { _enableDebugDrawHandControllers = isEnabled; if (!isEnabled) { - DebugDraw::getInstance().removeMarker("leftHandController"); - DebugDraw::getInstance().removeMarker("rightHandController"); + DebugDraw::getInstance().removeMarker("LEFT_HAND"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND"); + + DebugDraw::getInstance().removeMarker("LEFT_HAND_THUMB1"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_THUMB2"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_THUMB3"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_THUMB4"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_INDEX1"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_INDEX2"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_INDEX3"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_INDEX4"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_MIDDLE1"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_MIDDLE2"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_MIDDLE3"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_MIDDLE4"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_RING1"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_RING2"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_RING3"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_RING4"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_PINKY1"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_PINKY2"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_PINKY3"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_PINKY4"); + + DebugDraw::getInstance().removeMarker("RIGHT_HAND_THUMB1"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_THUMB2"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_THUMB3"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_THUMB4"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_INDEX1"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_INDEX2"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_INDEX3"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_INDEX4"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_MIDDLE1"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_MIDDLE2"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_MIDDLE3"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_MIDDLE4"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_RING1"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_RING2"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_RING3"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_RING4"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_PINKY1"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_PINKY2"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_PINKY3"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_PINKY4"); } } @@ -3097,6 +3151,16 @@ void MyAvatar::animGraphLoaded() { disconnect(&(_skeletonModel->getRig()), SIGNAL(onLoadComplete()), this, SLOT(animGraphLoaded())); } +void MyAvatar::debugDrawPose(controller::Action action, const char* channelName, float size) { + auto pose = getControllerPoseInWorldFrame(action); + if (pose.isValid()) { + DebugDraw::getInstance().addMarker(channelName, pose.getRotation(), pose.getTranslation(), glm::vec4(1), size); + } else { + DebugDraw::getInstance().removeMarker(channelName); + } +} + + void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { Avatar::postUpdate(deltaTime, scene); @@ -3137,20 +3201,50 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { } if (_enableDebugDrawHandControllers) { - auto leftHandPose = getControllerPoseInWorldFrame(controller::Action::LEFT_HAND); - auto rightHandPose = getControllerPoseInWorldFrame(controller::Action::RIGHT_HAND); + debugDrawPose(controller::Action::LEFT_HAND, "LEFT_HAND", 1.0); + debugDrawPose(controller::Action::RIGHT_HAND, "RIGHT_HAND", 1.0); - if (leftHandPose.isValid()) { - DebugDraw::getInstance().addMarker("leftHandController", leftHandPose.getRotation(), leftHandPose.getTranslation(), glm::vec4(1)); - } else { - DebugDraw::getInstance().removeMarker("leftHandController"); - } + debugDrawPose(controller::Action::LEFT_HAND_THUMB1, "LEFT_HAND_THUMB1", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_THUMB2, "LEFT_HAND_THUMB2", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_THUMB3, "LEFT_HAND_THUMB3", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_THUMB4, "LEFT_HAND_THUMB4", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_INDEX1, "LEFT_HAND_INDEX1", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_INDEX2, "LEFT_HAND_INDEX2", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_INDEX3, "LEFT_HAND_INDEX3", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_INDEX4, "LEFT_HAND_INDEX4", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_MIDDLE1, "LEFT_HAND_MIDDLE1", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_MIDDLE2, "LEFT_HAND_MIDDLE2", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_MIDDLE3, "LEFT_HAND_MIDDLE3", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_MIDDLE4, "LEFT_HAND_MIDDLE4", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_RING1, "LEFT_HAND_RING1", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_RING2, "LEFT_HAND_RING2", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_RING3, "LEFT_HAND_RING3", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_RING4, "LEFT_HAND_RING4", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_PINKY1, "LEFT_HAND_PINKY1", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_PINKY2, "LEFT_HAND_PINKY2", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_PINKY3, "LEFT_HAND_PINKY3", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_PINKY4, "LEFT_HAND_PINKY4", 0.1f); - if (rightHandPose.isValid()) { - DebugDraw::getInstance().addMarker("rightHandController", rightHandPose.getRotation(), rightHandPose.getTranslation(), glm::vec4(1)); - } else { - DebugDraw::getInstance().removeMarker("rightHandController"); - } + debugDrawPose(controller::Action::RIGHT_HAND_THUMB1, "RIGHT_HAND_THUMB1", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_THUMB2, "RIGHT_HAND_THUMB2", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_THUMB3, "RIGHT_HAND_THUMB3", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_THUMB4, "RIGHT_HAND_THUMB4", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_INDEX1, "RIGHT_HAND_INDEX1", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_INDEX2, "RIGHT_HAND_INDEX2", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_INDEX3, "RIGHT_HAND_INDEX3", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_INDEX4, "RIGHT_HAND_INDEX4", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_MIDDLE1, "RIGHT_HAND_MIDDLE1", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_MIDDLE2, "RIGHT_HAND_MIDDLE2", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_MIDDLE3, "RIGHT_HAND_MIDDLE3", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_MIDDLE4, "RIGHT_HAND_MIDDLE4", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_RING1, "RIGHT_HAND_RING1", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_RING2, "RIGHT_HAND_RING2", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_RING3, "RIGHT_HAND_RING3", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_RING4, "RIGHT_HAND_RING4", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_PINKY1, "RIGHT_HAND_PINKY1", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_PINKY2, "RIGHT_HAND_PINKY2", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_PINKY3, "RIGHT_HAND_PINKY3", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_PINKY4, "RIGHT_HAND_PINKY4", 0.1f); } DebugDraw::getInstance().updateMyAvatarPos(getWorldPosition()); @@ -6290,3 +6384,125 @@ void MyAvatar::endSit(const glm::vec3& position, const glm::quat& rotation) { }); } } + +bool MyAvatar::getIsJointOverridden(int jointIndex) const { + // has this joint been set by a script? + return _skeletonModel->getIsJointOverridden(jointIndex); +} + +void MyAvatar::updateLookAtPosition(FaceTracker* faceTracker, Camera& myCamera) { + + updateLookAtTargetAvatar(); + + bool isLookingAtSomeone = false; + glm::vec3 lookAtSpot; + + const MyHead* myHead = getMyHead(); + + int leftEyeJointIndex = getJointIndex("LeftEye"); + int rightEyeJointIndex = getJointIndex("RightEye"); + bool eyesAreOverridden = getIsJointOverridden(leftEyeJointIndex) || + getIsJointOverridden(rightEyeJointIndex); + if (eyesAreOverridden) { + // A script has set the eye rotations, so use these to set lookAtSpot + glm::quat leftEyeRotation = getAbsoluteJointRotationInObjectFrame(leftEyeJointIndex); + glm::quat rightEyeRotation = getAbsoluteJointRotationInObjectFrame(rightEyeJointIndex); + glm::vec3 leftVec = getWorldOrientation() * leftEyeRotation * IDENTITY_FORWARD; + glm::vec3 rightVec = getWorldOrientation() * rightEyeRotation * IDENTITY_FORWARD; + glm::vec3 leftEyePosition = myHead->getLeftEyePosition(); + glm::vec3 rightEyePosition = myHead->getRightEyePosition(); + float t1, t2; + bool success = findClosestApproachOfLines(leftEyePosition, leftVec, rightEyePosition, rightVec, t1, t2); + if (success) { + glm::vec3 leftFocus = leftEyePosition + leftVec * t1; + glm::vec3 rightFocus = rightEyePosition + rightVec * t2; + lookAtSpot = (leftFocus + rightFocus) / 2.0f; // average + } else { + lookAtSpot = myHead->getEyePosition() + glm::normalize(leftVec) * 1000.0f; + } + } else { + controller::Pose leftEyePose = getControllerPoseInAvatarFrame(controller::Action::LEFT_EYE); + controller::Pose rightEyePose = getControllerPoseInAvatarFrame(controller::Action::RIGHT_EYE); + if (leftEyePose.isValid() && rightEyePose.isValid()) { + // an eye tracker is in use, set lookAtSpot from this + glm::vec3 leftVec = getWorldOrientation() * leftEyePose.rotation * glm::vec3(0.0f, 0.0f, -1.0f); + glm::vec3 rightVec = getWorldOrientation() * rightEyePose.rotation * glm::vec3(0.0f, 0.0f, -1.0f); + + glm::vec3 leftEyePosition = myHead->getLeftEyePosition(); + glm::vec3 rightEyePosition = myHead->getRightEyePosition(); + float t1, t2; + bool success = findClosestApproachOfLines(leftEyePosition, leftVec, rightEyePosition, rightVec, t1, t2); + if (success) { + glm::vec3 leftFocus = leftEyePosition + leftVec * t1; + glm::vec3 rightFocus = rightEyePosition + rightVec * t2; + lookAtSpot = (leftFocus + rightFocus) / 2.0f; // average + } else { + lookAtSpot = myHead->getEyePosition() + glm::normalize(leftVec) * 1000.0f; + } + } else { + // no script override, no eye tracker, so do procedural eye motion + AvatarSharedPointer lookingAt = getLookAtTargetAvatar().lock(); + bool haveLookAtCandidate = lookingAt && this != lookingAt.get(); + auto avatar = static_pointer_cast(lookingAt); + bool mutualLookAtSnappingEnabled = + avatar && avatar->getLookAtSnappingEnabled() && getLookAtSnappingEnabled(); + if (haveLookAtCandidate && mutualLookAtSnappingEnabled) { + // If I am looking at someone else, look directly at one of their eyes + isLookingAtSomeone = true; + auto lookingAtHead = avatar->getHead(); + + const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; + glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; + glm::vec3 fromLookingAtToMe = glm::normalize(getHead()->getEyePosition() + - lookingAtHead->getEyePosition()); + float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); + + if (faceAngle < MAXIMUM_FACE_ANGLE) { + // Randomly look back and forth between look targets + eyeContactTarget target = Menu::getInstance()->isOptionChecked(MenuOption::FixGaze) ? + LEFT_EYE : getEyeContactTarget(); + switch (target) { + case LEFT_EYE: + lookAtSpot = lookingAtHead->getLeftEyePosition(); + break; + case RIGHT_EYE: + lookAtSpot = lookingAtHead->getRightEyePosition(); + break; + case MOUTH: + lookAtSpot = lookingAtHead->getMouthPosition(); + break; + } + } else { + // Just look at their head (mid point between eyes) + lookAtSpot = lookingAtHead->getEyePosition(); + } + } else { + // I am not looking at anyone else, so just look forward + auto headPose = getControllerPoseInWorldFrame(controller::Action::HEAD); + if (headPose.isValid()) { + lookAtSpot = transformPoint(headPose.getMatrix(), glm::vec3(0.0f, 0.0f, TREE_SCALE)); + } else { + lookAtSpot = myHead->getEyePosition() + + (getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE)); + } + } + + // Deflect the eyes a bit to match the detected gaze from the face tracker if active. + if (faceTracker && !faceTracker->isMuted()) { + float eyePitch = faceTracker->getEstimatedEyePitch(); + float eyeYaw = faceTracker->getEstimatedEyeYaw(); + const float GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT = 0.1f; + glm::vec3 origin = myHead->getEyePosition(); + float deflection = faceTracker->getEyeDeflection(); + if (isLookingAtSomeone) { + deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT; + } + lookAtSpot = origin + myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3( + eyePitch * deflection, eyeYaw * deflection, 0.0f))) * + glm::inverse(myCamera.getOrientation()) * (lookAtSpot - origin); + } + } + } + + getHead()->setLookAtPosition(lookAtSpot); +} diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 0108fb5eda..9cc863471a 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -29,10 +29,12 @@ #include #include #include +#include #include "AtRestDetector.h" #include "MyCharacterController.h" #include "RingBufferHistory.h" +#include "devices/DdeFaceTracker.h" class AvatarActionHold; class ModelItemID; @@ -1864,6 +1866,8 @@ public: bool getFlowActive() const; bool getNetworkGraphActive() const; + void updateLookAtPosition(FaceTracker* faceTracker, Camera& myCamera); + // sets the reaction enabled and triggered parameters of the passed in params // also clears internal reaction triggers void updateRigControllerParameters(Rig::ControllerParameters& params); @@ -1871,6 +1875,10 @@ public: // Don't substitute verify-fail: virtual const QUrl& getSkeletonModelURL() const override { return _skeletonModelURL; } + void debugDrawPose(controller::Action action, const char* channelName, float size); + + bool getIsJointOverridden(int jointIndex) const; + public slots: /**jsdoc diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 0229eeee76..38065c8095 100755 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -114,13 +114,12 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { Head* head = _owningAvatar->getHead(); - // make sure lookAt is not too close to face (avoid crosseyes) - glm::vec3 lookAt = head->getLookAtPosition(); - glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition(); - float focusDistance = glm::length(focusOffset); - const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f; - if (focusDistance < MIN_LOOK_AT_FOCUS_DISTANCE && focusDistance > EPSILON) { - lookAt = _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset; + bool eyePosesValid = !head->getHasProceduralEyeMovement(); + glm::vec3 lookAt; + if (eyePosesValid) { + lookAt = head->getLookAtPosition(); // don't apply no-crosseyes code when eyes are being tracked + } else { + lookAt = avoidCrossedEyes(head->getLookAtPosition()); } MyAvatar* myAvatar = static_cast(_owningAvatar); diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index 40b65c54a1..e0fed08955 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -93,19 +93,30 @@ void SkeletonModel::initJointStates() { emit skeletonLoaded(); } +glm::vec3 SkeletonModel::avoidCrossedEyes(const glm::vec3& lookAt) { + // make sure lookAt is not too close to face (avoid crosseyes) + glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition(); + float focusDistance = glm::length(focusOffset); + const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f; + if (focusDistance < MIN_LOOK_AT_FOCUS_DISTANCE && focusDistance > EPSILON) { + return _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset; + } else { + return lookAt; + } +} + // Called within Model::simulate call, below. void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { assert(!_owningAvatar->isMyAvatar()); Head* head = _owningAvatar->getHead(); - // make sure lookAt is not too close to face (avoid crosseyes) - glm::vec3 lookAt = head->getCorrectedLookAtPosition(); - glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition(); - float focusDistance = glm::length(focusOffset); - const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f; - if (focusDistance < MIN_LOOK_AT_FOCUS_DISTANCE && focusDistance > EPSILON) { - lookAt = _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset; + bool eyePosesValid = !head->getHasProceduralEyeMovement(); + glm::vec3 lookAt; + if (eyePosesValid) { + lookAt = head->getLookAtPosition(); // don't apply no-crosseyes code etc when eyes are being tracked + } else { + lookAt = avoidCrossedEyes(head->getCorrectedLookAtPosition()); } // no need to call Model::updateRig() because otherAvatars get their joint state @@ -288,6 +299,15 @@ bool SkeletonModel::getEyeModelPositions(glm::vec3& firstEyePosition, glm::vec3& return false; } + +bool SkeletonModel::getIsJointOverridden(int jointIndex) const { + // has this joint been set by a script? + if (!isLoaded() || _rig.jointStatesEmpty()) { + return false; + } + return _rig.getIsJointOverridden(jointIndex); +} + bool SkeletonModel::getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const { if (getEyeModelPositions(firstEyePosition, secondEyePosition)) { firstEyePosition = _translation + _rotation * firstEyePosition; @@ -352,4 +372,3 @@ bool SkeletonModel::hasSkeleton() { void SkeletonModel::onInvalidate() { } - diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h index 99f6632306..6b0bd79f0b 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h @@ -37,9 +37,12 @@ public: void initJointStates() override; void simulate(float deltaTime, bool fullUpdate = true) override; + glm::vec3 avoidCrossedEyes(const glm::vec3& lookAt); void updateRig(float deltaTime, glm::mat4 parentTransform) override; void updateAttitude(const glm::quat& orientation); + bool getIsJointOverridden(int jointIndex) const; + /// Returns the index of the left hand joint, or -1 if not found. int getLeftHandJointIndex() const { return isActive() ? _rig.indexOfJoint("LeftHand") : -1; } From 8875453585e18b42af7ea7f3fe1a0b6417ce8e2a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 Sep 2019 18:15:19 -0700 Subject: [PATCH 55/68] remove old/unused eyetracker code --- cmake/modules/FindiViewHMD.cmake | 38 --- interface/external/iViewHMD/readme.txt | 14 - .../trackers/src/trackers/EyeTracker.cpp | 307 ------------------ libraries/trackers/src/trackers/EyeTracker.h | 68 ---- 4 files changed, 427 deletions(-) delete mode 100644 cmake/modules/FindiViewHMD.cmake delete mode 100644 interface/external/iViewHMD/readme.txt delete mode 100644 libraries/trackers/src/trackers/EyeTracker.cpp delete mode 100644 libraries/trackers/src/trackers/EyeTracker.h diff --git a/cmake/modules/FindiViewHMD.cmake b/cmake/modules/FindiViewHMD.cmake deleted file mode 100644 index e408c92380..0000000000 --- a/cmake/modules/FindiViewHMD.cmake +++ /dev/null @@ -1,38 +0,0 @@ -# -# FindiViewHMD.cmake -# -# Try to find the SMI iViewHMD eye tracker library -# -# You must provide a IVIEWHMD_ROOT_DIR which contains 3rdParty, include, and libs directories -# -# Once done this will define -# -# IVIEWHMD_FOUND - system found iViewHMD -# IVIEWHMD_INCLUDE_DIRS - the iViewHMD include directory -# IVIEWHMD_LIBRARIES - link this to use iViewHMD -# -# Created on 27 Jul 2015 by David Rowe -# Copyright 2015 High Fidelity, Inc. -# - -if (WIN32) - - include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") - hifi_library_search_hints("iViewHMD") - - find_path(IVIEWHMD_INCLUDE_DIRS iViewHMDAPI.h PATH_SUFFIXES include HINTS ${IVIEWHMD_SEARCH_DIRS}) - find_library(IVIEWHMD_LIBRARIES NAMES iViewHMDAPI PATH_SUFFIXES libs/x86 HINTS ${IVIEWHMD_SEARCH_DIRS}) - find_path(IVIEWHMD_API_DLL_PATH iViewHMDAPI.dll PATH_SUFFIXES libs/x86 HINTS ${IVIEWHMD_SEARCH_DIRS}) - list(APPEND IVIEWHMD_REQUIREMENTS IVIEWHMD_INCLUDE_DIRS IVIEWHMD_LIBRARIES IVIEWHMD_API_DLL_PATH) - - find_path(IVIEWHMD_DLL_PATH_3RD_PARTY libiViewNG.dll PATH_SUFFIXES 3rdParty HINTS ${IVIEWHMD_SEARCH_DIRS}) - list(APPEND IVIEWHMD_REQUIREMENTS IVIEWHMD_DLL_PATH_3RD_PARTY) - - include(FindPackageHandleStandardArgs) - find_package_handle_standard_args(IVIEWHMD DEFAULT_MSG ${IVIEWHMD_REQUIREMENTS}) - - add_paths_to_fixup_libs(${IVIEWHMD_API_DLL_PATH} ${IVIEWHMD_DLL_PATH_3RD_PARTY}) - - mark_as_advanced(IVIEWHMD_INCLUDE_DIRS IVIEWHMD_LIBRARIES IVIEWHMD_SEARCH_DIRS) - -endif() diff --git a/interface/external/iViewHMD/readme.txt b/interface/external/iViewHMD/readme.txt deleted file mode 100644 index 4b3d59349b..0000000000 --- a/interface/external/iViewHMD/readme.txt +++ /dev/null @@ -1,14 +0,0 @@ - -Instructions for adding SMI HMD Eye Tracking to Interface on Windows -David Rowe, 27 Jul 2015. - -1. Download and install the SMI HMD Eye Tracking software from http://update.smivision.com/iViewNG-HMD.exe. - -2. Copy the SDK folders (3rdParty, include, libs) from the SDK installation folder C:\Program Files (x86)\SMI\iViewNG-HMD\SDK - into the interface/externals/iViewHMD folder. This readme.txt should be there as well. - - You may optionally choose to copy the SDK folders to a location outside the repository (so you can re-use with different - checkouts and different projects). If so, set the ENV variable "HIFI_LIB_DIR" to a directory containing a subfolder - "iViewHMD" that contains the folders mentioned above. - -3. Clear your build directory, run cmake and build, and you should be all set. diff --git a/libraries/trackers/src/trackers/EyeTracker.cpp b/libraries/trackers/src/trackers/EyeTracker.cpp deleted file mode 100644 index a64b945c55..0000000000 --- a/libraries/trackers/src/trackers/EyeTracker.cpp +++ /dev/null @@ -1,307 +0,0 @@ -// -// Created by David Rowe on 27 Jul 2015. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "EyeTracker.h" - -#include -#include - -#include - -#include "Logging.h" -#include - -#ifdef HAVE_IVIEWHMD -char* HIGH_FIDELITY_EYE_TRACKER_CALIBRATION = "HighFidelityEyeTrackerCalibration"; -#endif - -#ifdef HAVE_IVIEWHMD -static void CALLBACK eyeTrackerCallback(smi_CallbackDataStruct* data) { - auto eyeTracker = DependencyManager::get(); - if (eyeTracker) { // Guard against a few callbacks that continue to be received after smi_quit(). - eyeTracker->processData(data); - } -} -#endif - -EyeTracker::~EyeTracker() { -#ifdef HAVE_IVIEWHMD - if (_isStreaming) { - int result = smi_quit(); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error terminating tracking:" << smiReturnValueToString(result); - } - } -#endif -} - -#ifdef HAVE_IVIEWHMD -void EyeTracker::processData(smi_CallbackDataStruct* data) { - _lastProcessDataTimestamp = usecTimestampNow(); - - if (!_isEnabled) { - return; - } - - if (data->type == SMI_SIMPLE_GAZE_SAMPLE) { - // Calculate the intersections of the left and right eye look-at vectors with a vertical plane along the monocular - // gaze direction. Average these positions to give the look-at point. - // If the eyes are parallel or diverged, gaze at a distant look-at point calculated the same as for non eye tracking. - // Line-plane intersection: https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection - - smi_SampleHMDStruct* sample = (smi_SampleHMDStruct*)data->result; - // The iViewHMD coordinate system has x and z axes reversed compared to Interface, i.e., wearing the HMD: - // - x is left - // - y is up - // - z is forwards - - // Plane - smi_Vec3d point = sample->gazeBasePoint; // mm - smi_Vec3d direction = sample->gazeDirection; - glm::vec3 planePoint = glm::vec3(-point.x, point.y, -point.z) / 1000.0f; - glm::vec3 planeNormal = glm::vec3(-direction.z, 0.0f, direction.x); - glm::vec3 monocularDirection = glm::vec3(-direction.x, direction.y, -direction.z); - - // Left eye - point = sample->left.gazeBasePoint; // mm - direction = sample->left.gazeDirection; - glm::vec3 leftLinePoint = glm::vec3(-point.x, point.y, -point.z) / 1000.0f; - glm::vec3 leftLineDirection = glm::vec3(-direction.x, direction.y, -direction.z); - - // Right eye - point = sample->right.gazeBasePoint; // mm - direction = sample->right.gazeDirection; - glm::vec3 rightLinePoint = glm::vec3(-point.x, point.y, -point.z) / 1000.0f; - glm::vec3 rightLineDirection = glm::vec3(-direction.x, direction.y, -direction.z); - - // Plane - line dot products - float leftLinePlaneDotProduct = glm::dot(leftLineDirection, planeNormal); - float rightLinePlaneDotProduct = glm::dot(rightLineDirection, planeNormal); - - // Gaze into distance if eyes are parallel or diverged; otherwise the look-at is the average of look-at points - glm::vec3 lookAtPosition; - if (abs(leftLinePlaneDotProduct) <= FLT_EPSILON || abs(rightLinePlaneDotProduct) <= FLT_EPSILON) { - lookAtPosition = monocularDirection * (float)TREE_SCALE; - } else { - float leftDistance = glm::dot(planePoint - leftLinePoint, planeNormal) / leftLinePlaneDotProduct; - float rightDistance = glm::dot(planePoint - rightLinePoint, planeNormal) / rightLinePlaneDotProduct; - if (leftDistance <= 0.0f || rightDistance <= 0.0f - || leftDistance > (float)TREE_SCALE || rightDistance > (float)TREE_SCALE) { - lookAtPosition = monocularDirection * (float)TREE_SCALE; - } else { - glm::vec3 leftIntersectionPoint = leftLinePoint + leftDistance * leftLineDirection; - glm::vec3 rightIntersectionPoint = rightLinePoint + rightDistance * rightLineDirection; - lookAtPosition = (leftIntersectionPoint + rightIntersectionPoint) / 2.0f; - } - } - - if (glm::isnan(lookAtPosition.x) || glm::isnan(lookAtPosition.y) || glm::isnan(lookAtPosition.z)) { - return; - } - - _lookAtPosition = lookAtPosition; - } -} -#endif - -void EyeTracker::init() { - if (_isInitialized) { - qCWarning(trackers) << "Eye Tracker: Already initialized"; - return; - } -} - -#ifdef HAVE_IVIEWHMD -int EyeTracker::startStreaming(bool simulate) { - return smi_startStreaming(simulate); // This call blocks execution. -} -#endif - -#ifdef HAVE_IVIEWHMD -void EyeTracker::onStreamStarted() { - if (!_isInitialized) { - return; - } - - int result = _startStreamingWatcher.result(); - _isStreaming = (result == SMI_RET_SUCCESS); - - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error starting streaming:" << smiReturnValueToString(result); - // Display error dialog unless SMI SDK has already displayed an error message. - if (result != SMI_ERROR_HMD_NOT_SUPPORTED) { - OffscreenUi::asyncWarning(nullptr, "Eye Tracker Error", smiReturnValueToString(result)); - } - } else { - qCDebug(interfaceapp) << "Eye Tracker: Started streaming"; - } - - if (_isStreaming) { - // Automatically load calibration if one has been saved. - QString availableCalibrations = QString(smi_getAvailableCalibrations()); - if (availableCalibrations.contains(HIGH_FIDELITY_EYE_TRACKER_CALIBRATION)) { - result = smi_loadCalibration(HIGH_FIDELITY_EYE_TRACKER_CALIBRATION); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error loading calibration:" << smiReturnValueToString(result); - OffscreenUi::asyncWarning(nullptr, "Eye Tracker Error", "Error loading calibration" - + smiReturnValueToString(result)); - } else { - qCDebug(interfaceapp) << "Eye Tracker: Loaded calibration"; - } - } - } -} -#endif - -void EyeTracker::setEnabled(bool enabled, bool simulate) { - if (enabled && !_isInitialized) { -#ifdef HAVE_IVIEWHMD - int result = smi_setCallback(eyeTrackerCallback); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error setting callback:" << smiReturnValueToString(result); - OffscreenUi::asyncWarning(nullptr, "Eye Tracker Error", smiReturnValueToString(result)); - } else { - _isInitialized = true; - } - - connect(&_startStreamingWatcher, SIGNAL(finished()), this, SLOT(onStreamStarted())); -#endif - } - - if (!_isInitialized) { - return; - } - -#ifdef HAVE_IVIEWHMD - qCDebug(interfaceapp) << "Eye Tracker: Set enabled =" << enabled << ", simulate =" << simulate; - - // There is no smi_stopStreaming() method and after an smi_quit(), streaming cannot be restarted (at least not for - // simulated data). So keep streaming once started in case tracking is re-enabled after stopping. - - // Try to stop streaming if changing whether simulating or not. - if (enabled && _isStreaming && _isStreamSimulating != simulate) { - int result = smi_quit(); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error stopping streaming:" << smiReturnValueToString(result); - } - _isStreaming = false; - } - - if (enabled && !_isStreaming) { - // Start SMI streaming in a separate thread because it blocks. - QFuture future = QtConcurrent::run(this, &EyeTracker::startStreaming, simulate); - _startStreamingWatcher.setFuture(future); - _isStreamSimulating = simulate; - } - - _isEnabled = enabled; - _isSimulating = simulate; - -#endif -} - -void EyeTracker::reset() { - // Nothing to do. -} - -bool EyeTracker::isTracking() const { - static const quint64 ACTIVE_TIMEOUT_USECS = 2000000; // 2 secs - return _isEnabled && (usecTimestampNow() - _lastProcessDataTimestamp < ACTIVE_TIMEOUT_USECS); -} - -#ifdef HAVE_IVIEWHMD -void EyeTracker::calibrate(int points) { - - if (!_isStreaming) { - qCWarning(interfaceapp) << "Eye Tracker: Cannot calibrate because not streaming"; - return; - } - - smi_CalibrationHMDStruct* calibrationHMDStruct; - smi_createCalibrationHMDStruct(&calibrationHMDStruct); - - smi_CalibrationTypeEnum calibrationType; - switch (points) { - case 1: - calibrationType = SMI_ONE_POINT_CALIBRATION; - qCDebug(interfaceapp) << "Eye Tracker: One point calibration"; - break; - case 3: - calibrationType = SMI_THREE_POINT_CALIBRATION; - qCDebug(interfaceapp) << "Eye Tracker: Three point calibration"; - break; - case 5: - calibrationType = SMI_FIVE_POINT_CALIBRATION; - qCDebug(interfaceapp) << "Eye Tracker: Five point calibration"; - break; - default: - qCWarning(interfaceapp) << "Eye Tracker: Invalid calibration specified"; - return; - } - - calibrationHMDStruct->type = calibrationType; - calibrationHMDStruct->backgroundColor->blue = 0.5; - calibrationHMDStruct->backgroundColor->green = 0.5; - calibrationHMDStruct->backgroundColor->red = 0.5; - calibrationHMDStruct->foregroundColor->blue = 1.0; - calibrationHMDStruct->foregroundColor->green = 1.0; - calibrationHMDStruct->foregroundColor->red = 1.0; - - int result = smi_setupCalibration(calibrationHMDStruct); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error setting up calibration:" << smiReturnValueToString(result); - return; - } else { - result = smi_calibrate(); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error performing calibration:" << smiReturnValueToString(result); - } else { - result = smi_saveCalibration(HIGH_FIDELITY_EYE_TRACKER_CALIBRATION); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error saving calibration:" << smiReturnValueToString(result); - } - } - } - - if (result != SMI_RET_SUCCESS) { - OffscreenUi::asyncWarning(nullptr, "Eye Tracker Error", "Calibration error: " + smiReturnValueToString(result)); - } -} -#endif - -#ifdef HAVE_IVIEWHMD -QString EyeTracker::smiReturnValueToString(int value) { - switch (value) - { - case smi_ErrorReturnValue::SMI_ERROR_NO_CALLBACK_SET: - return "No callback set"; - case smi_ErrorReturnValue::SMI_ERROR_CONNECTING_TO_HMD: - return "Error connecting to HMD"; - case smi_ErrorReturnValue::SMI_ERROR_HMD_NOT_SUPPORTED: - return "HMD not supported"; - case smi_ErrorReturnValue::SMI_ERROR_NOT_IMPLEMENTED: - return "Not implmented"; - case smi_ErrorReturnValue::SMI_ERROR_INVALID_PARAMETER: - return "Invalid parameter"; - case smi_ErrorReturnValue::SMI_ERROR_EYECAMERAS_NOT_AVAILABLE: - return "Eye cameras not available"; - case smi_ErrorReturnValue::SMI_ERROR_OCULUS_RUNTIME_NOT_SUPPORTED: - return "Oculus runtime not supported"; - case smi_ErrorReturnValue::SMI_ERROR_FILE_NOT_FOUND: - return "File not found"; - case smi_ErrorReturnValue::SMI_ERROR_FILE_EMPTY: - return "File empty"; - case smi_ErrorReturnValue::SMI_ERROR_UNKNOWN: - return "Unknown error"; - default: - QString number; - number.setNum(value); - return number; - } -} -#endif diff --git a/libraries/trackers/src/trackers/EyeTracker.h b/libraries/trackers/src/trackers/EyeTracker.h deleted file mode 100644 index 9cf35d0f2a..0000000000 --- a/libraries/trackers/src/trackers/EyeTracker.h +++ /dev/null @@ -1,68 +0,0 @@ -// -// Created by David Rowe on 27 Jul 2015. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_EyeTracker_h -#define hifi_EyeTracker_h - -#include -#include - -#include - -#include -#ifdef HAVE_IVIEWHMD -#include -#endif - - -class EyeTracker : public QObject, public Dependency { - Q_OBJECT - SINGLETON_DEPENDENCY - -public: - ~EyeTracker(); - - void init(); - void setEnabled(bool enabled, bool simulate); - void reset(); - - bool isInitialized() const { return _isInitialized; } - bool isEnabled() const { return _isEnabled; } - bool isTracking() const; - bool isSimulating() const { return _isSimulating; } - - glm::vec3 getLookAtPosition() const { return _lookAtPosition; } // From mid eye point in head frame. - -#ifdef HAVE_IVIEWHMD - void processData(smi_CallbackDataStruct* data); - - void calibrate(int points); - - int startStreaming(bool simulate); - -private slots: - void onStreamStarted(); -#endif - -private: - QString smiReturnValueToString(int value); - - bool _isInitialized = false; - bool _isEnabled = false; - bool _isSimulating = false; - bool _isStreaming = false; - bool _isStreamSimulating = false; - - quint64 _lastProcessDataTimestamp; - - glm::vec3 _lookAtPosition; - - QFutureWatcher _startStreamingWatcher; -}; - -#endif // hifi_EyeTracker_h From 58cf51058b61b7440fa5ef38bd5251a6edb74ee6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 17 Sep 2019 09:42:26 -0700 Subject: [PATCH 56/68] code review --- interface/src/avatar/MyHead.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/interface/src/avatar/MyHead.cpp b/interface/src/avatar/MyHead.cpp index b5e8bc2171..1fc134169a 100644 --- a/interface/src/avatar/MyHead.cpp +++ b/interface/src/avatar/MyHead.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include "devices/DdeFaceTracker.h" #include "Application.h" @@ -45,6 +46,11 @@ void MyHead::simulate(float deltaTime) { auto player = DependencyManager::get(); // Only use face trackers when not playing back a recording. if (!player->isPlaying()) { + // TODO -- finish removing face-tracker specific code. To do this, add input channels for + // each blendshape-coefficient and update the various json files to relay them in a useful way. + // After that, input plugins can be used to drive the avatar's face, and the various "DDE" files + // can be ported into the plugin and removed. + // // auto faceTracker = qApp->getActiveFaceTracker(); // const bool hasActualFaceTrackerConnected = faceTracker && !faceTracker->isMuted(); // _isFaceTrackerConnected = hasActualFaceTrackerConnected || _owningAvatar->getHasScriptedBlendshapes(); @@ -63,13 +69,13 @@ void MyHead::simulate(float deltaTime) { float leftEyeBlink = userInputMapper->getActionState(controller::Action::LEFT_EYE_BLINK); float rightEyeBlink = userInputMapper->getActionState(controller::Action::RIGHT_EYE_BLINK); _blendshapeCoefficients.resize(std::max(_blendshapeCoefficients.size(), 2)); - _blendshapeCoefficients[0] = leftEyeBlink; - _blendshapeCoefficients[1] = rightEyeBlink; + _blendshapeCoefficients[EYE_BLINK_L_INDEX] = leftEyeBlink; + _blendshapeCoefficients[EYE_BLINK_R_INDEX] = rightEyeBlink; } else { const float FULLY_OPEN = 0.0f; _blendshapeCoefficients.resize(std::max(_blendshapeCoefficients.size(), 2)); - _blendshapeCoefficients[0] = FULLY_OPEN; - _blendshapeCoefficients[1] = FULLY_OPEN; + _blendshapeCoefficients[EYE_BLINK_L_INDEX] = FULLY_OPEN; + _blendshapeCoefficients[EYE_BLINK_R_INDEX] = FULLY_OPEN; } } Parent::simulate(deltaTime); From 3ece763a8e52c1b14721cbfd8f41f428dca469f3 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 17 Sep 2019 10:52:55 -0700 Subject: [PATCH 57/68] code review --- interface/src/avatar/MyHead.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/src/avatar/MyHead.cpp b/interface/src/avatar/MyHead.cpp index 1fc134169a..e5c8b71ea2 100644 --- a/interface/src/avatar/MyHead.cpp +++ b/interface/src/avatar/MyHead.cpp @@ -69,13 +69,13 @@ void MyHead::simulate(float deltaTime) { float leftEyeBlink = userInputMapper->getActionState(controller::Action::LEFT_EYE_BLINK); float rightEyeBlink = userInputMapper->getActionState(controller::Action::RIGHT_EYE_BLINK); _blendshapeCoefficients.resize(std::max(_blendshapeCoefficients.size(), 2)); - _blendshapeCoefficients[EYE_BLINK_L_INDEX] = leftEyeBlink; - _blendshapeCoefficients[EYE_BLINK_R_INDEX] = rightEyeBlink; + _blendshapeCoefficients[EYE_BLINK_INDICES[0]] = leftEyeBlink; + _blendshapeCoefficients[EYE_BLINK_INDICES[1]] = rightEyeBlink; } else { const float FULLY_OPEN = 0.0f; _blendshapeCoefficients.resize(std::max(_blendshapeCoefficients.size(), 2)); - _blendshapeCoefficients[EYE_BLINK_L_INDEX] = FULLY_OPEN; - _blendshapeCoefficients[EYE_BLINK_R_INDEX] = FULLY_OPEN; + _blendshapeCoefficients[EYE_BLINK_INDICES[0]] = FULLY_OPEN; + _blendshapeCoefficients[EYE_BLINK_INDICES[1]] = FULLY_OPEN; } } Parent::simulate(deltaTime); From 60f4189eb36def23a7aab5e9f8bdd99607013648 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 17 Sep 2019 11:36:12 -0700 Subject: [PATCH 58/68] BUGZ-1485: Fix Copyright date in About dialog --- interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml b/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml index 213540b334..87f307b9bc 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml @@ -106,7 +106,7 @@ Rectangle { } RalewayRegular { color: "white" - text: "© 2018 High Fidelity. All rights reserved." + text: "© 2012 - 2019 High Fidelity, Inc.. All rights reserved." size: 14 } RalewayRegular { From 1157d59f6750a8e3040b7d35ac7a4e6d8ff327df Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 17 Sep 2019 13:40:43 -0700 Subject: [PATCH 59/68] Sit bug fixes * You should not be able to move after being seated, even if you switch seats. * You should not be able to jump out of the chair by holding the space bar. * Fixed small issue with the sitting to standing transition being delayed. (causing the user to look like there were sitting in mid-air) This was due to a missing transition in the animation.json --- .../resources/avatar/avatar-animation.json | 4 ++ interface/src/avatar/MyAvatar.cpp | 42 +++++++++++-------- interface/src/avatar/MyAvatar.h | 3 ++ 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/interface/resources/avatar/avatar-animation.json b/interface/resources/avatar/avatar-animation.json index 1cf4663b1b..738d25e8ee 100644 --- a/interface/resources/avatar/avatar-animation.json +++ b/interface/resources/avatar/avatar-animation.json @@ -4598,6 +4598,10 @@ { "state": "strafeLeftHmd", "var": "isMovingLeftHmd" + }, + { + "state": "idle", + "var": "isNotSeated" } ] }, diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 4d1c20010c..5311d41d76 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -4374,8 +4374,15 @@ float MyAvatar::getRawDriveKey(DriveKeys key) const { } void MyAvatar::relayDriveKeysToCharacterController() { - if (getDriveKey(TRANSLATE_Y) > 0.0f && (!qApp->isHMDMode() || (useAdvancedMovementControls() && getFlyingHMDPref()))) { - _characterController.jump(); + if (_endSitKeyPressComplete) { + if (getDriveKey(TRANSLATE_Y) > 0.0f && (!qApp->isHMDMode() || (useAdvancedMovementControls() && getFlyingHMDPref()))) { + _characterController.jump(); + } + } else { + // used to prevent character from jumping after endSit is called. + if (getDriveKey(TRANSLATE_Y) == 0.0f) { + _endSitKeyPressComplete = true; + } } } @@ -6255,15 +6262,17 @@ void MyAvatar::beginSit(const glm::vec3& position, const glm::quat& rotation) { return; } - _characterController.setSeated(true); - setCollisionsEnabled(false); - setHMDLeanRecenterEnabled(false); - // Disable movement - setSitDriveKeysStatus(false); - centerBody(); - int hipIndex = getJointIndex("Hips"); - clearPinOnJoint(hipIndex); - pinJoint(hipIndex, position, rotation); + if (!_characterController.getSeated()) { + _characterController.setSeated(true); + setCollisionsEnabled(false); + setHMDLeanRecenterEnabled(false); + // Disable movement + setSitDriveKeysStatus(false); + centerBody(); + int hipIndex = getJointIndex("Hips"); + clearPinOnJoint(hipIndex); + pinJoint(hipIndex, position, rotation); + } } void MyAvatar::endSit(const glm::vec3& position, const glm::quat& rotation) { @@ -6281,12 +6290,9 @@ void MyAvatar::endSit(const glm::vec3& position, const glm::quat& rotation) { slamPosition(position); setWorldOrientation(rotation); - // the jump key is used to exit the chair. We add a delay here to prevent - // the avatar from jumping right as they exit the chair. - float TIME_BEFORE_DRIVE_ENABLED_MS = 150.0f; - QTimer::singleShot(TIME_BEFORE_DRIVE_ENABLED_MS, [this]() { - // Enable movement again - setSitDriveKeysStatus(true); - }); + // used to prevent character from jumping after endSit is called. + _endSitKeyPressComplete = false; + + setSitDriveKeysStatus(true); } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 0108fb5eda..a6cc315631 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -2903,6 +2903,9 @@ private: int _reactionEnabledRefCounts[NUM_AVATAR_BEGIN_END_REACTIONS] { 0, 0, 0 }; mutable std::mutex _reactionLock; + + // used to prevent character from jumping after endSit is called. + bool _endSitKeyPressComplete { false }; }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); From 1119b9f29c92ce049a91f6c46abce7edf7d64def Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 17 Sep 2019 15:05:37 -0700 Subject: [PATCH 60/68] update local QueryAACube on deactivation --- libraries/physics/src/EntityMotionState.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index de82dd6ace..7e4723a7d1 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -122,6 +122,13 @@ void EntityMotionState::handleDeactivation() { _body->setWorldTransform(worldTrans); // no need to update velocities... should already be zero } + if (!isLocallyOwned()) { + // HACK: To allow the ESS to move entities around in a kinematic way we had to remove the requirement that + // every moving+simulated entity has an authoritative simulation owner. As a result, we cannot rely + // on a simulation owner to update the QueryAACube on the entity-server. This HACK updates the local + // QueryAACube but the one on the ES will still be broken. + _entity->updateQueryAACube(); + } } // virtual From 54d3ceda284057ea55ecc4dbb4466900ba472ce6 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 17 Sep 2019 15:31:16 -0700 Subject: [PATCH 61/68] update QueryAACube of kinematic things with known parentage --- libraries/entities/src/EntitySimulation.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/libraries/entities/src/EntitySimulation.cpp b/libraries/entities/src/EntitySimulation.cpp index b5e4fed0fd..9f81572a4a 100644 --- a/libraries/entities/src/EntitySimulation.cpp +++ b/libraries/entities/src/EntitySimulation.cpp @@ -242,11 +242,21 @@ void EntitySimulation::moveSimpleKinematics(uint64_t now) { entity->getMaximumAACube(ancestryIsKnown); bool hasAvatarAncestor = entity->hasAncestorOfType(NestableType::Avatar); - if (entity->isMovingRelativeToParent() && !entity->getPhysicsInfo() && ancestryIsKnown && !hasAvatarAncestor) { + bool isMoving = entity->isMovingRelativeToParent(); + if (isMoving && !entity->getPhysicsInfo() && ancestryIsKnown && !hasAvatarAncestor) { entity->simulate(now); + if (ancestryIsKnown && !hasAvatarAncestor) { + entity->updateQueryAACube(); + } _entitiesToSort.insert(entity); ++itemItr; } else { + if (!isMoving && ancestryIsKnown && !hasAvatarAncestor) { + // HACK: This catches most cases where the entity's QueryAACube (and spatial sorting in the EntityTree) + // would otherwise be out of date at conclusion of its "unowned" simpleKinematicMotion. + entity->updateQueryAACube(); + _entitiesToSort.insert(entity); + } // the entity is no longer non-physical-kinematic itemItr = _simpleKinematicEntities.erase(itemItr); } From 5ad0dd20acfabf1f891794b3ab8f50ae15621af6 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 17 Sep 2019 15:43:53 -0700 Subject: [PATCH 62/68] update QueryAACube of unowned moving physical entities --- libraries/physics/src/PhysicalEntitySimulation.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 85c53af10a..43ced16119 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -528,6 +528,8 @@ void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionSta addOwnership(entityState); } else if (entityState->shouldSendBid()) { addOwnershipBid(entityState); + } else { + entityState->getEntity()->updateQueryAACube(); } } } From 434f5ff300bd8814fce2206f681ef6594ab1e416 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 17 Sep 2019 15:57:20 -0700 Subject: [PATCH 63/68] fix comment --- libraries/physics/src/EntityMotionState.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 7e4723a7d1..e48f0603bd 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -125,8 +125,7 @@ void EntityMotionState::handleDeactivation() { if (!isLocallyOwned()) { // HACK: To allow the ESS to move entities around in a kinematic way we had to remove the requirement that // every moving+simulated entity has an authoritative simulation owner. As a result, we cannot rely - // on a simulation owner to update the QueryAACube on the entity-server. This HACK updates the local - // QueryAACube but the one on the ES will still be broken. + // on a simulation owner to update the QueryAACube on the entity-server. _entity->updateQueryAACube(); } } From 59184edc8936cf0b6d470cef4b0ae3bad435cbe8 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 18 Sep 2019 13:31:22 -0700 Subject: [PATCH 64/68] DEV-521: Update Emoji window's search field to display emoji's name during hover --- .../simplifiedControls/TextField.qml | 6 +- .../emojiApp/ui/qml/SimplifiedEmoji.qml | 191 ++++++++++-------- 2 files changed, 108 insertions(+), 89 deletions(-) diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/TextField.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/TextField.qml index fd370be0ec..1dd3a80a52 100644 --- a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/TextField.qml +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/TextField.qml @@ -22,6 +22,8 @@ TextField { } property string rightGlyph: "" + property alias bottomBorderVisible: bottomRectangle.visible + property alias backgroundColor: textFieldBackground.color color: simplifiedUI.colors.text.white font.family: "Graphik Medium" @@ -45,7 +47,9 @@ TextField { } } - background: Item { + background: Rectangle { + id: textFieldBackground + color: Qt.rgba(0, 0, 0, 0); anchors.fill: parent Rectangle { diff --git a/scripts/simplifiedUI/simplifiedEmote/emojiApp/ui/qml/SimplifiedEmoji.qml b/scripts/simplifiedUI/simplifiedEmote/emojiApp/ui/qml/SimplifiedEmoji.qml index dd78b8fe09..a890be46f1 100644 --- a/scripts/simplifiedUI/simplifiedEmote/emojiApp/ui/qml/SimplifiedEmoji.qml +++ b/scripts/simplifiedUI/simplifiedEmote/emojiApp/ui/qml/SimplifiedEmoji.qml @@ -66,6 +66,7 @@ Rectangle { .forEach(function(item, index){ item.code = { utf: item.code[0] } item.keywords = { keywords: item.keywords } + item.shortName = item.shortName mainModel.append(item); filteredModel.append(item); }); @@ -73,6 +74,7 @@ Rectangle { .forEach(function(item, index){ item.code = { utf: item.name } item.keywords = { keywords: item.keywords } + item.shortName = item.name mainModel.append(item); filteredModel.append(item); }); @@ -230,7 +232,7 @@ Rectangle { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - height: 200 + height: 160 clip: true color: simplifiedUI.colors.darkBackground @@ -248,8 +250,8 @@ Rectangle { Image { id: mainEmojiImage - width: 180 - height: 180 + width: emojiIndicatorContainer.width - 20 + height: emojiIndicatorContainer.height - 20 anchors.centerIn: parent source: "" fillMode: Image.PreserveAspectFit @@ -281,6 +283,64 @@ Rectangle { } + Item { + id: searchBarContainer + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: emojiIndicatorContainer.bottom + width: Math.min(parent.width, 420) + height: 48 + + SimplifiedControls.TextField { + id: emojiSearchTextField + readonly property string defaultPlaceholderText: "Search Emojis" + bottomBorderVisible: false + backgroundColor: "#313131" + placeholderText: emojiSearchTextField.defaultPlaceholderText + maximumLength: 100 + clip: true + selectByMouse: true + autoScroll: true + horizontalAlignment: TextInput.AlignHCenter + anchors.left: parent.left + anchors.leftMargin: 16 + anchors.right: parent.right + anchors.rightMargin: 16 + anchors.verticalCenter: parent.verticalCenter + onTextChanged: { + if (text.length === 0) { + root.filterEmoji(emojiSearchTextField.text); + } else { + waitForMoreInputTimer.restart(); + } + } + onAccepted: { + root.filterEmoji(emojiSearchTextField.text); + waitForMoreInputTimer.stop(); + if (filteredModel.count === 1) { + root.selectEmoji(filteredModel.get(0).code.utf); + } else { + grid.forceActiveFocus(); + } + } + + KeyNavigation.backtab: grid + KeyNavigation.tab: grid + } + + Timer { + id: waitForMoreInputTimer + repeat: false + running: false + triggeredOnStart: false + interval: 300 + + onTriggered: { + root.filterEmoji(emojiSearchTextField.text); + } + } + } + + function selectEmoji(code) { sendToScript({ "source": "SimplifiedEmoji.qml", @@ -294,13 +354,45 @@ Rectangle { Rectangle { id: emojiIconListContainer - anchors.top: emojiIndicatorContainer.bottom + anchors.top: searchBarContainer.bottom + anchors.topMargin: 10 anchors.left: parent.left anchors.right: parent.right - anchors.bottom: bottomContainer.top + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 clip: true color: simplifiedUI.colors.darkBackground + Item { + id: helpGlyphContainer + anchors.left: parent.left + anchors.leftMargin: 4 + anchors.bottom: parent.bottom + anchors.bottomMargin: 2 + width: 22 + height: width + + HifiStylesUit.GraphikRegular { + text: "?" + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: simplifiedUI.colors.text.darkGrey + opacity: attributionMouseArea.containsMouse ? 1.0 : 0.8 + size: 22 + } + + MouseArea { + id: attributionMouseArea + hoverEnabled: enabled + anchors.fill: parent + propagateComposedEvents: false + onClicked: { + popupContainer.visible = true; + } + } + } + GridView { id: grid anchors.fill: parent @@ -319,13 +411,18 @@ Rectangle { anchors.fill: parent propagateComposedEvents: false onEntered: { - grid.currentIndex = index + grid.currentIndex = index; // don't allow a hover image change of the main emoji image if (root.isSelected) { return; } // Updates the selected image root.currentCode = model.code.utf; + // Ensures that the placeholder text is visible and updated + if (emojiSearchTextField.text === "") { + grid.forceActiveFocus(); + } + emojiSearchTextField.placeholderText = "::" + model.shortName + "::"; } onClicked: { root.selectEmoji(model.code.utf); @@ -379,88 +476,6 @@ Rectangle { } } - - Item { - id: bottomContainer - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 40 - - SimplifiedControls.TextField { - id: emojiSearchTextField - placeholderText: "Search" - maximumLength: 100 - clip: true - selectByMouse: true - autoScroll: true - anchors.left: parent.left - anchors.leftMargin: 16 - anchors.right: helpGlyphContainer.left - anchors.rightMargin: 16 - anchors.verticalCenter: parent.verticalCenter - onTextChanged: { - if (text.length === 0) { - root.filterEmoji(emojiSearchTextField.text); - } else { - waitForMoreInputTimer.restart(); - } - } - onAccepted: { - root.filterEmoji(emojiSearchTextField.text); - waitForMoreInputTimer.stop(); - if (filteredModel.count === 1) { - root.selectEmoji(filteredModel.get(0).code.utf); - } else { - grid.forceActiveFocus(); - } - } - - KeyNavigation.backtab: grid - KeyNavigation.tab: grid - } - - Timer { - id: waitForMoreInputTimer - repeat: false - running: false - triggeredOnStart: false - interval: 300 - - onTriggered: { - root.filterEmoji(emojiSearchTextField.text); - } - } - - Item { - id: helpGlyphContainer - anchors.right: parent.right - anchors.rightMargin: 8 - width: height - height: parent.height - - HifiStylesUit.GraphikRegular { - text: "?" - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: simplifiedUI.colors.text.darkGrey - opacity: attributionMouseArea.containsMouse ? 1.0 : 0.8 - size: 22 - } - - MouseArea { - id: attributionMouseArea - hoverEnabled: enabled - anchors.fill: parent - propagateComposedEvents: false - onClicked: { - popupContainer.visible = true; - } - } - } - } - function filterEmoji(filterText) { filteredModel.clear(); From f92c5d0882200a73123a24f76cadea63122d28b7 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 18 Sep 2019 14:23:04 -0700 Subject: [PATCH 65/68] DEV-576: Allow users to type HiFi locations into the SimplifiedUI top bar just like GOTO --- .../simplifiedUI/topBar/SimplifiedTopBar.qml | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml b/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml index d87431ea9c..5e82804cf6 100644 --- a/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml +++ b/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml @@ -9,6 +9,7 @@ // import QtQuick 2.10 +import hifi.simplifiedUI.simplifiedControls 1.0 as SimplifiedControls import "../simplifiedConstants" as SimplifiedConstants import "../inputDeviceButton" as InputDeviceButton import stylesUit 1.0 as HifiStylesUit @@ -157,7 +158,7 @@ Rectangle { Image { id: avatarButtonImage - source: "./images/defaultAvatar.svg" + source: "../images/defaultAvatar.svg" anchors.centerIn: parent width: 32 height: width @@ -350,6 +351,48 @@ Rectangle { } + TextMetrics { + id: goToTextFieldMetrics + font: goToTextField.font + text: goToTextField.longPlaceholderText + } + + + Item { + id: goToTextFieldContainer + anchors.left: statusButtonContainer.right + anchors.leftMargin: 12 + anchors.right: (hmdButtonContainer.visible ? hmdButtonContainer.left : helpButtonContainer.left) + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + height: parent.height + + SimplifiedControls.TextField { + id: goToTextField + readonly property string shortPlaceholderText: "Jump to..." + readonly property string longPlaceholderText: "Type the name of a location to quickly jump there..." + anchors.centerIn: parent + width: Math.min(parent.width, 600) + height: parent.height - 11 + leftPadding: 8 + rightPadding: 8 + bottomBorderVisible: false + backgroundColor: "#313131" + placeholderText: width - leftPadding - rightPadding < goToTextFieldMetrics.width ? shortPlaceholderText : longPlaceholderText + clip: true + selectByMouse: true + autoScroll: true + onAccepted: { + if (goToTextField.length > 0) { + AddressManager.handleLookupString(goToTextField.text); + goToTextField.text = ""; + parent.forceActiveFocus(); + } + } + } + } + + Item { id: hmdButtonContainer From 41978aa4a44082bd01c4f88c6362d9854a2174c0 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 18 Sep 2019 14:42:57 -0700 Subject: [PATCH 66/68] Fix additive blending on avatars with non identity scale. Some avatars that have non 1.0 scale values were incorrectly being scaled during additive blends, this fixes that. --- libraries/animation/src/AnimUtil.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp index 8830cb78b1..0a8b2fe286 100644 --- a/libraries/animation/src/AnimUtil.cpp +++ b/libraries/animation/src/AnimUtil.cpp @@ -54,12 +54,14 @@ void blend4(size_t numPoses, const AnimPose* a, const AnimPose* b, const AnimPos // additive blend void blendAdd(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, AnimPose* result) { - const glm::quat identity = glm::quat(); + const glm::vec3 IDENTITY_SCALE = glm::vec3(1.0f); + const glm::quat IDENTITY_ROT = glm::quat(); + for (size_t i = 0; i < numPoses; i++) { const AnimPose& aPose = a[i]; const AnimPose& bPose = b[i]; - result[i].scale() = lerp(aPose.scale(), bPose.scale(), alpha); + result[i].scale() = aPose.scale() * lerp(IDENTITY_SCALE, bPose.scale(), alpha); // ensure that delta has the same "polarity" as the identity quat. // we don't need to do a full dot product, just sign of w is sufficient. @@ -67,7 +69,7 @@ void blendAdd(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha if (delta.w < 0.0f) { delta = -delta; } - delta = glm::lerp(identity, delta, alpha); + delta = glm::lerp(IDENTITY_ROT, delta, alpha); result[i].rot() = glm::normalize(aPose.rot() * delta); result[i].trans() = aPose.trans() + (alpha * bPose.trans()); } From 5e96ce8822d699bf317b7e8de6e99079406fde2e Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 19 Sep 2019 09:48:11 -0700 Subject: [PATCH 67/68] DEV-152: Implement 'Smooth/Snap Turn' setting in SimplifiedUI > Settings > VR --- .../hifi/simplifiedUI/settingsApp/vr/VR.qml | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml b/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml index 5f0fbe49d5..420ee11a05 100644 --- a/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml +++ b/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml @@ -71,7 +71,7 @@ Flickable { ColumnLayout { id: controlsContainer Layout.preferredWidth: parent.width - Layout.topMargin: 24 + Layout.topMargin: 24 spacing: 0 HifiStylesUit.GraphikSemiBold { @@ -154,6 +154,45 @@ Flickable { } } } + + ColumnLayout { + Layout.preferredWidth: parent.width + spacing: 0 + + HifiStylesUit.GraphikSemiBold { + text: "VR Rotation Mode" + Layout.preferredWidth: parent.width + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.white + } + + ColumnLayout { + width: parent.width + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons + + ButtonGroup { id: rotationButtonGroup } + + SimplifiedControls.RadioButton { + text: "Snap Turn" + ButtonGroup.group: rotationButtonGroup + checked: MyAvatar.getSnapTurn() === true + onClicked: { + MyAvatar.setSnapTurn(true); + } + } + + SimplifiedControls.RadioButton { + text: "Smooth Turn" + ButtonGroup.group: rotationButtonGroup + checked: MyAvatar.getSnapTurn() === false + onClicked: { + MyAvatar.setSnapTurn(false); + } + } + } + } ColumnLayout { id: micControlsContainer From 80414a6f1c3bd44f8915cdf39499b9f0718d6268 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 19 Sep 2019 11:17:39 -0700 Subject: [PATCH 68/68] DEV-613: Remove unnecessary audio injector logging --- libraries/audio/src/AudioInjector.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index c09dba6190..2df766377f 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -210,7 +210,6 @@ qint64 writeStringToStream(const QString& string, QDataStream& stream) { int64_t AudioInjector::injectNextFrame() { if (stateHas(AudioInjectorState::NetworkInjectionFinished)) { - qCDebug(audio) << "AudioInjector::injectNextFrame called but AudioInjector has finished and was not restarted. Returning."; return NEXT_FRAME_DELTA_ERROR_OR_FINISHED; }