Showing the changes to plugins/JSAPIExample/src/JSAPIExample.cpp (1 of 5 files). Show all
Unified · Split
| @@ -1,141 +1,218 @@ | ||
| 1 | 1 | // |
| 2 | −// KasenAPIExample.cpp | |
| 3 | −// plugins/KasenAPIExample/src | |
| 2 | +// JSAPIExample.cpp | |
| 3 | +// plugins/JSAPIExample/src | |
| 4 | 4 | // |
| 5 | −// Created by Kasen IO on 2019.07.14 | realities.dev | kasenvr@gmail.com | |
| 6 | −// Copyright 2019 Kasen IO | |
| 7 | −// | |
| 8 | −// Authored by: Humbletim (humbletim@gmail.com) | |
| 5 | +// Copyright (c) 2019-2020 humbletim (humbletim@gmail.com) | |
| 6 | +// Copyright (c) 2019 Kalila L. (kasenvr@gmail.com) | |
| 9 | 7 | // |
| 10 | 8 | // Distributed under the Apache License, Version 2.0. |
| 11 | 9 | // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html |
| 12 | −// | |
| 13 | −// Example of prototyping new JS APIs by leveraging the existing plugin system. | |
| 14 | 10 | |
| 15 | −#include "ExampleScriptPlugin.h" | |
| 11 | +// Example of prototyping new JS APIs by leveraging the existing plugin system. | |
| 16 | 12 | |
| 17 | 13 | #include <QCoreApplication> |
| 14 | +#include <QtCore/QByteArray> | |
| 15 | +#include <QtCore/QDir> | |
| 16 | +#include <QtCore/QFileInfo> | |
| 18 | 17 | #include <QtCore/QJsonObject> |
| 19 | 18 | #include <QtCore/QLoggingCategory> |
| 20 | 19 | #include <QtCore/QThread> |
| 21 | 20 | #include <QtCore/QTimer> |
| 21 | +#include <QtScript/QScriptEngine> | |
| 22 | +#include <QtScript/QScriptable> | |
| 23 | + | |
| 24 | +#include <SettingHelpers.h> // for ::settingsFilename() | |
| 25 | +#include <SharedUtil.h> // for usecTimestampNow() | |
| 26 | +#include <shared/ScriptInitializerMixin.h> | |
| 22 | 27 | |
| 23 | −#include <SharedUtil.h> | |
| 24 | −#include <AvatarHashMap.h> | |
| 28 | +// NOTE: replace this with your own namespace when starting a new plugin (to avoid .so/.dll symbol clashes) | |
| 29 | +namespace REPLACE_ME_WITH_UNIQUE_NAME { | |
| 25 | 30 | |
| 26 | −namespace custom_api_example { | |
| 31 | +static constexpr auto JSAPI_SEMANTIC_VERSION = "0.0.1"; | |
| 32 | +static constexpr auto JSAPI_EXPORT_NAME = "JSAPIExample"; | |
| 33 | + | |
| 34 | +QLoggingCategory logger{ "jsapiexample" }; | |
| 35 | + | |
| 36 | +inline QVariant raiseScriptingError(QScriptContext* context, const QString& message, const QVariant& returnValue = QVariant()) { | |
| 37 | + if (context) { | |
| 38 | + // when a QScriptContext is available throw an actual JS Exception (which can be caught using try/catch on JS side) | |
| 39 | + context->throwError(message); | |
| 40 | + } else { | |
| 41 | + // otherwise just log the error | |
| 42 | + qCWarning(logger) << "error:" << message; | |
| 43 | + } | |
| 44 | + return returnValue; | |
| 45 | +} | |
| 27 | 46 | |
| 28 | −QLoggingCategory logger{ "custom_api_example" }; | |
| 47 | +QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error); | |
| 29 | 48 | |
| 30 | −class KasenAPIExample : public example::ScriptPlugin { | |
| 49 | +class JSAPIExample : public QObject, public QScriptable { | |
| 31 | 50 | Q_OBJECT |
| 32 | − Q_PLUGIN_METADATA(IID "KasenAPIExample" FILE "plugin.json") | |
| 51 | + Q_PLUGIN_METADATA(IID "JSAPIExample" FILE "plugin.json") | |
| 52 | + Q_PROPERTY(QString version MEMBER _version CONSTANT) | |
| 33 | 53 | public: |
| 34 | − KasenAPIExample() : example::ScriptPlugin("KasenAPIExample", "0.0.1") { | |
| 35 | − qCInfo(logger) << "plugin loaded" << qApp << toString() << QThread::currentThread(); | |
| 54 | + JSAPIExample() { | |
| 55 | + setObjectName(JSAPI_EXPORT_NAME); | |
| 56 | + auto scriptInit = DependencyManager::get<ScriptInitializers>(); | |
| 57 | + if (!scriptInit) { | |
| 58 | + qCWarning(logger) << "COULD NOT INITIALIZE (ScriptInitializers unavailable)" << qApp << this; | |
| 59 | + return; | |
| 60 | + } | |
| 61 | + qCWarning(logger) << "registering w/ScriptInitializerMixin..." << scriptInit.data(); | |
| 62 | + scriptInit->registerScriptInitializer([this](QScriptEngine* engine) { | |
| 63 | + auto value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater); | |
| 64 | + engine->globalObject().setProperty(objectName(), value); | |
| 65 | + // qCDebug(logger) << "setGlobalInstance" << objectName() << engine->property("fileName"); | |
| 66 | + }); | |
| 67 | + // qCInfo(logger) << "plugin loaded" << qApp << toString() << QThread::currentThread(); | |
| 36 | 68 | } |
| 37 | 69 | |
| 70 | + // NOTES: everything within the "public slots:" section below will be available from JS via overall plugin QObject | |
| 71 | + // also, to demonstrate future-proofing JS API code, QVariant's are used throughout most of these examples -- | |
| 72 | + // which still makes them very Qt-specific, but avoids depending directly on deprecated QtScript/QScriptValue APIs. | |
| 73 | + // (as such this plugin class and its methods remain forward-compatible with other engines like QML's QJSEngine) | |
| 74 | + | |
| 38 | 75 | public slots: |
| 76 | + // returns a pretty-printed representation for logging eg: print(JSAPIExample) | |
| 77 | + inline QString toString() const { return QString("[%1 version=%2]").arg(objectName()).arg(_version); } | |
| 78 | + | |
| 39 | 79 | /**jsdoc |
| 40 | 80 | * Returns current microseconds (usecs) since Epoch. note: 1000usecs == 1ms |
| 41 | 81 | * @example <caption>Measure current setTimeout accuracy.</caption> |
| 42 | 82 | * var expected = 1000; |
| 43 | − * var start = KasenAPIExample.now(); | |
| 83 | + * var start = JSAPIExample.now(); | |
| 44 | 84 | * Script.setTimeout(function () { |
| 45 | − * var elapsed = (KasenAPIExample.now() - start)/1000; | |
| 85 | + * var elapsed = (JSAPIExample.now() - start)/1000; | |
| 46 | 86 | * print("expected (ms):", expected, "actual (ms):", elapsed); |
| 47 | 87 | * }, expected); |
| 48 | 88 | */ |
| 49 | − QVariant now() const { | |
| 50 | − return usecTimestampNow(); | |
| 51 | − } | |
| 89 | + QVariant now() const { return usecTimestampNow(); } | |
| 52 | 90 | |
| 53 | 91 | /**jsdoc |
| 54 | − * Returns the available blendshape names for an avatar. | |
| 55 | − * @example <caption>Get blendshape names</caption> | |
| 56 | − * print(JSON.stringify(KasenAPIExample.getBlendshapeNames(MyAvatar.sessionUUID))); | |
| 92 | + * Example of returning a JS Object key-value map | |
| 93 | + * @example <caption>"zip" a list of keys and corresponding values to form key-value map</caption> | |
| 94 | + * print(JSON.stringify(JSAPIExample.zip(["a","b"], [1,2])); // { "a": 1, "b": 2 } | |
| 57 | 95 | */ |
| 58 | − QStringList getBlendshapeNames(const QUuid& avatarID) const { | |
| 59 | − QVector<QString> out; | |
| 60 | − if (auto head = getAvatarHead(avatarID)) { | |
| 61 | − for (const auto& kv : head->getBlendshapeMap().toStdMap()) { | |
| 62 | − if (kv.second >= out.size()) out.resize(kv.second+1); | |
| 63 | − out[kv.second] = kv.first; | |
| 64 | − } | |
| 96 | + QVariant zip(const QStringList& keys, const QVariantList& values) const { | |
| 97 | + QVariantMap out; | |
| 98 | + for (int i = 0; i < keys.size(); i++) { | |
| 99 | + out[keys[i]] = i < values.size() ? values[i] : QVariant(); | |
| 65 | 100 | } |
| 66 | − return out.toList(); | |
| 101 | + return out; | |
| 67 | 102 | } |
| 103 | + /**jsdoc | |
| 104 | + * Example of returning a JS Array result | |
| 105 | + * @example <caption>emulate Object.values(keyValues)</caption> | |
| 106 | + * print(JSON.stringify(JSAPIExample.values({ "a": 1, "b": 2 }))); // [1,2] | |
| 107 | + */ | |
| 108 | + QVariant values(const QVariantMap& keyValues) const { return keyValues.values(); } | |
| 68 | 109 | |
| 69 | 110 | /**jsdoc |
| 70 | − * Returns a key-value object with active (non-zero) blendshapes. | |
| 71 | − * eg: { JawOpen: 1.0, ... } | |
| 72 | − * @example <caption>Get active blendshape map</caption> | |
| 73 | − * print(JSON.stringify(KasenAPIExample.getActiveBlendshapes(MyAvatar.sessionUUID))); | |
| 111 | + * Another example of returning JS Array data | |
| 112 | + * @example <caption>generate an integer sequence (inclusive of [from, to])</caption> | |
| 113 | + * print(JSON.stringify(JSAPIExample.seq(1,5)));// [1,2,3,4,5] | |
| 74 | 114 | */ |
| 75 | − QVariant getActiveBlendshapes(const QUuid& avatarID) const { | |
| 76 | − if (auto head = getAvatarHead(avatarID)) { | |
| 77 | − return head->toJson()["blendShapes"].toVariant(); | |
| 115 | + QVariant seq(int from, int to) const { | |
| 116 | + QVariantList out; | |
| 117 | + for (int i = from; i <= to; i++) { | |
| 118 | + out.append(i); | |
| 78 | 119 | } |
| 79 | − return {}; | |
| 120 | + return out; | |
| 80 | 121 | } |
| 81 | 122 | |
| 82 | − QVariant getBlendshapeMapping(const QUuid& avatarID) const { | |
| 83 | − QVariantMap out; | |
| 84 | − if (auto head = getAvatarHead(avatarID)) { | |
| 85 | − for (const auto& kv : head->getBlendshapeMap().toStdMap()) { | |
| 86 | − out[kv.first] = kv.second; | |
| 87 | − } | |
| 88 | − } | |
| 89 | − return out; | |
| 123 | + /**jsdoc | |
| 124 | + * Example of returning arbitrary binary data from C++ (resulting in a JS ArrayBuffer) | |
| 125 | + * see also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer#Examples | |
| 126 | + * @example <caption>return compressed/decompressed versions of the input data</caption> | |
| 127 | + * var data = "testing 1 2 3"; | |
| 128 | + * var z = JSAPIExample.qCompressString(data); // z will be an ArrayBuffer | |
| 129 | + * var u = JSAPIExample.qUncompressString(z); // u will be a String value | |
| 130 | + * print(JSON.stringify({ input: data, compressed: z.byteLength, output: u, uncompressed: u.length })); | |
| 131 | + */ | |
| 132 | + QVariant qCompressString(const QString& data, int compress_level = -1) const { | |
| 133 | + return qCompress(data.toUtf8(), compress_level); | |
| 90 | 134 | } |
| 135 | + QString qUncompressString(const QByteArray& data) const { return QString::fromUtf8(qUncompress(data)); } | |
| 91 | 136 | |
| 92 | − QVariant getBlendshapes(const QUuid& avatarID) const { | |
| 93 | − QVariantMap result; | |
| 94 | − if (auto head = getAvatarHead(avatarID)) { | |
| 95 | − QStringList names = getBlendshapeNames(avatarID); | |
| 96 | − auto states = head->getBlendshapeStates(); | |
| 97 | − result = { | |
| 98 | − { "base", zipNonZeroValues(names, states.base) }, | |
| 99 | − { "summed", zipNonZeroValues(names, states.summed) }, | |
| 100 | − { "transient", zipNonZeroValues(names, states.transient) }, | |
| 101 | − }; | |
| 137 | + /** | |
| 138 | + * Example of exposing a custom "managed" C++ QObject to JS | |
| 139 | + * The lifecycle of the created QObject* instance becomes managed by the invoking QScriptEngine -- | |
| 140 | + * it will be automatically cleaned up once no longer reachable from any JS variables/closures. | |
| 141 | + * @example <caption>access persistent settings stored in separate .json files</caption> | |
| 142 | + * var settings = JSAPIExample.getScopedSettings("example"); | |
| 143 | + * print("example settings stored in:", settings.fileName()); | |
| 144 | + * print("(before) example::timestamp", settings.value("timestamp")); | |
| 145 | + * settings.setValue("timestamp", Date.now()); | |
| 146 | + * print("(after) example::timestamp", settings.value("timestamp")); | |
| 147 | + * print("all example::* keys", settings.allKeys()); | |
| 148 | + * settings = null; // optional best pratice; allows the object to be reclaimed ASAP by the JS garbage collector | |
| 149 | + */ | |
| 150 | + QScriptValue getScopedSettings(const QString& scope) { | |
| 151 | + auto engine = QScriptable::engine(); | |
| 152 | + if (!engine) return QScriptValue::NullValue; | |
| 153 | + QString error; | |
| 154 | + auto cppValue = createScopedSettings(scope, engine, error); | |
| 155 | + if (!cppValue) { | |
| 156 | + raiseScriptingError(context(), "error creating scoped settings instance: " + error); | |
| 157 | + return QScriptValue::NullValue; | |
| 102 | 158 | } |
| 103 | − return result; | |
| 159 | + return engine->newQObject(cppValue, QScriptEngine::ScriptOwnership, QScriptEngine::ExcludeDeleteLater); | |
| 104 | 160 | } |
| 105 | 161 | |
| 106 | 162 | private: |
| 107 | − static QVariantMap zipNonZeroValues(const QStringList& keys, const QVector<float>& values) { | |
| 108 | − QVariantMap out; | |
| 109 | − for (int i=1; i < values.size(); i++) { | |
| 110 | − if (fabs(values[i]) > 1.0e-6f) { | |
| 111 | − out[keys.value(i)] = values[i]; | |
| 112 | − } | |
| 113 | − } | |
| 114 | − return out; | |
| 163 | + const QString _version{ JSAPI_SEMANTIC_VERSION }; | |
| 164 | +}; | |
| 165 | + | |
| 166 | +// Example of how to create a QObject class that can have multiple instances created from the JS side | |
| 167 | +// JSSettingsHelper emulates a subset of QSetting APIs: | |
| 168 | +// fileName() -- full path to the scoped settings .json file | |
| 169 | +// allKeys() -- all previously stored keys available in the scoped settings file | |
| 170 | +// value(key, defaultValue) -- retrieve a stored value | |
| 171 | +// setValue(key, newValue) -- set/update a stored value | |
| 172 | +class JSSettingsHelper : public QObject { | |
| 173 | + Q_OBJECT | |
| 174 | + QString _scope; | |
| 175 | + QString _fileName; | |
| 176 | + QSharedPointer<QSettings> _settings; | |
| 177 | + | |
| 178 | +public: | |
| 179 | + operator bool() const { return (bool)_settings; } | |
| 180 | + JSSettingsHelper(const QString& scope, QObject* parent = nullptr) : | |
| 181 | + QObject(parent), _scope(scope), _fileName(getLocalSettingsPath(scope)), | |
| 182 | + _settings(_fileName.isEmpty() ? nullptr : new QSettings(_fileName, JSON_FORMAT)) {} | |
| 183 | + ~JSSettingsHelper() { qCDebug(logger) << "~JSSettingsHelper" << _scope << _fileName << this; } | |
| 184 | +public slots: | |
| 185 | + QString fileName() const { return _settings ? _settings->fileName() : ""; } | |
| 186 | + QString toString() const { return QString("[JSSettingsHelper scope=%1 valid=%2]").arg(_scope).arg((bool)_settings); } | |
| 187 | + QVariant value(const QString& key, const QVariant& defaultValue = QVariant()) { | |
| 188 | + return _settings ? _settings->value(key, defaultValue) : defaultValue; | |
| 115 | 189 | } |
| 116 | − struct _HeadHelper : public HeadData { | |
| 117 | − QMap<QString,int> getBlendshapeMap() const { | |
| 118 | − return BLENDSHAPE_LOOKUP_MAP; | |
| 190 | + bool setValue(const QString& key, const QVariant& value) { | |
| 191 | + if (_settings) { | |
| 192 | + _settings->setValue(key, value); | |
| 193 | + return true; | |
| 119 | 194 | } |
| 120 | − struct States { QVector<float> base, summed, transient; }; | |
| 121 | − States getBlendshapeStates() const { | |
| 122 | − return { | |
| 123 | − _blendshapeCoefficients, | |
| 124 | − _summedBlendshapeCoefficients, | |
| 125 | − _transientBlendshapeCoefficients | |
| 126 | − }; | |
| 127 | − } | |
| 128 | − }; | |
| 129 | − static const _HeadHelper* getAvatarHead(const QUuid& avatarID) { | |
| 130 | − auto avatars = DependencyManager::get<AvatarHashMap>(); | |
| 131 | − auto avatar = avatars ? avatars->getAvatarBySessionID(avatarID) : nullptr; | |
| 132 | − auto head = avatar ? avatar->getHeadData() : nullptr; | |
| 133 | − return reinterpret_cast<const _HeadHelper*>(head); | |
| 195 | + return false; | |
| 196 | + } | |
| 197 | + QStringList allKeys() const { return _settings ? _settings->allKeys() : QStringList{}; } | |
| 198 | + | |
| 199 | +protected: | |
| 200 | + QString getLocalSettingsPath(const QString& scope) const { | |
| 201 | + // generate a prefixed filename (relative to the main application's Interface.json file) | |
| 202 | + const QString fileName = QString("jsapi_%1.json").arg(scope); | |
| 203 | + return QFileInfo(::settingsFilename()).dir().filePath(fileName); | |
| 134 | 204 | } |
| 135 | 205 | }; |
| 136 | 206 | |
| 207 | +QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error) { | |
| 208 | + const QRegExp VALID_SETTINGS_SCOPE{ "[-_A-Za-z0-9]{1,64}" }; | |
| 209 | + if (!VALID_SETTINGS_SCOPE.exactMatch(scope)) { | |
| 210 | + error = QString("invalid scope (expected alphanumeric <= 64 chars not '%1')").arg(scope); | |
| 211 | + return nullptr; | |
| 212 | + } | |
| 213 | + return new JSSettingsHelper(scope, parent); | |
| 137 | 214 | } |
| 138 | 215 | |
| 139 | −const QLoggingCategory& example::logger{ custom_api_example::logger }; | |
| 216 | +} // namespace REPLACE_ME_WITH_UNIQUE_NAME | |
| 140 | 217 | |
| 141 | −#include "KasenAPIExample.moc" | |
| 218 | +#include "JSAPIExample.moc" | |