Merge pull request #9951 from sethalves/tablet-ui

Tablet ui -- merge from upstream
This commit is contained in:
Seth Alves 2017-03-20 08:23:15 -08:00 committed by GitHub
commit acdcb366b4
64 changed files with 2197 additions and 634 deletions

View file

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

View file

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

View file

@ -422,6 +422,9 @@ Menu::Menu() {
}
// Developer > Assets >>>
// Menu item is not currently needed but code should be kept in case it proves useful again at some stage.
//#define WANT_ASSET_MIGRATION
#ifdef WANT_ASSET_MIGRATION
MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets");
auto& atpMigrator = ATPAssetMigrator::getInstance();
atpMigrator.setDialogParent(this);
@ -429,6 +432,7 @@ Menu::Menu() {
addActionToQMenuAndActionHash(assetDeveloperMenu, MenuOption::AssetMigration,
0, &atpMigrator,
SLOT(loadEntityServerFile()));
#endif
// Developer > Avatar >>>
MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar");
@ -561,16 +565,14 @@ Menu::Menu() {
QString("../../hifi/tablet/TabletNetworkingPreferences.qml"), "NetworkingPreferencesDialog");
});
addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches()));
addActionToQMenuAndActionHash(networkMenu, MenuOption::ClearDiskCache, 0,
DependencyManager::get<AssetClient>().data(), SLOT(clearCache()));
addCheckableActionToQMenuAndActionHash(networkMenu,
MenuOption::DisableActivityLogger,
0,
false,
&UserActivityLogger::getInstance(),
SLOT(disable(bool)));
addActionToQMenuAndActionHash(networkMenu, MenuOption::CachesSize, 0,
dialogsManager.data(), SLOT(cachesSizeDialog()));
addActionToQMenuAndActionHash(networkMenu, MenuOption::DiskCacheEditor, 0,
dialogsManager.data(), SLOT(toggleDiskCacheEditor()));
addActionToQMenuAndActionHash(networkMenu, MenuOption::ShowDSConnectTable, 0,
qApp, SLOT(loadDomainConnectionDialog()));

View file

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

View file

@ -119,9 +119,7 @@ MyAvatar::MyAvatar(RigPointer rig) :
using namespace recording;
_skeletonModel->flagAsCauterized();
for (int i = 0; i < MAX_DRIVE_KEYS; i++) {
_driveKeys[i] = 0.0f;
}
clearDriveKeys();
// Necessary to select the correct slot
using SlotType = void(MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool);
@ -230,6 +228,21 @@ MyAvatar::~MyAvatar() {
_lookAtTargetAvatar.reset();
}
void MyAvatar::registerMetaTypes(QScriptEngine* engine) {
QScriptValue value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects);
engine->globalObject().setProperty("MyAvatar", value);
QScriptValue driveKeys = engine->newObject();
auto metaEnum = QMetaEnum::fromType<DriveKeys>();
for (int i = 0; i < MAX_DRIVE_KEYS; ++i) {
driveKeys.setProperty(metaEnum.key(i), metaEnum.value(i));
}
engine->globalObject().setProperty("DriveKeys", driveKeys);
qScriptRegisterMetaType(engine, audioListenModeToScriptValue, audioListenModeFromScriptValue);
qScriptRegisterMetaType(engine, driveKeysToScriptValue, driveKeysFromScriptValue);
}
void MyAvatar::setOrientationVar(const QVariant& newOrientationVar) {
Avatar::setOrientation(quatFromVariant(newOrientationVar));
}
@ -462,7 +475,7 @@ void MyAvatar::simulate(float deltaTime) {
// When there are no step values, we zero out the last step pulse.
// This allows a user to do faster snapping by tapping a control
for (int i = STEP_TRANSLATE_X; !stepAction && i <= STEP_YAW; ++i) {
if (_driveKeys[i] != 0.0f) {
if (getDriveKey((DriveKeys)i) != 0.0f) {
stepAction = true;
}
}
@ -1655,7 +1668,7 @@ bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const {
void MyAvatar::updateOrientation(float deltaTime) {
// Smoothly rotate body with arrow keys
float targetSpeed = _driveKeys[YAW] * _yawSpeed;
float targetSpeed = getDriveKey(YAW) * _yawSpeed;
if (targetSpeed != 0.0f) {
const float ROTATION_RAMP_TIMESCALE = 0.1f;
float blend = deltaTime / ROTATION_RAMP_TIMESCALE;
@ -1684,8 +1697,8 @@ void MyAvatar::updateOrientation(float deltaTime) {
// Comfort Mode: If you press any of the left/right rotation drive keys or input, you'll
// get an instantaneous 15 degree turn. If you keep holding the key down you'll get another
// snap turn every half second.
if (_driveKeys[STEP_YAW] != 0.0f) {
totalBodyYaw += _driveKeys[STEP_YAW];
if (getDriveKey(STEP_YAW) != 0.0f) {
totalBodyYaw += getDriveKey(STEP_YAW);
}
// use head/HMD orientation to turn while flying
@ -1722,7 +1735,7 @@ void MyAvatar::updateOrientation(float deltaTime) {
// update body orientation by movement inputs
setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f))));
getHead()->setBasePitch(getHead()->getBasePitch() + _driveKeys[PITCH] * _pitchSpeed * deltaTime);
getHead()->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime);
if (qApp->isHMDMode()) {
glm::quat orientation = glm::quat_cast(getSensorToWorldMatrix()) * getHMDSensorOrientation();
@ -1756,14 +1769,14 @@ void MyAvatar::updateActionMotor(float deltaTime) {
}
// compute action input
glm::vec3 front = (_driveKeys[TRANSLATE_Z]) * IDENTITY_FRONT;
glm::vec3 right = (_driveKeys[TRANSLATE_X]) * IDENTITY_RIGHT;
glm::vec3 front = (getDriveKey(TRANSLATE_Z)) * IDENTITY_FRONT;
glm::vec3 right = (getDriveKey(TRANSLATE_X)) * IDENTITY_RIGHT;
glm::vec3 direction = front + right;
CharacterController::State state = _characterController.getState();
if (state == CharacterController::State::Hover) {
// we're flying --> support vertical motion
glm::vec3 up = (_driveKeys[TRANSLATE_Y]) * IDENTITY_UP;
glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP;
direction += up;
}
@ -1802,7 +1815,7 @@ void MyAvatar::updateActionMotor(float deltaTime) {
_actionMotorVelocity = MAX_WALKING_SPEED * direction;
}
float boomChange = _driveKeys[ZOOM];
float boomChange = getDriveKey(ZOOM);
_boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange;
_boomLength = glm::clamp<float>(_boomLength, ZOOM_MIN, ZOOM_MAX);
}
@ -1833,11 +1846,11 @@ void MyAvatar::updatePosition(float deltaTime) {
}
// capture the head rotation, in sensor space, when the user first indicates they would like to move/fly.
if (!_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) > 0.1f || fabs(_driveKeys[TRANSLATE_X]) > 0.1f)) {
if (!_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) > 0.1f || fabs(getDriveKey(TRANSLATE_X)) > 0.1f)) {
_hoverReferenceCameraFacingIsCaptured = true;
// transform the camera facing vector into sensor space.
_hoverReferenceCameraFacing = transformVectorFast(glm::inverse(_sensorToWorldMatrix), getHead()->getCameraOrientation() * Vectors::UNIT_Z);
} else if (_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) <= 0.1f && fabs(_driveKeys[TRANSLATE_X]) <= 0.1f)) {
} else if (_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) <= 0.1f && fabs(getDriveKey(TRANSLATE_X)) <= 0.1f)) {
_hoverReferenceCameraFacingIsCaptured = false;
}
}
@ -2093,17 +2106,61 @@ bool MyAvatar::getCharacterControllerEnabled() {
}
void MyAvatar::clearDriveKeys() {
for (int i = 0; i < MAX_DRIVE_KEYS; ++i) {
_driveKeys[i] = 0.0f;
_driveKeys.fill(0.0f);
}
void MyAvatar::setDriveKey(DriveKeys key, float val) {
try {
_driveKeys.at(key) = val;
} catch (const std::exception&) {
qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds";
}
}
float MyAvatar::getDriveKey(DriveKeys key) const {
return isDriveKeyDisabled(key) ? 0.0f : getRawDriveKey(key);
}
float MyAvatar::getRawDriveKey(DriveKeys key) const {
try {
return _driveKeys.at(key);
} catch (const std::exception&) {
qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds";
return 0.0f;
}
}
void MyAvatar::relayDriveKeysToCharacterController() {
if (_driveKeys[TRANSLATE_Y] > 0.0f) {
if (getDriveKey(TRANSLATE_Y) > 0.0f) {
_characterController.jump();
}
}
void MyAvatar::disableDriveKey(DriveKeys key) {
try {
_disabledDriveKeys.set(key);
} catch (const std::exception&) {
qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds";
}
}
void MyAvatar::enableDriveKey(DriveKeys key) {
try {
_disabledDriveKeys.reset(key);
} catch (const std::exception&) {
qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds";
}
}
bool MyAvatar::isDriveKeyDisabled(DriveKeys key) const {
try {
return _disabledDriveKeys.test(key);
} catch (const std::exception&) {
qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds";
return true;
}
}
glm::vec3 MyAvatar::getWorldBodyPosition() const {
return transformPoint(_sensorToWorldMatrix, extractTranslation(_bodySensorMatrix));
}
@ -2189,7 +2246,15 @@ QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioList
}
void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode) {
audioListenerMode = (AudioListenerMode)object.toUInt16();
audioListenerMode = static_cast<AudioListenerMode>(object.toUInt16());
}
QScriptValue driveKeysToScriptValue(QScriptEngine* engine, const MyAvatar::DriveKeys& driveKeys) {
return driveKeys;
}
void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& driveKeys) {
driveKeys = static_cast<MyAvatar::DriveKeys>(object.toUInt16());
}
@ -2382,7 +2447,7 @@ bool MyAvatar::didTeleport() {
}
bool MyAvatar::hasDriveInput() const {
return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Y]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f;
return fabsf(getDriveKey(TRANSLATE_X)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Y)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Z)) > 0.0f;
}
void MyAvatar::setAway(bool value) {
@ -2498,7 +2563,7 @@ bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& o
return false;
}
setPosition(position);
slamPosition(position);
setOrientation(orientation);
_rig->setMaxHipsOffsetLength(0.05f);

