History

revamped modkit plugin example

humbletim committed 6 years ago

Commit f54b1c51 parent fa3a8865 files · +171 −111Browse files

Showing the changes to plugins/JSAPIExample/src/JSAPIExample.cpp (1 of 5 files). Show all

Unified · Split

plugins/JSAPIExample/src/JSAPIExample.cpp+164−87
@@ -1,141 +1,218 @@
1//1//
2// KasenAPIExample.cpp2// JSAPIExample.cpp
3// plugins/KasenAPIExample/src3// plugins/JSAPIExample/src
4//4//
5// Created by Kasen IO on 2019.07.14 | realities.dev | kasenvr@gmail.com5// Copyright (c) 2019-2020 humbletim (humbletim@gmail.com)
6// Copyright 2019 Kasen IO6// Copyright (c) 2019 Kalila L. (kasenvr@gmail.com)
7//
8// Authored by: Humbletim (humbletim@gmail.com)
9//7//
10// Distributed under the Apache License, Version 2.0.8// Distributed under the Apache License, Version 2.0.
11// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html9// 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#include <QCoreApplication>13#include <QCoreApplication>
14#include <QtCore/QByteArray>
15#include <QtCore/QDir>
16#include <QtCore/QFileInfo>
18#include <QtCore/QJsonObject>17#include <QtCore/QJsonObject>
19#include <QtCore/QLoggingCategory>18#include <QtCore/QLoggingCategory>
20#include <QtCore/QThread>19#include <QtCore/QThread>
21#include <QtCore/QTimer>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>28// NOTE: replace this with your own namespace when starting a new plugin (to avoid .so/.dll symbol clashes)
24#include <AvatarHashMap.h>29namespace REPLACE_ME_WITH_UNIQUE_NAME {
25 30
26namespace custom_api_example {31static constexpr auto JSAPI_SEMANTIC_VERSION = "0.0.1";
32static constexpr auto JSAPI_EXPORT_NAME = "JSAPIExample";
33
34QLoggingCategory logger{ "jsapiexample" };
35
36inline 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
28QLoggingCategory logger{ "custom_api_example" };47QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error);
29 48
30class KasenAPIExample : public example::ScriptPlugin {49class JSAPIExample : public QObject, public QScriptable {
31 Q_OBJECT50 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)
33public:53public:
34 KasenAPIExample() : example::ScriptPlugin("KasenAPIExample", "0.0.1") {54 JSAPIExample() {
35 qCInfo(logger) << "plugin loaded" << qApp << toString() << QThread::currentThread(); 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
38public slots:75public 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 /**jsdoc79 /**jsdoc
40 * Returns current microseconds (usecs) since Epoch. note: 1000usecs == 1ms80 * Returns current microseconds (usecs) since Epoch. note: 1000usecs == 1ms
41 * @example <caption>Measure current setTimeout accuracy.</caption>81 * @example <caption>Measure current setTimeout accuracy.</caption>
42 * var expected = 1000;82 * var expected = 1000;
43 * var start = KasenAPIExample.now();83 * var start = JSAPIExample.now();
44 * Script.setTimeout(function () {84 * Script.setTimeout(function () {
45 * var elapsed = (KasenAPIExample.now() - start)/1000;85 * var elapsed = (JSAPIExample.now() - start)/1000;
46 * print("expected (ms):", expected, "actual (ms):", elapsed);86 * print("expected (ms):", expected, "actual (ms):", elapsed);
47 * }, expected);87 * }, expected);
48 */88 */
49 QVariant now() const {89 QVariant now() const { return usecTimestampNow(); }
50 return usecTimestampNow();
51 }
52 90
53 /**jsdoc91 /**jsdoc
54 * Returns the available blendshape names for an avatar.92 * Example of returning a JS Object key-value map
55 * @example <caption>Get blendshape names</caption>93 * @example <caption>"zip" a list of keys and corresponding values to form key-value map</caption>
56 * print(JSON.stringify(KasenAPIExample.getBlendshapeNames(MyAvatar.sessionUUID)));94 * print(JSON.stringify(JSAPIExample.zip(["a","b"], [1,2])); // { "a": 1, "b": 2 }
57 */95 */
58 QStringList getBlendshapeNames(const QUuid& avatarID) const {96 QVariant zip(const QStringList& keys, const QVariantList& values) const {
59 QVector<QString> out;97 QVariantMap out;
60 if (auto head = getAvatarHead(avatarID)) {98 for (int i = 0; i < keys.size(); i++) {
61 for (const auto& kv : head->getBlendshapeMap().toStdMap()) {99 out[keys[i]] = i < values.size() ? values[i] : QVariant();
62 if (kv.second >= out.size()) out.resize(kv.second+1);
63 out[kv.second] = kv.first;
64 }
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 /**jsdoc110 /**jsdoc
70 * Returns a key-value object with active (non-zero) blendshapes.111 * Another example of returning JS Array data
71 * eg: { JawOpen: 1.0, ... }112 * @example <caption>generate an integer sequence (inclusive of [from, to])</caption>
72 * @example <caption>Get active blendshape map</caption>113 * print(JSON.stringify(JSAPIExample.seq(1,5)));// [1,2,3,4,5]
73 * print(JSON.stringify(KasenAPIExample.getActiveBlendshapes(MyAvatar.sessionUUID)));
74 */114 */
75 QVariant getActiveBlendshapes(const QUuid& avatarID) const {115 QVariant seq(int from, int to) const {
76 if (auto head = getAvatarHead(avatarID)) {116 QVariantList out;
77 return head->toJson()["blendShapes"].toVariant();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 {123 /**jsdoc
83 QVariantMap out;124 * Example of returning arbitrary binary data from C++ (resulting in a JS ArrayBuffer)
84 if (auto head = getAvatarHead(avatarID)) {125 * see also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer#Examples
85 for (const auto& kv : head->getBlendshapeMap().toStdMap()) {126 * @example <caption>return compressed/decompressed versions of the input data</caption>
86 out[kv.first] = kv.second;127 * var data = "testing 1 2 3";
87 }128 * var z = JSAPIExample.qCompressString(data); // z will be an ArrayBuffer
88 }129 * var u = JSAPIExample.qUncompressString(z); // u will be a String value
89 return out;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 {137 /**
93 QVariantMap result;138 * Example of exposing a custom "managed" C++ QObject to JS
94 if (auto head = getAvatarHead(avatarID)) {139 * The lifecycle of the created QObject* instance becomes managed by the invoking QScriptEngine --
95 QStringList names = getBlendshapeNames(avatarID);140 * it will be automatically cleaned up once no longer reachable from any JS variables/closures.
96 auto states = head->getBlendshapeStates();141 * @example <caption>access persistent settings stored in separate .json files</caption>
97 result = {142 * var settings = JSAPIExample.getScopedSettings("example");
98 { "base", zipNonZeroValues(names, states.base) },143 * print("example settings stored in:", settings.fileName());
99 { "summed", zipNonZeroValues(names, states.summed) },144 * print("(before) example::timestamp", settings.value("timestamp"));
100 { "transient", zipNonZeroValues(names, states.transient) },145 * settings.setValue("timestamp", Date.now());
101 };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
106private:162private:
107 static QVariantMap zipNonZeroValues(const QStringList& keys, const QVector<float>& values) {163 const QString _version{ JSAPI_SEMANTIC_VERSION };
108 QVariantMap out;164};
109 for (int i=1; i < values.size(); i++) {165
110 if (fabs(values[i]) > 1.0e-6f) {166// Example of how to create a QObject class that can have multiple instances created from the JS side
111 out[keys.value(i)] = values[i];167// JSSettingsHelper emulates a subset of QSetting APIs:
112 }168// fileName() -- full path to the scoped settings .json file
113 }169// allKeys() -- all previously stored keys available in the scoped settings file
114 return out;170// value(key, defaultValue) -- retrieve a stored value
171// setValue(key, newValue) -- set/update a stored value
172class JSSettingsHelper : public QObject {
173 Q_OBJECT
174 QString _scope;
175 QString _fileName;
176 QSharedPointer<QSettings> _settings;
177
178public:
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; }
184public 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 {190 bool setValue(const QString& key, const QVariant& value) {
117 QMap<QString,int> getBlendshapeMap() const {191 if (_settings) {
118 return BLENDSHAPE_LOOKUP_MAP;192 _settings->setValue(key, value);
193 return true;
119 }194 }
120 struct States { QVector<float> base, summed, transient; };195 return false;
121 States getBlendshapeStates() const {196 }
122 return {197 QStringList allKeys() const { return _settings ? _settings->allKeys() : QStringList{}; }
123 _blendshapeCoefficients,198
124 _summedBlendshapeCoefficients,199protected:
125 _transientBlendshapeCoefficients200 QString getLocalSettingsPath(const QString& scope) const {
126 };201 // generate a prefixed filename (relative to the main application's Interface.json file)
127 }202 const QString fileName = QString("jsapi_%1.json").arg(scope);
128 };203 return QFileInfo(::settingsFilename()).dir().filePath(fileName);
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);
134 }204 }
135};205};
136 206
207QObject* 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
139const QLoggingCategory& example::logger{ custom_api_example::logger };216} // namespace REPLACE_ME_WITH_UNIQUE_NAME
140 217
141#include "KasenAPIExample.moc"218#include "JSAPIExample.moc"