Merge branch 'master' of github.com:highfidelity/hifi into tablet-ui

This commit is contained in:
Seth Alves 2017-03-19 21:17:30 -08:00
commit 424af2255b
64 changed files with 2197 additions and 634 deletions

View file

@ -179,6 +179,8 @@
#include "FrameTimingsScriptingInterface.h" #include "FrameTimingsScriptingInterface.h"
#include <GPUIdent.h> #include <GPUIdent.h>
#include <gl/GLHelpers.h> #include <gl/GLHelpers.h>
#include <EntityScriptClient.h>
#include <ModelScriptingInterface.h>
// On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU
// FIXME seems to be broken. // FIXME seems to be broken.
@ -4391,16 +4393,16 @@ void Application::update(float deltaTime) {
myAvatar->clearDriveKeys(); myAvatar->clearDriveKeys();
if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) {
if (!_controllerScriptingInterface->areActionsCaptured()) { if (!_controllerScriptingInterface->areActionsCaptured()) {
myAvatar->setDriveKeys(TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z));
myAvatar->setDriveKeys(TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y));
myAvatar->setDriveKeys(TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X));
if (deltaTime > FLT_EPSILON) { if (deltaTime > FLT_EPSILON) {
myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH));
myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW));
myAvatar->setDriveKeys(STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_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); controller::Pose leftHandPose = userInputMapper->getPoseState(controller::Action::LEFT_HAND);
@ -5511,8 +5513,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this));
// hook our avatar and avatar hash map object into this script engine // hook our avatar and avatar hash map object into this script engine
scriptEngine->registerGlobalObject("MyAvatar", getMyAvatar().get()); getMyAvatar()->registerMetaTypes(scriptEngine);
qScriptRegisterMetaType(scriptEngine, audioListenModeToScriptValue, audioListenModeFromScriptValue);
scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get<AvatarManager>().data()); scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get<AvatarManager>().data());

View file

@ -72,6 +72,8 @@
#include <procedural/ProceduralSkybox.h> #include <procedural/ProceduralSkybox.h>
#include <model/Skybox.h> #include <model/Skybox.h>
#include <ModelScriptingInterface.h>
class OffscreenGLCanvas; class OffscreenGLCanvas;
class GLCanvas; class GLCanvas;

View file

@ -422,6 +422,9 @@ Menu::Menu() {
} }
// Developer > Assets >>> // 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"); MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets");
auto& atpMigrator = ATPAssetMigrator::getInstance(); auto& atpMigrator = ATPAssetMigrator::getInstance();
atpMigrator.setDialogParent(this); atpMigrator.setDialogParent(this);
@ -429,6 +432,7 @@ Menu::Menu() {
addActionToQMenuAndActionHash(assetDeveloperMenu, MenuOption::AssetMigration, addActionToQMenuAndActionHash(assetDeveloperMenu, MenuOption::AssetMigration,
0, &atpMigrator, 0, &atpMigrator,
SLOT(loadEntityServerFile())); SLOT(loadEntityServerFile()));
#endif
// Developer > Avatar >>> // Developer > Avatar >>>
MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar"); MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar");
@ -561,16 +565,14 @@ Menu::Menu() {
QString("../../hifi/tablet/TabletNetworkingPreferences.qml"), "NetworkingPreferencesDialog"); QString("../../hifi/tablet/TabletNetworkingPreferences.qml"), "NetworkingPreferencesDialog");
}); });
addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches()));
addActionToQMenuAndActionHash(networkMenu, MenuOption::ClearDiskCache, 0,
DependencyManager::get<AssetClient>().data(), SLOT(clearCache()));
addCheckableActionToQMenuAndActionHash(networkMenu, addCheckableActionToQMenuAndActionHash(networkMenu,
MenuOption::DisableActivityLogger, MenuOption::DisableActivityLogger,
0, 0,
false, false,
&UserActivityLogger::getInstance(), &UserActivityLogger::getInstance(),
SLOT(disable(bool))); 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, addActionToQMenuAndActionHash(networkMenu, MenuOption::ShowDSConnectTable, 0,
qApp, SLOT(loadDomainConnectionDialog())); qApp, SLOT(loadDomainConnectionDialog()));

View file

