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 @@
11 //
2−// KasenAPIExample.cpp
3−// plugins/KasenAPIExample/src
2+// JSAPIExample.cpp
3+// plugins/JSAPIExample/src
44 //
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)
97 //
108 // Distributed under the Apache License, Version 2.0.
119 // 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.
1410
15−#include "ExampleScriptPlugin.h"
11+// Example of prototyping new JS APIs by leveraging the existing plugin system.
1612
1713 #include <QCoreApplication>
14+#include <QtCore/QByteArray>
15+#include <QtCore/QDir>
16+#include <QtCore/QFileInfo>
1817 #include <QtCore/QJsonObject>
1918 #include <QtCore/QLoggingCategory>
2019 #include <QtCore/QThread>
2120 #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>
2227
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 {
2530
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+}
2746
28−QLoggingCategory logger{ "custom_api_example" };
47+QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error);
2948
30−class KasenAPIExample : public example::ScriptPlugin {
49+class JSAPIExample : public QObject, public QScriptable {
3150 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)
3353 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();
3668 }
3769
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+
3875 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+
3979 /**jsdoc
4080 * Returns current microseconds (usecs) since Epoch. note: 1000usecs == 1ms
4181 * @example <caption>Measure current setTimeout accuracy.</caption>
4282 * var expected = 1000;
43− * var start = KasenAPIExample.now();
83+ * var start = JSAPIExample.now();
4484 * Script.setTimeout(function () {
45− * var elapsed = (KasenAPIExample.now() - start)/1000;
85+ * var elapsed = (JSAPIExample.now() - start)/1000;
4686 * print("expected (ms):", expected, "actual (ms):", elapsed);
4787 * }, expected);
4888 */
49− QVariant now() const {
50− return usecTimestampNow();
51− }
89+ QVariant now() const { return usecTimestampNow(); }
5290
5391 /**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 }
5795 */
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();
65100 }
66− return out.toList();
101+ return out;
67102 }
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(); }
68109
69110 /**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]
74114 */
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);
78119 }
79− return {};
120+ return out;
80121 }
81122
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);
90134 }
135+ QString qUncompressString(const QByteArray& data) const { return QString::fromUtf8(qUncompress(data)); }
91136
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;
102158 }
103− return result;
159+ return engine->newQObject(cppValue, QScriptEngine::ScriptOwnership, QScriptEngine::ExcludeDeleteLater);
104160 }
105161
106162 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;
115189 }
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;
119194 }
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);
134204 }
135205 };
136206
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);
137214 }
138215
139−const QLoggingCategory& example::logger{ custom_api_example::logger };
216+} // namespace REPLACE_ME_WITH_UNIQUE_NAME
140217
141−#include "KasenAPIExample.moc"
218+#include "JSAPIExample.moc"