mirror of
https://github.com/overte-org/overte.git
synced 2025-04-20 12:04:18 +02:00
Merge pull request #9951 from sethalves/tablet-ui
Tablet ui -- merge from upstream
This commit is contained in:
commit
acdcb366b4
64 changed files with 2197 additions and 634 deletions
|
@ -179,6 +179,8 @@
|
|||
#include "FrameTimingsScriptingInterface.h"
|
||||
#include <GPUIdent.h>
|
||||
#include <gl/GLHelpers.h>
|
||||
#include <EntityScriptClient.h>
|
||||
#include <ModelScriptingInterface.h>
|
||||
|
||||
// On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU
|
||||
// FIXME seems to be broken.
|
||||
|
@ -4391,16 +4393,16 @@ void Application::update(float deltaTime) {
|
|||
myAvatar->clearDriveKeys();
|
||||
if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) {
|
||||
if (!_controllerScriptingInterface->areActionsCaptured()) {
|
||||
myAvatar->setDriveKeys(TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z));
|
||||
myAvatar->setDriveKeys(TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y));
|
||||
myAvatar->setDriveKeys(TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X));
|
||||
myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z));
|
||||
myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y));
|
||||
myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X));
|
||||
if (deltaTime > FLT_EPSILON) {
|
||||
myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH));
|
||||
myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW));
|
||||
myAvatar->setDriveKeys(STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW));
|
||||
myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH));
|
||||
myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW));
|
||||
myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW));
|
||||
}
|
||||
}
|
||||
myAvatar->setDriveKeys(ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z));
|
||||
myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z));
|
||||
}
|
||||
|
||||
controller::Pose leftHandPose = userInputMapper->getPoseState(controller::Action::LEFT_HAND);
|
||||
|
@ -5511,8 +5513,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
|
|||
scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this));
|
||||
|
||||
// hook our avatar and avatar hash map object into this script engine
|
||||
scriptEngine->registerGlobalObject("MyAvatar", getMyAvatar().get());
|
||||
qScriptRegisterMetaType(scriptEngine, audioListenModeToScriptValue, audioListenModeFromScriptValue);
|
||||
getMyAvatar()->registerMetaTypes(scriptEngine);
|
||||
|
||||
scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get<AvatarManager>().data());
|
||||
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
|
||||
#include <procedural/ProceduralSkybox.h>
|
||||
#include <model/Skybox.h>
|
||||
#include <ModelScriptingInterface.h>
|
||||
|
||||
|
||||
class OffscreenGLCanvas;
|
||||
class GLCanvas;
|
||||
|
|
|
@ -422,6 +422,9 @@ Menu::Menu() {
|
|||
}
|
||||
|
||||
// Developer > Assets >>>
|
||||
// Menu item is not currently needed but code should be kept in case it proves useful again at some stage.
|
||||
//#define WANT_ASSET_MIGRATION
|
||||
#ifdef WANT_ASSET_MIGRATION
|
||||
MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets");
|
||||
auto& atpMigrator = ATPAssetMigrator::getInstance();
|
||||
atpMigrator.setDialogParent(this);
|
||||
|
@ -429,6 +432,7 @@ Menu::Menu() {
|
|||
addActionToQMenuAndActionHash(assetDeveloperMenu, MenuOption::AssetMigration,
|
||||
0, &atpMigrator,
|
||||
SLOT(loadEntityServerFile()));
|
||||
#endif
|
||||
|
||||
// Developer > Avatar >>>
|
||||
MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar");
|
||||
|
@ -561,16 +565,14 @@ Menu::Menu() {
|
|||
QString("../../hifi/tablet/TabletNetworkingPreferences.qml"), "NetworkingPreferencesDialog");
|
||||
});
|
||||
addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches()));
|
||||
addActionToQMenuAndActionHash(networkMenu, MenuOption::ClearDiskCache, 0,
|
||||
DependencyManager::get<AssetClient>().data(), SLOT(clearCache()));
|
||||
addCheckableActionToQMenuAndActionHash(networkMenu,
|
||||
MenuOption::DisableActivityLogger,
|
||||
0,
|
||||
false,
|
||||
&UserActivityLogger::getInstance(),
|
||||
SLOT(disable(bool)));
|
||||
addActionToQMenuAndActionHash(networkMenu, MenuOption::CachesSize, 0,
|
||||
dialogsManager.data(), SLOT(cachesSizeDialog()));
|
||||
addActionToQMenuAndActionHash(networkMenu, MenuOption::DiskCacheEditor, 0,
|
||||
dialogsManager.data(), SLOT(toggleDiskCacheEditor()));
|
||||
addActionToQMenuAndActionHash(networkMenu, MenuOption::ShowDSConnectTable, 0,
|
||||
qApp, SLOT(loadDomainConnectionDialog()));
|
||||
|
||||
|
|
|
@ -52,11 +52,11 @@ namespace MenuOption {
|
|||
const QString BinaryEyelidControl = "Binary Eyelid Control";
|
||||
const QString BookmarkLocation = "Bookmark Location";
|
||||
const QString Bookmarks = "Bookmarks";
|
||||
const QString CachesSize = "RAM Caches Size";
|
||||
const QString CalibrateCamera = "Calibrate Camera";
|
||||
const QString CameraEntityMode = "Entity Mode";
|
||||
const QString CenterPlayerInView = "Center Player In View";
|
||||
const QString Chat = "Chat...";
|
||||
const QString ClearDiskCache = "Clear Disk Cache";
|
||||
const QString Collisions = "Collisions";
|
||||
const QString Connexion = "Activate 3D Connexion Devices";
|
||||
const QString Console = "Console...";
|
||||
|
@ -83,7 +83,6 @@ namespace MenuOption {
|
|||
const QString DisableActivityLogger = "Disable Activity Logger";
|
||||
const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment";
|
||||
const QString DisableLightEntities = "Disable Light Entities";
|
||||
const QString DiskCacheEditor = "Disk Cache Editor";
|
||||
const QString DisplayCrashOptions = "Display Crash Options";
|
||||
const QString DisplayHandTargets = "Show Hand Targets";
|
||||
const QString DisplayModelBounds = "Display Model Bounds";
|
||||
|
|
|
@ -119,9 +119,7 @@ MyAvatar::MyAvatar(RigPointer rig) :
|
|||
using namespace recording;
|
||||
_skeletonModel->flagAsCauterized();
|
||||
|
||||
for (int i = 0; i < MAX_DRIVE_KEYS; i++) {
|
||||
_driveKeys[i] = 0.0f;
|
||||
}
|
||||
clearDriveKeys();
|
||||
|
||||
// Necessary to select the correct slot
|
||||
using SlotType = void(MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool);
|
||||
|
@ -230,6 +228,21 @@ MyAvatar::~MyAvatar() {
|
|||
_lookAtTargetAvatar.reset();
|
||||
}
|
||||
|
||||
void MyAvatar::registerMetaTypes(QScriptEngine* engine) {
|
||||
QScriptValue value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects);
|
||||
engine->globalObject().setProperty("MyAvatar", value);
|
||||
|
||||
QScriptValue driveKeys = engine->newObject();
|
||||
auto metaEnum = QMetaEnum::fromType<DriveKeys>();
|
||||
for (int i = 0; i < MAX_DRIVE_KEYS; ++i) {
|
||||
driveKeys.setProperty(metaEnum.key(i), metaEnum.value(i));
|
||||
}
|
||||
engine->globalObject().setProperty("DriveKeys", driveKeys);
|
||||
|
||||
qScriptRegisterMetaType(engine, audioListenModeToScriptValue, audioListenModeFromScriptValue);
|
||||
qScriptRegisterMetaType(engine, driveKeysToScriptValue, driveKeysFromScriptValue);
|
||||
}
|
||||
|
||||
void MyAvatar::setOrientationVar(const QVariant& newOrientationVar) {
|
||||
Avatar::setOrientation(quatFromVariant(newOrientationVar));
|
||||
}
|
||||
|
@ -462,7 +475,7 @@ void MyAvatar::simulate(float deltaTime) {
|
|||
// When there are no step values, we zero out the last step pulse.
|
||||
// This allows a user to do faster snapping by tapping a control
|
||||
for (int i = STEP_TRANSLATE_X; !stepAction && i <= STEP_YAW; ++i) {
|
||||
if (_driveKeys[i] != 0.0f) {
|
||||
if (getDriveKey((DriveKeys)i) != 0.0f) {
|
||||
stepAction = true;
|
||||
}
|
||||
}
|
||||
|
@ -1655,7 +1668,7 @@ bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const {
|
|||
void MyAvatar::updateOrientation(float deltaTime) {
|
||||
|
||||
// Smoothly rotate body with arrow keys
|
||||
float targetSpeed = _driveKeys[YAW] * _yawSpeed;
|
||||
float targetSpeed = getDriveKey(YAW) * _yawSpeed;
|
||||
if (targetSpeed != 0.0f) {
|
||||
const float ROTATION_RAMP_TIMESCALE = 0.1f;
|
||||
float blend = deltaTime / ROTATION_RAMP_TIMESCALE;
|
||||
|
@ -1684,8 +1697,8 @@ void MyAvatar::updateOrientation(float deltaTime) {
|
|||
// Comfort Mode: If you press any of the left/right rotation drive keys or input, you'll
|
||||
// get an instantaneous 15 degree turn. If you keep holding the key down you'll get another
|
||||
// snap turn every half second.
|
||||
if (_driveKeys[STEP_YAW] != 0.0f) {
|
||||
totalBodyYaw += _driveKeys[STEP_YAW];
|
||||
if (getDriveKey(STEP_YAW) != 0.0f) {
|
||||
totalBodyYaw += getDriveKey(STEP_YAW);
|
||||
}
|
||||
|
||||
// use head/HMD orientation to turn while flying
|
||||
|
@ -1722,7 +1735,7 @@ void MyAvatar::updateOrientation(float deltaTime) {
|
|||
// update body orientation by movement inputs
|
||||
setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f))));
|
||||
|
||||
getHead()->setBasePitch(getHead()->getBasePitch() + _driveKeys[PITCH] * _pitchSpeed * deltaTime);
|
||||
getHead()->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime);
|
||||
|
||||
if (qApp->isHMDMode()) {
|
||||
glm::quat orientation = glm::quat_cast(getSensorToWorldMatrix()) * getHMDSensorOrientation();
|
||||
|
@ -1756,14 +1769,14 @@ void MyAvatar::updateActionMotor(float deltaTime) {
|
|||
}
|
||||
|
||||
// compute action input
|
||||
glm::vec3 front = (_driveKeys[TRANSLATE_Z]) * IDENTITY_FRONT;
|
||||
glm::vec3 right = (_driveKeys[TRANSLATE_X]) * IDENTITY_RIGHT;
|
||||
glm::vec3 front = (getDriveKey(TRANSLATE_Z)) * IDENTITY_FRONT;
|
||||
glm::vec3 right = (getDriveKey(TRANSLATE_X)) * IDENTITY_RIGHT;
|
||||
|
||||
glm::vec3 direction = front + right;
|
||||
CharacterController::State state = _characterController.getState();
|
||||
if (state == CharacterController::State::Hover) {
|
||||
// we're flying --> support vertical motion
|
||||
glm::vec3 up = (_driveKeys[TRANSLATE_Y]) * IDENTITY_UP;
|
||||
glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP;
|
||||
direction += up;
|
||||
}
|
||||
|
||||
|
@ -1802,7 +1815,7 @@ void MyAvatar::updateActionMotor(float deltaTime) {
|
|||
_actionMotorVelocity = MAX_WALKING_SPEED * direction;
|
||||
}
|
||||
|
||||
float boomChange = _driveKeys[ZOOM];
|
||||
float boomChange = getDriveKey(ZOOM);
|
||||
_boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange;
|
||||
_boomLength = glm::clamp<float>(_boomLength, ZOOM_MIN, ZOOM_MAX);
|
||||
}
|
||||
|
@ -1833,11 +1846,11 @@ void MyAvatar::updatePosition(float deltaTime) {
|
|||
}
|
||||
|
||||
// capture the head rotation, in sensor space, when the user first indicates they would like to move/fly.
|
||||
if (!_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) > 0.1f || fabs(_driveKeys[TRANSLATE_X]) > 0.1f)) {
|
||||
if (!_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) > 0.1f || fabs(getDriveKey(TRANSLATE_X)) > 0.1f)) {
|
||||
_hoverReferenceCameraFacingIsCaptured = true;
|
||||
// transform the camera facing vector into sensor space.
|
||||
_hoverReferenceCameraFacing = transformVectorFast(glm::inverse(_sensorToWorldMatrix), getHead()->getCameraOrientation() * Vectors::UNIT_Z);
|
||||
} else if (_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) <= 0.1f && fabs(_driveKeys[TRANSLATE_X]) <= 0.1f)) {
|
||||
} else if (_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) <= 0.1f && fabs(getDriveKey(TRANSLATE_X)) <= 0.1f)) {
|
||||
_hoverReferenceCameraFacingIsCaptured = false;
|
||||
}
|
||||
}
|
||||
|
@ -2093,17 +2106,61 @@ bool MyAvatar::getCharacterControllerEnabled() {
|
|||
}
|
||||
|
||||
void MyAvatar::clearDriveKeys() {
|
||||
for (int i = 0; i < MAX_DRIVE_KEYS; ++i) {
|
||||
_driveKeys[i] = 0.0f;
|
||||
_driveKeys.fill(0.0f);
|
||||
}
|
||||
|
||||
void MyAvatar::setDriveKey(DriveKeys key, float val) {
|
||||
try {
|
||||
_driveKeys.at(key) = val;
|
||||
} catch (const std::exception&) {
|
||||
qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds";
|
||||
}
|
||||
}
|
||||
|
||||
float MyAvatar::getDriveKey(DriveKeys key) const {
|
||||
return isDriveKeyDisabled(key) ? 0.0f : getRawDriveKey(key);
|
||||
}
|
||||
|
||||
float MyAvatar::getRawDriveKey(DriveKeys key) const {
|
||||
try {
|
||||
return _driveKeys.at(key);
|
||||
} catch (const std::exception&) {
|
||||
qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds";
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void MyAvatar::relayDriveKeysToCharacterController() {
|
||||
if (_driveKeys[TRANSLATE_Y] > 0.0f) {
|
||||
if (getDriveKey(TRANSLATE_Y) > 0.0f) {
|
||||
_characterController.jump();
|
||||
}
|
||||
}
|
||||
|
||||
void MyAvatar::disableDriveKey(DriveKeys key) {
|
||||
try {
|
||||
_disabledDriveKeys.set(key);
|
||||
} catch (const std::exception&) {
|
||||
qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds";
|
||||
}
|
||||
}
|
||||
|
||||
void MyAvatar::enableDriveKey(DriveKeys key) {
|
||||
try {
|
||||
_disabledDriveKeys.reset(key);
|
||||
} catch (const std::exception&) {
|
||||
qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds";
|
||||
}
|
||||
}
|
||||
|
||||
bool MyAvatar::isDriveKeyDisabled(DriveKeys key) const {
|
||||
try {
|
||||
return _disabledDriveKeys.test(key);
|
||||
} catch (const std::exception&) {
|
||||
qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec3 MyAvatar::getWorldBodyPosition() const {
|
||||
return transformPoint(_sensorToWorldMatrix, extractTranslation(_bodySensorMatrix));
|
||||
}
|
||||
|
@ -2189,7 +2246,15 @@ QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioList
|
|||
}
|
||||
|
||||
void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode) {
|
||||
audioListenerMode = (AudioListenerMode)object.toUInt16();
|
||||
audioListenerMode = static_cast<AudioListenerMode>(object.toUInt16());
|
||||
}
|
||||
|
||||
QScriptValue driveKeysToScriptValue(QScriptEngine* engine, const MyAvatar::DriveKeys& driveKeys) {
|
||||
return driveKeys;
|
||||
}
|
||||
|
||||
void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& driveKeys) {
|
||||
driveKeys = static_cast<MyAvatar::DriveKeys>(object.toUInt16());
|
||||
}
|
||||
|
||||
|
||||
|
@ -2382,7 +2447,7 @@ bool MyAvatar::didTeleport() {
|
|||
}
|
||||
|
||||
bool MyAvatar::hasDriveInput() const {
|
||||
return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Y]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f;
|
||||
return fabsf(getDriveKey(TRANSLATE_X)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Y)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Z)) > 0.0f;
|
||||
}
|
||||
|
||||
void MyAvatar::setAway(bool value) {
|
||||
|
@ -2498,7 +2563,7 @@ bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& o
|
|||
return false;
|
||||
}
|
||||
|
||||
setPosition(position);
|
||||
slamPosition(position);
|
||||
setOrientation(orientation);
|
||||
|
||||
_rig->setMaxHipsOffsetLength(0.05f);
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
#ifndef hifi_MyAvatar_h
|
||||
#define hifi_MyAvatar_h
|
||||
|
||||
#include <bitset>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <SettingHandle.h>
|
||||
|
@ -29,20 +31,6 @@
|
|||
class AvatarActionHold;
|
||||
class ModelItemID;
|
||||
|
||||
enum DriveKeys {
|
||||
TRANSLATE_X = 0,
|
||||
TRANSLATE_Y,
|
||||
TRANSLATE_Z,
|
||||
YAW,
|
||||
STEP_TRANSLATE_X,
|
||||
STEP_TRANSLATE_Y,
|
||||
STEP_TRANSLATE_Z,
|
||||
STEP_YAW,
|
||||
PITCH,
|
||||
ZOOM,
|
||||
MAX_DRIVE_KEYS
|
||||
};
|
||||
|
||||
enum eyeContactTarget {
|
||||
LEFT_EYE,
|
||||
RIGHT_EYE,
|
||||
|
@ -88,9 +76,26 @@ class MyAvatar : public Avatar {
|
|||
Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled)
|
||||
|
||||
public:
|
||||
enum DriveKeys {
|
||||
TRANSLATE_X = 0,
|
||||
TRANSLATE_Y,
|
||||
TRANSLATE_Z,
|
||||
YAW,
|
||||
STEP_TRANSLATE_X,
|
||||
STEP_TRANSLATE_Y,
|
||||
STEP_TRANSLATE_Z,
|
||||
STEP_YAW,
|
||||
PITCH,
|
||||
ZOOM,
|
||||
MAX_DRIVE_KEYS
|
||||
};
|
||||
Q_ENUM(DriveKeys)
|
||||
|
||||
explicit MyAvatar(RigPointer rig);
|
||||
~MyAvatar();
|
||||
|
||||
void registerMetaTypes(QScriptEngine* engine);
|
||||
|
||||
virtual void simulateAttachments(float deltaTime) override;
|
||||
|
||||
AudioListenerMode getAudioListenerModeHead() const { return FROM_HEAD; }
|
||||
|
@ -180,9 +185,15 @@ public:
|
|||
|
||||
// Set what driving keys are being pressed to control thrust levels
|
||||
void clearDriveKeys();
|
||||
void setDriveKeys(int key, float val) { _driveKeys[key] = val; };
|
||||
void setDriveKey(DriveKeys key, float val);
|
||||
float getDriveKey(DriveKeys key) const;
|
||||
Q_INVOKABLE float getRawDriveKey(DriveKeys key) const;
|
||||
void relayDriveKeysToCharacterController();
|
||||
|
||||
Q_INVOKABLE void disableDriveKey(DriveKeys key);
|
||||
Q_INVOKABLE void enableDriveKey(DriveKeys key);
|
||||
Q_INVOKABLE bool isDriveKeyDisabled(DriveKeys key) const;
|
||||
|
||||
eyeContactTarget getEyeContactTarget();
|
||||
|
||||
Q_INVOKABLE glm::vec3 getTrackedHeadPosition() const { return _trackedHeadPosition; }
|
||||
|
@ -352,7 +363,6 @@ private:
|
|||
virtual bool shouldRenderHead(const RenderArgs* renderArgs) const override;
|
||||
void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; setEnableMeshVisible(shouldRender); }
|
||||
bool getShouldRenderLocally() const { return _shouldRender; }
|
||||
bool getDriveKeys(int key) { return _driveKeys[key] != 0.0f; };
|
||||
bool isMyAvatar() const override { return true; }
|
||||
virtual int parseDataFromBuffer(const QByteArray& buffer) override;
|
||||
virtual glm::vec3 getSkeletonPosition() const override;
|
||||
|
@ -388,7 +398,9 @@ private:
|
|||
void clampScaleChangeToDomainLimits(float desiredScale);
|
||||
glm::mat4 computeCameraRelativeHandControllerMatrix(const glm::mat4& controllerSensorMatrix) const;
|
||||
|
||||
float _driveKeys[MAX_DRIVE_KEYS];
|
||||
std::array<float, MAX_DRIVE_KEYS> _driveKeys;
|
||||
std::bitset<MAX_DRIVE_KEYS> _disabledDriveKeys;
|
||||
|
||||
bool _wasPushing;
|
||||
bool _isPushing;
|
||||
bool _isBeingPushed;
|
||||
|
@ -541,4 +553,7 @@ private:
|
|||
QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode);
|
||||
void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode);
|
||||
|
||||
QScriptValue driveKeysToScriptValue(QScriptEngine* engine, const MyAvatar::DriveKeys& driveKeys);
|
||||
void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& driveKeys);
|
||||
|
||||
#endif // hifi_MyAvatar_h
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
//
|
||||
// CachesSizeDialog.cpp
|
||||
//
|
||||
//
|
||||
// Created by Clement on 1/12/15.
|
||||
// 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 <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QPushButton>
|
||||
|
||||
#include <AnimationCache.h>
|
||||
#include <DependencyManager.h>
|
||||
#include <GeometryCache.h>
|
||||
#include <SoundCache.h>
|
||||
#include <TextureCache.h>
|
||||
|
||||
#include "CachesSizeDialog.h"
|
||||
|
||||
|
||||
QDoubleSpinBox* createDoubleSpinBox(QWidget* parent) {
|
||||
QDoubleSpinBox* box = new QDoubleSpinBox(parent);
|
||||
box->setDecimals(0);
|
||||
box->setRange(MIN_UNUSED_MAX_SIZE / BYTES_PER_MEGABYTES, MAX_UNUSED_MAX_SIZE / BYTES_PER_MEGABYTES);
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
CachesSizeDialog::CachesSizeDialog(QWidget* parent) :
|
||||
QDialog(parent, Qt::Window | Qt::WindowCloseButtonHint)
|
||||
{
|
||||
setWindowTitle("Caches Size");
|
||||
|
||||
// Create layouter
|
||||
QFormLayout* form = new QFormLayout(this);
|
||||
setLayout(form);
|
||||
|
||||
form->addRow("Animations cache size (MB):", _animations = createDoubleSpinBox(this));
|
||||
form->addRow("Geometries cache size (MB):", _geometries = createDoubleSpinBox(this));
|
||||
form->addRow("Sounds cache size (MB):", _sounds = createDoubleSpinBox(this));
|
||||
form->addRow("Textures cache size (MB):", _textures = createDoubleSpinBox(this));
|
||||
|
||||
resetClicked(true);
|
||||
|
||||
// Add a button to reset
|
||||
QPushButton* confirmButton = new QPushButton("Confirm", this);
|
||||
QPushButton* resetButton = new QPushButton("Reset", this);
|
||||
form->addRow(confirmButton, resetButton);
|
||||
connect(confirmButton, SIGNAL(clicked(bool)), this, SLOT(confirmClicked(bool)));
|
||||
connect(resetButton, SIGNAL(clicked(bool)), this, SLOT(resetClicked(bool)));
|
||||
}
|
||||
|
||||
void CachesSizeDialog::confirmClicked(bool checked) {
|
||||
DependencyManager::get<AnimationCache>()->setUnusedResourceCacheSize(_animations->value() * BYTES_PER_MEGABYTES);
|
||||
DependencyManager::get<ModelCache>()->setUnusedResourceCacheSize(_geometries->value() * BYTES_PER_MEGABYTES);
|
||||
DependencyManager::get<SoundCache>()->setUnusedResourceCacheSize(_sounds->value() * BYTES_PER_MEGABYTES);
|
||||
// Disabling the texture cache because it's a liability in cases where we're overcommiting GPU memory
|
||||
#if 0
|
||||
DependencyManager::get<TextureCache>()->setUnusedResourceCacheSize(_textures->value() * BYTES_PER_MEGABYTES);
|
||||
#endif
|
||||
|
||||
QDialog::close();
|
||||
}
|
||||
|
||||
void CachesSizeDialog::resetClicked(bool checked) {
|
||||
_animations->setValue(DependencyManager::get<AnimationCache>()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES);
|
||||
_geometries->setValue(DependencyManager::get<ModelCache>()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES);
|
||||
_sounds->setValue(DependencyManager::get<SoundCache>()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES);
|
||||
_textures->setValue(DependencyManager::get<TextureCache>()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES);
|
||||
}
|
||||
|
||||
void CachesSizeDialog::reject() {
|
||||
// Just regularly close upon ESC
|
||||
QDialog::close();
|
||||
}
|
||||
|
||||
void CachesSizeDialog::closeEvent(QCloseEvent* event) {
|
||||
QDialog::closeEvent(event);
|
||||
emit closed();
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
//
|
||||
// CachesSizeDialog.h
|
||||
//
|
||||
//
|
||||
// Created by Clement on 1/12/15.
|
||||
// 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_CachesSizeDialog_h
|
||||
#define hifi_CachesSizeDialog_h
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class QDoubleSpinBox;
|
||||
|
||||
class CachesSizeDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
// Sets up the UI
|
||||
CachesSizeDialog(QWidget* parent);
|
||||
|
||||
signals:
|
||||
void closed();
|
||||
|
||||
public slots:
|
||||
void reject() override;
|
||||
void confirmClicked(bool checked);
|
||||
void resetClicked(bool checked);
|
||||
|
||||
protected:
|
||||
// Emits a 'closed' signal when this dialog is closed.
|
||||
void closeEvent(QCloseEvent* event) override;
|
||||
|
||||
private:
|
||||
QDoubleSpinBox* _animations = nullptr;
|
||||
QDoubleSpinBox* _geometries = nullptr;
|
||||
QDoubleSpinBox* _scripts = nullptr;
|
||||
QDoubleSpinBox* _sounds = nullptr;
|
||||
QDoubleSpinBox* _textures = nullptr;
|
||||
};
|
||||
|
||||
#endif // hifi_CachesSizeDialog_h
|
|
@ -19,9 +19,7 @@
|
|||
#include <PathUtils.h>
|
||||
|
||||
#include "AddressBarDialog.h"
|
||||
#include "CachesSizeDialog.h"
|
||||
#include "ConnectionFailureDialog.h"
|
||||
#include "DiskCacheEditor.h"
|
||||
#include "DomainConnectionDialog.h"
|
||||
#include "HMDToolsDialog.h"
|
||||
#include "LodToolsDialog.h"
|
||||
|
@ -70,11 +68,6 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) {
|
|||
}
|
||||
}
|
||||
|
||||
void DialogsManager::toggleDiskCacheEditor() {
|
||||
maybeCreateDialog(_diskCacheEditor);
|
||||
_diskCacheEditor->toggle();
|
||||
}
|
||||
|
||||
void DialogsManager::toggleLoginDialog() {
|
||||
LoginDialog::toggleAction();
|
||||
}
|
||||
|
@ -100,16 +93,6 @@ void DialogsManager::octreeStatsDetails() {
|
|||
_octreeStatsDialog->raise();
|
||||
}
|
||||
|
||||
void DialogsManager::cachesSizeDialog() {
|
||||
if (!_cachesSizeDialog) {
|
||||
maybeCreateDialog(_cachesSizeDialog);
|
||||
|
||||
connect(_cachesSizeDialog, SIGNAL(closed()), _cachesSizeDialog, SLOT(deleteLater()));
|
||||
_cachesSizeDialog->show();
|
||||
}
|
||||
_cachesSizeDialog->raise();
|
||||
}
|
||||
|
||||
void DialogsManager::lodTools() {
|
||||
if (!_lodToolsDialog) {
|
||||
maybeCreateDialog(_lodToolsDialog);
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
class AnimationsDialog;
|
||||
class AttachmentsDialog;
|
||||
class CachesSizeDialog;
|
||||
class DiskCacheEditor;
|
||||
class LodToolsDialog;
|
||||
class OctreeStatsDialog;
|
||||
class ScriptEditorWindow;
|
||||
|
@ -46,11 +45,9 @@ public slots:
|
|||
void showAddressBar();
|
||||
void showFeed();
|
||||
void setDomainConnectionFailureVisibility(bool visible);
|
||||
void toggleDiskCacheEditor();
|
||||
void toggleLoginDialog();
|
||||
void showLoginDialog();
|
||||
void octreeStatsDetails();
|
||||
void cachesSizeDialog();
|
||||
void lodTools();
|
||||
void hmdTools(bool showTools);
|
||||
void showScriptEditor();
|
||||
|
@ -77,7 +74,6 @@ private:
|
|||
QPointer<AnimationsDialog> _animationsDialog;
|
||||
QPointer<AttachmentsDialog> _attachmentsDialog;
|
||||
QPointer<CachesSizeDialog> _cachesSizeDialog;
|
||||
QPointer<DiskCacheEditor> _diskCacheEditor;
|
||||
QPointer<QMessageBox> _ircInfoBox;
|
||||
QPointer<HMDToolsDialog> _hmdToolsDialog;
|
||||
QPointer<LodToolsDialog> _lodToolsDialog;
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
//
|
||||
// DiskCacheEditor.cpp
|
||||
//
|
||||
//
|
||||
// Created by Clement on 3/4/15.
|
||||
// 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 "DiskCacheEditor.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDialog>
|
||||
#include <QGridLayout>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QTimer>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include <AssetClient.h>
|
||||
|
||||
#include "OffscreenUi.h"
|
||||
|
||||
DiskCacheEditor::DiskCacheEditor(QWidget* parent) : QObject(parent) {
|
||||
}
|
||||
|
||||
QWindow* DiskCacheEditor::windowHandle() {
|
||||
return (_dialog) ? _dialog->windowHandle() : nullptr;
|
||||
}
|
||||
|
||||
void DiskCacheEditor::toggle() {
|
||||
if (!_dialog) {
|
||||
makeDialog();
|
||||
}
|
||||
|
||||
if (!_dialog->isActiveWindow()) {
|
||||
_dialog->show();
|
||||
_dialog->raise();
|
||||
_dialog->activateWindow();
|
||||
} else {
|
||||
_dialog->close();
|
||||
}
|
||||
}
|
||||
|
||||
void DiskCacheEditor::makeDialog() {
|
||||
_dialog = new QDialog(static_cast<QWidget*>(parent()));
|
||||
Q_CHECK_PTR(_dialog);
|
||||
_dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
_dialog->setWindowTitle("Disk Cache Editor");
|
||||
|
||||
QGridLayout* layout = new QGridLayout(_dialog);
|
||||
Q_CHECK_PTR(layout);
|
||||
_dialog->setLayout(layout);
|
||||
|
||||
|
||||
QLabel* path = new QLabel("Path : ", _dialog);
|
||||
Q_CHECK_PTR(path);
|
||||
path->setAlignment(Qt::AlignRight);
|
||||
layout->addWidget(path, 0, 0);
|
||||
|
||||
QLabel* size = new QLabel("Current Size : ", _dialog);
|
||||
Q_CHECK_PTR(size);
|
||||
size->setAlignment(Qt::AlignRight);
|
||||
layout->addWidget(size, 1, 0);
|
||||
|
||||
QLabel* maxSize = new QLabel("Max Size : ", _dialog);
|
||||
Q_CHECK_PTR(maxSize);
|
||||
maxSize->setAlignment(Qt::AlignRight);
|
||||
layout->addWidget(maxSize, 2, 0);
|
||||
|
||||
|
||||
_path = new QLabel(_dialog);
|
||||
Q_CHECK_PTR(_path);
|
||||
_path->setAlignment(Qt::AlignLeft);
|
||||
layout->addWidget(_path, 0, 1, 1, 3);
|
||||
|
||||
_size = new QLabel(_dialog);
|
||||
Q_CHECK_PTR(_size);
|
||||
_size->setAlignment(Qt::AlignLeft);
|
||||
layout->addWidget(_size, 1, 1, 1, 3);
|
||||
|
||||
_maxSize = new QLabel(_dialog);
|
||||
Q_CHECK_PTR(_maxSize);
|
||||
_maxSize->setAlignment(Qt::AlignLeft);
|
||||
layout->addWidget(_maxSize, 2, 1, 1, 3);
|
||||
|
||||
refresh();
|
||||
|
||||
|
||||
static const int REFRESH_INTERVAL = 100; // msec
|
||||
_refreshTimer = new QTimer(_dialog);
|
||||
_refreshTimer->setInterval(REFRESH_INTERVAL); // Qt::CoarseTimer acceptable, no need for real time accuracy
|
||||
_refreshTimer->setSingleShot(false);
|
||||
QObject::connect(_refreshTimer.data(), &QTimer::timeout, this, &DiskCacheEditor::refresh);
|
||||
_refreshTimer->start();
|
||||
|
||||
QPushButton* clearCacheButton = new QPushButton(_dialog);
|
||||
Q_CHECK_PTR(clearCacheButton);
|
||||
clearCacheButton->setText("Clear");
|
||||
clearCacheButton->setToolTip("Erases the entire content of the disk cache.");
|
||||
connect(clearCacheButton, SIGNAL(clicked()), SLOT(clear()));
|
||||
layout->addWidget(clearCacheButton, 3, 3);
|
||||
}
|
||||
|
||||
void DiskCacheEditor::refresh() {
|
||||
DependencyManager::get<AssetClient>()->cacheInfoRequest(this, "cacheInfoCallback");
|
||||
}
|
||||
|
||||
void DiskCacheEditor::cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize) {
|
||||
static const auto stringify = [](qint64 number) {
|
||||
static const QStringList UNITS = QStringList() << "B" << "KB" << "MB" << "GB";
|
||||
static const qint64 CHUNK = 1024;
|
||||
QString unit;
|
||||
int i = 0;
|
||||
for (i = 0; i < 4; ++i) {
|
||||
if (number / CHUNK > 0) {
|
||||
number /= CHUNK;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return QString("%0 %1").arg(number).arg(UNITS[i]);
|
||||
};
|
||||
|
||||
if (_path) {
|
||||
_path->setText(cacheDirectory);
|
||||
}
|
||||
if (_size) {
|
||||
_size->setText(stringify(cacheSize));
|
||||
}
|
||||
if (_maxSize) {
|
||||
_maxSize->setText(stringify(maximumCacheSize));
|
||||
}
|
||||
}
|
||||
|
||||
void DiskCacheEditor::clear() {
|
||||
auto buttonClicked = OffscreenUi::question(_dialog, "Clearing disk cache",
|
||||
"You are about to erase all the content of the disk cache, "
|
||||
"are you sure you want to do that?",
|
||||
QMessageBox::Ok | QMessageBox::Cancel);
|
||||
if (buttonClicked == QMessageBox::Ok) {
|
||||
DependencyManager::get<AssetClient>()->clearCache();
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
//
|
||||
// DiskCacheEditor.h
|
||||
//
|
||||
//
|
||||
// Created by Clement on 3/4/15.
|
||||
// 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_DiskCacheEditor_h
|
||||
#define hifi_DiskCacheEditor_h
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
|
||||
class QDialog;
|
||||
class QLabel;
|
||||
class QWindow;
|
||||
class QTimer;
|
||||
|
||||
class DiskCacheEditor : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DiskCacheEditor(QWidget* parent = nullptr);
|
||||
|
||||
QWindow* windowHandle();
|
||||
|
||||
public slots:
|
||||
void toggle();
|
||||
|
||||
private slots:
|
||||
void refresh();
|
||||
void cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize);
|
||||
void clear();
|
||||
|
||||
private:
|
||||
void makeDialog();
|
||||
|
||||
QPointer<QDialog> _dialog;
|
||||
QPointer<QLabel> _path;
|
||||
QPointer<QLabel> _size;
|
||||
QPointer<QLabel> _maxSize;
|
||||
QPointer<QTimer> _refreshTimer;
|
||||
};
|
||||
|
||||
#endif // hifi_DiskCacheEditor_h
|
|
@ -160,7 +160,7 @@ AudioClient::AudioClient() :
|
|||
_loopbackAudioOutput(NULL),
|
||||
_loopbackOutputDevice(NULL),
|
||||
_inputRingBuffer(0),
|
||||
_localInjectorsStream(0),
|
||||
_localInjectorsStream(0, 1),
|
||||
_receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES),
|
||||
_isStereoInput(false),
|
||||
_outputStarveDetectionStartTimeMsec(0),
|
||||
|
|
|
@ -146,6 +146,7 @@ void EntityTreeRenderer::clear() {
|
|||
|
||||
void EntityTreeRenderer::reloadEntityScripts() {
|
||||
_entitiesScriptEngine->unloadAllEntityScripts();
|
||||
_entitiesScriptEngine->resetModuleCache();
|
||||
foreach(auto entity, _entitiesInScene) {
|
||||
if (!entity->getScript().isEmpty()) {
|
||||
_entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true);
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#include <QByteArray>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
#include <glm/gtx/transform.hpp>
|
||||
#include "ModelScriptingInterface.h"
|
||||
|
||||
#if defined(__GNUC__) && !defined(__clang__)
|
||||
#pragma GCC diagnostic push
|
||||
|
@ -53,6 +54,8 @@
|
|||
#include "PhysicalEntitySimulation.h"
|
||||
|
||||
gpu::PipelinePointer RenderablePolyVoxEntityItem::_pipeline = nullptr;
|
||||
gpu::PipelinePointer RenderablePolyVoxEntityItem::_wireframePipeline = nullptr;
|
||||
|
||||
const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5;
|
||||
|
||||
|
||||
|
@ -73,7 +76,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5;
|
|||
_meshDirty
|
||||
|
||||
In RenderablePolyVoxEntityItem::render, these flags are checked and changes are propagated along the chain.
|
||||
decompressVolumeData() is called to decompress _voxelData into _volData. getMesh() is called to invoke the
|
||||
decompressVolumeData() is called to decompress _voxelData into _volData. recomputeMesh() is called to invoke the
|
||||
polyVox surface extractor to create _mesh (as well as set Simulation _dirtyFlags). Because Simulation::DIRTY_SHAPE
|
||||
is set, isReadyToComputeShape() gets called and _shape is created either from _volData or _shape, depending on
|
||||
the surface style.
|
||||
|
@ -81,7 +84,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5;
|
|||
When a script changes _volData, compressVolumeDataAndSendEditPacket is called to update _voxelData and to
|
||||
send a packet to the entity-server.
|
||||
|
||||
decompressVolumeData, getMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive
|
||||
decompressVolumeData, recomputeMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive
|
||||
to run on a thread that has other things to do. These use QtConcurrent::run to spawn a thread. As each thread
|
||||
finishes, it adjusts the dirty flags so that the next call to render() will kick off the next step.
|
||||
|
||||
|
@ -682,7 +685,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
|
|||
if (voxelDataDirty) {
|
||||
decompressVolumeData();
|
||||
} else if (volDataDirty) {
|
||||
getMesh();
|
||||
recomputeMesh();
|
||||
}
|
||||
|
||||
model::MeshPointer mesh;
|
||||
|
@ -696,7 +699,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
|
|||
!mesh->getIndexBuffer()._buffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!_pipeline) {
|
||||
gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert));
|
||||
gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag));
|
||||
|
@ -715,6 +718,13 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
|
|||
state->setDepthTest(true, true, gpu::LESS_EQUAL);
|
||||
|
||||
_pipeline = gpu::Pipeline::create(program, state);
|
||||
|
||||
auto wireframeState = std::make_shared<gpu::State>();
|
||||
wireframeState->setCullMode(gpu::State::CULL_BACK);
|
||||
wireframeState->setDepthTest(true, true, gpu::LESS_EQUAL);
|
||||
wireframeState->setFillMode(gpu::State::FILL_LINE);
|
||||
|
||||
_wireframePipeline = gpu::Pipeline::create(program, wireframeState);
|
||||
}
|
||||
|
||||
if (!_vertexFormat) {
|
||||
|
@ -725,7 +735,11 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
|
|||
}
|
||||
|
||||
gpu::Batch& batch = *args->_batch;
|
||||
batch.setPipeline(_pipeline);
|
||||
|
||||
// Pick correct Pipeline
|
||||
bool wireframe = (render::ShapeKey(args->_globalShapeKey).isWireframe());
|
||||
auto pipeline = (wireframe ? _wireframePipeline : _pipeline);
|
||||
batch.setPipeline(pipeline);
|
||||
|
||||
Transform transform(voxelToWorldMatrix());
|
||||
batch.setModelTransform(transform);
|
||||
|
@ -762,7 +776,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
|
|||
batch.setResourceTexture(2, DependencyManager::get<TextureCache>()->getWhiteTexture());
|
||||
}
|
||||
|
||||
int voxelVolumeSizeLocation = _pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize");
|
||||
int voxelVolumeSizeLocation = pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize");
|
||||
batch._glUniform3f(voxelVolumeSizeLocation, voxelVolumeSize.x, voxelVolumeSize.y, voxelVolumeSize.z);
|
||||
|
||||
batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)mesh->getNumIndices(), 0);
|
||||
|
@ -1199,7 +1213,7 @@ void RenderablePolyVoxEntityItem::copyUpperEdgesFromNeighbors() {
|
|||
}
|
||||
}
|
||||
|
||||
void RenderablePolyVoxEntityItem::getMesh() {
|
||||
void RenderablePolyVoxEntityItem::recomputeMesh() {
|
||||
// use _volData to make a renderable mesh
|
||||
PolyVoxSurfaceStyle voxelSurfaceStyle;
|
||||
withReadLock([&] {
|
||||
|
@ -1269,12 +1283,20 @@ void RenderablePolyVoxEntityItem::getMesh() {
|
|||
vertexBufferPtr->getSize() ,
|
||||
sizeof(PolyVox::PositionMaterialNormal),
|
||||
gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW)));
|
||||
|
||||
std::vector<model::Mesh::Part> parts;
|
||||
parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex
|
||||
(model::Index)vecIndices.size(), // numIndices
|
||||
(model::Index)0, // baseVertex
|
||||
model::Mesh::TRIANGLES)); // topology
|
||||
mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part),
|
||||
(gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL));
|
||||
entity->setMesh(mesh);
|
||||
});
|
||||
}
|
||||
|
||||
void RenderablePolyVoxEntityItem::setMesh(model::MeshPointer mesh) {
|
||||
// this catches the payload from getMesh
|
||||
// this catches the payload from recomputeMesh
|
||||
bool neighborsNeedUpdate;
|
||||
withWriteLock([&] {
|
||||
if (!_collisionless) {
|
||||
|
@ -1531,7 +1553,6 @@ std::shared_ptr<RenderablePolyVoxEntityItem> RenderablePolyVoxEntityItem::getZPN
|
|||
return std::dynamic_pointer_cast<RenderablePolyVoxEntityItem>(_zPNeighbor.lock());
|
||||
}
|
||||
|
||||
|
||||
void RenderablePolyVoxEntityItem::bonkNeighbors() {
|
||||
// flag neighbors to the negative of this entity as needing to rebake their meshes.
|
||||
cacheNeighbors();
|
||||
|
@ -1551,7 +1572,6 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) {
|
||||
EntityItem::locationChanged(tellPhysics);
|
||||
if (!_pipeline || !render::Item::isValidID(_myItem)) {
|
||||
|
@ -1563,3 +1583,17 @@ void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) {
|
|||
|
||||
scene->enqueuePendingChanges(pendingChanges);
|
||||
}
|
||||
|
||||
bool RenderablePolyVoxEntityItem::getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) const {
|
||||
bool success = false;
|
||||
MeshProxy* meshProxy = nullptr;
|
||||
model::MeshPointer mesh = nullptr;
|
||||
withReadLock([&] {
|
||||
if (_meshInitialized) {
|
||||
success = true;
|
||||
meshProxy = new MeshProxy(_mesh);
|
||||
}
|
||||
});
|
||||
result = meshToScriptValue(engine, meshProxy);
|
||||
return success;
|
||||
}
|
||||
|
|
|
@ -133,6 +133,7 @@ public:
|
|||
QByteArray volDataToArray(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize) const;
|
||||
|
||||
void setMesh(model::MeshPointer mesh);
|
||||
bool getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) const override;
|
||||
void setCollisionPoints(ShapeInfo::PointCollection points, AABox box);
|
||||
PolyVox::SimpleVolume<uint8_t>* getVolData() { return _volData; }
|
||||
|
||||
|
@ -163,11 +164,12 @@ private:
|
|||
const int MATERIAL_GPU_SLOT = 3;
|
||||
render::ItemID _myItem{ render::Item::INVALID_ITEM_ID };
|
||||
static gpu::PipelinePointer _pipeline;
|
||||
static gpu::PipelinePointer _wireframePipeline;
|
||||
|
||||
ShapeInfo _shapeInfo;
|
||||
|
||||
PolyVox::SimpleVolume<uint8_t>* _volData = nullptr;
|
||||
bool _volDataDirty = false; // does getMesh need to be called?
|
||||
bool _volDataDirty = false; // does recomputeMesh need to be called?
|
||||
int _onCount; // how many non-zero voxels are in _volData
|
||||
|
||||
bool _neighborsNeedUpdate { false };
|
||||
|
@ -178,7 +180,7 @@ private:
|
|||
// these are run off the main thread
|
||||
void decompressVolumeData();
|
||||
void compressVolumeDataAndSendEditPacket();
|
||||
virtual void getMesh() override; // recompute mesh
|
||||
virtual void recomputeMesh() override; // recompute mesh
|
||||
void computeShapeInfoWorker();
|
||||
|
||||
// these are cached lookups of _xNNeighborID, _yNNeighborID, _zNNeighborID, _xPNeighborID, _yPNeighborID, _zPNeighborID
|
||||
|
|
|
@ -114,13 +114,22 @@ void RenderableShapeEntityItem::render(RenderArgs* args) {
|
|||
auto outColor = _procedural->getColor(color);
|
||||
outColor.a *= _procedural->isFading() ? Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) : 1.0f;
|
||||
batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a);
|
||||
DependencyManager::get<GeometryCache>()->renderShape(batch, MAPPING[_shape]);
|
||||
if (render::ShapeKey(args->_globalShapeKey).isWireframe()) {
|
||||
DependencyManager::get<GeometryCache>()->renderWireShape(batch, MAPPING[_shape]);
|
||||
} else {
|
||||
DependencyManager::get<GeometryCache>()->renderShape(batch, MAPPING[_shape]);
|
||||
}
|
||||
} else {
|
||||
// FIXME, support instanced multi-shape rendering using multidraw indirect
|
||||
color.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f;
|
||||
auto geometryCache = DependencyManager::get<GeometryCache>();
|
||||
auto pipeline = color.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline();
|
||||
geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline);
|
||||
|
||||
if (render::ShapeKey(args->_globalShapeKey).isWireframe()) {
|
||||
geometryCache->renderWireShapeInstance(batch, MAPPING[_shape], color, pipeline);
|
||||
} else {
|
||||
geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline);
|
||||
}
|
||||
}
|
||||
|
||||
static const auto triCount = DependencyManager::get<GeometryCache>()->getShapeTriangleCount(MAPPING[_shape]);
|
||||
|
|
|
@ -15,11 +15,13 @@
|
|||
#define hifi_EntitiesScriptEngineProvider_h
|
||||
|
||||
#include <QtCore/QString>
|
||||
#include <QFuture>
|
||||
#include "EntityItemID.h"
|
||||
|
||||
class EntitiesScriptEngineProvider {
|
||||
public:
|
||||
virtual void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) = 0;
|
||||
virtual QFuture<QVariant> getLocalEntityScriptDetails(const EntityItemID& entityID) = 0;
|
||||
};
|
||||
|
||||
#endif // hifi_EntitiesScriptEngineProvider_h
|
||||
#endif // hifi_EntitiesScriptEngineProvider_h
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
//
|
||||
#include "EntityScriptingInterface.h"
|
||||
|
||||
#include <QFutureWatcher>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
|
||||
#include "EntityItemID.h"
|
||||
#include <VariantMapToScriptValue.h>
|
||||
#include <SharedUtil.h>
|
||||
|
@ -680,6 +683,118 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) {
|
|||
return client->reloadServerScript(entityID);
|
||||
}
|
||||
|
||||
bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue handler) {
|
||||
using LocalScriptStatusRequest = QFutureWatcher<QVariant>;
|
||||
|
||||
LocalScriptStatusRequest* request = new LocalScriptStatusRequest;
|
||||
QObject::connect(request, &LocalScriptStatusRequest::finished, _engine, [=]() mutable {
|
||||
auto details = request->result().toMap();
|
||||
QScriptValue err, result;
|
||||
if (details.contains("isError")) {
|
||||
if (!details.contains("message")) {
|
||||
details["message"] = details["errorInfo"];
|
||||
}
|
||||
err = _engine->makeError(_engine->toScriptValue(details));
|
||||
} else {
|
||||
details["success"] = true;
|
||||
result = _engine->toScriptValue(details);
|
||||
}
|
||||
callScopedHandlerObject(handler, err, result);
|
||||
request->deleteLater();
|
||||
});
|
||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
entityScriptingInterface->withEntitiesScriptEngine([&](EntitiesScriptEngineProvider* entitiesScriptEngine) {
|
||||
if (entitiesScriptEngine) {
|
||||
request->setFuture(entitiesScriptEngine->getLocalEntityScriptDetails(entityID));
|
||||
}
|
||||
});
|
||||
if (!request->isStarted()) {
|
||||
request->deleteLater();
|
||||
callScopedHandlerObject(handler, _engine->makeError("Entities Scripting Provider unavailable", "InternalError"), QScriptValue());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EntityPropertyMetadataRequest::serverScripts(EntityItemID entityID, QScriptValue handler) {
|
||||
auto client = DependencyManager::get<EntityScriptClient>();
|
||||
auto request = client->createScriptStatusRequest(entityID);
|
||||
QPointer<BaseScriptEngine> engine = _engine;
|
||||
QObject::connect(request, &GetScriptStatusRequest::finished, _engine, [=](GetScriptStatusRequest* request) mutable {
|
||||
auto engine = _engine;
|
||||
if (!engine) {
|
||||
qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID;
|
||||
return;
|
||||
}
|
||||
QVariantMap details;
|
||||
details["success"] = request->getResponseReceived();
|
||||
details["isRunning"] = request->getIsRunning();
|
||||
details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower();
|
||||
details["errorInfo"] = request->getErrorInfo();
|
||||
|
||||
QScriptValue err, result;
|
||||
if (!details["success"].toBool()) {
|
||||
if (!details.contains("message") && details.contains("errorInfo")) {
|
||||
details["message"] = details["errorInfo"];
|
||||
}
|
||||
if (details["message"].toString().isEmpty()) {
|
||||
details["message"] = "entity server script details not found";
|
||||
}
|
||||
err = engine->makeError(engine->toScriptValue(details));
|
||||
} else {
|
||||
result = engine->toScriptValue(details);
|
||||
}
|
||||
callScopedHandlerObject(handler, err, result);
|
||||
request->deleteLater();
|
||||
});
|
||||
request->start();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) {
|
||||
auto name = property.toString();
|
||||
auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName);
|
||||
QPointer<BaseScriptEngine> engine = dynamic_cast<BaseScriptEngine*>(handler.engine());
|
||||
if (!engine) {
|
||||
qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name;
|
||||
return false;
|
||||
}
|
||||
#ifdef DEBUG_ENGINE_STATE
|
||||
connect(engine, &QObject::destroyed, this, [=]() {
|
||||
qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine");
|
||||
});
|
||||
#endif
|
||||
if (!handler.property("callback").isFunction()) {
|
||||
qDebug() << "!handler.callback.isFunction" << engine;
|
||||
engine->raiseException(engine->makeError("callback is not a function", "TypeError"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// NOTE: this approach is a work-in-progress and for now just meant to work 100% correctly and provide
|
||||
// some initial structure for organizing metadata adapters around.
|
||||
|
||||
// The extra layer of indirection is *essential* because in real world conditions errors are often introduced
|
||||
// by accident and sometimes without exact memory of "what just changed."
|
||||
|
||||
// Here the scripter only needs to know an entityID and a property name -- which means all scripters can
|
||||
// level this method when stuck in dead-end scenarios or to learn more about "magic" Entity properties
|
||||
// like .script that work in terms of side-effects.
|
||||
|
||||
// This is an async callback pattern -- so if needed C++ can easily throttle or restrict queries later.
|
||||
|
||||
EntityPropertyMetadataRequest request(engine);
|
||||
|
||||
if (name == "script") {
|
||||
return request.script(entityID, handler);
|
||||
} else if (name == "serverScripts") {
|
||||
return request.serverScripts(entityID, handler);
|
||||
} else {
|
||||
engine->raiseException(engine->makeError("metadata for property " + name + " is not yet queryable"));
|
||||
engine->maybeEmitUncaughtException(__FUNCTION__);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) {
|
||||
auto client = DependencyManager::get<EntityScriptClient>();
|
||||
auto request = client->createScriptStatusRequest(entityID);
|
||||
|
@ -815,8 +930,7 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra
|
|||
}
|
||||
}
|
||||
|
||||
bool EntityScriptingInterface::setVoxels(QUuid entityID,
|
||||
std::function<bool(PolyVoxEntityItem&)> actor) {
|
||||
bool EntityScriptingInterface::polyVoxWorker(QUuid entityID, std::function<bool(PolyVoxEntityItem&)> actor) {
|
||||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
if (!_entityTree) {
|
||||
|
@ -882,11 +996,9 @@ bool EntityScriptingInterface::setPoints(QUuid entityID, std::function<bool(Line
|
|||
return success;
|
||||
}
|
||||
|
||||
|
||||
bool EntityScriptingInterface::setVoxelSphere(QUuid entityID, const glm::vec3& center, float radius, int value) {
|
||||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
return setVoxels(entityID, [center, radius, value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxWorker(entityID, [center, radius, value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxEntity.setSphere(center, radius, value);
|
||||
});
|
||||
}
|
||||
|
@ -896,7 +1008,7 @@ bool EntityScriptingInterface::setVoxelCapsule(QUuid entityID,
|
|||
float radius, int value) {
|
||||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
return setVoxels(entityID, [start, end, radius, value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxWorker(entityID, [start, end, radius, value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxEntity.setCapsule(start, end, radius, value);
|
||||
});
|
||||
}
|
||||
|
@ -904,7 +1016,7 @@ bool EntityScriptingInterface::setVoxelCapsule(QUuid entityID,
|
|||
bool EntityScriptingInterface::setVoxel(QUuid entityID, const glm::vec3& position, int value) {
|
||||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
return setVoxels(entityID, [position, value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxWorker(entityID, [position, value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxEntity.setVoxelInVolume(position, value);
|
||||
});
|
||||
}
|
||||
|
@ -912,7 +1024,7 @@ bool EntityScriptingInterface::setVoxel(QUuid entityID, const glm::vec3& positio
|
|||
bool EntityScriptingInterface::setAllVoxels(QUuid entityID, int value) {
|
||||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
return setVoxels(entityID, [value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxWorker(entityID, [value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxEntity.setAll(value);
|
||||
});
|
||||
}
|
||||
|
@ -921,11 +1033,23 @@ bool EntityScriptingInterface::setVoxelsInCuboid(QUuid entityID, const glm::vec3
|
|||
const glm::vec3& cuboidSize, int value) {
|
||||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
return setVoxels(entityID, [lowPosition, cuboidSize, value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxWorker(entityID, [lowPosition, cuboidSize, value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxEntity.setCuboid(lowPosition, cuboidSize, value);
|
||||
});
|
||||
}
|
||||
|
||||
void EntityScriptingInterface::voxelsToMesh(QUuid entityID, QScriptValue callback) {
|
||||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
polyVoxWorker(entityID, [callback](PolyVoxEntityItem& polyVoxEntity) mutable {
|
||||
QScriptValue mesh;
|
||||
polyVoxEntity.getMeshAsScriptValue(callback.engine(), mesh);
|
||||
QScriptValueList args { mesh };
|
||||
callback.call(QScriptValue(), args);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool EntityScriptingInterface::setAllPoints(QUuid entityID, const QVector<glm::vec3>& points) {
|
||||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
|
|
|
@ -34,7 +34,23 @@
|
|||
#include "EntitiesScriptEngineProvider.h"
|
||||
#include "EntityItemProperties.h"
|
||||
|
||||
#include "BaseScriptEngine.h"
|
||||
|
||||
class EntityTree;
|
||||
class MeshProxy;
|
||||
|
||||
// helper factory to compose standardized, async metadata queries for "magic" Entity properties
|
||||
// like .script and .serverScripts. This is used for automated testing of core scripting features
|
||||
// as well as to provide early adopters a self-discoverable, consistent way to diagnose common
|
||||
// problems with their own Entity scripts.
|
||||
class EntityPropertyMetadataRequest {
|
||||
public:
|
||||
EntityPropertyMetadataRequest(BaseScriptEngine* engine) : _engine(engine) {};
|
||||
bool script(EntityItemID entityID, QScriptValue handler);
|
||||
bool serverScripts(EntityItemID entityID, QScriptValue handler);
|
||||
private:
|
||||
QPointer<BaseScriptEngine> _engine;
|
||||
};
|
||||
|
||||
class RayToEntityIntersectionResult {
|
||||
public:
|
||||
|
@ -67,6 +83,7 @@ class EntityScriptingInterface : public OctreeScriptingInterface, public Depende
|
|||
Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier)
|
||||
Q_PROPERTY(QUuid keyboardFocusEntity READ getKeyboardFocusEntity WRITE setKeyboardFocusEntity)
|
||||
|
||||
friend EntityPropertyMetadataRequest;
|
||||
public:
|
||||
EntityScriptingInterface(bool bidOnSimulationOwnership);
|
||||
|
||||
|
@ -211,6 +228,26 @@ public slots:
|
|||
Q_INVOKABLE RayToEntityIntersectionResult findRayIntersectionBlocking(const PickRay& ray, bool precisionPicking = false, const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue());
|
||||
|
||||
Q_INVOKABLE bool reloadServerScripts(QUuid entityID);
|
||||
|
||||
/**jsdoc
|
||||
* Query additional metadata for "magic" Entity properties like `script` and `serverScripts`.
|
||||
*
|
||||
* @function Entities.queryPropertyMetadata
|
||||
* @param {EntityID} entityID The ID of the entity.
|
||||
* @param {string} property The name of the property extended metadata is wanted for.
|
||||
* @param {ResultCallback} callback Executes callback(err, result) with the query results.
|
||||
*/
|
||||
/**jsdoc
|
||||
* Query additional metadata for "magic" Entity properties like `script` and `serverScripts`.
|
||||
*
|
||||
* @function Entities.queryPropertyMetadata
|
||||
* @param {EntityID} entityID The ID of the entity.
|
||||
* @param {string} property The name of the property extended metadata is wanted for.
|
||||
* @param {Object} thisObject The scoping "this" context that callback will be executed within.
|
||||
* @param {ResultCallback} callbackOrMethodName Executes thisObject[callbackOrMethodName](err, result) with the query results.
|
||||
*/
|
||||
Q_INVOKABLE bool queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue());
|
||||
|
||||
Q_INVOKABLE bool getServerScriptStatus(QUuid entityID, QScriptValue callback);
|
||||
|
||||
Q_INVOKABLE void setLightsArePickable(bool value);
|
||||
|
@ -229,6 +266,7 @@ public slots:
|
|||
Q_INVOKABLE bool setAllVoxels(QUuid entityID, int value);
|
||||
Q_INVOKABLE bool setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition,
|
||||
const glm::vec3& cuboidSize, int value);
|
||||
Q_INVOKABLE void voxelsToMesh(QUuid entityID, QScriptValue callback);
|
||||
|
||||
Q_INVOKABLE bool setAllPoints(QUuid entityID, const QVector<glm::vec3>& points);
|
||||
Q_INVOKABLE bool appendPoint(QUuid entityID, const glm::vec3& point);
|
||||
|
@ -323,9 +361,14 @@ signals:
|
|||
|
||||
void webEventReceived(const EntityItemID& entityItemID, const QVariant& message);
|
||||
|
||||
protected:
|
||||
void withEntitiesScriptEngine(std::function<void(EntitiesScriptEngineProvider*)> function) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_entitiesScriptEngineLock);
|
||||
function(_entitiesScriptEngine);
|
||||
};
|
||||
private:
|
||||
bool actionWorker(const QUuid& entityID, std::function<bool(EntitySimulationPointer, EntityItemPointer)> actor);
|
||||
bool setVoxels(QUuid entityID, std::function<bool(PolyVoxEntityItem&)> actor);
|
||||
bool polyVoxWorker(QUuid entityID, std::function<bool(PolyVoxEntityItem&)> actor);
|
||||
bool setPoints(QUuid entityID, std::function<bool(LineEntityItem&)> actor);
|
||||
void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties);
|
||||
|
||||
|
|
|
@ -242,3 +242,7 @@ const QByteArray PolyVoxEntityItem::getVoxelData() const {
|
|||
});
|
||||
return voxelDataCopy;
|
||||
}
|
||||
|
||||
bool PolyVoxEntityItem::getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) const {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -131,7 +131,9 @@ class PolyVoxEntityItem : public EntityItem {
|
|||
virtual void rebakeMesh() {};
|
||||
|
||||
void setVoxelDataDirty(bool value) { withWriteLock([&] { _voxelDataDirty = value; }); }
|
||||
virtual void getMesh() {}; // recompute mesh
|
||||
virtual void recomputeMesh() {};
|
||||
|
||||
virtual bool getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) const;
|
||||
|
||||
protected:
|
||||
glm::vec3 _voxelVolumeSize; // this is always 3 bytes
|
||||
|
|
|
@ -54,7 +54,8 @@ template<class T> QVariant readBinaryArray(QDataStream& in, int& position) {
|
|||
in.readRawData(compressed.data() + sizeof(quint32), compressedLength);
|
||||
position += compressedLength;
|
||||
arrayData = qUncompress(compressed);
|
||||
if (arrayData.isEmpty() || arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt
|
||||
if (arrayData.isEmpty() ||
|
||||
(unsigned int)arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt
|
||||
throw QString("corrupt fbx file");
|
||||
}
|
||||
} else {
|
||||
|
|
148
libraries/fbx/src/OBJWriter.cpp
Normal file
148
libraries/fbx/src/OBJWriter.cpp
Normal file
|
@ -0,0 +1,148 @@
|
|||
//
|
||||
// OBJWriter.cpp
|
||||
// libraries/fbx/src/
|
||||
//
|
||||
// Created by Seth Alves on 2017-1-27.
|
||||
// Copyright 2017 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 <QFile>
|
||||
#include <QFileInfo>
|
||||
#include "model/Geometry.h"
|
||||
#include "OBJWriter.h"
|
||||
#include "ModelFormatLogging.h"
|
||||
|
||||
static QString formatFloat(double n) {
|
||||
// limit precision to 6, but don't output trailing zeros.
|
||||
QString s = QString::number(n, 'f', 6);
|
||||
while (s.endsWith("0")) {
|
||||
s.remove(s.size() - 1, 1);
|
||||
}
|
||||
if (s.endsWith(".")) {
|
||||
s.remove(s.size() - 1, 1);
|
||||
}
|
||||
|
||||
// check for non-numbers. if we get NaN or inf or scientific notation, just return 0
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
auto c = s.at(i).toLatin1();
|
||||
if (c != '-' &&
|
||||
c != '.' &&
|
||||
(c < '0' || c > '9')) {
|
||||
qCDebug(modelformat) << "OBJWriter zeroing bad vertex coordinate:" << s << "because of" << c;
|
||||
return QString("0");
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
bool writeOBJToTextStream(QTextStream& out, QList<MeshPointer> meshes) {
|
||||
// each mesh's vertices are numbered from zero. We're combining all their vertices into one list here,
|
||||
// so keep track of the start index for each mesh.
|
||||
QList<int> meshVertexStartOffset;
|
||||
int currentVertexStartOffset = 0;
|
||||
|
||||
// write out all vertices
|
||||
foreach (const MeshPointer& mesh, meshes) {
|
||||
meshVertexStartOffset.append(currentVertexStartOffset);
|
||||
const gpu::BufferView& vertexBuffer = mesh->getVertexBuffer();
|
||||
int vertexCount = 0;
|
||||
gpu::BufferView::Iterator<const glm::vec3> vertexItr = vertexBuffer.cbegin<const glm::vec3>();
|
||||
while (vertexItr != vertexBuffer.cend<const glm::vec3>()) {
|
||||
glm::vec3 v = *vertexItr;
|
||||
out << "v ";
|
||||
out << formatFloat(v[0]) << " ";
|
||||
out << formatFloat(v[1]) << " ";
|
||||
out << formatFloat(v[2]) << "\n";
|
||||
vertexItr++;
|
||||
vertexCount++;
|
||||
}
|
||||
currentVertexStartOffset += vertexCount;
|
||||
}
|
||||
out << "\n";
|
||||
|
||||
// write out faces
|
||||
int nth = 0;
|
||||
foreach (const MeshPointer& mesh, meshes) {
|
||||
currentVertexStartOffset = meshVertexStartOffset.takeFirst();
|
||||
|
||||
const gpu::BufferView& partBuffer = mesh->getPartBuffer();
|
||||
const gpu::BufferView& indexBuffer = mesh->getIndexBuffer();
|
||||
|
||||
model::Index partCount = (model::Index)mesh->getNumParts();
|
||||
for (int partIndex = 0; partIndex < partCount; partIndex++) {
|
||||
const model::Mesh::Part& part = partBuffer.get<model::Mesh::Part>(partIndex);
|
||||
|
||||
out << "g part-" << nth++ << "\n";
|
||||
|
||||
// model::Mesh::TRIANGLES
|
||||
// TODO -- handle other formats
|
||||
gpu::BufferView::Iterator<const uint32_t> indexItr = indexBuffer.cbegin<uint32_t>();
|
||||
indexItr += part._startIndex;
|
||||
|
||||
int indexCount = 0;
|
||||
while (indexItr != indexBuffer.cend<uint32_t>() && indexCount < part._numIndices) {
|
||||
uint32_t index0 = *indexItr;
|
||||
indexItr++;
|
||||
indexCount++;
|
||||
if (indexItr == indexBuffer.cend<uint32_t>() || indexCount >= part._numIndices) {
|
||||
qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3";
|
||||
break;
|
||||
}
|
||||
uint32_t index1 = *indexItr;
|
||||
indexItr++;
|
||||
indexCount++;
|
||||
if (indexItr == indexBuffer.cend<uint32_t>() || indexCount >= part._numIndices) {
|
||||
qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3";
|
||||
break;
|
||||
}
|
||||
uint32_t index2 = *indexItr;
|
||||
indexItr++;
|
||||
indexCount++;
|
||||
|
||||
out << "f ";
|
||||
out << currentVertexStartOffset + index0 + 1 << " ";
|
||||
out << currentVertexStartOffset + index1 + 1 << " ";
|
||||
out << currentVertexStartOffset + index2 + 1 << "\n";
|
||||
}
|
||||
out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeOBJToFile(QString path, QList<MeshPointer> meshes) {
|
||||
if (QFileInfo(path).exists() && !QFile::remove(path)) {
|
||||
qCDebug(modelformat) << "OBJ writer failed, file exists:" << path;
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
qCDebug(modelformat) << "OBJ writer failed to open output file:" << path;
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream outStream(&file);
|
||||
|
||||
bool success;
|
||||
success = writeOBJToTextStream(outStream, meshes);
|
||||
|
||||
file.close();
|
||||
return success;
|
||||
}
|
||||
|
||||
QString writeOBJToString(QList<MeshPointer> meshes) {
|
||||
QString result;
|
||||
QTextStream outStream(&result, QIODevice::ReadWrite);
|
||||
bool success;
|
||||
success = writeOBJToTextStream(outStream, meshes);
|
||||
if (success) {
|
||||
return result;
|
||||
}
|
||||
return QString("");
|
||||
}
|
26
libraries/fbx/src/OBJWriter.h
Normal file
26
libraries/fbx/src/OBJWriter.h
Normal file
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// OBJWriter.h
|
||||
// libraries/fbx/src/
|
||||
//
|
||||
// Created by Seth Alves on 2017-1-27.
|
||||
// Copyright 2017 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_objwriter_h
|
||||
#define hifi_objwriter_h
|
||||
|
||||
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
#include <model/Geometry.h>
|
||||
|
||||
using MeshPointer = std::shared_ptr<model::Mesh>;
|
||||
|
||||
bool writeOBJToTextStream(QTextStream& out, QList<MeshPointer> meshes);
|
||||
bool writeOBJToFile(QString path, QList<MeshPointer> meshes);
|
||||
QString writeOBJToString(QList<MeshPointer> meshes);
|
||||
|
||||
#endif // hifi_objwriter_h
|
|
@ -133,6 +133,7 @@ void LightingModel::setSpotLight(bool enable) {
|
|||
bool LightingModel::isSpotLightEnabled() const {
|
||||
return (bool)_parametersBuffer.get<Parameters>().enableSpotLight;
|
||||
}
|
||||
|
||||
void LightingModel::setShowLightContour(bool enable) {
|
||||
if (enable != isShowLightContourEnabled()) {
|
||||
_parametersBuffer.edit<Parameters>().showLightContour = (float)enable;
|
||||
|
@ -142,6 +143,14 @@ bool LightingModel::isShowLightContourEnabled() const {
|
|||
return (bool)_parametersBuffer.get<Parameters>().showLightContour;
|
||||
}
|
||||
|
||||
void LightingModel::setWireframe(bool enable) {
|
||||
if (enable != isWireframeEnabled()) {
|
||||
_parametersBuffer.edit<Parameters>().enableWireframe = (float)enable;
|
||||
}
|
||||
}
|
||||
bool LightingModel::isWireframeEnabled() const {
|
||||
return (bool)_parametersBuffer.get<Parameters>().enableWireframe;
|
||||
}
|
||||
MakeLightingModel::MakeLightingModel() {
|
||||
_lightingModel = std::make_shared<LightingModel>();
|
||||
}
|
||||
|
@ -167,6 +176,7 @@ void MakeLightingModel::configure(const Config& config) {
|
|||
_lightingModel->setSpotLight(config.enableSpotLight);
|
||||
|
||||
_lightingModel->setShowLightContour(config.showLightContour);
|
||||
_lightingModel->setWireframe(config.enableWireframe);
|
||||
}
|
||||
|
||||
void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) {
|
||||
|
|
|
@ -64,6 +64,9 @@ public:
|
|||
void setShowLightContour(bool enable);
|
||||
bool isShowLightContourEnabled() const;
|
||||
|
||||
void setWireframe(bool enable);
|
||||
bool isWireframeEnabled() const;
|
||||
|
||||
UniformBufferView getParametersBuffer() const { return _parametersBuffer; }
|
||||
|
||||
protected:
|
||||
|
@ -89,13 +92,12 @@ protected:
|
|||
float enablePointLight{ 1.0f };
|
||||
float enableSpotLight{ 1.0f };
|
||||
|
||||
float showLightContour{ 0.0f }; // false by default
|
||||
float showLightContour { 0.0f }; // false by default
|
||||
|
||||
float enableObscurance{ 1.0f };
|
||||
|
||||
float enableMaterialTexturing { 1.0f };
|
||||
|
||||
float spares{ 0.0f };
|
||||
float enableWireframe { 0.0f }; // false by default
|
||||
|
||||
Parameters() {}
|
||||
};
|
||||
|
@ -129,6 +131,7 @@ class MakeLightingModelConfig : public render::Job::Config {
|
|||
Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty)
|
||||
Q_PROPERTY(bool enableSpotLight MEMBER enableSpotLight NOTIFY dirty)
|
||||
|
||||
Q_PROPERTY(bool enableWireframe MEMBER enableWireframe NOTIFY dirty)
|
||||
Q_PROPERTY(bool showLightContour MEMBER showLightContour NOTIFY dirty)
|
||||
|
||||
public:
|
||||
|
@ -152,9 +155,10 @@ public:
|
|||
bool enablePointLight{ true };
|
||||
bool enableSpotLight{ true };
|
||||
|
||||
|
||||
bool showLightContour { false }; // false by default
|
||||
|
||||
bool enableWireframe { false }; // false by default
|
||||
|
||||
signals:
|
||||
void dirty();
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ struct LightingModel {
|
|||
vec4 _UnlitEmissiveLightmapBackground;
|
||||
vec4 _ScatteringDiffuseSpecularAlbedo;
|
||||
vec4 _AmbientDirectionalPointSpot;
|
||||
vec4 _ShowContourObscuranceSpare2;
|
||||
vec4 _ShowContourObscuranceWireframe;
|
||||
};
|
||||
|
||||
uniform lightingModelBuffer{
|
||||
|
@ -37,7 +37,7 @@ float isBackgroundEnabled() {
|
|||
return lightingModel._UnlitEmissiveLightmapBackground.w;
|
||||
}
|
||||
float isObscuranceEnabled() {
|
||||
return lightingModel._ShowContourObscuranceSpare2.y;
|
||||
return lightingModel._ShowContourObscuranceWireframe.y;
|
||||
}
|
||||
|
||||
float isScatteringEnabled() {
|
||||
|
@ -67,9 +67,12 @@ float isSpotEnabled() {
|
|||
}
|
||||
|
||||
float isShowLightContour() {
|
||||
return lightingModel._ShowContourObscuranceSpare2.x;
|
||||
return lightingModel._ShowContourObscuranceWireframe.x;
|
||||
}
|
||||
|
||||
float isWireframeEnabled() {
|
||||
return lightingModel._ShowContourObscuranceWireframe.z;
|
||||
}
|
||||
|
||||
<@endfunc@>
|
||||
<$declareLightingModel()$>
|
||||
|
|
|
@ -259,8 +259,18 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont
|
|||
// Setup lighting model for all items;
|
||||
batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer());
|
||||
|
||||
renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn);
|
||||
// From the lighting model define a global shapKey ORED with individiual keys
|
||||
ShapeKey::Builder keyBuilder;
|
||||
if (lightingModel->isWireframeEnabled()) {
|
||||
keyBuilder.withWireframe();
|
||||
}
|
||||
ShapeKey globalKey = keyBuilder.build();
|
||||
args->_globalShapeKey = globalKey._flags.to_ulong();
|
||||
|
||||
renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey);
|
||||
|
||||
args->_batch = nullptr;
|
||||
args->_globalShapeKey = 0;
|
||||
});
|
||||
|
||||
config->setNumDrawn((int)inItems.size());
|
||||
|
@ -295,12 +305,21 @@ void DrawStateSortDeferred::run(const SceneContextPointer& sceneContext, const R
|
|||
// Setup lighting model for all items;
|
||||
batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer());
|
||||
|
||||
// From the lighting model define a global shapKey ORED with individiual keys
|
||||
ShapeKey::Builder keyBuilder;
|
||||
if (lightingModel->isWireframeEnabled()) {
|
||||
keyBuilder.withWireframe();
|
||||
}
|
||||
ShapeKey globalKey = keyBuilder.build();
|
||||
args->_globalShapeKey = globalKey._flags.to_ulong();
|
||||
|
||||
if (_stateSort) {
|
||||
renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn);
|
||||
renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey);
|
||||
} else {
|
||||
renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn);
|
||||
renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey);
|
||||
}
|
||||
args->_batch = nullptr;
|
||||
args->_globalShapeKey = 0;
|
||||
});
|
||||
|
||||
config->setNumDrawn((int)inItems.size());
|
||||
|
|
|
@ -307,7 +307,7 @@ void initForwardPipelines(render::ShapePlumber& plumber) {
|
|||
void addPlumberPipeline(ShapePlumber& plumber,
|
||||
const ShapeKey& key, const gpu::ShaderPointer& vertex, const gpu::ShaderPointer& pixel) {
|
||||
// These key-values' pipelines are added by this functor in addition to the key passed
|
||||
assert(!key.isWireFrame());
|
||||
assert(!key.isWireframe());
|
||||
assert(!key.isDepthBiased());
|
||||
assert(key.isCullFace());
|
||||
|
||||
|
|
|
@ -39,9 +39,9 @@ void render::renderItems(const SceneContextPointer& sceneContext, const RenderCo
|
|||
}
|
||||
}
|
||||
|
||||
void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, const Item& item) {
|
||||
void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, const Item& item, const ShapeKey& globalKey) {
|
||||
assert(item.getKey().isShape());
|
||||
const auto& key = item.getShapeKey();
|
||||
auto key = item.getShapeKey() | globalKey;
|
||||
if (key.isValid() && !key.hasOwnPipeline()) {
|
||||
args->_pipeline = shapeContext->pickPipeline(args, key);
|
||||
if (args->_pipeline) {
|
||||
|
@ -56,7 +56,7 @@ void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, cons
|
|||
}
|
||||
|
||||
void render::renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext,
|
||||
const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems) {
|
||||
const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems, const ShapeKey& globalKey) {
|
||||
auto& scene = sceneContext->_scene;
|
||||
RenderArgs* args = renderContext->args;
|
||||
|
||||
|
@ -66,12 +66,12 @@ void render::renderShapes(const SceneContextPointer& sceneContext, const RenderC
|
|||
}
|
||||
for (auto i = 0; i < numItemsToDraw; ++i) {
|
||||
auto& item = scene->getItem(inItems[i].id);
|
||||
renderShape(args, shapeContext, item);
|
||||
renderShape(args, shapeContext, item, globalKey);
|
||||
}
|
||||
}
|
||||
|
||||
void render::renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext,
|
||||
const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems) {
|
||||
const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems, const ShapeKey& globalKey) {
|
||||
auto& scene = sceneContext->_scene;
|
||||
RenderArgs* args = renderContext->args;
|
||||
|
||||
|
@ -91,7 +91,7 @@ void render::renderStateSortShapes(const SceneContextPointer& sceneContext, cons
|
|||
|
||||
{
|
||||
assert(item.getKey().isShape());
|
||||
const auto key = item.getShapeKey();
|
||||
auto key = item.getShapeKey() | globalKey;
|
||||
if (key.isValid() && !key.hasOwnPipeline()) {
|
||||
auto& bucket = sortedShapes[key];
|
||||
if (bucket.empty()) {
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
namespace render {
|
||||
|
||||
void renderItems(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems, int maxDrawnItems = -1);
|
||||
void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1);
|
||||
void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1);
|
||||
void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey());
|
||||
void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey());
|
||||
|
||||
class DrawLightConfig : public Job::Config {
|
||||
Q_OBJECT
|
||||
|
|
|
@ -46,6 +46,10 @@ public:
|
|||
ShapeKey() : _flags{ 0 } {}
|
||||
ShapeKey(const Flags& flags) : _flags{flags} {}
|
||||
|
||||
friend ShapeKey operator&(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags & _Right._flags); }
|
||||
friend ShapeKey operator|(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags | _Right._flags); }
|
||||
friend ShapeKey operator^(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags ^ _Right._flags); }
|
||||
|
||||
class Builder {
|
||||
public:
|
||||
Builder() {}
|
||||
|
@ -144,7 +148,7 @@ public:
|
|||
bool isSkinned() const { return _flags[SKINNED]; }
|
||||
bool isDepthOnly() const { return _flags[DEPTH_ONLY]; }
|
||||
bool isDepthBiased() const { return _flags[DEPTH_BIAS]; }
|
||||
bool isWireFrame() const { return _flags[WIREFRAME]; }
|
||||
bool isWireframe() const { return _flags[WIREFRAME]; }
|
||||
bool isCullFace() const { return !_flags[NO_CULL_FACE]; }
|
||||
|
||||
bool hasOwnPipeline() const { return _flags[OWN_PIPELINE]; }
|
||||
|
@ -180,7 +184,7 @@ inline QDebug operator<<(QDebug debug, const ShapeKey& key) {
|
|||
<< "isSkinned:" << key.isSkinned()
|
||||
<< "isDepthOnly:" << key.isDepthOnly()
|
||||
<< "isDepthBiased:" << key.isDepthBiased()
|
||||
<< "isWireFrame:" << key.isWireFrame()
|
||||
<< "isWireframe:" << key.isWireframe()
|
||||
<< "isCullFace:" << key.isCullFace()
|
||||
<< "]";
|
||||
}
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
//
|
||||
// BaseScriptEngine.h
|
||||
// libraries/script-engine/src
|
||||
//
|
||||
// Created by Timothy Dedischew on 02/01/17.
|
||||
// Copyright 2017 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_BaseScriptEngine_h
|
||||
#define hifi_BaseScriptEngine_h
|
||||
|
||||
#include <functional>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtScript/QScriptEngine>
|
||||
|
||||
#include "SettingHandle.h"
|
||||
|
||||
// common base class for extending QScriptEngine itself
|
||||
class BaseScriptEngine : public QScriptEngine {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static const QString SCRIPT_EXCEPTION_FORMAT;
|
||||
static const QString SCRIPT_BACKTRACE_SEP;
|
||||
|
||||
BaseScriptEngine() {}
|
||||
|
||||
Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
|
||||
|
||||
Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1);
|
||||
Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error");
|
||||
Q_INVOKABLE QString formatException(const QScriptValue& exception);
|
||||
QScriptValue cloneUncaughtException(const QString& detail = QString());
|
||||
|
||||
signals:
|
||||
void unhandledException(const QScriptValue& exception);
|
||||
|
||||
protected:
|
||||
void _emitUnhandledException(const QScriptValue& exception);
|
||||
QScriptValue newLambdaFunction(std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership);
|
||||
|
||||
static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS;
|
||||
Setting::Handle<bool> _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true };
|
||||
#ifdef DEBUG_JS
|
||||
static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString());
|
||||
#endif
|
||||
};
|
||||
|
||||
// Lambda helps create callable QScriptValues out of std::functions:
|
||||
// (just meant for use from within the script engine itself)
|
||||
class Lambda : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation, QScriptValue data);
|
||||
~Lambda();
|
||||
public slots:
|
||||
QScriptValue call();
|
||||
QString toString() const;
|
||||
private:
|
||||
QScriptEngine* engine;
|
||||
std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation;
|
||||
QScriptValue data;
|
||||
};
|
||||
|
||||
#endif // hifi_BaseScriptEngine_h
|
41
libraries/script-engine/src/MeshProxy.h
Normal file
41
libraries/script-engine/src/MeshProxy.h
Normal file
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// MeshProxy.h
|
||||
// libraries/script-engine/src
|
||||
//
|
||||
// Created by Seth Alves on 2017-1-27.
|
||||
// Copyright 2017 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_MeshProxy_h
|
||||
#define hifi_MeshProxy_h
|
||||
|
||||
#include <model/Geometry.h>
|
||||
|
||||
using MeshPointer = std::shared_ptr<model::Mesh>;
|
||||
|
||||
class MeshProxy : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MeshProxy(MeshPointer mesh) : _mesh(mesh) {}
|
||||
~MeshProxy() {}
|
||||
|
||||
MeshPointer getMeshPointer() const { return _mesh; }
|
||||
|
||||
Q_INVOKABLE int getNumVertices() const { return (int)_mesh->getNumVertices(); }
|
||||
Q_INVOKABLE glm::vec3 getPos3(int index) const { return _mesh->getPos3(index); }
|
||||
|
||||
|
||||
protected:
|
||||
MeshPointer _mesh;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(MeshProxy*);
|
||||
|
||||
class MeshProxyList : public QList<MeshProxy*> {}; // typedef and using fight with the Qt macros/templates, do this instead
|
||||
Q_DECLARE_METATYPE(MeshProxyList);
|
||||
|
||||
#endif // hifi_MeshProxy_h
|
53
libraries/script-engine/src/ModelScriptingInterface.cpp
Normal file
53
libraries/script-engine/src/ModelScriptingInterface.cpp
Normal file
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// ModelScriptingInterface.cpp
|
||||
// libraries/script-engine/src
|
||||
//
|
||||
// Created by Seth Alves on 2017-1-27.
|
||||
// Copyright 2017 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 <QScriptEngine>
|
||||
#include <QScriptValueIterator>
|
||||
#include <QtScript/QScriptValue>
|
||||
#include "ModelScriptingInterface.h"
|
||||
#include "OBJWriter.h"
|
||||
|
||||
|
||||
ModelScriptingInterface::ModelScriptingInterface(QObject* parent) : QObject(parent) {
|
||||
}
|
||||
|
||||
QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in) {
|
||||
return engine->newQObject(in, QScriptEngine::QtOwnership,
|
||||
QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects);
|
||||
}
|
||||
|
||||
void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out) {
|
||||
out = qobject_cast<MeshProxy*>(value.toQObject());
|
||||
}
|
||||
|
||||
QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in) {
|
||||
return engine->toScriptValue(in);
|
||||
}
|
||||
|
||||
void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out) {
|
||||
QScriptValueIterator itr(value);
|
||||
while(itr.hasNext()) {
|
||||
itr.next();
|
||||
MeshProxy* meshProxy = qscriptvalue_cast<MeshProxyList::value_type>(itr.value());
|
||||
if (meshProxy) {
|
||||
out.append(meshProxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString ModelScriptingInterface::meshToOBJ(MeshProxyList in) {
|
||||
QList<MeshPointer> meshes;
|
||||
foreach (const MeshProxy* meshProxy, in) {
|
||||
meshes.append(meshProxy->getMeshPointer());
|
||||
}
|
||||
|
||||
return writeOBJToString(meshes);
|
||||
}
|
39
libraries/script-engine/src/ModelScriptingInterface.h
Normal file
39
libraries/script-engine/src/ModelScriptingInterface.h
Normal file
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// ModelScriptingInterface.h
|
||||
// libraries/script-engine/src
|
||||
//
|
||||
// Created by Seth Alves on 2017-1-27.
|
||||
// Copyright 2017 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_ModelScriptingInterface_h
|
||||
#define hifi_ModelScriptingInterface_h
|
||||
|
||||
#include <QtCore/QObject>
|
||||
#include <QScriptValue>
|
||||
#include <OBJWriter.h>
|
||||
#include <model/Geometry.h>
|
||||
#include "MeshProxy.h"
|
||||
|
||||
using MeshPointer = std::shared_ptr<model::Mesh>;
|
||||
|
||||
class ModelScriptingInterface : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ModelScriptingInterface(QObject* parent);
|
||||
|
||||
Q_INVOKABLE QString meshToOBJ(MeshProxyList in);
|
||||
};
|
||||
|
||||
QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in);
|
||||
void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out);
|
||||
|
||||
QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in);
|
||||
void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out);
|
||||
|
||||
#endif // hifi_ModelScriptingInterface_h
|
|
@ -19,6 +19,9 @@
|
|||
#include <QtCore/QThread>
|
||||
#include <QtCore/QRegularExpression>
|
||||
|
||||
#include <QtCore/QFuture>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
|
||||
#include <QtWidgets/QMainWindow>
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
|
@ -65,18 +68,25 @@
|
|||
#include "RecordingScriptingInterface.h"
|
||||
#include "ScriptEngines.h"
|
||||
#include "TabletScriptingInterface.h"
|
||||
#include "ModelScriptingInterface.h"
|
||||
|
||||
|
||||
#include <Profile.h>
|
||||
|
||||
#include "MIDIEvent.h"
|
||||
|
||||
const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
|
||||
"com.highfidelity.experimental.enableExtendedJSExceptions"
|
||||
};
|
||||
|
||||
static const int MAX_MODULE_ID_LENGTH { 4096 };
|
||||
static const int MAX_DEBUG_VALUE_LENGTH { 80 };
|
||||
|
||||
static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =
|
||||
QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;
|
||||
static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable };
|
||||
static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration };
|
||||
|
||||
|
||||
|
||||
static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };
|
||||
|
||||
Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)
|
||||
|
@ -84,7 +94,7 @@ int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature
|
|||
|
||||
Q_LOGGING_CATEGORY(scriptengineScript, "hifi.scriptengine.script")
|
||||
|
||||
static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){
|
||||
static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) {
|
||||
QString message = "";
|
||||
for (int i = 0; i < context->argumentCount(); i++) {
|
||||
if (i > 0) {
|
||||
|
@ -141,7 +151,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID)
|
|||
}
|
||||
|
||||
QString ScriptEngine::logException(const QScriptValue& exception) {
|
||||
auto message = formatException(exception);
|
||||
auto message = formatException(exception, _enableExtendedJSExceptions.get());
|
||||
scriptErrorMessage(message);
|
||||
return message;
|
||||
}
|
||||
|
@ -333,7 +343,7 @@ void ScriptEngine::runInThread() {
|
|||
// The thread interface cannot live on itself, and we want to move this into the thread, so
|
||||
// the thread cannot have this as a parent.
|
||||
QThread* workerThread = new QThread();
|
||||
workerThread->setObjectName(QString("Script Thread:") + getFilename());
|
||||
workerThread->setObjectName(QString("js:") + getFilename().replace("about:",""));
|
||||
moveToThread(workerThread);
|
||||
|
||||
// NOTE: If you connect any essential signals for proper shutdown or cleanup of
|
||||
|
@ -532,6 +542,40 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) {
|
|||
return prototype;
|
||||
}
|
||||
|
||||
void ScriptEngine::resetModuleCache(bool deleteScriptCache) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); });
|
||||
return;
|
||||
}
|
||||
auto jsRequire = globalObject().property("Script").property("require");
|
||||
auto cache = jsRequire.property("cache");
|
||||
auto cacheMeta = jsRequire.data();
|
||||
|
||||
if (deleteScriptCache) {
|
||||
QScriptValueIterator it(cache);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
if (it.flags() & QScriptValue::SkipInEnumeration) {
|
||||
continue;
|
||||
}
|
||||
qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require";
|
||||
cacheMeta.setProperty(it.name(), true);
|
||||
}
|
||||
}
|
||||
cache = newObject();
|
||||
if (!cacheMeta.isObject()) {
|
||||
cacheMeta = newObject();
|
||||
cacheMeta.setProperty("id", "Script.require.cacheMeta");
|
||||
cacheMeta.setProperty("type", "cacheMeta");
|
||||
jsRequire.setData(cacheMeta);
|
||||
}
|
||||
cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration);
|
||||
#if DEBUG_JS_MODULES
|
||||
cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS);
|
||||
#endif
|
||||
jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS);
|
||||
}
|
||||
|
||||
void ScriptEngine::init() {
|
||||
if (_isInitialized) {
|
||||
return; // only initialize once
|
||||
|
@ -585,6 +629,15 @@ void ScriptEngine::init() {
|
|||
|
||||
registerGlobalObject("Script", this);
|
||||
|
||||
{
|
||||
// set up Script.require.resolve and Script.require.cache
|
||||
auto Script = globalObject().property("Script");
|
||||
auto require = Script.property("require");
|
||||
auto resolve = Script.property("_requireResolve");
|
||||
require.setProperty("resolve", resolve, READONLY_PROP_FLAGS);
|
||||
resetModuleCache();
|
||||
}
|
||||
|
||||
registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());
|
||||
registerGlobalObject("Entities", entityScriptingInterface.data());
|
||||
registerGlobalObject("Quat", &_quatLibrary);
|
||||
|
@ -594,7 +647,7 @@ void ScriptEngine::init() {
|
|||
registerGlobalObject("Messages", DependencyManager::get<MessagesClient>().data());
|
||||
|
||||
registerGlobalObject("File", new FileScriptingInterface(this));
|
||||
|
||||
|
||||
qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue);
|
||||
qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue);
|
||||
|
||||
|
@ -612,6 +665,10 @@ void ScriptEngine::init() {
|
|||
registerGlobalObject("Resources", DependencyManager::get<ResourceScriptingInterface>().data());
|
||||
|
||||
registerGlobalObject("DebugDraw", &DebugDraw::getInstance());
|
||||
|
||||
registerGlobalObject("Model", new ModelScriptingInterface(this));
|
||||
qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue);
|
||||
qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue);
|
||||
}
|
||||
|
||||
void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) {
|
||||
|
@ -853,6 +910,11 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString&
|
|||
handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().
|
||||
}
|
||||
|
||||
// this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE
|
||||
QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
|
||||
return BaseScriptEngine::evaluateInClosure(closure, program);
|
||||
}
|
||||
|
||||
QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
|
||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
||||
return QScriptValue(); // bail early
|
||||
|
@ -875,29 +937,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi
|
|||
// Check syntax
|
||||
auto syntaxError = lintScript(sourceCode, fileName);
|
||||
if (syntaxError.isError()) {
|
||||
if (isEvaluating()) {
|
||||
currentContext()->throwValue(syntaxError);
|
||||
} else {
|
||||
if (!isEvaluating()) {
|
||||
syntaxError.setProperty("detail", "evaluate");
|
||||
emit unhandledException(syntaxError);
|
||||
}
|
||||
raiseException(syntaxError);
|
||||
maybeEmitUncaughtException("lint");
|
||||
return syntaxError;
|
||||
}
|
||||
QScriptProgram program { sourceCode, fileName, lineNumber };
|
||||
if (program.isNull()) {
|
||||
// can this happen?
|
||||
auto err = makeError("could not create QScriptProgram for " + fileName);
|
||||
emit unhandledException(err);
|
||||
raiseException(err);
|
||||
maybeEmitUncaughtException("compile");
|
||||
return err;
|
||||
}
|
||||
|
||||
QScriptValue result;
|
||||
{
|
||||
result = BaseScriptEngine::evaluate(program);
|
||||
if (!isEvaluating() && hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(__FUNCTION__));
|
||||
clearExceptions();
|
||||
}
|
||||
maybeEmitUncaughtException("evaluate");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -920,10 +979,7 @@ void ScriptEngine::run() {
|
|||
|
||||
{
|
||||
evaluate(_scriptContents, _fileNameString);
|
||||
if (!isEvaluating() && hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(__FUNCTION__));
|
||||
clearExceptions();
|
||||
}
|
||||
maybeEmitUncaughtException(__FUNCTION__);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
// VS13 does not sleep_until unless it uses the system_clock, see:
|
||||
|
@ -1294,11 +1350,361 @@ void ScriptEngine::print(const QString& message) {
|
|||
emit printedMessage(message);
|
||||
}
|
||||
|
||||
// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js)
|
||||
QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return QString();
|
||||
}
|
||||
QUrl defaultScriptsLoc = defaultScriptsLocation();
|
||||
QUrl url(moduleId);
|
||||
|
||||
auto displayId = moduleId;
|
||||
if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) {
|
||||
displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";
|
||||
}
|
||||
auto message = QString("Cannot find module '%1' (%2)").arg(displayId);
|
||||
|
||||
auto throwResolveError = [&](const QScriptValue& error) -> QString {
|
||||
raiseException(error);
|
||||
maybeEmitUncaughtException("require.resolve");
|
||||
return QString();
|
||||
};
|
||||
|
||||
// de-fuzz the input a little by restricting to rational sizes
|
||||
auto idLength = url.toString().length();
|
||||
if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) {
|
||||
auto details = QString("rejecting invalid module id size (%1 chars [1,%2])")
|
||||
.arg(idLength).arg(MAX_MODULE_ID_LENGTH);
|
||||
return throwResolveError(makeError(message.arg(details), "RangeError"));
|
||||
}
|
||||
|
||||
// this regex matches: absolute, dotted or path-like URLs
|
||||
// (ie: the kind of stuff ScriptEngine::resolvePath already handles)
|
||||
QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)");
|
||||
|
||||
// this is for module.require (which is a bound version of require that's always relative to the module path)
|
||||
if (!relativeTo.isEmpty()) {
|
||||
url = QUrl(relativeTo).resolved(moduleId);
|
||||
url = resolvePath(url.toString());
|
||||
} else if (qualified.match(moduleId).hasMatch()) {
|
||||
url = resolvePath(moduleId);
|
||||
} else {
|
||||
// check if the moduleId refers to a "system" module
|
||||
QString systemPath = defaultScriptsLoc.path();
|
||||
QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId);
|
||||
url = defaultScriptsLoc;
|
||||
url.setPath(systemModulePath);
|
||||
if (!QFileInfo(url.toLocalFile()).isFile()) {
|
||||
if (!moduleId.contains("./")) {
|
||||
// the user might be trying to refer to a relative file without anchoring it
|
||||
// let's do them a favor and test for that case -- offering specific advice if detected
|
||||
auto unanchoredUrl = resolvePath("./" + moduleId);
|
||||
if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) {
|
||||
auto msg = QString("relative module ids must be anchored; use './%1' instead")
|
||||
.arg(moduleId);
|
||||
return throwResolveError(makeError(message.arg(msg)));
|
||||
}
|
||||
}
|
||||
return throwResolveError(makeError(message.arg("system module not found")));
|
||||
}
|
||||
}
|
||||
|
||||
if (url.isRelative()) {
|
||||
return throwResolveError(makeError(message.arg("could not resolve module id")));
|
||||
}
|
||||
|
||||
// if it looks like a local file, verify that it's an allowed path and really a file
|
||||
if (url.isLocalFile()) {
|
||||
QFileInfo file(url.toLocalFile());
|
||||
QUrl canonical = url;
|
||||
if (file.exists()) {
|
||||
canonical.setPath(file.canonicalFilePath());
|
||||
}
|
||||
|
||||
bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile();
|
||||
if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) {
|
||||
return throwResolveError(makeError(message.arg(
|
||||
QString("path '%1' outside of origin script '%2' '%3'")
|
||||
.arg(PathUtils::stripFilename(url))
|
||||
.arg(PathUtils::stripFilename(currentSandboxURL))
|
||||
.arg(canonical.toString())
|
||||
)));
|
||||
}
|
||||
if (!file.exists()) {
|
||||
return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile())));
|
||||
}
|
||||
if (!file.isFile()) {
|
||||
return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile())));
|
||||
}
|
||||
}
|
||||
|
||||
maybeEmitUncaughtException(__FUNCTION__);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// retrieves the current parent module from the JS scope chain
|
||||
QScriptValue ScriptEngine::currentModule() {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return unboundNullValue();
|
||||
}
|
||||
auto jsRequire = globalObject().property("Script").property("require");
|
||||
auto cache = jsRequire.property("cache");
|
||||
auto candidate = QScriptValue();
|
||||
for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) {
|
||||
QScriptContextInfo contextInfo { c };
|
||||
candidate = cache.property(contextInfo.fileName());
|
||||
}
|
||||
if (!candidate.isObject()) {
|
||||
return QScriptValue();
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
// replaces or adds "module" to "parent.children[]" array
|
||||
// (for consistency with Node.js and userscript cache invalidation without "cache busters")
|
||||
bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) {
|
||||
auto children = parent.property("children");
|
||||
if (children.isArray()) {
|
||||
auto key = module.property("id");
|
||||
auto length = children.property("length").toInt32();
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (children.property(i).property("id").strictlyEquals(key)) {
|
||||
qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module";
|
||||
children.setProperty(i, module);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module";
|
||||
children.setProperty(length, module);
|
||||
return true;
|
||||
} else if (parent.isValid()) {
|
||||
qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// creates a new JS "module" Object with default metadata properties
|
||||
QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) {
|
||||
auto closure = newObject();
|
||||
auto exports = newObject();
|
||||
auto module = newObject();
|
||||
qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString();
|
||||
|
||||
closure.setProperty("module", module, READONLY_PROP_FLAGS);
|
||||
|
||||
// note: this becomes the "exports" free variable, so should not be set read only
|
||||
closure.setProperty("exports", exports);
|
||||
|
||||
// make the closure available to module instantiation
|
||||
module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS);
|
||||
|
||||
// for consistency with Node.js Module
|
||||
module.setProperty("id", modulePath, READONLY_PROP_FLAGS);
|
||||
module.setProperty("filename", modulePath, READONLY_PROP_FLAGS);
|
||||
module.setProperty("exports", exports); // not readonly
|
||||
module.setProperty("loaded", false, READONLY_PROP_FLAGS);
|
||||
module.setProperty("parent", parent, READONLY_PROP_FLAGS);
|
||||
module.setProperty("children", newArray(), READONLY_PROP_FLAGS);
|
||||
|
||||
// module.require is a bound version of require that always resolves relative to that module's path
|
||||
auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)");
|
||||
module.setProperty("require", boundRequire, READONLY_PROP_FLAGS);
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
// synchronously fetch a module's source code using BatchLoader
|
||||
QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) {
|
||||
using UrlMap = QMap<QUrl, QString>;
|
||||
auto scriptCache = DependencyManager::get<ScriptCache>();
|
||||
QVariantMap req;
|
||||
qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread();
|
||||
|
||||
auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) {
|
||||
auto url = modulePath;
|
||||
auto status = _status[url];
|
||||
auto contents = data[url];
|
||||
qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread();
|
||||
if (isStopping()) {
|
||||
req["status"] = "Stopped";
|
||||
req["success"] = false;
|
||||
} else {
|
||||
req["url"] = url;
|
||||
req["status"] = status;
|
||||
req["success"] = ScriptCache::isSuccessStatus(status);
|
||||
req["contents"] = contents;
|
||||
}
|
||||
};
|
||||
|
||||
if (forceDownload) {
|
||||
qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath;
|
||||
scriptCache->deleteScript(modulePath);
|
||||
}
|
||||
BatchLoader* loader = new BatchLoader(QList<QUrl>({ modulePath }));
|
||||
connect(loader, &BatchLoader::finished, this, onload);
|
||||
connect(this, &QObject::destroyed, loader, &QObject::deleteLater);
|
||||
// fail faster? (since require() blocks the engine thread while resolving dependencies)
|
||||
const int MAX_RETRIES = 1;
|
||||
|
||||
loader->start(MAX_RETRIES);
|
||||
|
||||
if (!loader->isFinished()) {
|
||||
QTimer monitor;
|
||||
QEventLoop loop;
|
||||
QObject::connect(loader, &BatchLoader::finished, this, [this, &monitor, &loop]{
|
||||
monitor.stop();
|
||||
loop.quit();
|
||||
});
|
||||
|
||||
// this helps detect the case where stop() is invoked during the download
|
||||
// but not seen in time to abort processing in onload()...
|
||||
connect(&monitor, &QTimer::timeout, this, [this, &loop, &loader]{
|
||||
if (isStopping()) {
|
||||
loop.exit(-1);
|
||||
}
|
||||
});
|
||||
monitor.start(500);
|
||||
loop.exec();
|
||||
}
|
||||
loader->deleteLater();
|
||||
return req;
|
||||
}
|
||||
|
||||
// evaluate a pending module object using the fetched source code
|
||||
QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) {
|
||||
QScriptValue result;
|
||||
auto modulePath = module.property("filename").toString();
|
||||
auto closure = module.property("__closure__");
|
||||
|
||||
qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes")
|
||||
.arg(QUrl(modulePath).fileName()).arg(sourceCode.length());
|
||||
|
||||
if (module.property("content-type").toString() == "application/json") {
|
||||
qCDebug(scriptengine_module) << "... parsing as JSON";
|
||||
closure.setProperty("__json", sourceCode);
|
||||
result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath });
|
||||
} else {
|
||||
// scoped vars for consistency with Node.js
|
||||
closure.setProperty("require", module.property("require"));
|
||||
closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS);
|
||||
closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS);
|
||||
result = evaluateInClosure(closure, { sourceCode, modulePath });
|
||||
}
|
||||
maybeEmitUncaughtException(__FUNCTION__);
|
||||
return result;
|
||||
}
|
||||
|
||||
// CommonJS/Node.js like require/module support
|
||||
QScriptValue ScriptEngine::require(const QString& moduleId) {
|
||||
qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")";
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return unboundNullValue();
|
||||
}
|
||||
|
||||
auto jsRequire = globalObject().property("Script").property("require");
|
||||
auto cacheMeta = jsRequire.data();
|
||||
auto cache = jsRequire.property("cache");
|
||||
auto parent = currentModule();
|
||||
|
||||
auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) {
|
||||
cache.setProperty(modulePath, nullValue());
|
||||
if (!error.isNull()) {
|
||||
#ifdef DEBUG_JS_MODULES
|
||||
qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString();
|
||||
#endif
|
||||
raiseException(error);
|
||||
}
|
||||
maybeEmitUncaughtException("module");
|
||||
return unboundNullValue();
|
||||
};
|
||||
|
||||
// start by resolving the moduleId into a fully-qualified path/URL
|
||||
QString modulePath = _requireResolve(moduleId);
|
||||
if (modulePath.isNull() || hasUncaughtException()) {
|
||||
// the resolver already threw an exception -- bail early
|
||||
maybeEmitUncaughtException(__FUNCTION__);
|
||||
return unboundNullValue();
|
||||
}
|
||||
|
||||
// check the resolved path against the cache
|
||||
auto module = cache.property(modulePath);
|
||||
|
||||
// modules get cached in `Script.require.cache` and (similar to Node.js) users can access it
|
||||
// to inspect particular entries and invalidate them by deleting the key:
|
||||
// `delete Script.require.cache[Script.require.resolve(moduleId)];`
|
||||
|
||||
// cacheMeta is just used right now to tell deleted keys apart from undefined ones
|
||||
bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid();
|
||||
|
||||
// reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load
|
||||
cacheMeta.setProperty(modulePath, QScriptValue());
|
||||
|
||||
auto exports = module.property("exports");
|
||||
if (!invalidateCache && exports.isObject()) {
|
||||
// we have found a cached module -- just need to possibly register it with current parent
|
||||
qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)")
|
||||
.arg(modulePath).arg(moduleId).arg(module.property("loaded").toString());
|
||||
registerModuleWithParent(module, parent);
|
||||
maybeEmitUncaughtException("cached module");
|
||||
return exports;
|
||||
}
|
||||
|
||||
// bootstrap / register new empty module
|
||||
module = newModule(modulePath, parent);
|
||||
registerModuleWithParent(module, parent);
|
||||
|
||||
// add it to the cache (this is done early so any cyclic dependencies pick up)
|
||||
cache.setProperty(modulePath, module);
|
||||
|
||||
// download the module source
|
||||
auto req = fetchModuleSource(modulePath, invalidateCache);
|
||||
|
||||
if (!req.contains("success") || !req["success"].toBool()) {
|
||||
auto error = QString("error retrieving script (%1)").arg(req["status"].toString());
|
||||
return throwModuleError(modulePath, error);
|
||||
}
|
||||
|
||||
#if DEBUG_JS_MODULES
|
||||
qCDebug(scriptengine_module) << "require.loaded: " <<
|
||||
QUrl(req["url"].toString()).fileName() << req["status"].toString();
|
||||
#endif
|
||||
|
||||
auto sourceCode = req["contents"].toString();
|
||||
|
||||
if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) {
|
||||
module.setProperty("content-type", "application/json");
|
||||
} else {
|
||||
module.setProperty("content-type", "application/javascript");
|
||||
}
|
||||
|
||||
// evaluate the module
|
||||
auto result = instantiateModule(module, sourceCode);
|
||||
|
||||
if (result.isError() && !result.strictlyEquals(module.property("exports"))) {
|
||||
qCWarning(scriptengine_module) << "-- result.isError --" << result.toString();
|
||||
return throwModuleError(modulePath, result);
|
||||
}
|
||||
|
||||
// mark as fully-loaded
|
||||
module.setProperty("loaded", true, READONLY_PROP_FLAGS);
|
||||
|
||||
// set up a new reference point for detecting cache key deletion
|
||||
cacheMeta.setProperty(modulePath, module);
|
||||
|
||||
qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")";
|
||||
|
||||
maybeEmitUncaughtException(__FUNCTION__);
|
||||
return module.property("exports");
|
||||
}
|
||||
|
||||
// If a callback is specified, the included files will be loaded asynchronously and the callback will be called
|
||||
// when all of the files have finished loading.
|
||||
// If no callback is specified, the included files will be loaded synchronously and will block execution until
|
||||
// all of the files have finished loading.
|
||||
void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return;
|
||||
}
|
||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
||||
scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:"
|
||||
+ includeFiles.join(",") + "parent script:" + getFilename());
|
||||
|
@ -1361,7 +1767,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
|
|||
|
||||
doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);
|
||||
if (hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(__FUNCTION__));
|
||||
emit unhandledException(cloneUncaughtException("evaluateInclude"));
|
||||
clearExceptions();
|
||||
}
|
||||
} else {
|
||||
|
@ -1408,6 +1814,9 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) {
|
|||
// as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which
|
||||
// the Application or other context will connect to in order to know to actually load the script
|
||||
void ScriptEngine::load(const QString& loadFile) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return;
|
||||
}
|
||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
||||
scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:"
|
||||
+ loadFile + "parent script:" + getFilename());
|
||||
|
@ -1477,6 +1886,52 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const
|
|||
emit entityScriptDetailsUpdated();
|
||||
}
|
||||
|
||||
QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) {
|
||||
static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) };
|
||||
QVariantMap map;
|
||||
if (entityID.isNull()) {
|
||||
// TODO: find better way to report JS Error across thread/process boundaries
|
||||
map["isError"] = true;
|
||||
map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID";
|
||||
} else {
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread();
|
||||
#endif
|
||||
EntityScriptDetails scriptDetails;
|
||||
if (getEntityScriptDetails(entityID, scriptDetails)) {
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread();
|
||||
#endif
|
||||
map["isRunning"] = isEntityScriptRunning(entityID);
|
||||
map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower();
|
||||
map["errorInfo"] = scriptDetails.errorInfo;
|
||||
map["entityID"] = entityID.toString();
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
{
|
||||
auto debug = QVariantMap();
|
||||
debug["script"] = scriptDetails.scriptText;
|
||||
debug["scriptObject"] = scriptDetails.scriptObject.toVariant();
|
||||
debug["lastModified"] = (qlonglong)scriptDetails.lastModified;
|
||||
debug["sandboxURL"] = scriptDetails.definingSandboxURL;
|
||||
map["debug"] = debug;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
qDebug() << "!gotEntityScriptDetails" << QThread::currentThread();
|
||||
#endif
|
||||
map["isError"] = true;
|
||||
map["errorInfo"] = "Entity script details unavailable";
|
||||
map["entityID"] = entityID.toString();
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
QFuture<QVariant> ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) {
|
||||
return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID);
|
||||
}
|
||||
|
||||
bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
|
||||
auto it = _entityScripts.constFind(entityID);
|
||||
if (it == _entityScripts.constEnd()) {
|
||||
|
@ -1615,10 +2070,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString&
|
|||
|
||||
auto scriptCache = DependencyManager::get<ScriptCache>();
|
||||
// note: see EntityTreeRenderer.cpp for shared pointer lifecycle management
|
||||
QWeakPointer<ScriptEngine> weakRef(sharedFromThis());
|
||||
QWeakPointer<BaseScriptEngine> weakRef(sharedFromThis());
|
||||
scriptCache->getScriptContents(entityScript,
|
||||
[this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {
|
||||
QSharedPointer<ScriptEngine> strongRef(weakRef);
|
||||
QSharedPointer<BaseScriptEngine> strongRef(weakRef);
|
||||
if (!strongRef) {
|
||||
qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";
|
||||
return;
|
||||
|
@ -1737,13 +2192,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
timeout.setSingleShot(true);
|
||||
timeout.start(SANDBOX_TIMEOUT);
|
||||
connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{
|
||||
auto context = sandbox.currentContext();
|
||||
if (context) {
|
||||
qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")";
|
||||
|
||||
// Guard against infinite loops and non-performant code
|
||||
context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));
|
||||
}
|
||||
sandbox.raiseException(
|
||||
sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT))
|
||||
);
|
||||
});
|
||||
|
||||
testConstructor = sandbox.evaluate(program);
|
||||
|
@ -1759,7 +2213,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
if (exception.isError()) {
|
||||
// create a local copy using makeError to decouple from the sandbox engine
|
||||
exception = makeError(exception);
|
||||
setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
|
||||
setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
|
||||
emit unhandledException(exception);
|
||||
return;
|
||||
}
|
||||
|
@ -1771,9 +2225,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
testConstructorType = "empty";
|
||||
}
|
||||
QString testConstructorValue = testConstructor.toString();
|
||||
const int maxTestConstructorValueSize = 80;
|
||||
if (testConstructorValue.size() > maxTestConstructorValueSize) {
|
||||
testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";
|
||||
if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) {
|
||||
testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";
|
||||
}
|
||||
auto message = QString("failed to load entity script -- expected a function, got %1, %2")
|
||||
.arg(testConstructorType).arg(testConstructorValue);
|
||||
|
@ -1811,7 +2264,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
|
||||
if (entityScriptObject.isError()) {
|
||||
auto exception = entityScriptObject;
|
||||
setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
|
||||
setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
|
||||
emit unhandledException(exception);
|
||||
return;
|
||||
}
|
||||
|
@ -1855,10 +2308,12 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR
|
|||
const EntityScriptDetails &oldDetails = _entityScripts[entityID];
|
||||
if (isEntityScriptRunning(entityID)) {
|
||||
callEntityScriptMethod(entityID, "unload");
|
||||
} else {
|
||||
}
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
else {
|
||||
qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status;
|
||||
}
|
||||
|
||||
#endif
|
||||
if (shouldRemoveFromMap) {
|
||||
// this was a deleted entity, we've been asked to remove it from the map
|
||||
_entityScripts.remove(entityID);
|
||||
|
@ -1950,10 +2405,7 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s
|
|||
#else
|
||||
operation();
|
||||
#endif
|
||||
if (!isEvaluating() && hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__));
|
||||
clearExceptions();
|
||||
}
|
||||
maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__);
|
||||
currentEntityIdentifier = oldIdentifier;
|
||||
currentSandboxURL = oldSandboxURL;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
#include "ScriptCache.h"
|
||||
#include "ScriptUUID.h"
|
||||
#include "Vec3.h"
|
||||
#include "SettingHandle.h"
|
||||
|
||||
class QScriptEngineDebugger;
|
||||
|
||||
|
@ -78,7 +79,7 @@ public:
|
|||
QUrl definingSandboxURL { QUrl("about:EntityScript") };
|
||||
};
|
||||
|
||||
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis<ScriptEngine> {
|
||||
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString context READ getContext)
|
||||
public:
|
||||
|
@ -137,6 +138,8 @@ public:
|
|||
/// evaluate some code in the context of the ScriptEngine and return the result
|
||||
Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName, int lineNumber = 1); // this is also used by the script tool widget
|
||||
|
||||
Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
|
||||
|
||||
/// if the script engine is not already running, this will download the URL and start the process of seting it up
|
||||
/// to run... NOTE - this is used by Application currently to load the url. We don't really want it to be exposed
|
||||
/// to scripts. we may not need this to be invokable
|
||||
|
@ -157,6 +160,16 @@ public:
|
|||
Q_INVOKABLE void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue());
|
||||
Q_INVOKABLE void include(const QString& includeFile, QScriptValue callback = QScriptValue());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// MODULE related methods
|
||||
Q_INVOKABLE QScriptValue require(const QString& moduleId);
|
||||
Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false);
|
||||
QScriptValue currentModule();
|
||||
bool registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent);
|
||||
QScriptValue newModule(const QString& modulePath, const QScriptValue& parent = QScriptValue());
|
||||
QVariantMap fetchModuleSource(const QString& modulePath, const bool forceDownload = false);
|
||||
QScriptValue instantiateModule(const QScriptValue& module, const QString& sourceCode);
|
||||
|
||||
Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS);
|
||||
Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS);
|
||||
Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
|
||||
|
@ -170,6 +183,8 @@ public:
|
|||
Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) {
|
||||
return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING;
|
||||
}
|
||||
QVariant cloneEntityScriptDetails(const EntityItemID& entityID);
|
||||
QFuture<QVariant> getLocalEntityScriptDetails(const EntityItemID& entityID) override;
|
||||
Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload);
|
||||
Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false); // will call unload method
|
||||
Q_INVOKABLE void unloadAllEntityScripts();
|
||||
|
@ -237,6 +252,9 @@ signals:
|
|||
protected:
|
||||
void init();
|
||||
Q_INVOKABLE void executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type = Qt::QueuedConnection );
|
||||
// note: this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general;
|
||||
// then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;"
|
||||
Q_INVOKABLE QString _requireResolve(const QString& moduleId, const QString& relativeTo = QString());
|
||||
|
||||
QString logException(const QScriptValue& exception);
|
||||
void timerFired();
|
||||
|
@ -290,11 +308,16 @@ protected:
|
|||
|
||||
AssetScriptingInterface _assetScriptingInterface{ this };
|
||||
|
||||
std::function<bool()> _emitScriptUpdates{ [](){ return true; } };
|
||||
std::function<bool()> _emitScriptUpdates{ []() { return true; } };
|
||||
|
||||
std::recursive_mutex _lock;
|
||||
|
||||
std::chrono::microseconds _totalTimerExecution { 0 };
|
||||
|
||||
static const QString _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT;
|
||||
static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS;
|
||||
|
||||
Setting::Handle<bool> _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true };
|
||||
};
|
||||
|
||||
#endif // hifi_ScriptEngine_h
|
||||
|
|
|
@ -12,3 +12,4 @@
|
|||
#include "ScriptEngineLogging.h"
|
||||
|
||||
Q_LOGGING_CATEGORY(scriptengine, "hifi.scriptengine")
|
||||
Q_LOGGING_CATEGORY(scriptengine_module, "hifi.scriptengine.module")
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include <QLoggingCategory>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(scriptengine)
|
||||
Q_DECLARE_LOGGING_CATEGORY(scriptengine_module)
|
||||
|
||||
#endif // hifi_ScriptEngineLogging_h
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
//
|
||||
|
||||
#include "BaseScriptEngine.h"
|
||||
#include "SharedLogging.h"
|
||||
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QThread>
|
||||
|
@ -18,18 +19,27 @@
|
|||
#include <QtScript/QScriptValueIterator>
|
||||
#include <QtScript/QScriptContextInfo>
|
||||
|
||||
#include "ScriptEngineLogging.h"
|
||||
#include "Profile.h"
|
||||
|
||||
const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
|
||||
"com.highfidelity.experimental.enableExtendedJSExceptions"
|
||||
};
|
||||
|
||||
const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" };
|
||||
const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " };
|
||||
|
||||
bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method) {
|
||||
if (QThread::currentThread() == thread) {
|
||||
return true;
|
||||
}
|
||||
qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3")
|
||||
.arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName());
|
||||
qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)";
|
||||
Q_ASSERT(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// engine-aware JS Error copier and factory
|
||||
QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return unboundNullValue();
|
||||
}
|
||||
auto other = _other;
|
||||
if (other.isString()) {
|
||||
other = newObject();
|
||||
|
@ -41,7 +51,7 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri
|
|||
}
|
||||
if (!proto.isFunction()) {
|
||||
#ifdef DEBUG_JS_EXCEPTIONS
|
||||
qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
|
||||
qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
|
||||
#endif
|
||||
proto = globalObject().property("Error");
|
||||
}
|
||||
|
@ -64,6 +74,9 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri
|
|||
|
||||
// check syntax and when there are issues returns an actual "SyntaxError" with the details
|
||||
QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return unboundNullValue();
|
||||
}
|
||||
const auto syntaxCheck = checkSyntax(sourceCode);
|
||||
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
|
||||
auto err = globalObject().property("SyntaxError")
|
||||
|
@ -82,13 +95,16 @@ QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QStri
|
|||
}
|
||||
return err;
|
||||
}
|
||||
return undefinedValue();
|
||||
return QScriptValue();
|
||||
}
|
||||
|
||||
// this pulls from the best available information to create a detailed snapshot of the current exception
|
||||
QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return unboundNullValue();
|
||||
}
|
||||
if (!hasUncaughtException()) {
|
||||
return QScriptValue();
|
||||
return unboundNullValue();
|
||||
}
|
||||
auto exception = uncaughtException();
|
||||
// ensure the error object is engine-local
|
||||
|
@ -144,7 +160,10 @@ QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail
|
|||
return err;
|
||||
}
|
||||
|
||||
QString BaseScriptEngine::formatException(const QScriptValue& exception) {
|
||||
QString BaseScriptEngine::formatException(const QScriptValue& exception, bool includeExtendedDetails) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return QString();
|
||||
}
|
||||
QString note { "UncaughtException" };
|
||||
QString result;
|
||||
|
||||
|
@ -156,8 +175,8 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) {
|
|||
const auto lineNumber = exception.property("lineNumber").toString();
|
||||
const auto stacktrace = exception.property("stack").toString();
|
||||
|
||||
if (_enableExtendedJSExceptions.get()) {
|
||||
// This setting toggles display of the hints now being added during the loading process.
|
||||
if (includeExtendedDetails) {
|
||||
// Display additional exception / troubleshooting hints that can be added via the custom Error .detail property
|
||||
// Example difference:
|
||||
// [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n...
|
||||
// [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n...
|
||||
|
@ -173,14 +192,39 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) {
|
|||
return result;
|
||||
}
|
||||
|
||||
bool BaseScriptEngine::raiseException(const QScriptValue& exception) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return false;
|
||||
}
|
||||
if (currentContext()) {
|
||||
// we have an active context / JS stack frame so throw the exception per usual
|
||||
currentContext()->throwValue(makeError(exception));
|
||||
return true;
|
||||
} else {
|
||||
// we are within a pure C++ stack frame (ie: being called directly by other C++ code)
|
||||
// in this case no context information is available so just emit the exception for reporting
|
||||
emit unhandledException(makeError(exception));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BaseScriptEngine::maybeEmitUncaughtException(const QString& debugHint) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return false;
|
||||
}
|
||||
if (!isEvaluating() && hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(debugHint));
|
||||
clearExceptions();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
|
||||
PROFILE_RANGE(script, "evaluateInClosure");
|
||||
if (QThread::currentThread() != thread()) {
|
||||
qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only.";
|
||||
// note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE
|
||||
return QScriptValue();
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return unboundNullValue();
|
||||
}
|
||||
|
||||
const auto fileName = program.fileName();
|
||||
const auto shortName = QUrl(fileName).fileName();
|
||||
|
||||
|
@ -189,7 +233,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
|
|||
auto global = closure.property("global");
|
||||
if (global.isObject()) {
|
||||
#ifdef DEBUG_JS
|
||||
qCDebug(scriptengine) << " setting global = closure.global" << shortName;
|
||||
qCDebug(shared) << " setting global = closure.global" << shortName;
|
||||
#endif
|
||||
oldGlobal = globalObject();
|
||||
setGlobalObject(global);
|
||||
|
@ -200,34 +244,34 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
|
|||
auto thiz = closure.property("this");
|
||||
if (thiz.isObject()) {
|
||||
#ifdef DEBUG_JS
|
||||
qCDebug(scriptengine) << " setting this = closure.this" << shortName;
|
||||
qCDebug(shared) << " setting this = closure.this" << shortName;
|
||||
#endif
|
||||
context->setThisObject(thiz);
|
||||
}
|
||||
|
||||
context->pushScope(closure);
|
||||
#ifdef DEBUG_JS
|
||||
qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
|
||||
qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
|
||||
#endif
|
||||
{
|
||||
result = BaseScriptEngine::evaluate(program);
|
||||
if (hasUncaughtException()) {
|
||||
auto err = cloneUncaughtException(__FUNCTION__);
|
||||
#ifdef DEBUG_JS_EXCEPTIONS
|
||||
qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
|
||||
qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
|
||||
err.setProperty("_result", result);
|
||||
#endif
|
||||
result = err;
|
||||
}
|
||||
}
|
||||
#ifdef DEBUG_JS
|
||||
qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
|
||||
qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
|
||||
#endif
|
||||
popContext();
|
||||
|
||||
if (oldGlobal.isValid()) {
|
||||
#ifdef DEBUG_JS
|
||||
qCDebug(scriptengine) << " restoring global" << shortName;
|
||||
qCDebug(shared) << " restoring global" << shortName;
|
||||
#endif
|
||||
setGlobalObject(oldGlobal);
|
||||
}
|
||||
|
@ -236,7 +280,6 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
|
|||
}
|
||||
|
||||
// Lambda
|
||||
|
||||
QScriptValue BaseScriptEngine::newLambdaFunction(std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) {
|
||||
auto lambda = new Lambda(this, operation, data);
|
||||
auto object = newQObject(lambda, ownership);
|
||||
|
@ -262,26 +305,57 @@ Lambda::Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext
|
|||
#endif
|
||||
}
|
||||
QScriptValue Lambda::call() {
|
||||
if (!BaseScriptEngine::IS_THREADSAFE_INVOCATION(engine->thread(), __FUNCTION__)) {
|
||||
return BaseScriptEngine::unboundNullValue();
|
||||
}
|
||||
return operation(engine->currentContext(), engine);
|
||||
}
|
||||
|
||||
QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName) {
|
||||
auto engine = scopeOrCallback.engine();
|
||||
if (!engine) {
|
||||
return scopeOrCallback;
|
||||
}
|
||||
auto scope = QScriptValue();
|
||||
auto callback = scopeOrCallback;
|
||||
if (scopeOrCallback.isObject()) {
|
||||
if (methodOrName.isString()) {
|
||||
scope = scopeOrCallback;
|
||||
callback = scope.property(methodOrName.toString());
|
||||
} else if (methodOrName.isFunction()) {
|
||||
scope = scopeOrCallback;
|
||||
callback = methodOrName;
|
||||
}
|
||||
}
|
||||
auto handler = engine->newObject();
|
||||
handler.setProperty("scope", scope);
|
||||
handler.setProperty("callback", callback);
|
||||
return handler;
|
||||
}
|
||||
|
||||
QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result) {
|
||||
return handler.property("callback").call(handler.property("scope"), QScriptValueList({ err, result }));
|
||||
}
|
||||
|
||||
#ifdef DEBUG_JS
|
||||
void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return;
|
||||
}
|
||||
if (!header.isEmpty()) {
|
||||
qCDebug(scriptengine) << header;
|
||||
qCDebug(shared) << header;
|
||||
}
|
||||
if (!object.isObject()) {
|
||||
qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString();
|
||||
qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString();
|
||||
return;
|
||||
}
|
||||
QScriptValueIterator it(object);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
qCDebug(scriptengine) << it.name() << ":" << it.value().toString();
|
||||
qCDebug(shared) << it.name() << ":" << it.value().toString();
|
||||
}
|
||||
if (!footer.isEmpty()) {
|
||||
qCDebug(scriptengine) << footer;
|
||||
qCDebug(shared) << footer;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
90
libraries/shared/src/BaseScriptEngine.h
Normal file
90
libraries/shared/src/BaseScriptEngine.h
Normal file
|
@ -0,0 +1,90 @@
|
|||
//
|
||||
// BaseScriptEngine.h
|
||||
// libraries/script-engine/src
|
||||
//
|
||||
// Created by Timothy Dedischew on 02/01/17.
|
||||
// Copyright 2017 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_BaseScriptEngine_h
|
||||
#define hifi_BaseScriptEngine_h
|
||||
|
||||
#include <functional>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtScript/QScriptEngine>
|
||||
|
||||
// common base class for extending QScriptEngine itself
|
||||
class BaseScriptEngine : public QScriptEngine, public QEnableSharedFromThis<BaseScriptEngine> {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static const QString SCRIPT_EXCEPTION_FORMAT;
|
||||
static const QString SCRIPT_BACKTRACE_SEP;
|
||||
|
||||
// threadsafe "unbound" version of QScriptEngine::nullValue()
|
||||
static const QScriptValue unboundNullValue() { return QScriptValue(0, QScriptValue::NullValue); }
|
||||
|
||||
BaseScriptEngine() {}
|
||||
|
||||
Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1);
|
||||
Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error");
|
||||
Q_INVOKABLE QString formatException(const QScriptValue& exception, bool includeExtendedDetails);
|
||||
|
||||
QScriptValue cloneUncaughtException(const QString& detail = QString());
|
||||
QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
|
||||
|
||||
// if there is a pending exception and we are at the top level (non-recursive) stack frame, this emits and resets it
|
||||
bool maybeEmitUncaughtException(const QString& debugHint = QString());
|
||||
|
||||
// if the currentContext() is valid then throw the passed exception; otherwise, immediately emit it.
|
||||
// note: this is used in cases where C++ code might call into JS API methods directly
|
||||
bool raiseException(const QScriptValue& exception);
|
||||
|
||||
// helper to detect and log warnings when other code invokes QScriptEngine/BaseScriptEngine in thread-unsafe ways
|
||||
static bool IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method);
|
||||
signals:
|
||||
void unhandledException(const QScriptValue& exception);
|
||||
|
||||
protected:
|
||||
// like `newFunction`, but allows mapping inline C++ lambdas with captures as callable QScriptValues
|
||||
// even though the context/engine parameters are redundant in most cases, the function signature matches `newFunction`
|
||||
// anyway so that newLambdaFunction can be used to rapidly prototype / test utility APIs and then if becoming
|
||||
// permanent more easily promoted into regular static newFunction scenarios.
|
||||
QScriptValue newLambdaFunction(std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership);
|
||||
|
||||
#ifdef DEBUG_JS
|
||||
static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString());
|
||||
#endif
|
||||
};
|
||||
|
||||
// Standardized CPS callback helpers (see: http://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/)
|
||||
// These two helpers allow async JS APIs that use a callback parameter to be more friendly to scripters by accepting thisObject
|
||||
// context and adopting a consistent and intuitable callback signature:
|
||||
// function callback(err, result) { if (err) { ... } else { /* do stuff with result */ } }
|
||||
//
|
||||
// To use, first pass the user-specified callback args in the same order used with optionally-scoped Qt signal connections:
|
||||
// auto handler = makeScopedHandlerObject(scopeOrCallback, optionalMethodOrName);
|
||||
// And then invoke the scoped handler later per CPS conventions:
|
||||
// auto result = callScopedHandlerObject(handler, err, result);
|
||||
QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName);
|
||||
QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result);
|
||||
|
||||
// Lambda helps create callable QScriptValues out of std::functions:
|
||||
// (just meant for use from within the script engine itself)
|
||||
class Lambda : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation, QScriptValue data);
|
||||
~Lambda();
|
||||
public slots:
|
||||
QScriptValue call();
|
||||
QString toString() const;
|
||||
private:
|
||||
QScriptEngine* engine;
|
||||
std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation;
|
||||
QScriptValue data;
|
||||
};
|
||||
|
||||
#endif // hifi_BaseScriptEngine_h
|
|
@ -122,6 +122,7 @@ public:
|
|||
gpu::Batch* _batch = nullptr;
|
||||
|
||||
std::shared_ptr<gpu::Texture> _whiteTexture;
|
||||
uint32_t _globalShapeKey { 0 };
|
||||
bool _enableTexturing { true };
|
||||
|
||||
RenderDetails _details;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
var lastSpecStartTime;
|
||||
function ConsoleReporter(options) {
|
||||
var startTime = new Date().getTime();
|
||||
var errorCount = 0;
|
||||
var errorCount = 0, pending = [];
|
||||
this.jasmineStarted = function (obj) {
|
||||
print('Jasmine started with ' + obj.totalSpecsDefined + ' tests.');
|
||||
};
|
||||
|
@ -15,11 +15,14 @@
|
|||
var endTime = new Date().getTime();
|
||||
print('<hr />');
|
||||
if (errorCount === 0) {
|
||||
print ('<span style="color:green">All tests passed!</span>');
|
||||
print ('<span style="color:green">All enabled tests passed!</span>');
|
||||
} else {
|
||||
print('<span style="color:red">Tests completed with ' +
|
||||
errorCount + ' ' + ERROR + '.<span>');
|
||||
}
|
||||
if (pending.length)
|
||||
print ('<span style="color:darkorange">disabled: <br /> '+
|
||||
pending.join('<br /> ')+'</span>');
|
||||
print('Tests completed in ' + (endTime - startTime) + 'ms.');
|
||||
};
|
||||
this.suiteStarted = function(obj) {
|
||||
|
@ -32,6 +35,10 @@
|
|||
lastSpecStartTime = new Date().getTime();
|
||||
};
|
||||
this.specDone = function(obj) {
|
||||
if (obj.status === 'pending') {
|
||||
pending.push(obj.fullName);
|
||||
return print('...(pending ' + obj.fullName +')');
|
||||
}
|
||||
var specEndTime = new Date().getTime();
|
||||
var symbol = obj.status === PASSED ?
|
||||
'<span style="color:green">' + CHECKMARK + '</span>' :
|
||||
|
@ -55,7 +62,7 @@
|
|||
clearTimeout = Script.clearTimeout;
|
||||
clearInterval = Script.clearInterval;
|
||||
|
||||
var jasmine = jasmineRequire.core(jasmineRequire);
|
||||
var jasmine = this.jasmine = jasmineRequire.core(jasmineRequire);
|
||||
|
||||
var env = jasmine.getEnv();
|
||||
|
||||
|
|
10
scripts/developer/tests/unit_tests/moduleTests/cycles/a.js
Normal file
10
scripts/developer/tests/unit_tests/moduleTests/cycles/a.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/* eslint-env node */
|
||||
var a = exports;
|
||||
a.done = false;
|
||||
var b = require('./b.js');
|
||||
a.done = true;
|
||||
a.name = 'a';
|
||||
a['a.done?'] = a.done;
|
||||
a['b.done?'] = b.done;
|
||||
|
||||
print('from a.js a.done =', a.done, '/ b.done =', b.done);
|
10
scripts/developer/tests/unit_tests/moduleTests/cycles/b.js
Normal file
10
scripts/developer/tests/unit_tests/moduleTests/cycles/b.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/* eslint-env node */
|
||||
var b = exports;
|
||||
b.done = false;
|
||||
var a = require('./a.js');
|
||||
b.done = true;
|
||||
b.name = 'b';
|
||||
b['a.done?'] = a.done;
|
||||
b['b.done?'] = b.done;
|
||||
|
||||
print('from b.js a.done =', a.done, '/ b.done =', b.done);
|
|
@ -0,0 +1,17 @@
|
|||
/* eslint-env node */
|
||||
/* global print */
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
print('main.js');
|
||||
var a = require('./a.js'),
|
||||
b = require('./b.js');
|
||||
|
||||
print('from main.js a.done =', a.done, 'and b.done =', b.done);
|
||||
|
||||
module.exports = {
|
||||
name: 'main',
|
||||
a: a,
|
||||
b: b,
|
||||
'a.done?': a.done,
|
||||
'b.done?': b.done,
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/* eslint-disable comma-dangle */
|
||||
// test module method exception being thrown within main constructor
|
||||
(function() {
|
||||
var apiMethod = Script.require('../exceptions/exceptionInFunction.js');
|
||||
print(Script.resolvePath(''), "apiMethod", apiMethod);
|
||||
// this next line throws from within apiMethod
|
||||
print(apiMethod());
|
||||
return {
|
||||
preload: function(uuid) {
|
||||
print("entityConstructorAPIException::preload -- never seen --", uuid, Script.resolvePath(''));
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/* global module */
|
||||
/* eslint-disable comma-dangle */
|
||||
// test dual-purpose module and standalone Entity script
|
||||
function MyEntity(filename) {
|
||||
return {
|
||||
preload: function(uuid) {
|
||||
print("entityConstructorModule.js::preload");
|
||||
if (typeof module === 'object') {
|
||||
print("module.filename", module.filename);
|
||||
print("module.parent.filename", module.parent && module.parent.filename);
|
||||
}
|
||||
},
|
||||
clickDownOnEntity: function(uuid, evt) {
|
||||
print("entityConstructorModule.js::clickDownOnEntity");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
module.exports = MyEntity;
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
print('entityConstructorModule::MyEntity', typeof MyEntity);
|
||||
(MyEntity);
|
|
@ -0,0 +1,14 @@
|
|||
/* global module */
|
||||
// test Entity constructor based on inherited constructor from a module
|
||||
function constructor() {
|
||||
print("entityConstructorNested::constructor");
|
||||
var MyEntity = Script.require('./entityConstructorModule.js');
|
||||
return new MyEntity("-- created from entityConstructorNested --");
|
||||
}
|
||||
|
||||
try {
|
||||
module.exports = constructor;
|
||||
} catch (e) {
|
||||
constructor;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/* global module */
|
||||
// test Entity constructor based on nested, inherited module constructors
|
||||
function constructor() {
|
||||
print("entityConstructorNested2::constructor");
|
||||
|
||||
// inherit from entityConstructorNested
|
||||
var MyEntity = Script.require('./entityConstructorNested.js');
|
||||
function SubEntity() {}
|
||||
SubEntity.prototype = new MyEntity('-- created from entityConstructorNested2 --');
|
||||
|
||||
// create new instance
|
||||
var entity = new SubEntity();
|
||||
// "override" clickDownOnEntity for just this new instance
|
||||
entity.clickDownOnEntity = function(uuid, evt) {
|
||||
print("entityConstructorNested2::clickDownOnEntity");
|
||||
SubEntity.prototype.clickDownOnEntity.apply(this, arguments);
|
||||
};
|
||||
return entity;
|
||||
}
|
||||
|
||||
try {
|
||||
module.exports = constructor;
|
||||
} catch (e) {
|
||||
constructor;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable comma-dangle */
|
||||
// test module-related exception from within "require" evaluation itself
|
||||
(function() {
|
||||
var mod = Script.require('../exceptions/exception.js');
|
||||
return {
|
||||
preload: function(uuid) {
|
||||
print("entityConstructorRequireException::preload (never happens)", uuid, Script.resolvePath(''), mod);
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
/* eslint-disable comma-dangle */
|
||||
// test module method exception being thrown within preload
|
||||
(function() {
|
||||
var apiMethod = Script.require('../exceptions/exceptionInFunction.js');
|
||||
print(Script.resolvePath(''), "apiMethod", apiMethod);
|
||||
return {
|
||||
preload: function(uuid) {
|
||||
// this next line throws from within apiMethod
|
||||
print(apiMethod());
|
||||
print("entityPreloadAPIException::preload -- never seen --", uuid, Script.resolvePath(''));
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable comma-dangle */
|
||||
// test requiring a module from within preload
|
||||
(function constructor() {
|
||||
return {
|
||||
preload: function(uuid) {
|
||||
print("entityPreloadRequire::preload");
|
||||
var example = Script.require('../example.json');
|
||||
print("entityPreloadRequire::example::name", example.name);
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "Example JSON Module",
|
||||
"last-modified": 1485789862,
|
||||
"config": {
|
||||
"title": "My Title",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/* eslint-env node */
|
||||
module.exports = "n/a";
|
||||
throw new Error('exception on line 2');
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/* eslint-env node */
|
||||
// dummy lines to make sure exception line number is well below parent test script
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
|
||||
function myfunc() {
|
||||
throw new Error('exception on line 32 in myfunc');
|
||||
}
|
||||
module.exports = myfunc;
|
||||
if (Script[module.filename] === 'throw') {
|
||||
myfunc();
|
||||
}
|
378
scripts/developer/tests/unit_tests/moduleUnitTests.js
Normal file
378
scripts/developer/tests/unit_tests/moduleUnitTests.js
Normal file
|
@ -0,0 +1,378 @@
|
|||
/* eslint-env jasmine, node */
|
||||
/* global print:true, Script:true, global:true, require:true */
|
||||
/* eslint-disable comma-dangle */
|
||||
var isNode = instrumentTestrunner(),
|
||||
runInterfaceTests = !isNode,
|
||||
runNetworkTests = true;
|
||||
|
||||
// describe wrappers (note: `xdescribe` indicates a disabled or "pending" jasmine test)
|
||||
var INTERFACE = { describe: runInterfaceTests ? describe : xdescribe },
|
||||
NETWORK = { describe: runNetworkTests ? describe : xdescribe };
|
||||
|
||||
describe('require', function() {
|
||||
describe('resolve', function() {
|
||||
it('should resolve relative filenames', function() {
|
||||
var expected = Script.resolvePath('./moduleTests/example.json');
|
||||
expect(require.resolve('./moduleTests/example.json')).toEqual(expected);
|
||||
});
|
||||
describe('exceptions', function() {
|
||||
it('should reject blank "" module identifiers', function() {
|
||||
expect(function() {
|
||||
require.resolve('');
|
||||
}).toThrowError(/Cannot find/);
|
||||
});
|
||||
it('should reject excessive identifier sizes', function() {
|
||||
expect(function() {
|
||||
require.resolve(new Array(8193).toString());
|
||||
}).toThrowError(/Cannot find/);
|
||||
});
|
||||
it('should reject implicitly-relative filenames', function() {
|
||||
expect(function() {
|
||||
var mod = require.resolve('example.js');
|
||||
mod.exists;
|
||||
}).toThrowError(/Cannot find/);
|
||||
});
|
||||
it('should reject unanchored, existing filenames with advice', function() {
|
||||
expect(function() {
|
||||
var mod = require.resolve('moduleTests/example.json');
|
||||
mod.exists;
|
||||
}).toThrowError(/use '.\/moduleTests\/example\.json'/);
|
||||
});
|
||||
it('should reject unanchored, non-existing filenames', function() {
|
||||
expect(function() {
|
||||
var mod = require.resolve('asdfssdf/example.json');
|
||||
mod.exists;
|
||||
}).toThrowError(/Cannot find.*system module not found/);
|
||||
});
|
||||
it('should reject non-existent filenames', function() {
|
||||
expect(function() {
|
||||
require.resolve('./404error.js');
|
||||
}).toThrowError(/Cannot find/);
|
||||
});
|
||||
it('should reject identifiers resolving to a directory', function() {
|
||||
expect(function() {
|
||||
var mod = require.resolve('.');
|
||||
mod.exists;
|
||||
// console.warn('resolved(.)', mod);
|
||||
}).toThrowError(/Cannot find/);
|
||||
expect(function() {
|
||||
var mod = require.resolve('..');
|
||||
mod.exists;
|
||||
// console.warn('resolved(..)', mod);
|
||||
}).toThrowError(/Cannot find/);
|
||||
expect(function() {
|
||||
var mod = require.resolve('../');
|
||||
mod.exists;
|
||||
// console.warn('resolved(../)', mod);
|
||||
}).toThrowError(/Cannot find/);
|
||||
});
|
||||
(isNode ? xit : it)('should reject non-system, extensionless identifiers', function() {
|
||||
expect(function() {
|
||||
require.resolve('./example');
|
||||
}).toThrowError(/Cannot find/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSON', function() {
|
||||
it('should import .json modules', function() {
|
||||
var example = require('./moduleTests/example.json');
|
||||
expect(example.name).toEqual('Example JSON Module');
|
||||
});
|
||||
// noet: support for loading JSON via content type workarounds reverted
|
||||
// (leaving these tests intact in case ever revisited later)
|
||||
// INTERFACE.describe('interface', function() {
|
||||
// NETWORK.describe('network', function() {
|
||||
// xit('should import #content-type=application/json modules', function() {
|
||||
// var results = require('https://jsonip.com#content-type=application/json');
|
||||
// expect(results.ip).toMatch(/^[.0-9]+$/);
|
||||
// });
|
||||
// xit('should import content-type: application/json modules', function() {
|
||||
// var scope = { 'content-type': 'application/json' };
|
||||
// var results = require.call(scope, 'https://jsonip.com');
|
||||
// expect(results.ip).toMatch(/^[.0-9]+$/);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
});
|
||||
|
||||
INTERFACE.describe('system', function() {
|
||||
it('require("vec3")', function() {
|
||||
expect(require('vec3')).toEqual(jasmine.any(Function));
|
||||
});
|
||||
it('require("vec3").method', function() {
|
||||
expect(require('vec3')().isValid).toEqual(jasmine.any(Function));
|
||||
});
|
||||
it('require("vec3") as constructor', function() {
|
||||
var vec3 = require('vec3');
|
||||
var v = vec3(1.1, 2.2, 3.3);
|
||||
expect(v).toEqual(jasmine.any(Object));
|
||||
expect(v.isValid).toEqual(jasmine.any(Function));
|
||||
expect(v.isValid()).toBe(true);
|
||||
expect(v.toString()).toEqual('[Vec3 (1.100,2.200,3.300)]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cache', function() {
|
||||
it('should cache modules by resolved module id', function() {
|
||||
var value = new Date;
|
||||
var example = require('./moduleTests/example.json');
|
||||
// earmark the module object with a unique value
|
||||
example['.test'] = value;
|
||||
var example2 = require('../../tests/unit_tests/moduleTests/example.json');
|
||||
expect(example2).toBe(example);
|
||||
// verify earmark is still the same after a second require()
|
||||
expect(example2['.test']).toBe(example['.test']);
|
||||
});
|
||||
it('should reload cached modules set to null', function() {
|
||||
var value = new Date;
|
||||
var example = require('./moduleTests/example.json');
|
||||
example['.test'] = value;
|
||||
require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')] = null;
|
||||
var example2 = require('../../tests/unit_tests/moduleTests/example.json');
|
||||
// verify the earmark is *not* the same as before
|
||||
expect(example2).not.toBe(example);
|
||||
expect(example2['.test']).not.toBe(example['.test']);
|
||||
});
|
||||
it('should reload when module property is deleted', function() {
|
||||
var value = new Date;
|
||||
var example = require('./moduleTests/example.json');
|
||||
example['.test'] = value;
|
||||
delete require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')];
|
||||
var example2 = require('../../tests/unit_tests/moduleTests/example.json');
|
||||
// verify the earmark is *not* the same as before
|
||||
expect(example2).not.toBe(example);
|
||||
expect(example2['.test']).not.toBe(example['.test']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cyclic dependencies', function() {
|
||||
describe('should allow lazy-ref cyclic module resolution', function() {
|
||||
var main;
|
||||
beforeEach(function() {
|
||||
// eslint-disable-next-line
|
||||
try { this._print = print; } catch (e) {}
|
||||
// during these tests print() is no-op'd so that it doesn't disrupt the reporter output
|
||||
print = function() {};
|
||||
Script.resetModuleCache();
|
||||
});
|
||||
afterEach(function() {
|
||||
print = this._print;
|
||||
});
|
||||
it('main is requirable', function() {
|
||||
main = require('./moduleTests/cycles/main.js');
|
||||
expect(main).toEqual(jasmine.any(Object));
|
||||
});
|
||||
it('transient a and b done values', function() {
|
||||
expect(main.a['b.done?']).toBe(true);
|
||||
expect(main.b['a.done?']).toBe(false);
|
||||
});
|
||||
it('ultimate a.done?', function() {
|
||||
expect(main['a.done?']).toBe(true);
|
||||
});
|
||||
it('ultimate b.done?', function() {
|
||||
expect(main['b.done?']).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('JS', function() {
|
||||
it('should throw catchable local file errors', function() {
|
||||
expect(function() {
|
||||
require('file:///dev/null/non-existent-file.js');
|
||||
}).toThrowError(/path not found|Cannot find.*non-existent-file/);
|
||||
});
|
||||
it('should throw catchable invalid id errors', function() {
|
||||
expect(function() {
|
||||
require(new Array(4096 * 2).toString());
|
||||
}).toThrowError(/invalid.*size|Cannot find.*,{30}/);
|
||||
});
|
||||
it('should throw catchable unresolved id errors', function() {
|
||||
expect(function() {
|
||||
require('foobar:/baz.js');
|
||||
}).toThrowError(/could not resolve|Cannot find.*foobar:/);
|
||||
});
|
||||
|
||||
NETWORK.describe('network', function() {
|
||||
// note: depending on retries these tests can take up to 60 seconds each to timeout
|
||||
var timeout = 75 * 1000;
|
||||
it('should throw catchable host errors', function() {
|
||||
expect(function() {
|
||||
var mod = require('http://non.existent.highfidelity.io/moduleUnitTest.js');
|
||||
print("mod", Object.keys(mod));
|
||||
}).toThrowError(/error retrieving script .ServerUnavailable.|Cannot find.*non.existent/);
|
||||
}, timeout);
|
||||
it('should throw catchable network timeouts', function() {
|
||||
expect(function() {
|
||||
require('http://ping.highfidelity.io:1024');
|
||||
}).toThrowError(/error retrieving script .Timeout.|Cannot find.*ping.highfidelity/);
|
||||
}, timeout);
|
||||
});
|
||||
});
|
||||
|
||||
INTERFACE.describe('entity', function() {
|
||||
var sampleScripts = [
|
||||
'entityConstructorAPIException.js',
|
||||
'entityConstructorModule.js',
|
||||
'entityConstructorNested2.js',
|
||||
'entityConstructorNested.js',
|
||||
'entityConstructorRequireException.js',
|
||||
'entityPreloadAPIError.js',
|
||||
'entityPreloadRequire.js',
|
||||
].filter(Boolean).map(function(id) {
|
||||
return Script.require.resolve('./moduleTests/entity/'+id);
|
||||
});
|
||||
|
||||
var uuids = [];
|
||||
function cleanup() {
|
||||
uuids.splice(0,uuids.length).forEach(function(uuid) {
|
||||
Entities.deleteEntity(uuid);
|
||||
});
|
||||
}
|
||||
afterAll(cleanup);
|
||||
// extra sanity check to avoid lingering entities
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
|
||||
for (var i=0; i < sampleScripts.length; i++) {
|
||||
maketest(i);
|
||||
}
|
||||
|
||||
function maketest(i) {
|
||||
var script = sampleScripts[ i % sampleScripts.length ];
|
||||
var shortname = '['+i+'] ' + script.split('/').pop();
|
||||
var position = MyAvatar.position;
|
||||
position.y -= i/2;
|
||||
// define a unique jasmine test for the current entity script
|
||||
it(shortname, function(done) {
|
||||
var uuid = Entities.addEntity({
|
||||
text: shortname,
|
||||
description: Script.resolvePath('').split('/').pop(),
|
||||
type: 'Text',
|
||||
position: position,
|
||||
rotation: MyAvatar.orientation,
|
||||
script: script,
|
||||
scriptTimestamp: +new Date,
|
||||
lifetime: 20,
|
||||
lineHeight: 1/8,
|
||||
dimensions: { x: 2, y: 0.5, z: 0.01 },
|
||||
backgroundColor: { red: 0, green: 0, blue: 0 },
|
||||
color: { red: 0xff, green: 0xff, blue: 0xff },
|
||||
}, !Entities.serversExist() || !Entities.canRezTmp());
|
||||
uuids.push(uuid);
|
||||
function stopChecking() {
|
||||
if (ii) {
|
||||
Script.clearInterval(ii);
|
||||
ii = 0;
|
||||
}
|
||||
}
|
||||
var ii = Script.setInterval(function() {
|
||||
Entities.queryPropertyMetadata(uuid, "script", function(err, result) {
|
||||
if (err) {
|
||||
stopChecking();
|
||||
throw new Error(err);
|
||||
}
|
||||
if (result.success) {
|
||||
stopChecking();
|
||||
if (/Exception/.test(script)) {
|
||||
expect(result.status).toMatch(/^error_(loading|running)_script$/);
|
||||
} else {
|
||||
expect(result.status).toEqual("running");
|
||||
}
|
||||
Entities.deleteEntity(uuid);
|
||||
done();
|
||||
} else {
|
||||
print('!result.success', JSON.stringify(result));
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
Script.setTimeout(stopChecking, 4900);
|
||||
}, 5000 /* jasmine async timeout */);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// support for isomorphic Node.js / Interface unit testing
|
||||
// note: run `npm install` from unit_tests/ and then `node moduleUnitTests.js`
|
||||
function run() {}
|
||||
function instrumentTestrunner() {
|
||||
var isNode = typeof process === 'object' && process.title === 'node';
|
||||
if (typeof describe === 'function') {
|
||||
// already running within a test runner; assume jasmine is ready-to-go
|
||||
return isNode;
|
||||
}
|
||||
if (isNode) {
|
||||
/* eslint-disable no-console */
|
||||
// Node.js test mode
|
||||
// to keep things consistent Node.js uses the local jasmine.js library (instead of an npm version)
|
||||
var jasmineRequire = require('../../libraries/jasmine/jasmine.js');
|
||||
var jasmine = jasmineRequire.core(jasmineRequire);
|
||||
var env = jasmine.getEnv();
|
||||
var jasmineInterface = jasmineRequire.interface(jasmine, env);
|
||||
for (var p in jasmineInterface) {
|
||||
global[p] = jasmineInterface[p];
|
||||
}
|
||||
env.addReporter(new (require('jasmine-console-reporter')));
|
||||
// testing mocks
|
||||
Script = {
|
||||
resetModuleCache: function() {
|
||||
module.require.cache = {};
|
||||
},
|
||||
setTimeout: setTimeout,
|
||||
clearTimeout: clearTimeout,
|
||||
resolvePath: function(id) {
|
||||
// this attempts to accurately emulate how Script.resolvePath works
|
||||
var trace = {}; Error.captureStackTrace(trace);
|
||||
var base = trace.stack.split('\n')[2].replace(/^.*[(]|[)].*$/g,'').replace(/:[0-9]+:[0-9]+.*$/,'');
|
||||
if (!id) {
|
||||
return base;
|
||||
}
|
||||
var rel = base.replace(/[^\/]+$/, id);
|
||||
console.info('rel', rel);
|
||||
return require.resolve(rel);
|
||||
},
|
||||
require: function(mod) {
|
||||
return require(Script.require.resolve(mod));
|
||||
},
|
||||
};
|
||||
Script.require.cache = require.cache;
|
||||
Script.require.resolve = function(mod) {
|
||||
if (mod === '.' || /^\.\.($|\/)/.test(mod)) {
|
||||
throw new Error("Cannot find module '"+mod+"' (is dir)");
|
||||
}
|
||||
var path = require.resolve(mod);
|
||||
// console.info('node-require-reoslved', mod, path);
|
||||
try {
|
||||
if (require('fs').lstatSync(path).isDirectory()) {
|
||||
throw new Error("Cannot find module '"+path+"' (is directory)");
|
||||
}
|
||||
// console.info('!path', path);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
print = console.info.bind(console, '[print]');
|
||||
/* eslint-enable no-console */
|
||||
} else {
|
||||
// Interface test mode
|
||||
global = this;
|
||||
Script.require('../../../system/libraries/utils.js');
|
||||
this.jasmineRequire = Script.require('../../libraries/jasmine/jasmine.js');
|
||||
Script.require('../../libraries/jasmine/hifi-boot.js');
|
||||
require = Script.require;
|
||||
// polyfill console
|
||||
/* global console:true */
|
||||
console = {
|
||||
log: print,
|
||||
info: print.bind(this, '[info]'),
|
||||
warn: print.bind(this, '[warn]'),
|
||||
error: print.bind(this, '[error]'),
|
||||
debug: print.bind(this, '[debug]'),
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
run = function() { global.jasmine.getEnv().execute(); };
|
||||
return isNode;
|
||||
}
|
||||
run();
|
6
scripts/developer/tests/unit_tests/package.json
Normal file
6
scripts/developer/tests/unit_tests/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "unit_tests",
|
||||
"devDependencies": {
|
||||
"jasmine-console-reporter": "^1.2.7"
|
||||
}
|
||||
}
|
|
@ -15,10 +15,20 @@ describe('Script', function () {
|
|||
// characterization tests
|
||||
// initially these are just to capture how the app works currently
|
||||
var testCases = {
|
||||
// special relative resolves
|
||||
'': filename,
|
||||
'.': dirname,
|
||||
'..': parentdir,
|
||||
|
||||
// local file "magic" tilde path expansion
|
||||
'/~/defaultScripts.js': ScriptDiscoveryService.defaultScriptsPath + '/defaultScripts.js',
|
||||
|
||||
// these schemes appear to always get resolved to empty URLs
|
||||
'qrc://test': '',
|
||||
'about:Entities 1': '',
|
||||
'ftp://host:port/path': '',
|
||||
'data:text/html;text,foo': '',
|
||||
|
||||
'Entities 1': dirname + 'Entities 1',
|
||||
'./file.js': dirname + 'file.js',
|
||||
'c:/temp/': 'file:///c:/temp/',
|
||||
|
@ -31,6 +41,12 @@ describe('Script', function () {
|
|||
'/~/libraries/utils.js': 'file:///~/libraries/utils.js',
|
||||
'/temp/file.js': 'file:///temp/file.js',
|
||||
'/~/': 'file:///~/',
|
||||
|
||||
// these schemes appear to always get resolved to the same URL again
|
||||
'http://highfidelity.com': 'http://highfidelity.com',
|
||||
'atp:/highfidelity': 'atp:/highfidelity',
|
||||
'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f':
|
||||
'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f',
|
||||
};
|
||||
describe('resolvePath', function () {
|
||||
Object.keys(testCases).forEach(function(input) {
|
||||
|
@ -42,7 +58,7 @@ describe('Script', function () {
|
|||
|
||||
describe('include', function () {
|
||||
var old_cache_buster;
|
||||
var cache_buster = '#' + +new Date;
|
||||
var cache_buster = '#' + new Date().getTime().toString(36);
|
||||
beforeAll(function() {
|
||||
old_cache_buster = Settings.getValue('cache_buster');
|
||||
Settings.setValue('cache_buster', cache_buster);
|
||||
|
|
|
@ -25,7 +25,7 @@ Column {
|
|||
"Lightmap:LightingModel:enableLightmap",
|
||||
"Background:LightingModel:enableBackground",
|
||||
"ssao:AmbientOcclusion:enabled",
|
||||
"Textures:LightingModel:enableMaterialTexturing",
|
||||
"Textures:LightingModel:enableMaterialTexturing"
|
||||
]
|
||||
CheckBox {
|
||||
text: modelData.split(":")[0]
|
||||
|
@ -45,6 +45,7 @@ Column {
|
|||
"Diffuse:LightingModel:enableDiffuse",
|
||||
"Specular:LightingModel:enableSpecular",
|
||||
"Albedo:LightingModel:enableAlbedo",
|
||||
"Wireframe:LightingModel:enableWireframe"
|
||||
]
|
||||
CheckBox {
|
||||
text: modelData.split(":")[0]
|
||||
|
|
69
scripts/modules/vec3.js
Normal file
69
scripts/modules/vec3.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
// Example of using a "system module" to decouple Vec3's implementation details.
|
||||
//
|
||||
// Users would bring Vec3 support in as a module:
|
||||
// var vec3 = Script.require('vec3');
|
||||
//
|
||||
|
||||
// (this example is compatible with using as a Script.include and as a Script.require module)
|
||||
try {
|
||||
// Script.require
|
||||
module.exports = vec3;
|
||||
} catch(e) {
|
||||
// Script.include
|
||||
Script.registerValue("vec3", vec3);
|
||||
}
|
||||
|
||||
vec3.fromObject = function(v) {
|
||||
//return new vec3(v.x, v.y, v.z);
|
||||
//... this is even faster and achieves the same effect
|
||||
v.__proto__ = vec3.prototype;
|
||||
return v;
|
||||
};
|
||||
|
||||
vec3.prototype = {
|
||||
multiply: function(v2) {
|
||||
// later on could support overrides like so:
|
||||
// if (v2 instanceof quat) { [...] }
|
||||
// which of the below is faster (C++ or JS)?
|
||||
// (dunno -- but could systematically find out and go with that version)
|
||||
|
||||
// pure JS option
|
||||
// return new vec3(this.x * v2.x, this.y * v2.y, this.z * v2.z);
|
||||
|
||||
// hybrid C++ option
|
||||
return vec3.fromObject(Vec3.multiply(this, v2));
|
||||
},
|
||||
// detects any NaN and Infinity values
|
||||
isValid: function() {
|
||||
return isFinite(this.x) && isFinite(this.y) && isFinite(this.z);
|
||||
},
|
||||
// format Vec3's, eg:
|
||||
// var v = vec3();
|
||||
// print(v); // outputs [Vec3 (0.000, 0.000, 0.000)]
|
||||
toString: function() {
|
||||
if (this === vec3.prototype) {
|
||||
return "{Vec3 prototype}";
|
||||
}
|
||||
function fixed(n) { return n.toFixed(3); }
|
||||
return "[Vec3 (" + [this.x, this.y, this.z].map(fixed) + ")]";
|
||||
},
|
||||
};
|
||||
|
||||
vec3.DEBUG = true;
|
||||
|
||||
function vec3(x, y, z) {
|
||||
if (!(this instanceof vec3)) {
|
||||
// if vec3 is called as a function then re-invoke as a constructor
|
||||
// (so that `value instanceof vec3` holds true for created values)
|
||||
return new vec3(x, y, z);
|
||||
}
|
||||
|
||||
// unfold default arguments (vec3(), vec3(.5), vec3(0,1), etc.)
|
||||
this.x = x !== undefined ? x : 0;
|
||||
this.y = y !== undefined ? y : this.x;
|
||||
this.z = z !== undefined ? z : this.y;
|
||||
|
||||
if (vec3.DEBUG && !this.isValid())
|
||||
throw new Error('vec3() -- invalid initial values ['+[].slice.call(arguments)+']');
|
||||
};
|
||||
|
|
@ -6,26 +6,35 @@
|
|||
var ANIMATION_FPS = 30;
|
||||
var ANIMATION_FIRST_FRAME = 1;
|
||||
var ANIMATION_LAST_FRAME = 10;
|
||||
var RELEASE_KEYS = ['w', 'a', 's', 'd', 'UP', 'LEFT', 'DOWN', 'RIGHT'];
|
||||
var RELEASE_TIME = 500; // ms
|
||||
var RELEASE_DISTANCE = 0.2; // meters
|
||||
var MAX_IK_ERROR = 30;
|
||||
var IK_SETTLE_TIME = 250; // ms
|
||||
var DESKTOP_UI_CHECK_INTERVAL = 100;
|
||||
var DESKTOP_MAX_DISTANCE = 5;
|
||||
var SIT_DELAY = 25
|
||||
var MAX_RESET_DISTANCE = 0.5
|
||||
var SIT_DELAY = 25;
|
||||
var MAX_RESET_DISTANCE = 0.5; // meters
|
||||
var OVERRIDEN_DRIVE_KEYS = [
|
||||
DriveKeys.TRANSLATE_X,
|
||||
DriveKeys.TRANSLATE_Y,
|
||||
DriveKeys.TRANSLATE_Z,
|
||||
DriveKeys.STEP_TRANSLATE_X,
|
||||
DriveKeys.STEP_TRANSLATE_Y,
|
||||
DriveKeys.STEP_TRANSLATE_Z,
|
||||
];
|
||||
|
||||
this.entityID = null;
|
||||
this.timers = {};
|
||||
this.animStateHandlerID = null;
|
||||
this.interval = null;
|
||||
this.sitDownSettlePeriod = null;
|
||||
this.lastTimeNoDriveKeys = null;
|
||||
|
||||
this.preload = function(entityID) {
|
||||
this.entityID = entityID;
|
||||
}
|
||||
this.unload = function() {
|
||||
if (Settings.getValue(SETTING_KEY) === this.entityID) {
|
||||
this.sitUp();
|
||||
this.standUp();
|
||||
}
|
||||
if (this.interval !== null) {
|
||||
Script.clearInterval(this.interval);
|
||||
|
@ -96,6 +105,11 @@
|
|||
print("Someone is already sitting in that chair.");
|
||||
return;
|
||||
}
|
||||
print("Sitting down (" + this.entityID + ")");
|
||||
|
||||
var now = Date.now();
|
||||
this.sitDownSettlePeriod = now + IK_SETTLE_TIME;
|
||||
this.lastTimeNoDriveKeys = now;
|
||||
|
||||
var previousValue = Settings.getValue(SETTING_KEY);
|
||||
Settings.setValue(SETTING_KEY, this.entityID);
|
||||
|
@ -118,20 +132,17 @@
|
|||
return { headType: 0 };
|
||||
}, ["headType"]);
|
||||
Script.update.connect(this, this.update);
|
||||
Controller.keyPressEvent.connect(this, this.keyPressed);
|
||||
Controller.keyReleaseEvent.connect(this, this.keyReleased);
|
||||
for (var i in RELEASE_KEYS) {
|
||||
Controller.captureKeyEvents({ text: RELEASE_KEYS[i] });
|
||||
for (var i in OVERRIDEN_DRIVE_KEYS) {
|
||||
MyAvatar.disableDriveKey(OVERRIDEN_DRIVE_KEYS[i]);
|
||||
}
|
||||
}
|
||||
|
||||
this.sitUp = function() {
|
||||
this.standUp = function() {
|
||||
print("Standing up (" + this.entityID + ")");
|
||||
MyAvatar.removeAnimationStateHandler(this.animStateHandlerID);
|
||||
Script.update.disconnect(this, this.update);
|
||||
Controller.keyPressEvent.disconnect(this, this.keyPressed);
|
||||
Controller.keyReleaseEvent.disconnect(this, this.keyReleased);
|
||||
for (var i in RELEASE_KEYS) {
|
||||
Controller.releaseKeyEvents({ text: RELEASE_KEYS[i] });
|
||||
for (var i in OVERRIDEN_DRIVE_KEYS) {
|
||||
MyAvatar.enableDriveKey(OVERRIDEN_DRIVE_KEYS[i]);
|
||||
}
|
||||
|
||||
this.setSeatUser(null);
|
||||
|
@ -156,6 +167,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
// function called by teleport.js if it detects the appropriate userData
|
||||
this.sit = function () {
|
||||
this.sitDown();
|
||||
}
|
||||
|
@ -207,7 +219,33 @@
|
|||
var properties = Entities.getEntityProperties(this.entityID);
|
||||
var avatarDistance = Vec3.distance(MyAvatar.position, properties.position);
|
||||
var ikError = MyAvatar.getIKErrorOnLastSolve();
|
||||
if (avatarDistance > RELEASE_DISTANCE || ikError > MAX_IK_ERROR) {
|
||||
var now = Date.now();
|
||||
var shouldStandUp = false;
|
||||
|
||||
// Check if a drive key is pressed
|
||||
var hasActiveDriveKey = false;
|
||||
for (var i in OVERRIDEN_DRIVE_KEYS) {
|
||||
if (MyAvatar.getRawDriveKey(OVERRIDEN_DRIVE_KEYS[i]) != 0.0) {
|
||||
hasActiveDriveKey = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only standup if user has been pushing a drive key for RELEASE_TIME
|
||||
if (hasActiveDriveKey) {
|
||||
var elapsed = now - this.lastTimeNoDriveKeys;
|
||||
shouldStandUp = elapsed > RELEASE_TIME;
|
||||
} else {
|
||||
this.lastTimeNoDriveKeys = Date.now();
|
||||
}
|
||||
|
||||
// Allow some time for the IK to settle
|
||||
if (ikError > MAX_IK_ERROR && now > this.sitDownSettlePeriod) {
|
||||
shouldStandUp = true;
|
||||
}
|
||||
|
||||
|
||||
if (shouldStandUp || avatarDistance > RELEASE_DISTANCE) {
|
||||
print("IK error: " + ikError + ", distance from chair: " + avatarDistance);
|
||||
|
||||
// Move avatar in front of the chair to avoid getting stuck in collision hulls
|
||||
|
@ -215,45 +253,13 @@
|
|||
var offset = { x: 0, y: 1.0, z: -0.5 - properties.dimensions.z * properties.registrationPoint.z };
|
||||
var position = Vec3.sum(properties.position, Vec3.multiplyQbyV(properties.rotation, offset));
|
||||
MyAvatar.position = position;
|
||||
print("Moving Avatar in front of the chair.")
|
||||
}
|
||||
|
||||
this.sitUp();
|
||||
this.standUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
this.keyPressed = function(event) {
|
||||
if (isInEditMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RELEASE_KEYS.indexOf(event.text) !== -1) {
|
||||
var that = this;
|
||||
this.timers[event.text] = Script.setTimeout(function() {
|
||||
delete that.timers[event.text];
|
||||
|
||||
var properties = Entities.getEntityProperties(that.entityID);
|
||||
var avatarDistance = Vec3.distance(MyAvatar.position, properties.position);
|
||||
|
||||
// Move avatar in front of the chair to avoid getting stuck in collision hulls
|
||||
if (avatarDistance < MAX_RESET_DISTANCE) {
|
||||
var offset = { x: 0, y: 1.0, z: -0.5 - properties.dimensions.z * properties.registrationPoint.z };
|
||||
var position = Vec3.sum(properties.position, Vec3.multiplyQbyV(properties.rotation, offset));
|
||||
MyAvatar.position = position;
|
||||
}
|
||||
|
||||
that.sitUp();
|
||||
}, RELEASE_TIME);
|
||||
}
|
||||
}
|
||||
this.keyReleased = function(event) {
|
||||
if (RELEASE_KEYS.indexOf(event.text) !== -1) {
|
||||
if (this.timers[event.text]) {
|
||||
Script.clearTimeout(this.timers[event.text]);
|
||||
delete this.timers[event.text];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.canSitDesktop = function() {
|
||||
var properties = Entities.getEntityProperties(this.entityID, ["position"]);
|
||||
var distanceFromSeat = Vec3.distance(MyAvatar.position, properties.position);
|
||||
|
|
Loading…
Reference in a new issue