@ -52,11 +52,11 @@ namespace MenuOption {
const QString BinaryEyelidControl = "Binary Eyelid Control"; const QString BinaryEyelidControl = "Binary Eyelid Control";
const QString BookmarkLocation = "Bookmark Location"; const QString BookmarkLocation = "Bookmark Location";
const QString Bookmarks = "Bookmarks"; const QString Bookmarks = "Bookmarks";
const QString CachesSize = "RAM Caches Size";
const QString CalibrateCamera = "Calibrate Camera"; const QString CalibrateCamera = "Calibrate Camera";
const QString CameraEntityMode = "Entity Mode"; const QString CameraEntityMode = "Entity Mode";
const QString CenterPlayerInView = "Center Player In View"; const QString CenterPlayerInView = "Center Player In View";
const QString Chat = "Chat..."; const QString Chat = "Chat...";
const QString ClearDiskCache = "Clear Disk Cache";
const QString Collisions = "Collisions"; const QString Collisions = "Collisions";
const QString Connexion = "Activate 3D Connexion Devices"; const QString Connexion = "Activate 3D Connexion Devices";
const QString Console = "Console..."; const QString Console = "Console...";
@ -83,7 +83,6 @@ namespace MenuOption {
const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableActivityLogger = "Disable Activity Logger";
const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment"; const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment";
const QString DisableLightEntities = "Disable Light Entities"; const QString DisableLightEntities = "Disable Light Entities";
const QString DiskCacheEditor = "Disk Cache Editor";
const QString DisplayCrashOptions = "Display Crash Options"; const QString DisplayCrashOptions = "Display Crash Options";
const QString DisplayHandTargets = "Show Hand Targets"; const QString DisplayHandTargets = "Show Hand Targets";
const QString DisplayModelBounds = "Display Model Bounds"; const QString DisplayModelBounds = "Display Model Bounds";

View file

@ -119,9 +119,7 @@ MyAvatar::MyAvatar(RigPointer rig) :
using namespace recording; using namespace recording;
_skeletonModel->flagAsCauterized(); _skeletonModel->flagAsCauterized();
for (int i = 0; i < MAX_DRIVE_KEYS; i++) { clearDriveKeys();
_driveKeys[i] = 0.0f;
}
// Necessary to select the correct slot // Necessary to select the correct slot
using SlotType = void(MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool); using SlotType = void(MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool);
@ -230,6 +228,21 @@ MyAvatar::~MyAvatar() {
_lookAtTargetAvatar.reset(); _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) { void MyAvatar::setOrientationVar(const QVariant& newOrientationVar) {
Avatar::setOrientation(quatFromVariant(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. // 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 // This allows a user to do faster snapping by tapping a control
for (int i = STEP_TRANSLATE_X; !stepAction && i <= STEP_YAW; ++i) { for (int i = STEP_TRANSLATE_X; !stepAction && i <= STEP_YAW; ++i) {
if (_driveKeys[i] != 0.0f) { if (getDriveKey((DriveKeys)i) != 0.0f) {
stepAction = true; stepAction = true;
} }
} }
@ -1655,7 +1668,7 @@ bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const {
void MyAvatar::updateOrientation(float deltaTime) { void MyAvatar::updateOrientation(float deltaTime) {
// Smoothly rotate body with arrow keys // Smoothly rotate body with arrow keys
float targetSpeed = _driveKeys[YAW] * _yawSpeed; float targetSpeed = getDriveKey(YAW) * _yawSpeed;
if (targetSpeed != 0.0f) { if (targetSpeed != 0.0f) {
const float ROTATION_RAMP_TIMESCALE = 0.1f; const float ROTATION_RAMP_TIMESCALE = 0.1f;
float blend = deltaTime / ROTATION_RAMP_TIMESCALE; 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 // 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 // get an instantaneous 15 degree turn. If you keep holding the key down you'll get another
// snap turn every half second. // snap turn every half second.
if (_driveKeys[STEP_YAW] != 0.0f) { if (getDriveKey(STEP_YAW) != 0.0f) {
totalBodyYaw += _driveKeys[STEP_YAW]; totalBodyYaw += getDriveKey(STEP_YAW);
} }
// use head/HMD orientation to turn while flying // use head/HMD orientation to turn while flying
@ -1722,7 +1735,7 @@ void MyAvatar::updateOrientation(float deltaTime) {
// update body orientation by movement inputs // update body orientation by movement inputs
setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f)))); 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()) { if (qApp->isHMDMode()) {
glm::quat orientation = glm::quat_cast(getSensorToWorldMatrix()) * getHMDSensorOrientation(); glm::quat orientation = glm::quat_cast(getSensorToWorldMatrix()) * getHMDSensorOrientation();
@ -1756,14 +1769,14 @@ void MyAvatar::updateActionMotor(float deltaTime) {
} }
// compute action input // compute action input
glm::vec3 front = (_driveKeys[TRANSLATE_Z]) * IDENTITY_FRONT; glm::vec3 front = (getDriveKey(TRANSLATE_Z)) * IDENTITY_FRONT;
glm::vec3 right = (_driveKeys[TRANSLATE_X]) * IDENTITY_RIGHT; glm::vec3 right = (getDriveKey(TRANSLATE_X)) * IDENTITY_RIGHT;
glm::vec3 direction = front + right; glm::vec3 direction = front + right;
CharacterController::State state = _characterController.getState(); CharacterController::State state = _characterController.getState();
if (state == CharacterController::State::Hover) { if (state == CharacterController::State::Hover) {
// we're flying --> support vertical motion // we're flying --> support vertical motion
glm::vec3 up = (_driveKeys[TRANSLATE_Y]) * IDENTITY_UP; glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP;
direction += up; direction += up;
} }
@ -1802,7 +1815,7 @@ void MyAvatar::updateActionMotor(float deltaTime) {
_actionMotorVelocity = MAX_WALKING_SPEED * direction; _actionMotorVelocity = MAX_WALKING_SPEED * direction;
} }
float boomChange = _driveKeys[ZOOM]; float boomChange = getDriveKey(ZOOM);
_boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange; _boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange;
_boomLength = glm::clamp<float>(_boomLength, ZOOM_MIN, ZOOM_MAX); _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. // 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; _hoverReferenceCameraFacingIsCaptured = true;
// transform the camera facing vector into sensor space. // transform the camera facing vector into sensor space.
_hoverReferenceCameraFacing = transformVectorFast(glm::inverse(_sensorToWorldMatrix), getHead()->getCameraOrientation() * Vectors::UNIT_Z); _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; _hoverReferenceCameraFacingIsCaptured = false;
} }
} }
@ -2093,17 +2106,61 @@ bool MyAvatar::getCharacterControllerEnabled() {
} }
void MyAvatar::clearDriveKeys() { void MyAvatar::clearDriveKeys() {
for (int i = 0; i < MAX_DRIVE_KEYS; ++i) { _driveKeys.fill(0.0f);
_driveKeys[i] = 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() { void MyAvatar::relayDriveKeysToCharacterController() {
if (_driveKeys[TRANSLATE_Y] > 0.0f) { if (getDriveKey(TRANSLATE_Y) > 0.0f) {
_characterController.jump(); _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 { glm::vec3 MyAvatar::getWorldBodyPosition() const {
return transformPoint(_sensorToWorldMatrix, extractTranslation(_bodySensorMatrix)); return transformPoint(_sensorToWorldMatrix, extractTranslation(_bodySensorMatrix));
} }
@ -2189,7 +2246,15 @@ QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioList
} }
void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode) { 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 { 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) { void MyAvatar::setAway(bool value) {
@ -2498,7 +2563,7 @@ bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& o
return false; return false;
} }
setPosition(position); slamPosition(position);
setOrientation(orientation); setOrientation(orientation);
_rig->setMaxHipsOffsetLength(0.05f); _rig->setMaxHipsOffsetLength(0.05f);

View file

@ -12,6 +12,8 @@
#ifndef hifi_MyAvatar_h #ifndef hifi_MyAvatar_h
#define hifi_MyAvatar_h #define hifi_MyAvatar_h
#include <bitset>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <SettingHandle.h> #include <SettingHandle.h>
@ -29,20 +31,6 @@
class AvatarActionHold; class AvatarActionHold;
class ModelItemID; 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 { enum eyeContactTarget {
LEFT_EYE, LEFT_EYE,
RIGHT_EYE, RIGHT_EYE,
@ -88,9 +76,26 @@ class MyAvatar : public Avatar {
Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled)
public: 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); explicit MyAvatar(RigPointer rig);
~MyAvatar(); ~MyAvatar();
void registerMetaTypes(QScriptEngine* engine);
virtual void simulateAttachments(float deltaTime) override; virtual void simulateAttachments(float deltaTime) override;
AudioListenerMode getAudioListenerModeHead() const { return FROM_HEAD; } AudioListenerMode getAudioListenerModeHead() const { return FROM_HEAD; }
@ -180,9 +185,15 @@ public:
// Set what driving keys are being pressed to control thrust levels // Set what driving keys are being pressed to control thrust levels
void clearDriveKeys(); 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(); void relayDriveKeysToCharacterController();
Q_INVOKABLE void disableDriveKey(DriveKeys key);
Q_INVOKABLE void enableDriveKey(DriveKeys key);
Q_INVOKABLE bool isDriveKeyDisabled(DriveKeys key) const;
eyeContactTarget getEyeContactTarget(); eyeContactTarget getEyeContactTarget();
Q_INVOKABLE glm::vec3 getTrackedHeadPosition() const { return _trackedHeadPosition; } Q_INVOKABLE glm::vec3 getTrackedHeadPosition() const { return _trackedHeadPosition; }
@ -352,7 +363,6 @@ private:
virtual bool shouldRenderHead(const RenderArgs* renderArgs) const override; virtual bool shouldRenderHead(const RenderArgs* renderArgs) const override;
void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; setEnableMeshVisible(shouldRender); } void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; setEnableMeshVisible(shouldRender); }
bool getShouldRenderLocally() const { return _shouldRender; } bool getShouldRenderLocally() const { return _shouldRender; }
bool getDriveKeys(int key) { return _driveKeys[key] != 0.0f; };
bool isMyAvatar() const override { return true; } bool isMyAvatar() const override { return true; }
virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual int parseDataFromBuffer(const QByteArray& buffer) override;
virtual glm::vec3 getSkeletonPosition() const override; virtual glm::vec3 getSkeletonPosition() const override;
@ -388,7 +398,9 @@ private:
void clampScaleChangeToDomainLimits(float desiredScale); void clampScaleChangeToDomainLimits(float desiredScale);
glm::mat4 computeCameraRelativeHandControllerMatrix(const glm::mat4& controllerSensorMatrix) const; 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 _wasPushing;
bool _isPushing; bool _isPushing;
bool _isBeingPushed; bool _isBeingPushed;
@ -541,4 +553,7 @@ private:
QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode);
void audioListenModeFromScriptValue(const QScriptValue& object, 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 #endif // hifi_MyAvatar_h

View file

@ -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();
}

View file

@ -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

View file

@ -19,9 +19,7 @@
#include <PathUtils.h> #include <PathUtils.h>
#include "AddressBarDialog.h" #include "AddressBarDialog.h"
#include "CachesSizeDialog.h"
#include "ConnectionFailureDialog.h" #include "ConnectionFailureDialog.h"
#include "DiskCacheEditor.h"
#include "DomainConnectionDialog.h" #include "DomainConnectionDialog.h"
#include "HMDToolsDialog.h" #include "HMDToolsDialog.h"
#include "LodToolsDialog.h" #include "LodToolsDialog.h"
@ -70,11 +68,6 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) {
} }
} }
void DialogsManager::toggleDiskCacheEditor() {
maybeCreateDialog(_diskCacheEditor);
_diskCacheEditor->toggle();
}
void DialogsManager::toggleLoginDialog() { void DialogsManager::toggleLoginDialog() {
LoginDialog::toggleAction(); LoginDialog::toggleAction();
} }
@ -100,16 +93,6 @@ void DialogsManager::octreeStatsDetails() {
_octreeStatsDialog->raise(); _octreeStatsDialog->raise();
} }
void DialogsManager::cachesSizeDialog() {
if (!_cachesSizeDialog) {
maybeCreateDialog(_cachesSizeDialog);
connect(_cachesSizeDialog, SIGNAL(closed()), _cachesSizeDialog, SLOT(deleteLater()));
_cachesSizeDialog->show();
}
_cachesSizeDialog->raise();
}
void DialogsManager::lodTools() { void DialogsManager::lodTools() {
if (!_lodToolsDialog) { if (!_lodToolsDialog) {
maybeCreateDialog(_lodToolsDialog); maybeCreateDialog(_lodToolsDialog);

View file

@ -22,7 +22,6 @@
class AnimationsDialog; class AnimationsDialog;
class AttachmentsDialog; class AttachmentsDialog;
class CachesSizeDialog; class CachesSizeDialog;
class DiskCacheEditor;
class LodToolsDialog; class LodToolsDialog;
class OctreeStatsDialog; class OctreeStatsDialog;
class ScriptEditorWindow; class ScriptEditorWindow;
@ -46,11 +45,9 @@ public slots:
void showAddressBar(); void showAddressBar();
void showFeed(); void showFeed();
void setDomainConnectionFailureVisibility(bool visible); void setDomainConnectionFailureVisibility(bool visible);
void toggleDiskCacheEditor();
void toggleLoginDialog(); void toggleLoginDialog();
void showLoginDialog(); void showLoginDialog();
void octreeStatsDetails(); void octreeStatsDetails();
void cachesSizeDialog();
void lodTools(); void lodTools();
void hmdTools(bool showTools); void hmdTools(bool showTools);
void showScriptEditor(); void showScriptEditor();
@ -77,7 +74,6 @@ private:
QPointer<AnimationsDialog> _animationsDialog; QPointer<AnimationsDialog> _animationsDialog;
QPointer<AttachmentsDialog> _attachmentsDialog; QPointer<AttachmentsDialog> _attachmentsDialog;
QPointer<CachesSizeDialog> _cachesSizeDialog; QPointer<CachesSizeDialog> _cachesSizeDialog;
QPointer<DiskCacheEditor> _diskCacheEditor;
QPointer<QMessageBox> _ircInfoBox; QPointer<QMessageBox> _ircInfoBox;
QPointer<HMDToolsDialog> _hmdToolsDialog; QPointer<HMDToolsDialog> _hmdToolsDialog;
QPointer<LodToolsDialog> _lodToolsDialog; QPointer<LodToolsDialog> _lodToolsDialog;

View file

@ -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();
}
}

View file

@ -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

View file

@ -160,7 +160,7 @@ AudioClient::AudioClient() :
_loopbackAudioOutput(NULL), _loopbackAudioOutput(NULL),
_loopbackOutputDevice(NULL), _loopbackOutputDevice(NULL),
_inputRingBuffer(0), _inputRingBuffer(0),
_localInjectorsStream(0), _localInjectorsStream(0, 1),
_receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES),
_isStereoInput(false), _isStereoInput(false),
_outputStarveDetectionStartTimeMsec(0), _outputStarveDetectionStartTimeMsec(0),

View file

@ -146,6 +146,7 @@ void EntityTreeRenderer::clear() {
void EntityTreeRenderer::reloadEntityScripts() { void EntityTreeRenderer::reloadEntityScripts() {
_entitiesScriptEngine->unloadAllEntityScripts(); _entitiesScriptEngine->unloadAllEntityScripts();
_entitiesScriptEngine->resetModuleCache();
foreach(auto entity, _entitiesInScene) { foreach(auto entity, _entitiesInScene) {
if (!entity->getScript().isEmpty()) { if (!entity->getScript().isEmpty()) {
_entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true); _entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true);

View file

@ -14,6 +14,7 @@
#include <QByteArray> #include <QByteArray>
#include <QtConcurrent/QtConcurrentRun> #include <QtConcurrent/QtConcurrentRun>
#include <glm/gtx/transform.hpp> #include <glm/gtx/transform.hpp>
#include "ModelScriptingInterface.h"
#if defined(__GNUC__) && !defined(__clang__) #if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic push #pragma GCC diagnostic push
@ -53,6 +54,8 @@
#include "PhysicalEntitySimulation.h" #include "PhysicalEntitySimulation.h"
gpu::PipelinePointer RenderablePolyVoxEntityItem::_pipeline = nullptr; gpu::PipelinePointer RenderablePolyVoxEntityItem::_pipeline = nullptr;
gpu::PipelinePointer RenderablePolyVoxEntityItem::_wireframePipeline = nullptr;
const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5;
@ -73,7 +76,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5;
_meshDirty _meshDirty
In RenderablePolyVoxEntityItem::render, these flags are checked and changes are propagated along the chain. 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 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 is set, isReadyToComputeShape() gets called and _shape is created either from _volData or _shape, depending on
the surface style. 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 When a script changes _volData, compressVolumeDataAndSendEditPacket is called to update _voxelData and to
send a packet to the entity-server. 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 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. 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) { if (voxelDataDirty) {
decompressVolumeData(); decompressVolumeData();
} else if (volDataDirty) { } else if (volDataDirty) {
getMesh(); recomputeMesh();
} }
model::MeshPointer mesh; model::MeshPointer mesh;
@ -696,7 +699,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
!mesh->getIndexBuffer()._buffer) { !mesh->getIndexBuffer()._buffer) {
return; return;
} }
if (!_pipeline) { if (!_pipeline) {
gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert)); gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert));
gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag)); 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); state->setDepthTest(true, true, gpu::LESS_EQUAL);
_pipeline = gpu::Pipeline::create(program, state); _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) { if (!_vertexFormat) {
@ -725,7 +735,11 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
} }
gpu::Batch& batch = *args->_batch; 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()); Transform transform(voxelToWorldMatrix());
batch.setModelTransform(transform); batch.setModelTransform(transform);
@ -762,7 +776,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
batch.setResourceTexture(2, DependencyManager::get<TextureCache>()->getWhiteTexture()); 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._glUniform3f(voxelVolumeSizeLocation, voxelVolumeSize.x, voxelVolumeSize.y, voxelVolumeSize.z);
batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)mesh->getNumIndices(), 0); 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 // use _volData to make a renderable mesh
PolyVoxSurfaceStyle voxelSurfaceStyle; PolyVoxSurfaceStyle voxelSurfaceStyle;
withReadLock([&] { withReadLock([&] {
@ -1269,12 +1283,20 @@ void RenderablePolyVoxEntityItem::getMesh() {
vertexBufferPtr->getSize() , vertexBufferPtr->getSize() ,
sizeof(PolyVox::PositionMaterialNormal), sizeof(PolyVox::PositionMaterialNormal),
gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW))); 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); entity->setMesh(mesh);
}); });
} }
void RenderablePolyVoxEntityItem::setMesh(model::MeshPointer mesh) { void RenderablePolyVoxEntityItem::setMesh(model::MeshPointer mesh) {
// this catches the payload from getMesh // this catches the payload from recomputeMesh
bool neighborsNeedUpdate; bool neighborsNeedUpdate;
withWriteLock([&] { withWriteLock([&] {
if (!_collisionless) { if (!_collisionless) {
@ -1531,7 +1553,6 @@ std::shared_ptr<RenderablePolyVoxEntityItem> RenderablePolyVoxEntityItem::getZPN
return std::dynamic_pointer_cast<RenderablePolyVoxEntityItem>(_zPNeighbor.lock()); return std::dynamic_pointer_cast<RenderablePolyVoxEntityItem>(_zPNeighbor.lock());
} }
void RenderablePolyVoxEntityItem::bonkNeighbors() { void RenderablePolyVoxEntityItem::bonkNeighbors() {
// flag neighbors to the negative of this entity as needing to rebake their meshes. // flag neighbors to the negative of this entity as needing to rebake their meshes.
cacheNeighbors(); cacheNeighbors();
@ -1551,7 +1572,6 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() {
} }
} }
void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) {
EntityItem::locationChanged(tellPhysics); EntityItem::locationChanged(tellPhysics);
if (!_pipeline || !render::Item::isValidID(_myItem)) { if (!_pipeline || !render::Item::isValidID(_myItem)) {
@ -1563,3 +1583,17 @@ void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) {
scene->enqueuePendingChanges(pendingChanges); 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;
}

View file

@ -133,6 +133,7 @@ public:
QByteArray volDataToArray(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize) const; QByteArray volDataToArray(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize) const;
void setMesh(model::MeshPointer mesh); void setMesh(model::MeshPointer mesh);
bool getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) const override;
void setCollisionPoints(ShapeInfo::PointCollection points, AABox box); void setCollisionPoints(ShapeInfo::PointCollection points, AABox box);
PolyVox::SimpleVolume<uint8_t>* getVolData() { return _volData; } PolyVox::SimpleVolume<uint8_t>* getVolData() { return _volData; }
@ -163,11 +164,12 @@ private:
const int MATERIAL_GPU_SLOT = 3; const int MATERIAL_GPU_SLOT = 3;
render::ItemID _myItem{ render::Item::INVALID_ITEM_ID }; render::ItemID _myItem{ render::Item::INVALID_ITEM_ID };
static gpu::PipelinePointer _pipeline; static gpu::PipelinePointer _pipeline;
static gpu::PipelinePointer _wireframePipeline;
ShapeInfo _shapeInfo; ShapeInfo _shapeInfo;
PolyVox::SimpleVolume<uint8_t>* _volData = nullptr; 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 int _onCount; // how many non-zero voxels are in _volData
bool _neighborsNeedUpdate { false }; bool _neighborsNeedUpdate { false };
@ -178,7 +180,7 @@ private:
// these are run off the main thread // these are run off the main thread
void decompressVolumeData(); void decompressVolumeData();
void compressVolumeDataAndSendEditPacket(); void compressVolumeDataAndSendEditPacket();
virtual void getMesh() override; // recompute mesh virtual void recomputeMesh() override; // recompute mesh
void computeShapeInfoWorker(); void computeShapeInfoWorker();
// these are cached lookups of _xNNeighborID, _yNNeighborID, _zNNeighborID, _xPNeighborID, _yPNeighborID, _zPNeighborID // these are cached lookups of _xNNeighborID, _yNNeighborID, _zNNeighborID, _xPNeighborID, _yPNeighborID, _zPNeighborID

View file

@ -114,13 +114,22 @@ void RenderableShapeEntityItem::render(RenderArgs* args) {
auto outColor = _procedural->getColor(color); auto outColor = _procedural->getColor(color);
outColor.a *= _procedural->isFading() ? Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) : 1.0f; outColor.a *= _procedural->isFading() ? Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) : 1.0f;
batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a); 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 { } else {
// FIXME, support instanced multi-shape rendering using multidraw indirect // FIXME, support instanced multi-shape rendering using multidraw indirect
color.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; color.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f;
auto geometryCache = DependencyManager::get<GeometryCache>(); auto geometryCache = DependencyManager::get<GeometryCache>();
auto pipeline = color.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); 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]); static const auto triCount = DependencyManager::get<GeometryCache>()->getShapeTriangleCount(MAPPING[_shape]);

View file

@ -15,11 +15,13 @@
#define hifi_EntitiesScriptEngineProvider_h #define hifi_EntitiesScriptEngineProvider_h
#include <QtCore/QString> #include <QtCore/QString>
#include <QFuture>
#include "EntityItemID.h" #include "EntityItemID.h"
class EntitiesScriptEngineProvider { class EntitiesScriptEngineProvider {
public: public:
virtual void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) = 0; 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

View file

@ -10,6 +10,9 @@
// //
#include "EntityScriptingInterface.h" #include "EntityScriptingInterface.h"
#include <QFutureWatcher>
#include <QtConcurrent/QtConcurrentRun>
#include "EntityItemID.h" #include "EntityItemID.h"
#include <VariantMapToScriptValue.h> #include <VariantMapToScriptValue.h>
#include <SharedUtil.h> #include <SharedUtil.h>
@ -680,6 +683,118 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) {
return client->reloadServerScript(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) { bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) {
auto client = DependencyManager::get<EntityScriptClient>(); auto client = DependencyManager::get<EntityScriptClient>();
auto request = client->createScriptStatusRequest(entityID); auto request = client->createScriptStatusRequest(entityID);
@ -815,8 +930,7 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra
} }
} }
bool EntityScriptingInterface::setVoxels(QUuid entityID, bool EntityScriptingInterface::polyVoxWorker(QUuid entityID, std::function<bool(PolyVoxEntityItem&)> actor) {
std::function<bool(PolyVoxEntityItem&)> actor) {
PROFILE_RANGE(script_entities, __FUNCTION__); PROFILE_RANGE(script_entities, __FUNCTION__);
if (!_entityTree) { if (!_entityTree) {
@ -882,11 +996,9 @@ bool EntityScriptingInterface::setPoints(QUuid entityID, std::function<bool(Line
return success; return success;
} }
bool EntityScriptingInterface::setVoxelSphere(QUuid entityID, const glm::vec3& center, float radius, int value) { bool EntityScriptingInterface::setVoxelSphere(QUuid entityID, const glm::vec3& center, float radius, int value) {
PROFILE_RANGE(script_entities, __FUNCTION__); PROFILE_RANGE(script_entities, __FUNCTION__);
return polyVoxWorker(entityID, [center, radius, value](PolyVoxEntityItem& polyVoxEntity) {
return setVoxels(entityID, [center, radius, value](PolyVoxEntityItem& polyVoxEntity) {
return polyVoxEntity.setSphere(center, radius, value); return polyVoxEntity.setSphere(center, radius, value);
}); });
} }
@ -896,7 +1008,7 @@ bool EntityScriptingInterface::setVoxelCapsule(QUuid entityID,
float radius, int value) { float radius, int value) {
PROFILE_RANGE(script_entities, __FUNCTION__); 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); 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) { bool EntityScriptingInterface::setVoxel(QUuid entityID, const glm::vec3& position, int value) {
PROFILE_RANGE(script_entities, __FUNCTION__); PROFILE_RANGE(script_entities, __FUNCTION__);
return setVoxels(entityID, [position, value](PolyVoxEntityItem& polyVoxEntity) { return polyVoxWorker(entityID, [position, value](PolyVoxEntityItem& polyVoxEntity) {
return polyVoxEntity.setVoxelInVolume(position, value); 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) { bool EntityScriptingInterface::setAllVoxels(QUuid entityID, int value) {
PROFILE_RANGE(script_entities, __FUNCTION__); PROFILE_RANGE(script_entities, __FUNCTION__);
return setVoxels(entityID, [value](PolyVoxEntityItem& polyVoxEntity) { return polyVoxWorker(entityID, [value](PolyVoxEntityItem& polyVoxEntity) {
return polyVoxEntity.setAll(value); return polyVoxEntity.setAll(value);
}); });
} }
@ -921,11 +1033,23 @@ bool EntityScriptingInterface::setVoxelsInCuboid(QUuid entityID, const glm::vec3
const glm::vec3& cuboidSize, int value) { const glm::vec3& cuboidSize, int value) {
PROFILE_RANGE(script_entities, __FUNCTION__); 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); 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) { bool EntityScriptingInterface::setAllPoints(QUuid entityID, const QVector<glm::vec3>& points) {
PROFILE_RANGE(script_entities, __FUNCTION__); PROFILE_RANGE(script_entities, __FUNCTION__);

View file

@ -34,7 +34,23 @@
#include "EntitiesScriptEngineProvider.h" #include "EntitiesScriptEngineProvider.h"
#include "EntityItemProperties.h" #include "EntityItemProperties.h"
#include "BaseScriptEngine.h"
class EntityTree; 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 { class RayToEntityIntersectionResult {
public: public:
@ -67,6 +83,7 @@ class EntityScriptingInterface : public OctreeScriptingInterface, public Depende
Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier) Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier)
Q_PROPERTY(QUuid keyboardFocusEntity READ getKeyboardFocusEntity WRITE setKeyboardFocusEntity) Q_PROPERTY(QUuid keyboardFocusEntity READ getKeyboardFocusEntity WRITE setKeyboardFocusEntity)
friend EntityPropertyMetadataRequest;
public: public:
EntityScriptingInterface(bool bidOnSimulationOwnership); 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 RayToEntityIntersectionResult findRayIntersectionBlocking(const PickRay& ray, bool precisionPicking = false, const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue());
Q_INVOKABLE bool reloadServerScripts(QUuid entityID); 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 bool getServerScriptStatus(QUuid entityID, QScriptValue callback);
Q_INVOKABLE void setLightsArePickable(bool value); Q_INVOKABLE void setLightsArePickable(bool value);
@ -229,6 +266,7 @@ public slots:
Q_INVOKABLE bool setAllVoxels(QUuid entityID, int value); Q_INVOKABLE bool setAllVoxels(QUuid entityID, int value);
Q_INVOKABLE bool setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition, Q_INVOKABLE bool setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition,
const glm::vec3& cuboidSize, int value); 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 setAllPoints(QUuid entityID, const QVector<glm::vec3>& points);
Q_INVOKABLE bool appendPoint(QUuid entityID, const glm::vec3& point); Q_INVOKABLE bool appendPoint(QUuid entityID, const glm::vec3& point);
@ -323,9 +361,14 @@ signals:
void webEventReceived(const EntityItemID& entityItemID, const QVariant& message); 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: private:
bool actionWorker(const QUuid& entityID, std::function<bool(EntitySimulationPointer, EntityItemPointer)> actor); 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); bool setPoints(QUuid entityID, std::function<bool(LineEntityItem&)> actor);
void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties); void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties);

View file

@ -242,3 +242,7 @@ const QByteArray PolyVoxEntityItem::getVoxelData() const {
}); });
return voxelDataCopy; return voxelDataCopy;
} }
bool PolyVoxEntityItem::getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) const {
return false;
}