View file

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

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

View file

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

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),
_loopbackOutputDevice(NULL),
_inputRingBuffer(0),
_localInjectorsStream(0),
_localInjectorsStream(0, 1),
_receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES),
_isStereoInput(false),
_outputStarveDetectionStartTimeMsec(0),

View file

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

View file

@ -14,6 +14,7 @@
#include <QByteArray>
#include <QtConcurrent/QtConcurrentRun>
#include <glm/gtx/transform.hpp>
#include "ModelScriptingInterface.h"
#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic push
@ -53,6 +54,8 @@
#include "PhysicalEntitySimulation.h"
gpu::PipelinePointer RenderablePolyVoxEntityItem::_pipeline = nullptr;
gpu::PipelinePointer RenderablePolyVoxEntityItem::_wireframePipeline = nullptr;
const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5;
@ -73,7 +76,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5;
_meshDirty
In RenderablePolyVoxEntityItem::render, these flags are checked and changes are propagated along the chain.
decompressVolumeData() is called to decompress _voxelData into _volData. getMesh() is called to invoke the
decompressVolumeData() is called to decompress _voxelData into _volData. recomputeMesh() is called to invoke the
polyVox surface extractor to create _mesh (as well as set Simulation _dirtyFlags). Because Simulation::DIRTY_SHAPE
is set, isReadyToComputeShape() gets called and _shape is created either from _volData or _shape, depending on
the surface style.
@ -81,7 +84,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5;
When a script changes _volData, compressVolumeDataAndSendEditPacket is called to update _voxelData and to
send a packet to the entity-server.
decompressVolumeData, getMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive
decompressVolumeData, recomputeMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive
to run on a thread that has other things to do. These use QtConcurrent::run to spawn a thread. As each thread
finishes, it adjusts the dirty flags so that the next call to render() will kick off the next step.
@ -682,7 +685,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
if (voxelDataDirty) {
decompressVolumeData();
} else if (volDataDirty) {
getMesh();
recomputeMesh();
}
model::MeshPointer mesh;
@ -696,7 +699,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
!mesh->getIndexBuffer()._buffer) {
return;
}
if (!_pipeline) {
gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert));
gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag));
@ -715,6 +718,13 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
state->setDepthTest(true, true, gpu::LESS_EQUAL);
_pipeline = gpu::Pipeline::create(program, state);
auto wireframeState = std::make_shared<gpu::State>();
wireframeState->setCullMode(gpu::State::CULL_BACK);
wireframeState->setDepthTest(true, true, gpu::LESS_EQUAL);
wireframeState->setFillMode(gpu::State::FILL_LINE);
_wireframePipeline = gpu::Pipeline::create(program, wireframeState);
}
if (!_vertexFormat) {
@ -725,7 +735,11 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
}
gpu::Batch& batch = *args->_batch;
batch.setPipeline(_pipeline);
// Pick correct Pipeline
bool wireframe = (render::ShapeKey(args->_globalShapeKey).isWireframe());
auto pipeline = (wireframe ? _wireframePipeline : _pipeline);
batch.setPipeline(pipeline);
Transform transform(voxelToWorldMatrix());
batch.setModelTransform(transform);
@ -762,7 +776,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
batch.setResourceTexture(2, DependencyManager::get<TextureCache>()->getWhiteTexture());
}
int voxelVolumeSizeLocation = _pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize");
int voxelVolumeSizeLocation = pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize");
batch._glUniform3f(voxelVolumeSizeLocation, voxelVolumeSize.x, voxelVolumeSize.y, voxelVolumeSize.z);
batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)mesh->getNumIndices(), 0);
@ -1199,7 +1213,7 @@ void RenderablePolyVoxEntityItem::copyUpperEdgesFromNeighbors() {
}
}
void RenderablePolyVoxEntityItem::getMesh() {
void RenderablePolyVoxEntityItem::recomputeMesh() {
// use _volData to make a renderable mesh
PolyVoxSurfaceStyle voxelSurfaceStyle;
withReadLock([&] {
@ -1269,12 +1283,20 @@ void RenderablePolyVoxEntityItem::getMesh() {
vertexBufferPtr->getSize() ,
sizeof(PolyVox::PositionMaterialNormal),
gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW)));
std::vector<model::Mesh::Part> parts;
parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex
(model::Index)vecIndices.size(), // numIndices
(model::Index)0, // baseVertex
model::Mesh::TRIANGLES)); // topology
mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part),
(gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL));
entity->setMesh(mesh);
});
}
void RenderablePolyVoxEntityItem::setMesh(model::MeshPointer mesh) {
// this catches the payload from getMesh
// this catches the payload from recomputeMesh
bool neighborsNeedUpdate;
withWriteLock([&] {
if (!_collisionless) {
@ -1531,7 +1553,6 @@ std::shared_ptr<RenderablePolyVoxEntityItem> RenderablePolyVoxEntityItem::getZPN
return std::dynamic_pointer_cast<RenderablePolyVoxEntityItem>(_zPNeighbor.lock());
}
void RenderablePolyVoxEntityItem::bonkNeighbors() {
// flag neighbors to the negative of this entity as needing to rebake their meshes.
cacheNeighbors();
@ -1551,7 +1572,6 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() {
}
}
void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) {
EntityItem::locationChanged(tellPhysics);
if (!_pipeline || !render::Item::isValidID(_myItem)) {
@ -1563,3 +1583,17 @@ void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) {
scene->enqueuePendingChanges(pendingChanges);
}
bool RenderablePolyVoxEntityItem::getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) const {
bool success = false;
MeshProxy* meshProxy = nullptr;
model::MeshPointer mesh = nullptr;
withReadLock([&] {
if (_meshInitialized) {
success = true;
meshProxy = new MeshProxy(_mesh);
}
});
result = meshToScriptValue(engine, meshProxy);
return success;
}

View file

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

View file

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

View file

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

View file

@ -10,6 +10,9 @@
//
#include "EntityScriptingInterface.h"
#include <QFutureWatcher>
#include <QtConcurrent/QtConcurrentRun>
#include "EntityItemID.h"
#include <VariantMapToScriptValue.h>
#include <SharedUtil.h>
@ -680,6 +683,118 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) {
return client->reloadServerScript(entityID);
}
bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue handler) {
using LocalScriptStatusRequest = QFutureWatcher<QVariant>;
LocalScriptStatusRequest* request = new LocalScriptStatusRequest;
QObject::connect(request, &LocalScriptStatusRequest::finished, _engine, [=]() mutable {
auto details = request->result().toMap();
QScriptValue err, result;
if (details.contains("isError")) {
if (!details.contains("message")) {
details["message"] = details["errorInfo"];
}
err = _engine->makeError(_engine->toScriptValue(details));
} else {
details["success"] = true;
result = _engine->toScriptValue(details);
}
callScopedHandlerObject(handler, err, result);
request->deleteLater();
});
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
entityScriptingInterface->withEntitiesScriptEngine([&](EntitiesScriptEngineProvider* entitiesScriptEngine) {
if (entitiesScriptEngine) {
request->setFuture(entitiesScriptEngine->getLocalEntityScriptDetails(entityID));
}
});
if (!request->isStarted()) {
request->deleteLater();
callScopedHandlerObject(handler, _engine->makeError("Entities Scripting Provider unavailable", "InternalError"), QScriptValue());
return false;
}
return true;
}
bool EntityPropertyMetadataRequest::serverScripts(EntityItemID entityID, QScriptValue handler) {
auto client = DependencyManager::get<EntityScriptClient>();
auto request = client->createScriptStatusRequest(entityID);
QPointer<BaseScriptEngine> engine = _engine;
QObject::connect(request, &GetScriptStatusRequest::finished, _engine, [=](GetScriptStatusRequest* request) mutable {
auto engine = _engine;
if (!engine) {
qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID;
return;
}
QVariantMap details;
details["success"] = request->getResponseReceived();
details["isRunning"] = request->getIsRunning();
details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower();
details["errorInfo"] = request->getErrorInfo();
QScriptValue err, result;
if (!details["success"].toBool()) {
if (!details.contains("message") && details.contains("errorInfo")) {
details["message"] = details["errorInfo"];
}
if (details["message"].toString().isEmpty()) {
details["message"] = "entity server script details not found";
}
err = engine->makeError(engine->toScriptValue(details));
} else {
result = engine->toScriptValue(details);
}
callScopedHandlerObject(handler, err, result);
request->deleteLater();
});
request->start();
return true;
}
bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) {
auto name = property.toString();
auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName);
QPointer<BaseScriptEngine> engine = dynamic_cast<BaseScriptEngine*>(handler.engine());
if (!engine) {
qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name;
return false;
}
#ifdef DEBUG_ENGINE_STATE
connect(engine, &QObject::destroyed, this, [=]() {
qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine");
});
#endif
if (!handler.property("callback").isFunction()) {
qDebug() << "!handler.callback.isFunction" << engine;
engine->raiseException(engine->makeError("callback is not a function", "TypeError"));
return false;
}
// NOTE: this approach is a work-in-progress and for now just meant to work 100% correctly and provide
// some initial structure for organizing metadata adapters around.
// The extra layer of indirection is *essential* because in real world conditions errors are often introduced
// by accident and sometimes without exact memory of "what just changed."
// Here the scripter only needs to know an entityID and a property name -- which means all scripters can
// level this method when stuck in dead-end scenarios or to learn more about "magic" Entity properties
// like .script that work in terms of side-effects.
// This is an async callback pattern -- so if needed C++ can easily throttle or restrict queries later.
EntityPropertyMetadataRequest request(engine);
if (name == "script") {
return request.script(entityID, handler);
} else if (name == "serverScripts") {
return request.serverScripts(entityID, handler);
} else {
engine->raiseException(engine->makeError("metadata for property " + name + " is not yet queryable"));
engine->maybeEmitUncaughtException(__FUNCTION__);
return false;
}
}
bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) {
auto client = DependencyManager::get<EntityScriptClient>();
auto request = client->createScriptStatusRequest(entityID);
@ -815,8 +930,7 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra
}
}
bool EntityScriptingInterface::setVoxels(QUuid entityID,
std::function<bool(PolyVoxEntityItem&)> actor) {
bool EntityScriptingInterface::polyVoxWorker(QUuid entityID, std::function<bool(PolyVoxEntityItem&)> actor) {
PROFILE_RANGE(script_entities, __FUNCTION__);
if (!_entityTree) {
@ -882,11 +996,9 @@ bool EntityScriptingInterface::setPoints(QUuid entityID, std::function<bool(Line
return success;
}
bool EntityScriptingInterface::setVoxelSphere(QUuid entityID, const glm::vec3& center, float radius, int value) {
PROFILE_RANGE(script_entities, __FUNCTION__);
return setVoxels(entityID, [center, radius, value](PolyVoxEntityItem& polyVoxEntity) {
return polyVoxWorker(entityID, [center, radius, value](PolyVoxEntityItem& polyVoxEntity) {
return polyVoxEntity.setSphere(center, radius, value);
});
}
@ -896,7 +1008,7 @@ bool EntityScriptingInterface::setVoxelCapsule(QUuid entityID,
float radius, int value) {
PROFILE_RANGE(script_entities, __FUNCTION__);
return setVoxels(entityID, [start, end, radius, value](PolyVoxEntityItem& polyVoxEntity) {
return polyVoxWorker(entityID, [start, end, radius, value](PolyVoxEntityItem& polyVoxEntity) {
return polyVoxEntity.setCapsule(start, end, radius, value);
});
}
@ -904,7 +1016,7 @@ bool EntityScriptingInterface::setVoxelCapsule(QUuid entityID,
bool EntityScriptingInterface::setVoxel(QUuid entityID, const glm::vec3& position, int value) {
PROFILE_RANGE(script_entities, __FUNCTION__);
return setVoxels(entityID, [position, value](PolyVoxEntityItem& polyVoxEntity) {
return polyVoxWorker(entityID, [position, value](PolyVoxEntityItem& polyVoxEntity) {
return polyVoxEntity.setVoxelInVolume(position, value);
});
}
@ -912,7 +1024,7 @@ bool EntityScriptingInterface::setVoxel(QUuid entityID, const glm::vec3& positio
bool EntityScriptingInterface::setAllVoxels(QUuid entityID, int value) {
PROFILE_RANGE(script_entities, __FUNCTION__);
return setVoxels(entityID, [value](PolyVoxEntityItem& polyVoxEntity) {
return polyVoxWorker(entityID, [value](PolyVoxEntityItem& polyVoxEntity) {
return polyVoxEntity.setAll(value);
});
}
@ -921,11 +1033,23 @@ bool EntityScriptingInterface::setVoxelsInCuboid(QUuid entityID, const glm::vec3
const glm::vec3& cuboidSize, int value) {
PROFILE_RANGE(script_entities, __FUNCTION__);
return setVoxels(entityID, [lowPosition, cuboidSize, value](PolyVoxEntityItem& polyVoxEntity) {
return polyVoxWorker(entityID, [lowPosition, cuboidSize, value](PolyVoxEntityItem& polyVoxEntity) {
return polyVoxEntity.setCuboid(lowPosition, cuboidSize, value);
});
}
void EntityScriptingInterface::voxelsToMesh(QUuid entityID, QScriptValue callback) {
PROFILE_RANGE(script_entities, __FUNCTION__);
polyVoxWorker(entityID, [callback](PolyVoxEntityItem& polyVoxEntity) mutable {
QScriptValue mesh;
polyVoxEntity.getMeshAsScriptValue(callback.engine(), mesh);
QScriptValueList args { mesh };
callback.call(QScriptValue(), args);
return true;
});
}
bool EntityScriptingInterface::setAllPoints(QUuid entityID, const QVector<glm::vec3>& points) {
PROFILE_RANGE(script_entities, __FUNCTION__);

View file

@ -34,7 +34,23 @@
#include "EntitiesScriptEngineProvider.h"
#include "EntityItemProperties.h"
#include "BaseScriptEngine.h"
class EntityTree;
class MeshProxy;
// helper factory to compose standardized, async metadata queries for "magic" Entity properties
// like .script and .serverScripts. This is used for automated testing of core scripting features
// as well as to provide early adopters a self-discoverable, consistent way to diagnose common
// problems with their own Entity scripts.
class EntityPropertyMetadataRequest {
public:
EntityPropertyMetadataRequest(BaseScriptEngine* engine) : _engine(engine) {};
bool script(EntityItemID entityID, QScriptValue handler);
bool serverScripts(EntityItemID entityID, QScriptValue handler);
private:
QPointer<BaseScriptEngine> _engine;
};
class RayToEntityIntersectionResult {
public:
@ -67,6 +83,7 @@ class EntityScriptingInterface : public OctreeScriptingInterface, public Depende
Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier)
Q_PROPERTY(QUuid keyboardFocusEntity READ getKeyboardFocusEntity WRITE setKeyboardFocusEntity)
friend EntityPropertyMetadataRequest;
public:
EntityScriptingInterface(bool bidOnSimulationOwnership);
@ -211,6 +228,26 @@ public slots:
Q_INVOKABLE RayToEntityIntersectionResult findRayIntersectionBlocking(const PickRay& ray, bool precisionPicking = false, const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue());
Q_INVOKABLE bool reloadServerScripts(QUuid entityID);
/**jsdoc
* Query additional metadata for "magic" Entity properties like `script` and `serverScripts`.
*
* @function Entities.queryPropertyMetadata
* @param {EntityID} entityID The ID of the entity.
* @param {string} property The name of the property extended metadata is wanted for.
* @param {ResultCallback} callback Executes callback(err, result) with the query results.
*/
/**jsdoc
* Query additional metadata for "magic" Entity properties like `script` and `serverScripts`.
*
* @function Entities.queryPropertyMetadata
* @param {EntityID} entityID The ID of the entity.
* @param {string} property The name of the property extended metadata is wanted for.
* @param {Object} thisObject The scoping "this" context that callback will be executed within.
* @param {ResultCallback} callbackOrMethodName Executes thisObject[callbackOrMethodName](err, result) with the query results.
*/
Q_INVOKABLE bool queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue());
Q_INVOKABLE bool getServerScriptStatus(QUuid entityID, QScriptValue callback);
Q_INVOKABLE void setLightsArePickable(bool value);
@ -229,6 +266,7 @@ public slots:
Q_INVOKABLE bool setAllVoxels(QUuid entityID, int value);
Q_INVOKABLE bool setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition,
const glm::vec3& cuboidSize, int value);
Q_INVOKABLE void voxelsToMesh(QUuid entityID, QScriptValue callback);
Q_INVOKABLE bool setAllPoints(QUuid entityID, const QVector<glm::vec3>& points);
Q_INVOKABLE bool appendPoint(QUuid entityID, const glm::vec3& point);
@ -323,9 +361,14 @@ signals:
void webEventReceived(const EntityItemID& entityItemID, const QVariant& message);
protected:
void withEntitiesScriptEngine(std::function<void(EntitiesScriptEngineProvider*)> function) {
std::lock_guard<std::recursive_mutex> lock(_entitiesScriptEngineLock);
function(_entitiesScriptEngine);
};
private:
bool actionWorker(const QUuid& entityID, std::function<bool(EntitySimulationPointer, EntityItemPointer)> actor);
bool setVoxels(QUuid entityID, std::function<bool(PolyVoxEntityItem&)> actor);
bool polyVoxWorker(QUuid entityID, std::function<bool(PolyVoxEntityItem&)> actor);
bool setPoints(QUuid entityID, std::function<bool(LineEntityItem&)> actor);
void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties);

View file

@ -242,3 +242,7 @@ const QByteArray PolyVoxEntityItem::getVoxelData() const {
});
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() {};
void setVoxelDataDirty(bool value) { withWriteLock([&] { _voxelDataDirty = value; }); }
virtual void getMesh() {}; // recompute mesh
virtual void recomputeMesh() {};
virtual bool getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) const;
protected:
glm::vec3 _voxelVolumeSize; // this is always 3 bytes

View file

@ -54,7 +54,8 @@ template<class T> QVariant readBinaryArray(QDataStream& in, int& position) {
in.readRawData(compressed.data() + sizeof(quint32), compressedLength);
position += compressedLength;
arrayData = qUncompress(compressed);
if (arrayData.isEmpty() || arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt
if (arrayData.isEmpty() ||
(unsigned int)arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt
throw QString("corrupt fbx file");
}
} else {

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

View file

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

View file

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

View file

@ -259,8 +259,18 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont
// Setup lighting model for all items;
batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer());
renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn);
// From the lighting model define a global shapKey ORED with individiual keys
ShapeKey::Builder keyBuilder;
if (lightingModel->isWireframeEnabled()) {
keyBuilder.withWireframe();
}
ShapeKey globalKey = keyBuilder.build();
args->_globalShapeKey = globalKey._flags.to_ulong();
renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey);
args->_batch = nullptr;
args->_globalShapeKey = 0;
});
config->setNumDrawn((int)inItems.size());
@ -295,12 +305,21 @@ void DrawStateSortDeferred::run(const SceneContextPointer& sceneContext, const R
// Setup lighting model for all items;
batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer());
// From the lighting model define a global shapKey ORED with individiual keys
ShapeKey::Builder keyBuilder;
if (lightingModel->isWireframeEnabled()) {
keyBuilder.withWireframe();
}
ShapeKey globalKey = keyBuilder.build();
args->_globalShapeKey = globalKey._flags.to_ulong();
if (_stateSort) {
renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn);
renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey);
} else {
renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn);
renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey);
}
args->_batch = nullptr;
args->_globalShapeKey = 0;
});
config->setNumDrawn((int)inItems.size());

View file

@ -307,7 +307,7 @@ void initForwardPipelines(render::ShapePlumber& plumber) {
void addPlumberPipeline(ShapePlumber& plumber,
const ShapeKey& key, const gpu::ShaderPointer& vertex, const gpu::ShaderPointer& pixel) {
// These key-values' pipelines are added by this functor in addition to the key passed
assert(!key.isWireFrame());
assert(!key.isWireframe());
assert(!key.isDepthBiased());
assert(key.isCullFace());

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

View file

@ -17,8 +17,8 @@
namespace render {
void renderItems(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems, int maxDrawnItems = -1);
void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1);
void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1);
void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey());
void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey());
class DrawLightConfig : public Job::Config {
Q_OBJECT

View file

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

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/QRegularExpression>
#include <QtCore/QFuture>
#include <QtConcurrent/QtConcurrentRun>
#include <QtWidgets/QMainWindow>
#include <QtWidgets/QApplication>
@ -65,18 +68,25 @@
#include "RecordingScriptingInterface.h"
#include "ScriptEngines.h"
#include "TabletScriptingInterface.h"
#include "ModelScriptingInterface.h"
#include <Profile.h>
#include "MIDIEvent.h"
const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
"com.highfidelity.experimental.enableExtendedJSExceptions"
};
static const int MAX_MODULE_ID_LENGTH { 4096 };
static const int MAX_DEBUG_VALUE_LENGTH { 80 };
static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =
QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;
static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable };
static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration };
static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };
Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)
@ -84,7 +94,7 @@ int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature
Q_LOGGING_CATEGORY(scriptengineScript, "hifi.scriptengine.script")
static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){
static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) {
QString message = "";
for (int i = 0; i < context->argumentCount(); i++) {
if (i > 0) {
@ -141,7 +151,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID)
}
QString ScriptEngine::logException(const QScriptValue& exception) {
auto message = formatException(exception);
auto message = formatException(exception, _enableExtendedJSExceptions.get());
scriptErrorMessage(message);
return message;
}
@ -333,7 +343,7 @@ void ScriptEngine::runInThread() {
// The thread interface cannot live on itself, and we want to move this into the thread, so
// the thread cannot have this as a parent.
QThread* workerThread = new QThread();
workerThread->setObjectName(QString("Script Thread:") + getFilename());
workerThread->setObjectName(QString("js:") + getFilename().replace("about:",""));
moveToThread(workerThread);
// NOTE: If you connect any essential signals for proper shutdown or cleanup of
@ -532,6 +542,40 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) {
return prototype;
}
void ScriptEngine::resetModuleCache(bool deleteScriptCache) {
if (QThread::currentThread() != thread()) {
executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); });
return;
}
auto jsRequire = globalObject().property("Script").property("require");
auto cache = jsRequire.property("cache");
auto cacheMeta = jsRequire.data();
if (deleteScriptCache) {
QScriptValueIterator it(cache);
while (it.hasNext()) {
it.next();
if (it.flags() & QScriptValue::SkipInEnumeration) {
continue;
}
qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require";
cacheMeta.setProperty(it.name(), true);
}
}
cache = newObject();
if (!cacheMeta.isObject()) {
cacheMeta = newObject();
cacheMeta.setProperty("id", "Script.require.cacheMeta");
cacheMeta.setProperty("type", "cacheMeta");
jsRequire.setData(cacheMeta);
}
cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration);
#if DEBUG_JS_MODULES
cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS);
#endif
jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS);
}
void ScriptEngine::init() {
if (_isInitialized) {
return; // only initialize once
@ -585,6 +629,15 @@ void ScriptEngine::init() {
registerGlobalObject("Script", this);
{
// set up Script.require.resolve and Script.require.cache
auto Script = globalObject().property("Script");
auto require = Script.property("require");
auto resolve = Script.property("_requireResolve");
require.setProperty("resolve", resolve, READONLY_PROP_FLAGS);
resetModuleCache();
}
registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());
registerGlobalObject("Entities", entityScriptingInterface.data());
registerGlobalObject("Quat", &_quatLibrary);
@ -594,7 +647,7 @@ void ScriptEngine::init() {
registerGlobalObject("Messages", DependencyManager::get<MessagesClient>().data());
registerGlobalObject("File", new FileScriptingInterface(this));
qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue);
qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue);
@ -612,6 +665,10 @@ void ScriptEngine::init() {
registerGlobalObject("Resources", DependencyManager::get<ResourceScriptingInterface>().data());
registerGlobalObject("DebugDraw", &DebugDraw::getInstance());
registerGlobalObject("Model", new ModelScriptingInterface(this));
qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue);
qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue);
}
void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) {
@ -853,6 +910,11 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString&
handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().
}
// this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE
QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
return BaseScriptEngine::evaluateInClosure(closure, program);
}
QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
return QScriptValue(); // bail early
@ -875,29 +937,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi
// Check syntax
auto syntaxError = lintScript(sourceCode, fileName);
if (syntaxError.isError()) {
if (isEvaluating()) {
currentContext()->throwValue(syntaxError);
} else {
if (!isEvaluating()) {
syntaxError.setProperty("detail", "evaluate");
emit unhandledException(syntaxError);
}
raiseException(syntaxError);
maybeEmitUncaughtException("lint");
return syntaxError;
}
QScriptProgram program { sourceCode, fileName, lineNumber };
if (program.isNull()) {
// can this happen?
auto err = makeError("could not create QScriptProgram for " + fileName);
emit unhandledException(err);
raiseException(err);
maybeEmitUncaughtException("compile");
return err;
}
QScriptValue result;
{
result = BaseScriptEngine::evaluate(program);
if (!isEvaluating() && hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(__FUNCTION__));
clearExceptions();
}
maybeEmitUncaughtException("evaluate");
}
return result;
}
@ -920,10 +979,7 @@ void ScriptEngine::run() {
{
evaluate(_scriptContents, _fileNameString);
if (!isEvaluating() && hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(__FUNCTION__));
clearExceptions();
}
maybeEmitUncaughtException(__FUNCTION__);
}
#ifdef _WIN32
// VS13 does not sleep_until unless it uses the system_clock, see:
@ -1294,11 +1350,361 @@ void ScriptEngine::print(const QString& message) {
emit printedMessage(message);
}
// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js)
QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return QString();
}
QUrl defaultScriptsLoc = defaultScriptsLocation();
QUrl url(moduleId);
auto displayId = moduleId;
if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) {
displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";
}
auto message = QString("Cannot find module '%1' (%2)").arg(displayId);
auto throwResolveError = [&](const QScriptValue& error) -> QString {
raiseException(error);
maybeEmitUncaughtException("require.resolve");
return QString();
};
// de-fuzz the input a little by restricting to rational sizes
auto idLength = url.toString().length();
if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) {
auto details = QString("rejecting invalid module id size (%1 chars [1,%2])")
.arg(idLength).arg(MAX_MODULE_ID_LENGTH);
return throwResolveError(makeError(message.arg(details), "RangeError"));
}
// this regex matches: absolute, dotted or path-like URLs
// (ie: the kind of stuff ScriptEngine::resolvePath already handles)
QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)");
// this is for module.require (which is a bound version of require that's always relative to the module path)
if (!relativeTo.isEmpty()) {
url = QUrl(relativeTo).resolved(moduleId);
url = resolvePath(url.toString());
} else if (qualified.match(moduleId).hasMatch()) {
url = resolvePath(moduleId);
} else {
// check if the moduleId refers to a "system" module
QString systemPath = defaultScriptsLoc.path();
QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId);
url = defaultScriptsLoc;
url.setPath(systemModulePath);
if (!QFileInfo(url.toLocalFile()).isFile()) {
if (!moduleId.contains("./")) {
// the user might be trying to refer to a relative file without anchoring it
// let's do them a favor and test for that case -- offering specific advice if detected
auto unanchoredUrl = resolvePath("./" + moduleId);
if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) {
auto msg = QString("relative module ids must be anchored; use './%1' instead")
.arg(moduleId);
return throwResolveError(makeError(message.arg(msg)));
}
}
return throwResolveError(makeError(message.arg("system module not found")));
}
}
if (url.isRelative()) {
return throwResolveError(makeError(message.arg("could not resolve module id")));
}
// if it looks like a local file, verify that it's an allowed path and really a file
if (url.isLocalFile()) {
QFileInfo file(url.toLocalFile());
QUrl canonical = url;
if (file.exists()) {
canonical.setPath(file.canonicalFilePath());
}
bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile();
if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) {
return throwResolveError(makeError(message.arg(
QString("path '%1' outside of origin script '%2' '%3'")
.arg(PathUtils::stripFilename(url))
.arg(PathUtils::stripFilename(currentSandboxURL))
.arg(canonical.toString())
)));
}
if (!file.exists()) {
return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile())));
}
if (!file.isFile()) {
return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile())));
}
}
maybeEmitUncaughtException(__FUNCTION__);
return url.toString();
}
// retrieves the current parent module from the JS scope chain
QScriptValue ScriptEngine::currentModule() {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
auto jsRequire = globalObject().property("Script").property("require");
auto cache = jsRequire.property("cache");
auto candidate = QScriptValue();
for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) {
QScriptContextInfo contextInfo { c };
candidate = cache.property(contextInfo.fileName());
}
if (!candidate.isObject()) {
return QScriptValue();
}
return candidate;
}
// replaces or adds "module" to "parent.children[]" array
// (for consistency with Node.js and userscript cache invalidation without "cache busters")
bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) {
auto children = parent.property("children");
if (children.isArray()) {
auto key = module.property("id");
auto length = children.property("length").toInt32();
for (int i = 0; i < length; i++) {
if (children.property(i).property("id").strictlyEquals(key)) {
qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module";
children.setProperty(i, module);
return true;
}
}
qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module";
children.setProperty(length, module);
return true;
} else if (parent.isValid()) {
qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString();
}
return false;
}
// creates a new JS "module" Object with default metadata properties
QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) {
auto closure = newObject();
auto exports = newObject();
auto module = newObject();
qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString();
closure.setProperty("module", module, READONLY_PROP_FLAGS);
// note: this becomes the "exports" free variable, so should not be set read only
closure.setProperty("exports", exports);
// make the closure available to module instantiation
module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS);
// for consistency with Node.js Module
module.setProperty("id", modulePath, READONLY_PROP_FLAGS);
module.setProperty("filename", modulePath, READONLY_PROP_FLAGS);
module.setProperty("exports", exports); // not readonly
module.setProperty("loaded", false, READONLY_PROP_FLAGS);
module.setProperty("parent", parent, READONLY_PROP_FLAGS);
module.setProperty("children", newArray(), READONLY_PROP_FLAGS);
// module.require is a bound version of require that always resolves relative to that module's path
auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)");
module.setProperty("require", boundRequire, READONLY_PROP_FLAGS);
return module;
}
// synchronously fetch a module's source code using BatchLoader
QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) {
using UrlMap = QMap<QUrl, QString>;
auto scriptCache = DependencyManager::get<ScriptCache>();
QVariantMap req;
qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread();
auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) {
auto url = modulePath;
auto status = _status[url];
auto contents = data[url];
qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread();
if (isStopping()) {
req["status"] = "Stopped";
req["success"] = false;
} else {
req["url"] = url;
req["status"] = status;
req["success"] = ScriptCache::isSuccessStatus(status);
req["contents"] = contents;
}
};
if (forceDownload) {
qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath;
scriptCache->deleteScript(modulePath);
}
BatchLoader* loader = new BatchLoader(QList<QUrl>({ modulePath }));
connect(loader, &BatchLoader::finished, this, onload);
connect(this, &QObject::destroyed, loader, &QObject::deleteLater);
// fail faster? (since require() blocks the engine thread while resolving dependencies)
const int MAX_RETRIES = 1;
loader->start(MAX_RETRIES);
if (!loader->isFinished()) {
QTimer monitor;
QEventLoop loop;
QObject::connect(loader, &BatchLoader::finished, this, [this, &monitor, &loop]{
monitor.stop();
loop.quit();
});
// this helps detect the case where stop() is invoked during the download
// but not seen in time to abort processing in onload()...
connect(&monitor, &QTimer::timeout, this, [this, &loop, &loader]{
if (isStopping()) {
loop.exit(-1);
}
});
monitor.start(500);
loop.exec();
}
loader->deleteLater();
return req;
}
// evaluate a pending module object using the fetched source code
QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) {
QScriptValue result;
auto modulePath = module.property("filename").toString();
auto closure = module.property("__closure__");
qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes")
.arg(QUrl(modulePath).fileName()).arg(sourceCode.length());
if (module.property("content-type").toString() == "application/json") {
qCDebug(scriptengine_module) << "... parsing as JSON";
closure.setProperty("__json", sourceCode);
result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath });
} else {
// scoped vars for consistency with Node.js
closure.setProperty("require", module.property("require"));
closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS);
closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS);
result = evaluateInClosure(closure, { sourceCode, modulePath });
}
maybeEmitUncaughtException(__FUNCTION__);
return result;
}
// CommonJS/Node.js like require/module support
QScriptValue ScriptEngine::require(const QString& moduleId) {
qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")";
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
auto jsRequire = globalObject().property("Script").property("require");
auto cacheMeta = jsRequire.data();
auto cache = jsRequire.property("cache");
auto parent = currentModule();
auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) {
cache.setProperty(modulePath, nullValue());
if (!error.isNull()) {
#ifdef DEBUG_JS_MODULES
qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString();
#endif
raiseException(error);
}
maybeEmitUncaughtException("module");
return unboundNullValue();
};
// start by resolving the moduleId into a fully-qualified path/URL
QString modulePath = _requireResolve(moduleId);
if (modulePath.isNull() || hasUncaughtException()) {
// the resolver already threw an exception -- bail early
maybeEmitUncaughtException(__FUNCTION__);
return unboundNullValue();
}
// check the resolved path against the cache
auto module = cache.property(modulePath);
// modules get cached in `Script.require.cache` and (similar to Node.js) users can access it
// to inspect particular entries and invalidate them by deleting the key:
// `delete Script.require.cache[Script.require.resolve(moduleId)];`
// cacheMeta is just used right now to tell deleted keys apart from undefined ones
bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid();
// reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load
cacheMeta.setProperty(modulePath, QScriptValue());
auto exports = module.property("exports");
if (!invalidateCache && exports.isObject()) {
// we have found a cached module -- just need to possibly register it with current parent
qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)")
.arg(modulePath).arg(moduleId).arg(module.property("loaded").toString());
registerModuleWithParent(module, parent);
maybeEmitUncaughtException("cached module");
return exports;
}
// bootstrap / register new empty module
module = newModule(modulePath, parent);
registerModuleWithParent(module, parent);
// add it to the cache (this is done early so any cyclic dependencies pick up)
cache.setProperty(modulePath, module);
// download the module source
auto req = fetchModuleSource(modulePath, invalidateCache);
if (!req.contains("success") || !req["success"].toBool()) {
auto error = QString("error retrieving script (%1)").arg(req["status"].toString());
return throwModuleError(modulePath, error);
}
#if DEBUG_JS_MODULES
qCDebug(scriptengine_module) << "require.loaded: " <<
QUrl(req["url"].toString()).fileName() << req["status"].toString();
#endif
auto sourceCode = req["contents"].toString();
if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) {
module.setProperty("content-type", "application/json");
} else {
module.setProperty("content-type", "application/javascript");
}
// evaluate the module
auto result = instantiateModule(module, sourceCode);
if (result.isError() && !result.strictlyEquals(module.property("exports"))) {
qCWarning(scriptengine_module) << "-- result.isError --" << result.toString();
return throwModuleError(modulePath, result);
}
// mark as fully-loaded
module.setProperty("loaded", true, READONLY_PROP_FLAGS);
// set up a new reference point for detecting cache key deletion
cacheMeta.setProperty(modulePath, module);
qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")";
maybeEmitUncaughtException(__FUNCTION__);
return module.property("exports");
}
// If a callback is specified, the included files will be loaded asynchronously and the callback will be called
// when all of the files have finished loading.
// If no callback is specified, the included files will be loaded synchronously and will block execution until
// all of the files have finished loading.
void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return;
}
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:"
+ includeFiles.join(",") + "parent script:" + getFilename());
@ -1361,7 +1767,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);
if (hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(__FUNCTION__));
emit unhandledException(cloneUncaughtException("evaluateInclude"));
clearExceptions();
}
} else {
@ -1408,6 +1814,9 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) {
// as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which
// the Application or other context will connect to in order to know to actually load the script
void ScriptEngine::load(const QString& loadFile) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return;
}
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:"
+ loadFile + "parent script:" + getFilename());
@ -1477,6 +1886,52 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const
emit entityScriptDetailsUpdated();
}
QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) {
static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) };
QVariantMap map;
if (entityID.isNull()) {
// TODO: find better way to report JS Error across thread/process boundaries
map["isError"] = true;
map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID";
} else {
#ifdef DEBUG_ENTITY_STATES
qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread();
#endif
EntityScriptDetails scriptDetails;
if (getEntityScriptDetails(entityID, scriptDetails)) {
#ifdef DEBUG_ENTITY_STATES
qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread();
#endif
map["isRunning"] = isEntityScriptRunning(entityID);
map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower();
map["errorInfo"] = scriptDetails.errorInfo;
map["entityID"] = entityID.toString();
#ifdef DEBUG_ENTITY_STATES
{
auto debug = QVariantMap();
debug["script"] = scriptDetails.scriptText;
debug["scriptObject"] = scriptDetails.scriptObject.toVariant();
debug["lastModified"] = (qlonglong)scriptDetails.lastModified;
debug["sandboxURL"] = scriptDetails.definingSandboxURL;
map["debug"] = debug;
}
#endif
} else {
#ifdef DEBUG_ENTITY_STATES
qDebug() << "!gotEntityScriptDetails" << QThread::currentThread();
#endif
map["isError"] = true;
map["errorInfo"] = "Entity script details unavailable";
map["entityID"] = entityID.toString();
}
}
return map;
}
QFuture<QVariant> ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) {
return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID);
}
bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
auto it = _entityScripts.constFind(entityID);
if (it == _entityScripts.constEnd()) {
@ -1615,10 +2070,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString&
auto scriptCache = DependencyManager::get<ScriptCache>();
// note: see EntityTreeRenderer.cpp for shared pointer lifecycle management
QWeakPointer<ScriptEngine> weakRef(sharedFromThis());
QWeakPointer<BaseScriptEngine> weakRef(sharedFromThis());
scriptCache->getScriptContents(entityScript,
[this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {
QSharedPointer<ScriptEngine> strongRef(weakRef);
QSharedPointer<BaseScriptEngine> strongRef(weakRef);
if (!strongRef) {
qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";
return;
@ -1737,13 +2192,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
timeout.setSingleShot(true);
timeout.start(SANDBOX_TIMEOUT);
connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{
auto context = sandbox.currentContext();
if (context) {
qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")";
// Guard against infinite loops and non-performant code
context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));
}
sandbox.raiseException(
sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT))
);
});
testConstructor = sandbox.evaluate(program);
@ -1759,7 +2213,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
if (exception.isError()) {
// create a local copy using makeError to decouple from the sandbox engine
exception = makeError(exception);
setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
emit unhandledException(exception);
return;
}
@ -1771,9 +2225,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
testConstructorType = "empty";
}
QString testConstructorValue = testConstructor.toString();
const int maxTestConstructorValueSize = 80;
if (testConstructorValue.size() > maxTestConstructorValueSize) {
testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";
if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) {
testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";
}
auto message = QString("failed to load entity script -- expected a function, got %1, %2")
.arg(testConstructorType).arg(testConstructorValue);
@ -1811,7 +2264,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
if (entityScriptObject.isError()) {
auto exception = entityScriptObject;
setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
emit unhandledException(exception);
return;
}
@ -1855,10 +2308,12 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR
const EntityScriptDetails &oldDetails = _entityScripts[entityID];
if (isEntityScriptRunning(entityID)) {
callEntityScriptMethod(entityID, "unload");
} else {
}
#ifdef DEBUG_ENTITY_STATES
else {
qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status;
}
#endif
if (shouldRemoveFromMap) {
// this was a deleted entity, we've been asked to remove it from the map
_entityScripts.remove(entityID);
@ -1950,10 +2405,7 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s
#else
operation();
#endif
if (!isEvaluating() && hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__));
clearExceptions();
}
maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__);
currentEntityIdentifier = oldIdentifier;
currentSandboxURL = oldSandboxURL;
}