View file

@ -131,7 +131,9 @@ class PolyVoxEntityItem : public EntityItem {
virtual void rebakeMesh() {}; virtual void rebakeMesh() {};
void setVoxelDataDirty(bool value) { withWriteLock([&] { _voxelDataDirty = value; }); } void setVoxelDataDirty(bool value) { withWriteLock([&] { _voxelDataDirty = value; }); }
virtual void getMesh() {}; // recompute mesh virtual void recomputeMesh() {};
virtual bool getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) const;
protected: protected:
glm::vec3 _voxelVolumeSize; // this is always 3 bytes glm::vec3 _voxelVolumeSize; // this is always 3 bytes

View file

@ -54,7 +54,8 @@ template<class T> QVariant readBinaryArray(QDataStream& in, int& position) {
in.readRawData(compressed.data() + sizeof(quint32), compressedLength); in.readRawData(compressed.data() + sizeof(quint32), compressedLength);
position += compressedLength; position += compressedLength;
arrayData = qUncompress(compressed); 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"); throw QString("corrupt fbx file");
} }
} else { } else {

View 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("");
}

View 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

View file

@ -133,6 +133,7 @@ void LightingModel::setSpotLight(bool enable) {
bool LightingModel::isSpotLightEnabled() const { bool LightingModel::isSpotLightEnabled() const {
return (bool)_parametersBuffer.get<Parameters>().enableSpotLight; return (bool)_parametersBuffer.get<Parameters>().enableSpotLight;
} }
void LightingModel::setShowLightContour(bool enable) { void LightingModel::setShowLightContour(bool enable) {
if (enable != isShowLightContourEnabled()) { if (enable != isShowLightContourEnabled()) {
_parametersBuffer.edit<Parameters>().showLightContour = (float)enable; _parametersBuffer.edit<Parameters>().showLightContour = (float)enable;
@ -142,6 +143,14 @@ bool LightingModel::isShowLightContourEnabled() const {
return (bool)_parametersBuffer.get<Parameters>().showLightContour; 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() { MakeLightingModel::MakeLightingModel() {
_lightingModel = std::make_shared<LightingModel>(); _lightingModel = std::make_shared<LightingModel>();
} }
@ -167,6 +176,7 @@ void MakeLightingModel::configure(const Config& config) {
_lightingModel->setSpotLight(config.enableSpotLight); _lightingModel->setSpotLight(config.enableSpotLight);
_lightingModel->setShowLightContour(config.showLightContour); _lightingModel->setShowLightContour(config.showLightContour);
_lightingModel->setWireframe(config.enableWireframe);
} }
void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) { void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) {

View file

@ -64,6 +64,9 @@ public:
void setShowLightContour(bool enable); void setShowLightContour(bool enable);
bool isShowLightContourEnabled() const; bool isShowLightContourEnabled() const;
void setWireframe(bool enable);
bool isWireframeEnabled() const;
UniformBufferView getParametersBuffer() const { return _parametersBuffer; } UniformBufferView getParametersBuffer() const { return _parametersBuffer; }
protected: protected:
@ -89,13 +92,12 @@ protected:
float enablePointLight{ 1.0f }; float enablePointLight{ 1.0f };
float enableSpotLight{ 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 enableObscurance{ 1.0f };
float enableMaterialTexturing { 1.0f }; float enableMaterialTexturing { 1.0f };
float enableWireframe { 0.0f }; // false by default
float spares{ 0.0f };
Parameters() {} Parameters() {}
}; };
@ -129,6 +131,7 @@ class MakeLightingModelConfig : public render::Job::Config {
Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty) Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty)
Q_PROPERTY(bool enableSpotLight MEMBER enableSpotLight 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) Q_PROPERTY(bool showLightContour MEMBER showLightContour NOTIFY dirty)
public: public:
@ -152,9 +155,10 @@ public:
bool enablePointLight{ true }; bool enablePointLight{ true };
bool enableSpotLight{ true }; bool enableSpotLight{ true };
bool showLightContour { false }; // false by default bool showLightContour { false }; // false by default
bool enableWireframe { false }; // false by default
signals: signals:
void dirty(); void dirty();
}; };

View file

@ -17,7 +17,7 @@ struct LightingModel {
vec4 _UnlitEmissiveLightmapBackground; vec4 _UnlitEmissiveLightmapBackground;
vec4 _ScatteringDiffuseSpecularAlbedo; vec4 _ScatteringDiffuseSpecularAlbedo;
vec4 _AmbientDirectionalPointSpot; vec4 _AmbientDirectionalPointSpot;
vec4 _ShowContourObscuranceSpare2; vec4 _ShowContourObscuranceWireframe;
}; };
uniform lightingModelBuffer{ uniform lightingModelBuffer{
@ -37,7 +37,7 @@ float isBackgroundEnabled() {
return lightingModel._UnlitEmissiveLightmapBackground.w; return lightingModel._UnlitEmissiveLightmapBackground.w;
} }
float isObscuranceEnabled() { float isObscuranceEnabled() {
return lightingModel._ShowContourObscuranceSpare2.y; return lightingModel._ShowContourObscuranceWireframe.y;
} }
float isScatteringEnabled() { float isScatteringEnabled() {
@ -67,9 +67,12 @@ float isSpotEnabled() {
} }
float isShowLightContour() { float isShowLightContour() {
return lightingModel._ShowContourObscuranceSpare2.x; return lightingModel._ShowContourObscuranceWireframe.x;
} }
float isWireframeEnabled() {
return lightingModel._ShowContourObscuranceWireframe.z;
}
<@endfunc@> <@endfunc@>
<$declareLightingModel()$> <$declareLightingModel()$>

View file

@ -259,8 +259,18 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont
// Setup lighting model for all items; // Setup lighting model for all items;
batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); 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->_batch = nullptr;
args->_globalShapeKey = 0;
}); });
config->setNumDrawn((int)inItems.size()); config->setNumDrawn((int)inItems.size());
@ -295,12 +305,21 @@ void DrawStateSortDeferred::run(const SceneContextPointer& sceneContext, const R
// Setup lighting model for all items; // Setup lighting model for all items;
batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); 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) { if (_stateSort) {
renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey);
} else { } else {
renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey);
} }
args->_batch = nullptr; args->_batch = nullptr;
args->_globalShapeKey = 0;
}); });
config->setNumDrawn((int)inItems.size()); config->setNumDrawn((int)inItems.size());

View file

@ -307,7 +307,7 @@ void initForwardPipelines(render::ShapePlumber& plumber) {
void addPlumberPipeline(ShapePlumber& plumber, void addPlumberPipeline(ShapePlumber& plumber,
const ShapeKey& key, const gpu::ShaderPointer& vertex, const gpu::ShaderPointer& pixel) { 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 // 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.isDepthBiased());
assert(key.isCullFace()); assert(key.isCullFace());

View file

@ -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()); assert(item.getKey().isShape());
const auto& key = item.getShapeKey(); auto key = item.getShapeKey() | globalKey;
if (key.isValid() && !key.hasOwnPipeline()) { if (key.isValid() && !key.hasOwnPipeline()) {
args->_pipeline = shapeContext->pickPipeline(args, key); args->_pipeline = shapeContext->pickPipeline(args, key);
if (args->_pipeline) { if (args->_pipeline) {
@ -56,7 +56,7 @@ void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, cons
} }
void render::renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, 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; auto& scene = sceneContext->_scene;
RenderArgs* args = renderContext->args; RenderArgs* args = renderContext->args;
@ -66,12 +66,12 @@ void render::renderShapes(const SceneContextPointer& sceneContext, const RenderC
} }
for (auto i = 0; i < numItemsToDraw; ++i) { for (auto i = 0; i < numItemsToDraw; ++i) {
auto& item = scene->getItem(inItems[i].id); 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, 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; auto& scene = sceneContext->_scene;
RenderArgs* args = renderContext->args; RenderArgs* args = renderContext->args;
@ -91,7 +91,7 @@ void render::renderStateSortShapes(const SceneContextPointer& sceneContext, cons
{ {
assert(item.getKey().isShape()); assert(item.getKey().isShape());
const auto key = item.getShapeKey(); auto key = item.getShapeKey() | globalKey;
if (key.isValid() && !key.hasOwnPipeline()) { if (key.isValid() && !key.hasOwnPipeline()) {
auto& bucket = sortedShapes[key]; auto& bucket = sortedShapes[key];
if (bucket.empty()) { if (bucket.empty()) {

View file

@ -17,8 +17,8 @@
namespace render { namespace render {
void renderItems(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems, int maxDrawnItems = -1); 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 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); 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 { class DrawLightConfig : public Job::Config {
Q_OBJECT Q_OBJECT

View file

@ -46,6 +46,10 @@ public:
ShapeKey() : _flags{ 0 } {} ShapeKey() : _flags{ 0 } {}
ShapeKey(const Flags& flags) : _flags{flags} {} 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 { class Builder {
public: public:
Builder() {} Builder() {}
@ -144,7 +148,7 @@ public:
bool isSkinned() const { return _flags[SKINNED]; } bool isSkinned() const { return _flags[SKINNED]; }
bool isDepthOnly() const { return _flags[DEPTH_ONLY]; } bool isDepthOnly() const { return _flags[DEPTH_ONLY]; }
bool isDepthBiased() const { return _flags[DEPTH_BIAS]; } 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 isCullFace() const { return !_flags[NO_CULL_FACE]; }
bool hasOwnPipeline() const { return _flags[OWN_PIPELINE]; } bool hasOwnPipeline() const { return _flags[OWN_PIPELINE]; }
@ -180,7 +184,7 @@ inline QDebug operator<<(QDebug debug, const ShapeKey& key) {
<< "isSkinned:" << key.isSkinned() << "isSkinned:" << key.isSkinned()
<< "isDepthOnly:" << key.isDepthOnly() << "isDepthOnly:" << key.isDepthOnly()
<< "isDepthBiased:" << key.isDepthBiased() << "isDepthBiased:" << key.isDepthBiased()
<< "isWireFrame:" << key.isWireFrame() << "isWireframe:" << key.isWireframe()
<< "isCullFace:" << key.isCullFace() << "isCullFace:" << key.isCullFace()
<< "]"; << "]";
} }

View file

@ -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

View 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

View 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);
}

View 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

View file

@ -19,6 +19,9 @@
#include <QtCore/QThread> #include <QtCore/QThread>
#include <QtCore/QRegularExpression> #include <QtCore/QRegularExpression>
#include <QtCore/QFuture>
#include <QtConcurrent/QtConcurrentRun>
#include <QtWidgets/QMainWindow> #include <QtWidgets/QMainWindow>
#include <QtWidgets/QApplication> #include <QtWidgets/QApplication>
@ -65,18 +68,25 @@
#include "RecordingScriptingInterface.h" #include "RecordingScriptingInterface.h"
#include "ScriptEngines.h" #include "ScriptEngines.h"
#include "TabletScriptingInterface.h" #include "TabletScriptingInterface.h"
#include "ModelScriptingInterface.h"
#include <Profile.h> #include <Profile.h>
#include "MIDIEvent.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 = static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =
QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects; QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;
static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable }; 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 QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration };
static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };
Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)
@ -84,7 +94,7 @@ int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature
Q_LOGGING_CATEGORY(scriptengineScript, "hifi.scriptengine.script") Q_LOGGING_CATEGORY(scriptengineScript, "hifi.scriptengine.script")
static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) {
QString message = ""; QString message = "";
for (int i = 0; i < context->argumentCount(); i++) { for (int i = 0; i < context->argumentCount(); i++) {
if (i > 0) { if (i > 0) {
@ -141,7 +151,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID)
} }
QString ScriptEngine::logException(const QScriptValue& exception) { QString ScriptEngine::logException(const QScriptValue& exception) {
auto message = formatException(exception); auto message = formatException(exception, _enableExtendedJSExceptions.get());
scriptErrorMessage(message); scriptErrorMessage(message);
return 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 interface cannot live on itself, and we want to move this into the thread, so
// the thread cannot have this as a parent. // the thread cannot have this as a parent.
QThread* workerThread = new QThread(); QThread* workerThread = new QThread();
workerThread->setObjectName(QString("Script Thread:") + getFilename()); workerThread->setObjectName(QString("js:") + getFilename().replace("about:",""));
moveToThread(workerThread); moveToThread(workerThread);
// NOTE: If you connect any essential signals for proper shutdown or cleanup of // NOTE: If you connect any essential signals for proper shutdown or cleanup of
@ -532,6 +542,40 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) {
return prototype; 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() { void ScriptEngine::init() {
if (_isInitialized) { if (_isInitialized) {
return; // only initialize once return; // only initialize once
@ -585,6 +629,15 @@ void ScriptEngine::init() {
registerGlobalObject("Script", this); 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("Audio", &AudioScriptingInterface::getInstance());
registerGlobalObject("Entities", entityScriptingInterface.data()); registerGlobalObject("Entities", entityScriptingInterface.data());
registerGlobalObject("Quat", &_quatLibrary); registerGlobalObject("Quat", &_quatLibrary);
@ -594,7 +647,7 @@ void ScriptEngine::init() {
registerGlobalObject("Messages", DependencyManager::get<MessagesClient>().data()); registerGlobalObject("Messages", DependencyManager::get<MessagesClient>().data());
registerGlobalObject("File", new FileScriptingInterface(this)); registerGlobalObject("File", new FileScriptingInterface(this));
qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue); qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue);
qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue); qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue);
@ -612,6 +665,10 @@ void ScriptEngine::init() {
registerGlobalObject("Resources", DependencyManager::get<ResourceScriptingInterface>().data()); registerGlobalObject("Resources", DependencyManager::get<ResourceScriptingInterface>().data());
registerGlobalObject("DebugDraw", &DebugDraw::getInstance()); 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) { 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(). 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) { QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
if (DependencyManager::get<ScriptEngines>()->isStopped()) { if (DependencyManager::get<ScriptEngines>()->isStopped()) {
return QScriptValue(); // bail early return QScriptValue(); // bail early
@ -875,29 +937,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi
// Check syntax // Check syntax
auto syntaxError = lintScript(sourceCode, fileName); auto syntaxError = lintScript(sourceCode, fileName);
if (syntaxError.isError()) { if (syntaxError.isError()) {
if (isEvaluating()) { if (!isEvaluating()) {
currentContext()->throwValue(syntaxError);
} else {
syntaxError.setProperty("detail", "evaluate"); syntaxError.setProperty("detail", "evaluate");
emit unhandledException(syntaxError);
} }
raiseException(syntaxError);
maybeEmitUncaughtException("lint");
return syntaxError; return syntaxError;
} }
QScriptProgram program { sourceCode, fileName, lineNumber }; QScriptProgram program { sourceCode, fileName, lineNumber };
if (program.isNull()) { if (program.isNull()) {
// can this happen? // can this happen?
auto err = makeError("could not create QScriptProgram for " + fileName); auto err = makeError("could not create QScriptProgram for " + fileName);
emit unhandledException(err); raiseException(err);
maybeEmitUncaughtException("compile");
return err; return err;
} }
QScriptValue result; QScriptValue result;
{ {
result = BaseScriptEngine::evaluate(program); result = BaseScriptEngine::evaluate(program);
if (!isEvaluating() && hasUncaughtException()) { maybeEmitUncaughtException("evaluate");
emit unhandledException(cloneUncaughtException(__FUNCTION__));
clearExceptions();
}
} }
return result; return result;
} }
@ -920,10 +979,7 @@ void ScriptEngine::run() {
{ {
evaluate(_scriptContents, _fileNameString); evaluate(_scriptContents, _fileNameString);
if (!isEvaluating() && hasUncaughtException()) { maybeEmitUncaughtException(__FUNCTION__);
emit unhandledException(cloneUncaughtException(__FUNCTION__));
clearExceptions();
}
} }
#ifdef _WIN32 #ifdef _WIN32
// VS13 does not sleep_until unless it uses the system_clock, see: // 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); 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 // 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. // 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 // If no callback is specified, the included files will be loaded synchronously and will block execution until
// all of the files have finished loading. // all of the files have finished loading.
void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return;
}
if (DependencyManager::get<ScriptEngines>()->isStopped()) { if (DependencyManager::get<ScriptEngines>()->isStopped()) {
scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:"
+ includeFiles.join(",") + "parent script:" + getFilename()); + includeFiles.join(",") + "parent script:" + getFilename());
@ -1361,7 +1767,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);
if (hasUncaughtException()) { if (hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(__FUNCTION__)); emit unhandledException(cloneUncaughtException("evaluateInclude"));
clearExceptions(); clearExceptions();
} }
} else { } 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 // 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 // the Application or other context will connect to in order to know to actually load the script
void ScriptEngine::load(const QString& loadFile) { void ScriptEngine::load(const QString& loadFile) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return;
}
if (DependencyManager::get<ScriptEngines>()->isStopped()) { if (DependencyManager::get<ScriptEngines>()->isStopped()) {
scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:"
+ loadFile + "parent script:" + getFilename()); + loadFile + "parent script:" + getFilename());
@ -1477,6 +1886,52 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const
emit entityScriptDetailsUpdated(); 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 { bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
auto it = _entityScripts.constFind(entityID); auto it = _entityScripts.constFind(entityID);
if (it == _entityScripts.constEnd()) { if (it == _entityScripts.constEnd()) {
@ -1615,10 +2070,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString&
auto scriptCache = DependencyManager::get<ScriptCache>(); auto scriptCache = DependencyManager::get<ScriptCache>();
// note: see EntityTreeRenderer.cpp for shared pointer lifecycle management // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management
QWeakPointer<ScriptEngine> weakRef(sharedFromThis()); QWeakPointer<BaseScriptEngine> weakRef(sharedFromThis());
scriptCache->getScriptContents(entityScript, scriptCache->getScriptContents(entityScript,
[this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { [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) { if (!strongRef) {
qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!"; qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";
return; return;
@ -1737,13 +2192,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
timeout.setSingleShot(true); timeout.setSingleShot(true);
timeout.start(SANDBOX_TIMEOUT); timeout.start(SANDBOX_TIMEOUT);
connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{ connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{
auto context = sandbox.currentContext();
if (context) {
qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")"; qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")";
// Guard against infinite loops and non-performant code // 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); testConstructor = sandbox.evaluate(program);
@ -1759,7 +2213,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
if (exception.isError()) { if (exception.isError()) {
// create a local copy using makeError to decouple from the sandbox engine // create a local copy using makeError to decouple from the sandbox engine
exception = makeError(exception); exception = makeError(exception);
setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
emit unhandledException(exception); emit unhandledException(exception);
return; return;
} }
@ -1771,9 +2225,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
testConstructorType = "empty"; testConstructorType = "empty";
} }
QString testConstructorValue = testConstructor.toString(); QString testConstructorValue = testConstructor.toString();
const int maxTestConstructorValueSize = 80; if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) {
if (testConstructorValue.size() > maxTestConstructorValueSize) { testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";
testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";
} }
auto message = QString("failed to load entity script -- expected a function, got %1, %2") auto message = QString("failed to load entity script -- expected a function, got %1, %2")
.arg(testConstructorType).arg(testConstructorValue); .arg(testConstructorType).arg(testConstructorValue);
@ -1811,7 +2264,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
if (entityScriptObject.isError()) { if (entityScriptObject.isError()) {
auto exception = entityScriptObject; auto exception = entityScriptObject;
setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
emit unhandledException(exception); emit unhandledException(exception);
return; return;
} }
@ -1855,10 +2308,12 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR
const EntityScriptDetails &oldDetails = _entityScripts[entityID]; const EntityScriptDetails &oldDetails = _entityScripts[entityID];
if (isEntityScriptRunning(entityID)) { if (isEntityScriptRunning(entityID)) {
callEntityScriptMethod(entityID, "unload"); callEntityScriptMethod(entityID, "unload");
} else { }
#ifdef DEBUG_ENTITY_STATES
else {
qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status;
} }
#endif
if (shouldRemoveFromMap) { if (shouldRemoveFromMap) {
// this was a deleted entity, we've been asked to remove it from the map // this was a deleted entity, we've been asked to remove it from the map
_entityScripts.remove(entityID); _entityScripts.remove(entityID);
@ -1950,10 +2405,7 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s
#else #else
operation(); operation();
#endif #endif
if (!isEvaluating() && hasUncaughtException()) { maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__);
emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__));
clearExceptions();
}
currentEntityIdentifier = oldIdentifier; currentEntityIdentifier = oldIdentifier;
currentSandboxURL = oldSandboxURL; currentSandboxURL = oldSandboxURL;
} }

View file

@ -41,6 +41,7 @@
#include "ScriptCache.h" #include "ScriptCache.h"
#include "ScriptUUID.h" #include "ScriptUUID.h"
#include "Vec3.h" #include "Vec3.h"
#include "SettingHandle.h"
class QScriptEngineDebugger; class QScriptEngineDebugger;
@ -78,7 +79,7 @@ public:
QUrl definingSandboxURL { QUrl("about:EntityScript") }; QUrl definingSandboxURL { QUrl("about:EntityScript") };
}; };
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis<ScriptEngine> { class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QString context READ getContext) Q_PROPERTY(QString context READ getContext)
public: public:
@ -137,6 +138,8 @@ public:
/// evaluate some code in the context of the ScriptEngine and return the result /// 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 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 /// 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 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 /// 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 QStringList& includeFiles, QScriptValue callback = QScriptValue());
Q_INVOKABLE void include(const QString& includeFile, 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* setInterval(const QScriptValue& function, int intervalMS);
Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS); Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS);
Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); } Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
@ -170,6 +183,8 @@ public:
Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) { Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) {
return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING; 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 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 unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false); // will call unload method
Q_INVOKABLE void unloadAllEntityScripts(); Q_INVOKABLE void unloadAllEntityScripts();
@ -237,6 +252,9 @@ signals:
protected: protected:
void init(); void init();
Q_INVOKABLE void executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type = Qt::QueuedConnection ); 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); QString logException(const QScriptValue& exception);
void timerFired(); void timerFired();
@ -290,11 +308,16 @@ protected:
AssetScriptingInterface _assetScriptingInterface{ this }; AssetScriptingInterface _assetScriptingInterface{ this };
std::function<bool()> _emitScriptUpdates{ [](){ return true; } }; std::function<bool()> _emitScriptUpdates{ []() { return true; } };
std::recursive_mutex _lock; std::recursive_mutex _lock;
std::chrono::microseconds _totalTimerExecution { 0 }; 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 #endif // hifi_ScriptEngine_h

View file

@ -12,3 +12,4 @@
#include "ScriptEngineLogging.h" #include "ScriptEngineLogging.h"
Q_LOGGING_CATEGORY(scriptengine, "hifi.scriptengine") Q_LOGGING_CATEGORY(scriptengine, "hifi.scriptengine")
Q_LOGGING_CATEGORY(scriptengine_module, "hifi.scriptengine.module")

View file

@ -15,6 +15,7 @@
#include <QLoggingCategory> #include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(scriptengine) Q_DECLARE_LOGGING_CATEGORY(scriptengine)
Q_DECLARE_LOGGING_CATEGORY(scriptengine_module)
#endif // hifi_ScriptEngineLogging_h #endif // hifi_ScriptEngineLogging_h

View file

@ -10,6 +10,7 @@
// //
#include "BaseScriptEngine.h" #include "BaseScriptEngine.h"
#include "SharedLogging.h"
#include <QtCore/QString> #include <QtCore/QString>
#include <QtCore/QThread> #include <QtCore/QThread>
@ -18,18 +19,27 @@
#include <QtScript/QScriptValueIterator> #include <QtScript/QScriptValueIterator>
#include <QtScript/QScriptContextInfo> #include <QtScript/QScriptContextInfo>
#include "ScriptEngineLogging.h"
#include "Profile.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_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" };
const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " }; 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 // engine-aware JS Error copier and factory
QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) { QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
auto other = _other; auto other = _other;
if (other.isString()) { if (other.isString()) {
other = newObject(); other = newObject();
@ -41,7 +51,7 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri
} }
if (!proto.isFunction()) { if (!proto.isFunction()) {
#ifdef DEBUG_JS_EXCEPTIONS #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 #endif
proto = globalObject().property("Error"); 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 // 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) { 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); const auto syntaxCheck = checkSyntax(sourceCode);
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
auto err = globalObject().property("SyntaxError") auto err = globalObject().property("SyntaxError")
@ -82,13 +95,16 @@ QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QStri
} }
return err; return err;
} }
return undefinedValue(); return QScriptValue();
} }
// this pulls from the best available information to create a detailed snapshot of the current exception // this pulls from the best available information to create a detailed snapshot of the current exception
QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) { QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
if (!hasUncaughtException()) { if (!hasUncaughtException()) {
return QScriptValue(); return unboundNullValue();
} }
auto exception = uncaughtException(); auto exception = uncaughtException();
// ensure the error object is engine-local // ensure the error object is engine-local
@ -144,7 +160,10 @@ QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail
return err; 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 note { "UncaughtException" };
QString result; QString result;
@ -156,8 +175,8 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) {
const auto lineNumber = exception.property("lineNumber").toString(); const auto lineNumber = exception.property("lineNumber").toString();
const auto stacktrace = exception.property("stack").toString(); const auto stacktrace = exception.property("stack").toString();
if (_enableExtendedJSExceptions.get()) { if (includeExtendedDetails) {
// This setting toggles display of the hints now being added during the loading process. // Display additional exception / troubleshooting hints that can be added via the custom Error .detail property
// Example difference: // Example difference:
// [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n... // [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... // [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; 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) { QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
PROFILE_RANGE(script, "evaluateInClosure"); PROFILE_RANGE(script, "evaluateInClosure");
if (QThread::currentThread() != thread()) { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only."; return unboundNullValue();
// note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE
return QScriptValue();
} }
const auto fileName = program.fileName(); const auto fileName = program.fileName();
const auto shortName = QUrl(fileName).fileName(); const auto shortName = QUrl(fileName).fileName();
@ -189,7 +233,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
auto global = closure.property("global"); auto global = closure.property("global");
if (global.isObject()) { if (global.isObject()) {
#ifdef DEBUG_JS #ifdef DEBUG_JS
qCDebug(scriptengine) << " setting global = closure.global" << shortName; qCDebug(shared) << " setting global = closure.global" << shortName;
#endif #endif
oldGlobal = globalObject(); oldGlobal = globalObject();
setGlobalObject(global); setGlobalObject(global);
@ -200,34 +244,34 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
auto thiz = closure.property("this"); auto thiz = closure.property("this");
if (thiz.isObject()) { if (thiz.isObject()) {
#ifdef DEBUG_JS #ifdef DEBUG_JS
qCDebug(scriptengine) << " setting this = closure.this" << shortName; qCDebug(shared) << " setting this = closure.this" << shortName;
#endif #endif
context->setThisObject(thiz); context->setThisObject(thiz);
} }
context->pushScope(closure); context->pushScope(closure);
#ifdef DEBUG_JS #ifdef DEBUG_JS
qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName); qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
#endif #endif
{ {
result = BaseScriptEngine::evaluate(program); result = BaseScriptEngine::evaluate(program);
if (hasUncaughtException()) { if (hasUncaughtException()) {
auto err = cloneUncaughtException(__FUNCTION__); auto err = cloneUncaughtException(__FUNCTION__);
#ifdef DEBUG_JS_EXCEPTIONS #ifdef DEBUG_JS_EXCEPTIONS
qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
err.setProperty("_result", result); err.setProperty("_result", result);
#endif #endif
result = err; result = err;
} }
} }
#ifdef DEBUG_JS #ifdef DEBUG_JS
qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName); qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
#endif #endif
popContext(); popContext();
if (oldGlobal.isValid()) { if (oldGlobal.isValid()) {
#ifdef DEBUG_JS #ifdef DEBUG_JS
qCDebug(scriptengine) << " restoring global" << shortName; qCDebug(shared) << " restoring global" << shortName;
#endif #endif
setGlobalObject(oldGlobal); setGlobalObject(oldGlobal);
} }
@ -236,7 +280,6 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
} }
// Lambda // Lambda
QScriptValue BaseScriptEngine::newLambdaFunction(std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) { QScriptValue BaseScriptEngine::newLambdaFunction(std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) {
auto lambda = new Lambda(this, operation, data); auto lambda = new Lambda(this, operation, data);
auto object = newQObject(lambda, ownership); auto object = newQObject(lambda, ownership);
@ -262,26 +305,57 @@ Lambda::Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext
#endif #endif
} }
QScriptValue Lambda::call() { QScriptValue Lambda::call() {
if (!BaseScriptEngine::IS_THREADSAFE_INVOCATION(engine->thread(), __FUNCTION__)) {
return BaseScriptEngine::unboundNullValue();
}
return operation(engine->currentContext(), engine); 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 #ifdef DEBUG_JS
void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) { void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return;
}
if (!header.isEmpty()) { if (!header.isEmpty()) {
qCDebug(scriptengine) << header; qCDebug(shared) << header;
} }
if (!object.isObject()) { if (!object.isObject()) {
qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString(); qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString();
return; return;
} }
QScriptValueIterator it(object); QScriptValueIterator it(object);
while (it.hasNext()) { while (it.hasNext()) {
it.next(); it.next();
qCDebug(scriptengine) << it.name() << ":" << it.value().toString(); qCDebug(shared) << it.name() << ":" << it.value().toString();
} }
if (!footer.isEmpty()) { if (!footer.isEmpty()) {
qCDebug(scriptengine) << footer; qCDebug(shared) << footer;
} }
} }
#endif #endif

View 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

View file

@ -122,6 +122,7 @@ public:
gpu::Batch* _batch = nullptr; gpu::Batch* _batch = nullptr;
std::shared_ptr<gpu::Texture> _whiteTexture; std::shared_ptr<gpu::Texture> _whiteTexture;
uint32_t _globalShapeKey { 0 };
bool _enableTexturing { true }; bool _enableTexturing { true };
RenderDetails _details; RenderDetails _details;

View file

@ -6,7 +6,7 @@
var lastSpecStartTime; var lastSpecStartTime;
function ConsoleReporter(options) { function ConsoleReporter(options) {
var startTime = new Date().getTime(); var startTime = new Date().getTime();
var errorCount = 0; var errorCount = 0, pending = [];
this.jasmineStarted = function (obj) { this.jasmineStarted = function (obj) {
print('Jasmine started with ' + obj.totalSpecsDefined + ' tests.'); print('Jasmine started with ' + obj.totalSpecsDefined + ' tests.');
}; };
@ -15,11 +15,14 @@
var endTime = new Date().getTime(); var endTime = new Date().getTime();
print('<hr />'); print('<hr />');
if (errorCount === 0) { if (errorCount === 0) {
print ('<span style="color:green">All tests passed!</span>'); print ('<span style="color:green">All enabled tests passed!</span>');
} else { } else {
print('<span style="color:red">Tests completed with ' + print('<span style="color:red">Tests completed with ' +
errorCount + ' ' + ERROR + '.<span>'); errorCount + ' ' + ERROR + '.<span>');
} }
if (pending.length)
print ('<span style="color:darkorange">disabled: <br />&nbsp;&nbsp;&nbsp;'+
pending.join('<br />&nbsp;&nbsp;&nbsp;')+'</span>');
print('Tests completed in ' + (endTime - startTime) + 'ms.'); print('Tests completed in ' + (endTime - startTime) + 'ms.');
}; };
this.suiteStarted = function(obj) { this.suiteStarted = function(obj) {
@ -32,6 +35,10 @@
lastSpecStartTime = new Date().getTime(); lastSpecStartTime = new Date().getTime();
}; };
this.specDone = function(obj) { this.specDone = function(obj) {
if (obj.status === 'pending') {
pending.push(obj.fullName);
return print('...(pending ' + obj.fullName +')');
}
var specEndTime = new Date().getTime(); var specEndTime = new Date().getTime();
var symbol = obj.status === PASSED ? var symbol = obj.status === PASSED ?
'<span style="color:green">' + CHECKMARK + '</span>' : '<span style="color:green">' + CHECKMARK + '</span>' :
@ -55,7 +62,7 @@
clearTimeout = Script.clearTimeout; clearTimeout = Script.clearTimeout;
clearInterval = Script.clearInterval; clearInterval = Script.clearInterval;
var jasmine = jasmineRequire.core(jasmineRequire); var jasmine = this.jasmine = jasmineRequire.core(jasmineRequire);
var env = jasmine.getEnv(); var env = jasmine.getEnv();

View 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);

View 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);

View file

@ -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,
};

View file

@ -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(''));
},
};
});