View file

@ -41,6 +41,7 @@
#include "ScriptCache.h"
#include "ScriptUUID.h"
#include "Vec3.h"
#include "SettingHandle.h"
class QScriptEngineDebugger;
@ -78,7 +79,7 @@ public:
QUrl definingSandboxURL { QUrl("about:EntityScript") };
};
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis<ScriptEngine> {
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider {
Q_OBJECT
Q_PROPERTY(QString context READ getContext)
public:
@ -137,6 +138,8 @@ public:
/// evaluate some code in the context of the ScriptEngine and return the result
Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName, int lineNumber = 1); // this is also used by the script tool widget
Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
/// if the script engine is not already running, this will download the URL and start the process of seting it up
/// to run... NOTE - this is used by Application currently to load the url. We don't really want it to be exposed
/// to scripts. we may not need this to be invokable
@ -157,6 +160,16 @@ public:
Q_INVOKABLE void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue());
Q_INVOKABLE void include(const QString& includeFile, QScriptValue callback = QScriptValue());
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MODULE related methods
Q_INVOKABLE QScriptValue require(const QString& moduleId);
Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false);
QScriptValue currentModule();
bool registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent);
QScriptValue newModule(const QString& modulePath, const QScriptValue& parent = QScriptValue());
QVariantMap fetchModuleSource(const QString& modulePath, const bool forceDownload = false);
QScriptValue instantiateModule(const QScriptValue& module, const QString& sourceCode);
Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS);
Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS);
Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
@ -170,6 +183,8 @@ public:
Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) {
return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING;
}
QVariant cloneEntityScriptDetails(const EntityItemID& entityID);
QFuture<QVariant> getLocalEntityScriptDetails(const EntityItemID& entityID) override;
Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload);
Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false); // will call unload method
Q_INVOKABLE void unloadAllEntityScripts();
@ -237,6 +252,9 @@ signals:
protected:
void init();
Q_INVOKABLE void executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type = Qt::QueuedConnection );
// note: this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general;
// then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;"
Q_INVOKABLE QString _requireResolve(const QString& moduleId, const QString& relativeTo = QString());
QString logException(const QScriptValue& exception);
void timerFired();
@ -290,11 +308,16 @@ protected:
AssetScriptingInterface _assetScriptingInterface{ this };
std::function<bool()> _emitScriptUpdates{ [](){ return true; } };
std::function<bool()> _emitScriptUpdates{ []() { return true; } };
std::recursive_mutex _lock;
std::chrono::microseconds _totalTimerExecution { 0 };
static const QString _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT;
static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS;
Setting::Handle<bool> _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true };
};
#endif // hifi_ScriptEngine_h