View file

@ -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);

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);
},
};
});

View file

@ -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(''));
},
};
});

View file

@ -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);
},
};
});

View file

@ -0,0 +1,9 @@
{
"name": "Example JSON Module",
"last-modified": 1485789862,
"config": {
"title": "My Title",
"width": 800,
"height": 600
}
}

View file

@ -0,0 +1,4 @@
/* eslint-env node */
module.exports = "n/a";
throw new Error('exception on line 2');

View file

@ -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();
}

View 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();

View file

@ -0,0 +1,6 @@
{
"name": "unit_tests",
"devDependencies": {
"jasmine-console-reporter": "^1.2.7"
}
}

View file

@ -15,10 +15,20 @@ describe('Script', function () {
// characterization tests // characterization tests
// initially these are just to capture how the app works currently // initially these are just to capture how the app works currently
var testCases = { var testCases = {
// special relative resolves
'': filename, '': filename,
'.': dirname, '.': dirname,
'..': parentdir, '..': 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': '', 'about:Entities 1': '',
'ftp://host:port/path': '',
'data:text/html;text,foo': '',
'Entities 1': dirname + 'Entities 1', 'Entities 1': dirname + 'Entities 1',
'./file.js': dirname + 'file.js', './file.js': dirname + 'file.js',
'c:/temp/': 'file:///c:/temp/', 'c:/temp/': 'file:///c:/temp/',
@ -31,6 +41,12 @@ describe('Script', function () {
'/~/libraries/utils.js': 'file:///~/libraries/utils.js', '/~/libraries/utils.js': 'file:///~/libraries/utils.js',
'/temp/file.js': 'file:///temp/file.js', '/temp/file.js': 'file:///temp/file.js',
'/~/': 'file:///~/', '/~/': '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 () { describe('resolvePath', function () {
Object.keys(testCases).forEach(function(input) { Object.keys(testCases).forEach(function(input) {
@ -42,7 +58,7 @@ describe('Script', function () {
describe('include', function () { describe('include', function () {
var old_cache_buster; var old_cache_buster;
var cache_buster = '#' + +new Date; var cache_buster = '#' + new Date().getTime().toString(36);
beforeAll(function() { beforeAll(function() {
old_cache_buster = Settings.getValue('cache_buster'); old_cache_buster = Settings.getValue('cache_buster');
Settings.setValue('cache_buster', cache_buster); Settings.setValue('cache_buster', cache_buster);

View file

@ -25,7 +25,7 @@ Column {
"Lightmap:LightingModel:enableLightmap", "Lightmap:LightingModel:enableLightmap",
"Background:LightingModel:enableBackground", "Background:LightingModel:enableBackground",
"ssao:AmbientOcclusion:enabled", "ssao:AmbientOcclusion:enabled",
"Textures:LightingModel:enableMaterialTexturing", "Textures:LightingModel:enableMaterialTexturing"
] ]
CheckBox { CheckBox {
text: modelData.split(":")[0] text: modelData.split(":")[0]
@ -45,6 +45,7 @@ Column {
"Diffuse:LightingModel:enableDiffuse", "Diffuse:LightingModel:enableDiffuse",
"Specular:LightingModel:enableSpecular", "Specular:LightingModel:enableSpecular",
"Albedo:LightingModel:enableAlbedo", "Albedo:LightingModel:enableAlbedo",
"Wireframe:LightingModel:enableWireframe"
] ]
CheckBox { CheckBox {
text: modelData.split(":")[0] text: modelData.split(":")[0]

69
scripts/modules/vec3.js Normal file
View 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)+']');
};

View file

@ -6,26 +6,35 @@
var ANIMATION_FPS = 30; var ANIMATION_FPS = 30;
var ANIMATION_FIRST_FRAME = 1; var ANIMATION_FIRST_FRAME = 1;
var ANIMATION_LAST_FRAME = 10; var ANIMATION_LAST_FRAME = 10;
var RELEASE_KEYS = ['w', 'a', 's', 'd', 'UP', 'LEFT', 'DOWN', 'RIGHT'];
var RELEASE_TIME = 500; // ms var RELEASE_TIME = 500; // ms
var RELEASE_DISTANCE = 0.2; // meters var RELEASE_DISTANCE = 0.2; // meters
var MAX_IK_ERROR = 30; var MAX_IK_ERROR = 30;
var IK_SETTLE_TIME = 250; // ms
var DESKTOP_UI_CHECK_INTERVAL = 100; var DESKTOP_UI_CHECK_INTERVAL = 100;
var DESKTOP_MAX_DISTANCE = 5; var DESKTOP_MAX_DISTANCE = 5;
var SIT_DELAY = 25 var SIT_DELAY = 25;
var MAX_RESET_DISTANCE = 0.5 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.entityID = null;
this.timers = {};
this.animStateHandlerID = null; this.animStateHandlerID = null;
this.interval = null; this.interval = null;
this.sitDownSettlePeriod = null;
this.lastTimeNoDriveKeys = null;
this.preload = function(entityID) { this.preload = function(entityID) {
this.entityID = entityID; this.entityID = entityID;
} }
this.unload = function() { this.unload = function() {
if (Settings.getValue(SETTING_KEY) === this.entityID) { if (Settings.getValue(SETTING_KEY) === this.entityID) {
this.sitUp(); this.standUp();
} }
if (this.interval !== null) { if (this.interval !== null) {
Script.clearInterval(this.interval); Script.clearInterval(this.interval);
@ -96,6 +105,11 @@
print("Someone is already sitting in that chair."); print("Someone is already sitting in that chair.");
return; 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); var previousValue = Settings.getValue(SETTING_KEY);
Settings.setValue(SETTING_KEY, this.entityID); Settings.setValue(SETTING_KEY, this.entityID);
@ -118,20 +132,17 @@
return { headType: 0 }; return { headType: 0 };
}, ["headType"]); }, ["headType"]);
Script.update.connect(this, this.update); Script.update.connect(this, this.update);
Controller.keyPressEvent.connect(this, this.keyPressed); for (var i in OVERRIDEN_DRIVE_KEYS) {
Controller.keyReleaseEvent.connect(this, this.keyReleased); MyAvatar.disableDriveKey(OVERRIDEN_DRIVE_KEYS[i]);
for (var i in RELEASE_KEYS) {
Controller.captureKeyEvents({ text: RELEASE_KEYS[i] });
} }
} }
this.sitUp = function() { this.standUp = function() {
print("Standing up (" + this.entityID + ")");
MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); MyAvatar.removeAnimationStateHandler(this.animStateHandlerID);
Script.update.disconnect(this, this.update); Script.update.disconnect(this, this.update);
Controller.keyPressEvent.disconnect(this, this.keyPressed); for (var i in OVERRIDEN_DRIVE_KEYS) {
Controller.keyReleaseEvent.disconnect(this, this.keyReleased); MyAvatar.enableDriveKey(OVERRIDEN_DRIVE_KEYS[i]);
for (var i in RELEASE_KEYS) {
Controller.releaseKeyEvents({ text: RELEASE_KEYS[i] });
} }
this.setSeatUser(null); this.setSeatUser(null);
@ -156,6 +167,7 @@
} }
} }
// function called by teleport.js if it detects the appropriate userData
this.sit = function () { this.sit = function () {
this.sitDown(); this.sitDown();
} }
@ -207,7 +219,33 @@
var properties = Entities.getEntityProperties(this.entityID); var properties = Entities.getEntityProperties(this.entityID);
var avatarDistance = Vec3.distance(MyAvatar.position, properties.position); var avatarDistance = Vec3.distance(MyAvatar.position, properties.position);
var ikError = MyAvatar.getIKErrorOnLastSolve(); 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); print("IK error: " + ikError + ", distance from chair: " + avatarDistance);
// Move avatar in front of the chair to avoid getting stuck in collision hulls // 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 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)); var position = Vec3.sum(properties.position, Vec3.multiplyQbyV(properties.rotation, offset));
MyAvatar.position = position; 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() { this.canSitDesktop = function() {
var properties = Entities.getEntityProperties(this.entityID, ["position"]); var properties = Entities.getEntityProperties(this.entityID, ["position"]);
var distanceFromSeat = Vec3.distance(MyAvatar.position, properties.position); var distanceFromSeat = Vec3.distance(MyAvatar.position, properties.position);