View file

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

View file

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

View file

@ -10,6 +10,7 @@
//
#include "BaseScriptEngine.h"
#include "SharedLogging.h"
#include <QtCore/QString>
#include <QtCore/QThread>
@ -18,18 +19,27 @@
#include <QtScript/QScriptValueIterator>
#include <QtScript/QScriptContextInfo>
#include "ScriptEngineLogging.h"
#include "Profile.h"
const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
"com.highfidelity.experimental.enableExtendedJSExceptions"
};
const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" };
const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " };
bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method) {
if (QThread::currentThread() == thread) {
return true;
}
qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3")
.arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName());
qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)";
Q_ASSERT(false);
return false;
}
// engine-aware JS Error copier and factory
QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
auto other = _other;
if (other.isString()) {
other = newObject();
@ -41,7 +51,7 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri
}
if (!proto.isFunction()) {
#ifdef DEBUG_JS_EXCEPTIONS
qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
#endif
proto = globalObject().property("Error");
}
@ -64,6 +74,9 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri
// check syntax and when there are issues returns an actual "SyntaxError" with the details
QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
const auto syntaxCheck = checkSyntax(sourceCode);
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
auto err = globalObject().property("SyntaxError")
@ -82,13 +95,16 @@ QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QStri
}
return err;
}
return undefinedValue();
return QScriptValue();
}
// this pulls from the best available information to create a detailed snapshot of the current exception
QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
if (!hasUncaughtException()) {
return QScriptValue();
return unboundNullValue();
}
auto exception = uncaughtException();
// ensure the error object is engine-local
@ -144,7 +160,10 @@ QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail
return err;
}
QString BaseScriptEngine::formatException(const QScriptValue& exception) {
QString BaseScriptEngine::formatException(const QScriptValue& exception, bool includeExtendedDetails) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return QString();
}
QString note { "UncaughtException" };
QString result;
@ -156,8 +175,8 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) {
const auto lineNumber = exception.property("lineNumber").toString();
const auto stacktrace = exception.property("stack").toString();
if (_enableExtendedJSExceptions.get()) {
// This setting toggles display of the hints now being added during the loading process.
if (includeExtendedDetails) {
// Display additional exception / troubleshooting hints that can be added via the custom Error .detail property
// Example difference:
// [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n...
// [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n...
@ -173,14 +192,39 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) {
return result;
}
bool BaseScriptEngine::raiseException(const QScriptValue& exception) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return false;
}
if (currentContext()) {
// we have an active context / JS stack frame so throw the exception per usual
currentContext()->throwValue(makeError(exception));
return true;
} else {
// we are within a pure C++ stack frame (ie: being called directly by other C++ code)
// in this case no context information is available so just emit the exception for reporting
emit unhandledException(makeError(exception));
}
return false;
}
bool BaseScriptEngine::maybeEmitUncaughtException(const QString& debugHint) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return false;
}
if (!isEvaluating() && hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(debugHint));
clearExceptions();
return true;
}
return false;
}
QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
PROFILE_RANGE(script, "evaluateInClosure");
if (QThread::currentThread() != thread()) {
qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only.";
// note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE
return QScriptValue();
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
const auto fileName = program.fileName();
const auto shortName = QUrl(fileName).fileName();
@ -189,7 +233,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
auto global = closure.property("global");
if (global.isObject()) {
#ifdef DEBUG_JS
qCDebug(scriptengine) << " setting global = closure.global" << shortName;
qCDebug(shared) << " setting global = closure.global" << shortName;
#endif
oldGlobal = globalObject();
setGlobalObject(global);
@ -200,34 +244,34 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
auto thiz = closure.property("this");
if (thiz.isObject()) {
#ifdef DEBUG_JS
qCDebug(scriptengine) << " setting this = closure.this" << shortName;
qCDebug(shared) << " setting this = closure.this" << shortName;
#endif
context->setThisObject(thiz);
}
context->pushScope(closure);
#ifdef DEBUG_JS
qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
#endif
{
result = BaseScriptEngine::evaluate(program);
if (hasUncaughtException()) {
auto err = cloneUncaughtException(__FUNCTION__);
#ifdef DEBUG_JS_EXCEPTIONS
qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
err.setProperty("_result", result);
#endif
result = err;
}
}
#ifdef DEBUG_JS
qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
#endif
popContext();
if (oldGlobal.isValid()) {
#ifdef DEBUG_JS
qCDebug(scriptengine) << " restoring global" << shortName;
qCDebug(shared) << " restoring global" << shortName;
#endif
setGlobalObject(oldGlobal);
}
@ -236,7 +280,6 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
}
// Lambda
QScriptValue BaseScriptEngine::newLambdaFunction(std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) {
auto lambda = new Lambda(this, operation, data);
auto object = newQObject(lambda, ownership);
@ -262,26 +305,57 @@ Lambda::Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext
#endif
}
QScriptValue Lambda::call() {
if (!BaseScriptEngine::IS_THREADSAFE_INVOCATION(engine->thread(), __FUNCTION__)) {
return BaseScriptEngine::unboundNullValue();
}
return operation(engine->currentContext(), engine);
}
QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName) {
auto engine = scopeOrCallback.engine();
if (!engine) {
return scopeOrCallback;
}
auto scope = QScriptValue();
auto callback = scopeOrCallback;
if (scopeOrCallback.isObject()) {
if (methodOrName.isString()) {
scope = scopeOrCallback;
callback = scope.property(methodOrName.toString());
} else if (methodOrName.isFunction()) {
scope = scopeOrCallback;
callback = methodOrName;
}
}
auto handler = engine->newObject();
handler.setProperty("scope", scope);
handler.setProperty("callback", callback);
return handler;
}
QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result) {
return handler.property("callback").call(handler.property("scope"), QScriptValueList({ err, result }));
}
#ifdef DEBUG_JS
void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return;
}
if (!header.isEmpty()) {
qCDebug(scriptengine) << header;
qCDebug(shared) << header;
}
if (!object.isObject()) {
qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString();
qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString();
return;
}
QScriptValueIterator it(object);
while (it.hasNext()) {
it.next();
qCDebug(scriptengine) << it.name() << ":" << it.value().toString();
qCDebug(shared) << it.name() << ":" << it.value().toString();
}
if (!footer.isEmpty()) {
qCDebug(scriptengine) << footer;
qCDebug(shared) << footer;
}
}
#endif

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;
std::shared_ptr<gpu::Texture> _whiteTexture;
uint32_t _globalShapeKey { 0 };
bool _enableTexturing { true };
RenderDetails _details;

View file

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

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
// initially these are just to capture how the app works currently
var testCases = {
// special relative resolves
'': filename,
'.': dirname,
'..': parentdir,
// local file "magic" tilde path expansion
'/~/defaultScripts.js': ScriptDiscoveryService.defaultScriptsPath + '/defaultScripts.js',
// these schemes appear to always get resolved to empty URLs
'qrc://test': '',
'about:Entities 1': '',
'ftp://host:port/path': '',
'data:text/html;text,foo': '',
'Entities 1': dirname + 'Entities 1',
'./file.js': dirname + 'file.js',
'c:/temp/': 'file:///c:/temp/',
@ -31,6 +41,12 @@ describe('Script', function () {
'/~/libraries/utils.js': 'file:///~/libraries/utils.js',
'/temp/file.js': 'file:///temp/file.js',
'/~/': 'file:///~/',
// these schemes appear to always get resolved to the same URL again
'http://highfidelity.com': 'http://highfidelity.com',
'atp:/highfidelity': 'atp:/highfidelity',
'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f':
'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f',
};
describe('resolvePath', function () {
Object.keys(testCases).forEach(function(input) {
@ -42,7 +58,7 @@ describe('Script', function () {
describe('include', function () {
var old_cache_buster;
var cache_buster = '#' + +new Date;
var cache_buster = '#' + new Date().getTime().toString(36);
beforeAll(function() {
old_cache_buster = Settings.getValue('cache_buster');
Settings.setValue('cache_buster', cache_buster);

View file

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

69
scripts/modules/vec3.js Normal file
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_FIRST_FRAME = 1;
var ANIMATION_LAST_FRAME = 10;
var RELEASE_KEYS = ['w', 'a', 's', 'd', 'UP', 'LEFT', 'DOWN', 'RIGHT'];
var RELEASE_TIME = 500; // ms
var RELEASE_DISTANCE = 0.2; // meters
var MAX_IK_ERROR = 30;
var IK_SETTLE_TIME = 250; // ms
var DESKTOP_UI_CHECK_INTERVAL = 100;
var DESKTOP_MAX_DISTANCE = 5;
var SIT_DELAY = 25
var MAX_RESET_DISTANCE = 0.5
var SIT_DELAY = 25;
var MAX_RESET_DISTANCE = 0.5; // meters
var OVERRIDEN_DRIVE_KEYS = [
DriveKeys.TRANSLATE_X,
DriveKeys.TRANSLATE_Y,
DriveKeys.TRANSLATE_Z,
DriveKeys.STEP_TRANSLATE_X,
DriveKeys.STEP_TRANSLATE_Y,
DriveKeys.STEP_TRANSLATE_Z,
];
this.entityID = null;
this.timers = {};
this.animStateHandlerID = null;
this.interval = null;
this.sitDownSettlePeriod = null;
this.lastTimeNoDriveKeys = null;
this.preload = function(entityID) {
this.entityID = entityID;
}
this.unload = function() {
if (Settings.getValue(SETTING_KEY) === this.entityID) {
this.sitUp();
this.standUp();
}
if (this.interval !== null) {
Script.clearInterval(this.interval);
@ -96,6 +105,11 @@
print("Someone is already sitting in that chair.");
return;
}
print("Sitting down (" + this.entityID + ")");
var now = Date.now();
this.sitDownSettlePeriod = now + IK_SETTLE_TIME;
this.lastTimeNoDriveKeys = now;
var previousValue = Settings.getValue(SETTING_KEY);
Settings.setValue(SETTING_KEY, this.entityID);
@ -118,20 +132,17 @@
return { headType: 0 };
}, ["headType"]);
Script.update.connect(this, this.update);
Controller.keyPressEvent.connect(this, this.keyPressed);
Controller.keyReleaseEvent.connect(this, this.keyReleased);
for (var i in RELEASE_KEYS) {
Controller.captureKeyEvents({ text: RELEASE_KEYS[i] });
for (var i in OVERRIDEN_DRIVE_KEYS) {
MyAvatar.disableDriveKey(OVERRIDEN_DRIVE_KEYS[i]);
}
}
this.sitUp = function() {
this.standUp = function() {
print("Standing up (" + this.entityID + ")");
MyAvatar.removeAnimationStateHandler(this.animStateHandlerID);
Script.update.disconnect(this, this.update);
Controller.keyPressEvent.disconnect(this, this.keyPressed);
Controller.keyReleaseEvent.disconnect(this, this.keyReleased);
for (var i in RELEASE_KEYS) {
Controller.releaseKeyEvents({ text: RELEASE_KEYS[i] });
for (var i in OVERRIDEN_DRIVE_KEYS) {
MyAvatar.enableDriveKey(OVERRIDEN_DRIVE_KEYS[i]);
}
this.setSeatUser(null);
@ -156,6 +167,7 @@
}
}
// function called by teleport.js if it detects the appropriate userData
this.sit = function () {
this.sitDown();
}
@ -207,7 +219,33 @@
var properties = Entities.getEntityProperties(this.entityID);
var avatarDistance = Vec3.distance(MyAvatar.position, properties.position);
var ikError = MyAvatar.getIKErrorOnLastSolve();
if (avatarDistance > RELEASE_DISTANCE || ikError > MAX_IK_ERROR) {
var now = Date.now();
var shouldStandUp = false;
// Check if a drive key is pressed
var hasActiveDriveKey = false;
for (var i in OVERRIDEN_DRIVE_KEYS) {
if (MyAvatar.getRawDriveKey(OVERRIDEN_DRIVE_KEYS[i]) != 0.0) {
hasActiveDriveKey = true;
break;
}
}
// Only standup if user has been pushing a drive key for RELEASE_TIME
if (hasActiveDriveKey) {
var elapsed = now - this.lastTimeNoDriveKeys;
shouldStandUp = elapsed > RELEASE_TIME;
} else {
this.lastTimeNoDriveKeys = Date.now();
}
// Allow some time for the IK to settle
if (ikError > MAX_IK_ERROR && now > this.sitDownSettlePeriod) {
shouldStandUp = true;
}
if (shouldStandUp || avatarDistance > RELEASE_DISTANCE) {
print("IK error: " + ikError + ", distance from chair: " + avatarDistance);
// Move avatar in front of the chair to avoid getting stuck in collision hulls
@ -215,45 +253,13 @@
var offset = { x: 0, y: 1.0, z: -0.5 - properties.dimensions.z * properties.registrationPoint.z };
var position = Vec3.sum(properties.position, Vec3.multiplyQbyV(properties.rotation, offset));
MyAvatar.position = position;
print("Moving Avatar in front of the chair.")
}
this.sitUp();
this.standUp();
}
}
}
this.keyPressed = function(event) {
if (isInEditMode()) {
return;
}
if (RELEASE_KEYS.indexOf(event.text) !== -1) {
var that = this;
this.timers[event.text] = Script.setTimeout(function() {
delete that.timers[event.text];
var properties = Entities.getEntityProperties(that.entityID);
var avatarDistance = Vec3.distance(MyAvatar.position, properties.position);
// Move avatar in front of the chair to avoid getting stuck in collision hulls
if (avatarDistance < MAX_RESET_DISTANCE) {
var offset = { x: 0, y: 1.0, z: -0.5 - properties.dimensions.z * properties.registrationPoint.z };
var position = Vec3.sum(properties.position, Vec3.multiplyQbyV(properties.rotation, offset));
MyAvatar.position = position;
}
that.sitUp();
}, RELEASE_TIME);
}
}
this.keyReleased = function(event) {
if (RELEASE_KEYS.indexOf(event.text) !== -1) {
if (this.timers[event.text]) {
Script.clearTimeout(this.timers[event.text]);
delete this.timers[event.text];
}
}
}
this.canSitDesktop = function() {
var properties = Entities.getEntityProperties(this.entityID, ["position"]);
var distanceFromSeat = Vec3.distance(MyAvatar.position, properties.position);