Merge branch 'master' of https://github.com/highfidelity/hifi into black-bis

This commit is contained in:
Sam Gateau 2018-10-03 12:12:15 -07:00
commit c194e795ac
48 changed files with 1576 additions and 363 deletions

View file

@ -34,7 +34,7 @@ Preference {
left: parent.left
right: parent.right
}
height: isFirstCheckBox ? hifi.dimensions.controlInterlineHeight : 0
height: isFirstCheckBox && !preference.indented ? 16 : 2
}
CheckBox {
@ -54,6 +54,7 @@ Preference {
left: parent.left
right: parent.right
bottom: parent.bottom
leftMargin: preference.indented ? 20 : 0
}
text: root.label
colorScheme: hifi.colorSchemes.dark

View file

@ -11,14 +11,27 @@
import QtQuick 2.5
import "../../controls-uit"
import "../../styles-uit"
Preference {
id: root
height: control.height + hifi.dimensions.controlInterlineHeight
property int value: 0
Component.onCompleted: {
repeater.itemAt(preference.value).checked = true
value = preference.value;
repeater.itemAt(preference.value).checked = true;
}
function updateValue() {
for (var i = 0; i < repeater.count; i++) {
if (repeater.itemAt(i).checked) {
value = i;
break;
}
}
}
function save() {
@ -33,24 +46,36 @@ Preference {
preference.save();
}
Row {
Column {
id: control
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
spacing: 5
spacing: 3
RalewaySemiBold {
id: heading
size: hifi.fontSizes.inputLabel
text: preference.heading
color: hifi.colors.lightGrayText
visible: text !== ""
bottomPadding: 3
}
Repeater {
id: repeater
model: preference.items.length
delegate: RadioButton {
text: preference.items[index]
letterSpacing: 0
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
}
leftPadding: 0
colorScheme: hifi.colorSchemes.dark
onClicked: updateValue();
}
}
}

View file

@ -138,11 +138,12 @@ Preference {
break;
case Preference.PrimaryHand:
checkBoxCount++;
checkBoxCount = 0;
builder = primaryHandBuilder;
break;
case Preference.RadioButtons:
checkBoxCount++;
checkBoxCount = 0;
builder = radioButtonsBuilder;
break;
};

View file

@ -48,7 +48,7 @@ Rectangle {
HifiModels.PSFListModel {
id: connectionsUserModel;
http: http;
endpoint: "/api/v1/users?filter=connections";
endpoint: "/api/v1/users/connections";
property var sortColumn: connectionsTable.getColumn(connectionsTable.sortIndicatorColumn);
sortProperty: switch (sortColumn && sortColumn.role) {
case 'placeName':

View file

@ -0,0 +1,43 @@
import QtQuick 2.0
Item {
property alias source: sourceImage.sourceItem
property alias maskSource: sourceMask.sourceItem
anchors.fill: parent
ShaderEffectSource {
id: sourceMask
smooth: true
hideSource: true
}
ShaderEffectSource {
id: sourceImage
hideSource: true
}
ShaderEffect {
id: maskEffect
anchors.fill: parent
property variant source: sourceImage
property variant mask: sourceMask
fragmentShader: {
"
varying highp vec2 qt_TexCoord0;
uniform lowp sampler2D source;
uniform lowp sampler2D mask;
void main() {
highp vec4 maskColor = texture2D(mask, vec2(qt_TexCoord0.x, qt_TexCoord0.y));
highp vec4 sourceColor = texture2D(source, vec2(qt_TexCoord0.x, qt_TexCoord0.y));
if (maskColor.a > 0.0)
gl_FragColor = sourceColor;
else
gl_FragColor = maskColor;
}
"
}
}
}

View file

@ -24,20 +24,20 @@ Item {
fragmentShader: {
"
#version 150 core
varying highp vec2 qt_TexCoord0;
uniform lowp sampler2D source;
uniform lowp sampler2D mask;
void main() {
highp vec4 maskColor = texture2D(mask, vec2(qt_TexCoord0.x, qt_TexCoord0.y));
highp vec4 sourceColor = texture2D(source, vec2(qt_TexCoord0.x, qt_TexCoord0.y));
if (maskColor.a > 0.0)
gl_FragColor = sourceColor;
else
gl_FragColor = maskColor;
}
#version 410
in vec2 qt_TexCoord0;
out vec4 color;
uniform sampler2D source;
uniform sampler2D mask;
void main()
{
vec4 maskColor = texture(mask, vec2(qt_TexCoord0.x, qt_TexCoord0.y));
vec4 sourceColor = texture(source, vec2(qt_TexCoord0.x, qt_TexCoord0.y));
if (maskColor.a > 0.0)
color = sourceColor;
else
color = maskColor;
}
"
}
}

View file

@ -122,6 +122,22 @@ Item {
}
}
// Runtime customization of preferences.
var locomotionPreference = findPreference("VR Movement", "Teleporting only / Walking and teleporting");
var flyingPreference = findPreference("VR Movement", "Jumping and flying");
if (locomotionPreference && flyingPreference) {
flyingPreference.visible = (locomotionPreference.value === 1);
locomotionPreference.valueChanged.connect(function () {
flyingPreference.visible = (locomotionPreference.value === 1);
});
}
if (HMD.isHeadControllerAvailable("Oculus")) {
var boundariesPreference = findPreference("VR Movement", "Show room boundaries while teleporting");
if (boundariesPreference) {
boundariesPreference.label = "Show room boundaries and sensors while teleporting";
}
}
if (sections.length) {
// Default sections to expanded/collapsed as appropriate for dialog.
if (sections.length === 1) {
@ -234,4 +250,32 @@ Item {
}
}
}
function findPreference(category, name) {
var section = null;
var preference = null;
var i;
// Find category section.
i = 0;
while (!section && i < sections.length) {
if (sections[i].name === category) {
section = sections[i];
}
i++;
}
// Find named preference.
if (section) {
i = 0;
while (!preference && i < section.preferences.length) {
if (section.preferences[i].preference && section.preferences[i].preference.name === name) {
preference = section.preferences[i];
}
i++;
}
}
return preference;
}
}

View file

@ -153,11 +153,12 @@ Preference {
break;
case Preference.PrimaryHand:
checkBoxCount++;
checkBoxCount = 0;
builder = primaryHandBuilder;
break;
case Preference.RadioButtons:
checkBoxCount++;
checkBoxCount = 0;
builder = radioButtonsBuilder;
break;
};

View file

@ -410,6 +410,10 @@ public:
});
}
void setMainThreadID(Qt::HANDLE threadID) {
_mainThreadID = threadID;
}
static void updateHeartbeat() {
auto now = usecTimestampNow();
auto elapsed = now - _heartbeat;
@ -417,7 +421,9 @@ public:
_heartbeat = now;
}
static void deadlockDetectionCrash() {
void deadlockDetectionCrash() {
setCrashAnnotation("_mod_faulting_tid", std::to_string((uint64_t)_mainThreadID));
setCrashAnnotation("deadlock", "1");
uint32_t* crashTrigger = nullptr;
*crashTrigger = 0xDEAD10CC;
}
@ -504,6 +510,8 @@ public:
static ThreadSafeMovingAverage<int, HEARTBEAT_SAMPLES> _movingAverage;
bool _quit { false };
Qt::HANDLE _mainThreadID = nullptr;
};
std::atomic<bool> DeadlockWatchdogThread::_paused;
@ -1053,6 +1061,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
auto controllerScriptingInterface = DependencyManager::get<controller::ScriptingInterface>().data();
_controllerScriptingInterface = dynamic_cast<ControllerScriptingInterface*>(controllerScriptingInterface);
connect(PluginManager::getInstance().data(), &PluginManager::inputDeviceRunningChanged,
controllerScriptingInterface, &controller::ScriptingInterface::updateRunningInputDevices);
_entityClipboard->createRootElement();
@ -1092,7 +1102,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
}
// Set up a watchdog thread to intentionally crash the application on deadlocks
if (!DISABLE_WATCHDOG) {
(new DeadlockWatchdogThread())->start();
auto deadlockWatchdogThread = new DeadlockWatchdogThread();
deadlockWatchdogThread->setMainThreadID(QThread::currentThreadId());
deadlockWatchdogThread->start();
}
// Set File Logger Session UUID

View file

@ -106,6 +106,7 @@ MyAvatar::MyAvatar(QThread* thread) :
_realWorldFieldOfView("realWorldFieldOfView",
DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES),
_useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", true),
_showPlayArea("showPlayArea", true),
_smoothOrientationTimer(std::numeric_limits<float>::max()),
_smoothOrientationInitial(),
_smoothOrientationTarget(),
@ -3307,7 +3308,7 @@ float MyAvatar::getRawDriveKey(DriveKeys key) const {
}
void MyAvatar::relayDriveKeysToCharacterController() {
if (getDriveKey(TRANSLATE_Y) > 0.0f) {
if (getDriveKey(TRANSLATE_Y) > 0.0f && (!qApp->isHMDMode() || (useAdvancedMovementControls() && getFlyingHMDPref()))) {
_characterController.jump();
}
}

View file

@ -122,8 +122,10 @@ class MyAvatar : public Avatar {
* zone may disallow collisionless avatars.
* @property {boolean} characterControllerEnabled - Synonym of <code>collisionsEnabled</code>.
* <strong>Deprecated:</strong> Use <code>collisionsEnabled</code> instead.
* @property {boolean} useAdvancedMovementControls - Returns the value of the Interface setting, Settings > Advanced
* Movement for Hand Controller. Note: Setting the value has no effect unless Interface is restarted.
* @property {boolean} useAdvancedMovementControls - Returns and sets the value of the Interface setting, Settings >
* Walking and teleporting. Note: Setting the value has no effect unless Interface is restarted.
* @property {boolean} showPlayArea - Returns and sets the value of the Interface setting, Settings > Show room boundaries
* while teleporting. Note: Setting the value has no effect unless Interface is restarted.
* @property {number} yawSpeed=75
* @property {number} pitchSpeed=50
* @property {boolean} hmdRollControlEnabled=true - If <code>true</code>, the roll angle of your HMD turns your avatar
@ -223,6 +225,7 @@ class MyAvatar : public Avatar {
Q_PROPERTY(bool collisionsEnabled READ getCollisionsEnabled WRITE setCollisionsEnabled)
Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled)
Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls)
Q_PROPERTY(bool showPlayArea READ getShowPlayArea WRITE setShowPlayArea)
Q_PROPERTY(float yawSpeed MEMBER _yawSpeed)
Q_PROPERTY(float pitchSpeed MEMBER _pitchSpeed)
@ -542,6 +545,9 @@ public:
void setUseAdvancedMovementControls(bool useAdvancedMovementControls)
{ _useAdvancedMovementControls.set(useAdvancedMovementControls); }
bool getShowPlayArea() const { return _showPlayArea.get(); }
void setShowPlayArea(bool showPlayArea) { _showPlayArea.set(showPlayArea); }
void setHMDRollControlEnabled(bool value) { _hmdRollControlEnabled = value; }
bool getHMDRollControlEnabled() const { return _hmdRollControlEnabled; }
void setHMDRollControlDeadZone(float value) { _hmdRollControlDeadZone = value; }
@ -1631,6 +1637,7 @@ private:
Setting::Handle<float> _realWorldFieldOfView;
Setting::Handle<bool> _useAdvancedMovementControls;
Setting::Handle<bool> _showPlayArea;
// Smoothing.
const float SMOOTH_TIME_ORIENTATION = 0.5f;

View file

@ -18,6 +18,7 @@
const glm::vec4 ParabolaPointer::RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_COLOR { 1.0f };
const float ParabolaPointer::RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_WIDTH { 0.01f };
const bool ParabolaPointer::RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_ISVISIBLEINSECONDARYCAMERA { false };
const bool ParabolaPointer::RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_DRAWINFRONT { false };
gpu::PipelinePointer ParabolaPointer::RenderState::ParabolaRenderItem::_parabolaPipeline { nullptr };
gpu::PipelinePointer ParabolaPointer::RenderState::ParabolaRenderItem::_transparentParabolaPipeline { nullptr };
@ -46,6 +47,7 @@ void ParabolaPointer::editRenderStatePath(const std::string& state, const QVaria
float alpha = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_COLOR.a;
float width = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_WIDTH;
bool isVisibleInSecondaryCamera = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_ISVISIBLEINSECONDARYCAMERA;
bool drawInFront = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_DRAWINFRONT;
bool enabled = false;
if (!pathMap.isEmpty()) {
enabled = true;
@ -63,8 +65,11 @@ void ParabolaPointer::editRenderStatePath(const std::string& state, const QVaria
if (pathMap["isVisibleInSecondaryCamera"].isValid()) {
isVisibleInSecondaryCamera = pathMap["isVisibleInSecondaryCamera"].toBool();
}
if (pathMap["drawInFront"].isValid()) {
drawInFront = pathMap["drawInFront"].toBool();
}
}
renderState->editParabola(color, alpha, width, isVisibleInSecondaryCamera, enabled);
renderState->editParabola(color, alpha, width, isVisibleInSecondaryCamera, drawInFront, enabled);
}
}
@ -146,7 +151,7 @@ void ParabolaPointer::setVisualPickResultInternal(PickResultPointer pickResult,
}
ParabolaPointer::RenderState::RenderState(const OverlayID& startID, const OverlayID& endID, const glm::vec3& pathColor, float pathAlpha, float pathWidth,
bool isVisibleInSecondaryCamera, bool pathEnabled) :
bool isVisibleInSecondaryCamera, bool drawInFront, bool pathEnabled) :
StartEndRenderState(startID, endID)
{
render::Transaction transaction;
@ -154,7 +159,7 @@ ParabolaPointer::RenderState::RenderState(const OverlayID& startID, const Overla
_pathID = scene->allocateID();
_pathWidth = pathWidth;
if (render::Item::isValidID(_pathID)) {
auto renderItem = std::make_shared<ParabolaRenderItem>(pathColor, pathAlpha, pathWidth, isVisibleInSecondaryCamera, pathEnabled);
auto renderItem = std::make_shared<ParabolaRenderItem>(pathColor, pathAlpha, pathWidth, isVisibleInSecondaryCamera, drawInFront, pathEnabled);
transaction.resetItem(_pathID, std::make_shared<ParabolaRenderItem::Payload>(renderItem));
scene->enqueueTransaction(transaction);
}
@ -182,15 +187,16 @@ void ParabolaPointer::RenderState::disable() {
}
}
void ParabolaPointer::RenderState::editParabola(const glm::vec3& color, float alpha, float width, bool isVisibleInSecondaryCamera, bool enabled) {
void ParabolaPointer::RenderState::editParabola(const glm::vec3& color, float alpha, float width, bool isVisibleInSecondaryCamera, bool drawInFront, bool enabled) {
if (render::Item::isValidID(_pathID)) {
render::Transaction transaction;
auto scene = qApp->getMain3DScene();
transaction.updateItem<ParabolaRenderItem>(_pathID, [color, alpha, width, isVisibleInSecondaryCamera, enabled](ParabolaRenderItem& item) {
transaction.updateItem<ParabolaRenderItem>(_pathID, [color, alpha, width, isVisibleInSecondaryCamera, drawInFront, enabled](ParabolaRenderItem& item) {
item.setColor(color);
item.setAlpha(alpha);
item.setWidth(width);
item.setIsVisibleInSecondaryCamera(isVisibleInSecondaryCamera);
item.setDrawInFront(drawInFront);
item.setEnabled(enabled);
item.updateKey();
});
@ -238,6 +244,7 @@ std::shared_ptr<StartEndRenderState> ParabolaPointer::buildRenderState(const QVa
float alpha = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_COLOR.a;
float width = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_WIDTH;
bool isVisibleInSecondaryCamera = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_ISVISIBLEINSECONDARYCAMERA;
bool drawInFront = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_DRAWINFRONT;
bool enabled = false;
if (propMap["path"].isValid()) {
enabled = true;
@ -258,6 +265,10 @@ std::shared_ptr<StartEndRenderState> ParabolaPointer::buildRenderState(const QVa
if (pathMap["isVisibleInSecondaryCamera"].isValid()) {
isVisibleInSecondaryCamera = pathMap["isVisibleInSecondaryCamera"].toBool();
}
if (pathMap["drawInFront"].isValid()) {
drawInFront = pathMap["drawInFront"].toBool();
}
}
QUuid endID;
@ -269,7 +280,7 @@ std::shared_ptr<StartEndRenderState> ParabolaPointer::buildRenderState(const QVa
}
}
return std::make_shared<RenderState>(startID, endID, color, alpha, width, isVisibleInSecondaryCamera, enabled);
return std::make_shared<RenderState>(startID, endID, color, alpha, width, isVisibleInSecondaryCamera, drawInFront, enabled);
}
PointerEvent ParabolaPointer::buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button, bool hover) {
@ -321,8 +332,8 @@ glm::vec3 ParabolaPointer::findIntersection(const PickedObject& pickedObject, co
}
ParabolaPointer::RenderState::ParabolaRenderItem::ParabolaRenderItem(const glm::vec3& color, float alpha, float width,
bool isVisibleInSecondaryCamera, bool enabled) :
_isVisibleInSecondaryCamera(isVisibleInSecondaryCamera), _enabled(enabled)
bool isVisibleInSecondaryCamera, bool drawInFront, bool enabled) :
_isVisibleInSecondaryCamera(isVisibleInSecondaryCamera), _drawInFront(drawInFront), _enabled(enabled)
{
_uniformBuffer->resize(sizeof(ParabolaData));
setColor(color);
@ -358,6 +369,10 @@ void ParabolaPointer::RenderState::ParabolaRenderItem::updateKey() {
builder.withTagBits(render::hifi::TAG_MAIN_VIEW);
}
if (_drawInFront) {
builder.withLayer(render::hifi::LAYER_3D_FRONT);
}
_key = builder.build();
}

View file

@ -21,7 +21,7 @@ public:
using Pointer = Payload::DataPointer;
ParabolaRenderItem(const glm::vec3& color, float alpha, float width,
bool isVisibleInSecondaryCamera, bool enabled);
bool isVisibleInSecondaryCamera, bool drawInFront, bool enabled);
~ParabolaRenderItem() {}
static gpu::PipelinePointer _parabolaPipeline;
@ -46,11 +46,13 @@ public:
void setAcceleration(const glm::vec3& acceleration) { _parabolaData.acceleration = acceleration; }
void setOrigin(const glm::vec3& origin) { _origin = origin; }
void setIsVisibleInSecondaryCamera(const bool& isVisibleInSecondaryCamera) { _isVisibleInSecondaryCamera = isVisibleInSecondaryCamera; }
void setDrawInFront(const bool& drawInFront) { _drawInFront = drawInFront; }
void setEnabled(const bool& enabled) { _enabled = enabled; }
static const glm::vec4 DEFAULT_PARABOLA_COLOR;
static const float DEFAULT_PARABOLA_WIDTH;
static const bool DEFAULT_PARABOLA_ISVISIBLEINSECONDARYCAMERA;
static const bool DEFAULT_PARABOLA_DRAWINFRONT;
private:
render::Item::Bound _bound;
@ -58,6 +60,7 @@ public:
glm::vec3 _origin { 0.0f };
bool _isVisibleInSecondaryCamera { DEFAULT_PARABOLA_ISVISIBLEINSECONDARYCAMERA };
bool _drawInFront { DEFAULT_PARABOLA_DRAWINFRONT };
bool _visible { false };
bool _enabled { false };
@ -77,7 +80,7 @@ public:
RenderState() {}
RenderState(const OverlayID& startID, const OverlayID& endID, const glm::vec3& pathColor, float pathAlpha, float pathWidth,
bool isVisibleInSecondaryCamera, bool pathEnabled);
bool isVisibleInSecondaryCamera, bool drawInFront, bool pathEnabled);
void setPathWidth(float width) { _pathWidth = width; }
float getPathWidth() const { return _pathWidth; }
@ -87,7 +90,7 @@ public:
void update(const glm::vec3& origin, const glm::vec3& end, const glm::vec3& surfaceNormal, bool scaleWithAvatar, bool distanceScaleEnd, bool centerEndY,
bool faceAvatar, bool followNormal, float followNormalStrength, float distance, const PickResultPointer& pickResult) override;
void editParabola(const glm::vec3& color, float alpha, float width, bool isVisibleInSecondaryCamera, bool enabled);
void editParabola(const glm::vec3& color, float alpha, float width, bool isVisibleInSecondaryCamera, bool drawInFront, bool enabled);
private:
int _pathID;

View file

@ -218,6 +218,7 @@ unsigned int PointerScriptingInterface::createLaserPointer(const QVariant& prope
* @property {number} alpha=1.0 The alpha of the parabola.
* @property {number} width=0.01 The width of the parabola, in meters.
* @property {boolean} isVisibleInSecondaryCamera=false The width of the parabola, in meters.
* @property {boolean} drawInFront=false If <code>true</code>, the parabola is rendered in front of other items in the scene.
*/
/**jsdoc
* A set of properties used to define the visual aspect of a Parabola Pointer in the case that the Pointer is not intersecting something. Same as a {@link Pointers.ParabolaPointerRenderState},
@ -393,4 +394,4 @@ QVariantMap PointerScriptingInterface::getPrevPickResult(unsigned int uid) const
QVariantMap PointerScriptingInterface::getPointerProperties(unsigned int uid) const {
return DependencyManager::get<PointerManager>()->getPointerProperties(uid);
}
}

View file

@ -205,7 +205,7 @@ public:
/**jsdoc
* Returns information about an existing Pointer
* @function Pointers.getPointerState
* @function Pointers.getPointerProperties
* @param {number} uid The ID of the Pointer, as returned by {@link Pointers.createPointer}.
* @returns {Pointers.LaserPointerProperties|Pointers.StylusPointerProperties|Pointers.ParabolaPointerProperties} The information about the Pointer.
* Currently only includes renderStates and defaultRenderStates with associated overlay IDs.

View file

@ -241,4 +241,4 @@ glm::vec2 StylusPointer::findPos2D(const PickedObject& pickedObject, const glm::
default:
return glm::vec2(NAN);
}
}
}

View file

@ -201,3 +201,12 @@ bool HMDScriptingInterface::isKeyboardVisible() {
void HMDScriptingInterface::centerUI() {
QMetaObject::invokeMethod(qApp, "centerUI", Qt::QueuedConnection);
}
QVariant HMDScriptingInterface::getPlayAreaRect() {
auto rect = qApp->getActiveDisplayPlugin()->getPlayAreaRect();
return qRectFToVariant(rect);
}
QVector<glm::vec3> HMDScriptingInterface::getSensorPositions() {
return qApp->getActiveDisplayPlugin()->getSensorPositions();
}

View file

@ -61,6 +61,8 @@ class QScriptEngine;
* @property {Uuid} miniTabletScreenID - The UUID of the mini tablet's screen overlay. <code>null</code> if not in HMD mode.
* @property {number} miniTabletHand - The hand that the mini tablet is displayed on: <code>0</code> for left hand,
* <code>1</code> for right hand, <code>-1</code> if not in HMD mode.
* @property {Rect} playArea=0,0,0,0 - The size and position of the HMD play area in sensor coordinates. <em>Read-only.</em>
* @property {Vec3[]} sensorPositions=[]] - The positions of the VR system sensors in sensor coordinates. <em>Read-only.</em>
*/
class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Dependency {
Q_OBJECT
@ -75,6 +77,8 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen
Q_PROPERTY(QUuid miniTabletID READ getCurrentMiniTabletID WRITE setCurrentMiniTabletID)
Q_PROPERTY(QUuid miniTabletScreenID READ getCurrentMiniTabletScreenID WRITE setCurrentMiniTabletScreenID)
Q_PROPERTY(int miniTabletHand READ getCurrentMiniTabletHand WRITE setCurrentMiniTabletHand)
Q_PROPERTY(QVariant playArea READ getPlayAreaRect);
Q_PROPERTY(QVector<glm::vec3> sensorPositions READ getSensorPositions);
public:
@ -384,6 +388,9 @@ public:
void setCurrentMiniTabletHand(int miniTabletHand) { _miniTabletHand = miniTabletHand; }
int getCurrentMiniTabletHand() const { return _miniTabletHand; }
QVariant getPlayAreaRect();
QVector<glm::vec3> getSensorPositions();
private:
bool _showTablet { false };
bool _tabletContextualMode { false };

View file

@ -226,18 +226,22 @@ void setupPreferences() {
static const QString VR_MOVEMENT{ "VR Movement" };
{
static const QString movementsControlChannel = QStringLiteral("Hifi-Advanced-Movement-Disabler");
auto getter = [myAvatar]()->bool { return myAvatar->useAdvancedMovementControls(); };
auto setter = [myAvatar](bool value) { myAvatar->setUseAdvancedMovementControls(value); };
preferences->addPreference(new CheckPreference(VR_MOVEMENT,
QStringLiteral("Advanced movement in VR (Teleport movement when unchecked)"),
getter, setter));
auto getter = [myAvatar]()->int { return myAvatar->useAdvancedMovementControls() ? 1 : 0; };
auto setter = [myAvatar](int value) { myAvatar->setUseAdvancedMovementControls(value == 1); };
auto preference =
new RadioButtonsPreference(VR_MOVEMENT, "Teleporting only / Walking and teleporting", getter, setter);
QStringList items;
items << "Teleporting only" << "Walking and teleporting";
preference->setHeading("Movement mode");
preference->setItems(items);
preferences->addPreference(preference);
}
{
auto getter = [myAvatar]()->bool { return myAvatar->getFlyingHMDPref(); };
auto setter = [myAvatar](bool value) { myAvatar->setFlyingHMDPref(value); };
preferences->addPreference(new CheckPreference(VR_MOVEMENT, "Flying & jumping (HMD)", getter, setter));
auto preference = new CheckPreference(VR_MOVEMENT, "Jumping and flying", getter, setter);
preference->setIndented(true);
preferences->addPreference(preference);
}
{
auto getter = [myAvatar]()->int { return myAvatar->getSnapTurn() ? 0 : 1; };
@ -245,9 +249,16 @@ void setupPreferences() {
auto preference = new RadioButtonsPreference(VR_MOVEMENT, "Snap turn / Smooth turn", getter, setter);
QStringList items;
items << "Snap turn" << "Smooth turn";
preference->setHeading("Rotation mode");
preference->setItems(items);
preferences->addPreference(preference);
}
{
auto getter = [myAvatar]()->bool { return myAvatar->getShowPlayArea(); };
auto setter = [myAvatar](bool value) { myAvatar->setShowPlayArea(value); };
auto preference = new CheckPreference(VR_MOVEMENT, "Show room boundaries while teleporting", getter, setter);
preferences->addPreference(preference);
}
{
auto getter = [=]()->float { return myAvatar->getUserHeight(); };
auto setter = [=](float value) { myAvatar->setUserHeight(value); };
@ -258,12 +269,6 @@ void setupPreferences() {
preference->setStep(0.001f);
preferences->addPreference(preference);
}
{
auto preference = new ButtonPreference(VR_MOVEMENT, "RESET SENSORS", [] {
qApp->resetSensors();
});
preferences->addPreference(preference);
}
static const QString AVATAR_CAMERA{ "Mouse Sensitivity" };
{

View file

@ -178,6 +178,17 @@ namespace controller {
return inputRecorder->getSaveDirectory();
}
QStringList ScriptingInterface::getRunningInputDeviceNames() {
QMutexLocker locker(&_runningDevicesMutex);
return _runningInputDeviceNames;
}
void ScriptingInterface::updateRunningInputDevices(const QString& deviceName, bool isRunning, const QStringList& runningDevices) {
QMutexLocker locker(&_runningDevicesMutex);
_runningInputDeviceNames = runningDevices;
emit inputDeviceRunningChanged(deviceName, isRunning);
}
bool ScriptingInterface::triggerHapticPulseOnDevice(unsigned int device, float strength, float duration, controller::Hand hand) const {
return DependencyManager::get<UserInputMapper>()->triggerHapticPulseOnDevice(device, strength, duration, hand);
}

View file

@ -26,6 +26,7 @@
#include <QThread>
#include <QtCore/QObject>
#include <QtCore/QVariant>
#include <QMutex>
#include <QtQml/QJSValue>
#include <QtScript/QScriptValue>
@ -431,6 +432,13 @@ namespace controller {
*/
Q_INVOKABLE QString getInputRecorderSaveDirectory();
/**jsdoc
* Get all the active and enabled (running) input devices
* @function Controller.getRunningInputDevices
* @returns {string[]} An array of strings with the names
*/
Q_INVOKABLE QStringList getRunningInputDeviceNames();
bool isMouseCaptured() const { return _mouseCaptured; }
bool isTouchCaptured() const { return _touchCaptured; }
bool isWheelCaptured() const { return _wheelCaptured; }
@ -531,6 +539,8 @@ namespace controller {
*/
virtual void releaseActionEvents() { _actionsCaptured = false; }
void updateRunningInputDevices(const QString& deviceName, bool isRunning, const QStringList& runningDevices);
signals:
/**jsdoc
* Triggered when an action occurs.
@ -590,6 +600,17 @@ namespace controller {
*/
void hardwareChanged();
/**jsdoc
* Triggered when a device is enabled/disabled
* Enabling/Disabling Leapmotion on settings/controls will trigger this signal.
* @function Controller.deviceRunningChanged
* @param {string} deviceName - The name of the device that is getting enabled/disabled
* @param {boolean} isEnabled - Return if the device is enabled.
* @returns {Signal}
*/
void inputDeviceRunningChanged(QString deviceName, bool isRunning);
private:
// Update the exposed variant maps reporting active hardware
void updateMaps();
@ -598,10 +619,14 @@ namespace controller {
QVariantMap _actions;
QVariantMap _standard;
QStringList _runningInputDeviceNames;
std::atomic<bool> _mouseCaptured{ false };
std::atomic<bool> _touchCaptured { false };
std::atomic<bool> _wheelCaptured { false };
std::atomic<bool> _actionsCaptured { false };
QMutex _runningDevicesMutex;
};
}

View file

@ -112,6 +112,9 @@ public:
virtual bool suppressKeyboard() { return false; }
virtual void unsuppressKeyboard() {};
virtual bool isKeyboardVisible() { return false; }
virtual QRectF getPlayAreaRect() { return QRectF(); }
virtual QVector<glm::vec3> getSensorPositions() { return QVector<glm::vec3>(); }
};
class DisplayPlugin : public Plugin, public HmdDisplay {

View file

@ -75,8 +75,11 @@ public:
virtual void saveSettings() const {}
virtual void loadSettings() {}
virtual bool isRunning() const { return _active; }
signals:
void deviceStatusChanged(const QString& deviceName, bool isRunning) const;
// These signals should be emitted when a device is first known to be available. In some cases this will
// be in `init()`, in other cases, like Neuron, this isn't known until activation.
// SDL2 isn't a device itself, but can have 0+ subdevices. subdeviceConnected is used in this case.
@ -85,6 +88,7 @@ signals:
protected:
bool _active { false };
bool _enabled { false };
bool _sessionStatus { false };
PluginContainer* _container { nullptr };
static const char* UNKNOWN_PLUGIN_ID;

View file

@ -225,8 +225,11 @@ void PluginManager::disableDisplayPlugin(const QString& name) {
const InputPluginList& PluginManager::getInputPlugins() {
static std::once_flag once;
static auto deviceAddedCallback = [](QString deviceName) {
static auto deviceAddedCallback = [&](QString deviceName) {
qCDebug(plugins) << "Added device: " << deviceName;
QStringList runningDevices = getRunningInputDeviceNames();
bool isDeviceRunning = runningDevices.indexOf(deviceName) >= 0;
emit inputDeviceRunningChanged(deviceName, isDeviceRunning, runningDevices);
UserActivityLogger::getInstance().connectedDevice("input", deviceName);
};
static auto subdeviceAddedCallback = [](QString pluginName, QString deviceName) {
@ -252,6 +255,9 @@ const InputPluginList& PluginManager::getInputPlugins() {
for (auto plugin : _inputPlugins) {
connect(plugin.get(), &Plugin::deviceConnected, this, deviceAddedCallback, Qt::QueuedConnection);
connect(plugin.get(), &Plugin::subdeviceConnected, this, subdeviceAddedCallback, Qt::QueuedConnection);
connect(plugin.get(), &Plugin::deviceStatusChanged, this, [&](const QString& deviceName, bool isRunning) {
emit inputDeviceRunningChanged(deviceName, isRunning, getRunningInputDeviceNames());
}, Qt::QueuedConnection);
plugin->setContainer(_container);
plugin->init();
}
@ -259,6 +265,16 @@ const InputPluginList& PluginManager::getInputPlugins() {
return _inputPlugins;
}
QStringList PluginManager::getRunningInputDeviceNames() const {
QStringList runningDevices;
for (auto plugin: _inputPlugins) {
if (plugin->isRunning()) {
runningDevices << plugin->getName();
}
}
return runningDevices;
}
void PluginManager::setPreferredDisplayPlugins(const QStringList& displays) {
preferredDisplayPlugins = displays;
}

View file

@ -19,6 +19,7 @@ using PluginManagerPointer = QSharedPointer<PluginManager>;
class PluginManager : public QObject, public Dependency {
SINGLETON_DEPENDENCY
Q_OBJECT
public:
static PluginManagerPointer getInstance();
@ -44,6 +45,10 @@ public:
void setInputPluginProvider(const InputPluginProvider& provider);
void setCodecPluginProvider(const CodecPluginProvider& provider);
void setInputPluginSettingsPersister(const InputPluginSettingsPersister& persister);
QStringList getRunningInputDeviceNames() const;
signals:
void inputDeviceRunningChanged(const QString& pluginName, bool isRunning, const QStringList& runningDevices);
private:
PluginManager() = default;

View file

@ -153,4 +153,4 @@ bool PointerManager::isMouse(unsigned int uid) {
return pointer->isMouse();
}
return false;
}
}

View file

@ -26,7 +26,6 @@ layout(location=0) in vec2 varTexCoord0;
layout(location=0) out vec4 outFragColor;
const float FAR_Z = 1.0;
const float LINEAR_DEPTH_BIAS = 5e-3;
const float OPACITY_EPSILON = 5e-3;
<@func main(IS_FILLED)@>
@ -46,7 +45,7 @@ void main(void) {
highlightedDepth = -evalZeyeFromZdb(highlightedDepth);
sceneDepth = -evalZeyeFromZdb(sceneDepth);
if (sceneDepth < (highlightedDepth-LINEAR_DEPTH_BIAS)) {
if (sceneDepth < highlightedDepth) {
outFragColor = vec4(params._fillOccludedColor, params._fillOccludedAlpha);
} else {
outFragColor = vec4(params._fillUnoccludedColor, params._fillUnoccludedAlpha);
@ -107,7 +106,7 @@ void main(void) {
sceneDepth = -evalZeyeFromZdb(sceneDepth);
// Are we occluded?
if (sceneDepth < (outlinedDepth/*-LINEAR_DEPTH_BIAS*/)) {
if (sceneDepth < outlinedDepth) {
outFragColor = vec4(params._outlineOccludedColor, intensity * params._outlineOccludedAlpha);
} else {
outFragColor = vec4(params._outlineUnoccludedColor, intensity * params._outlineUnoccludedAlpha);

View file

@ -340,10 +340,16 @@ public:
class CheckPreference : public BoolPreference {
Q_OBJECT
Q_PROPERTY(bool indented READ getIndented CONSTANT)
public:
CheckPreference(const QString& category, const QString& name, Getter getter, Setter setter)
: BoolPreference(category, name, getter, setter) { }
Type getType() override { return Checkbox; }
bool getIndented() { return _isIndented; }
void setIndented(const bool indented) { _isIndented = indented; }
protected:
bool _isIndented { false };
};
class PrimaryHandPreference : public StringPreference {
@ -356,16 +362,20 @@ public:
class RadioButtonsPreference : public IntPreference {
Q_OBJECT
Q_PROPERTY(QString heading READ getHeading CONSTANT)
Q_PROPERTY(QStringList items READ getItems CONSTANT)
public:
RadioButtonsPreference(const QString& category, const QString& name, Getter getter, Setter setter)
: IntPreference(category, name, getter, setter) { }
Type getType() override { return RadioButtons; }
const QString& getHeading() { return _heading; }
const QStringList& getItems() { return _items; }
void setHeading(const QString& heading) { _heading = heading; }
void setItems(const QStringList& items) { _items = items; }
protected:
QString _heading;
QStringList _items;
};
#endif

View file

@ -552,6 +552,14 @@ glm::vec2 vec2FromVariant(const QVariant &object) {
return vec2FromVariant(object, valid);
}
/**jsdoc
* Defines a rectangular portion of an image or screen, or similar.
* @typedef {object} Rect
* @property {number} x - Left, x-coordinate value.
* @property {number} y - Top, y-coordinate value.
* @property {number} width - Width of the rectangle.
* @property {number} height - Height of the rectangle.
*/
QScriptValue qRectToScriptValue(QScriptEngine* engine, const QRect& rect) {
QScriptValue obj = engine->newObject();
obj.setProperty("x", rect.x());
@ -568,22 +576,6 @@ void qRectFromScriptValue(const QScriptValue &object, QRect& rect) {
rect.setHeight(object.property("height").toVariant().toInt());
}
QScriptValue xColorToScriptValue(QScriptEngine *engine, const xColor& color) {
QScriptValue obj = engine->newObject();
obj.setProperty("red", color.red);
obj.setProperty("green", color.green);
obj.setProperty("blue", color.blue);
return obj;
}
/**jsdoc
* Defines a rectangular portion of an image or screen.
* @typedef {object} Rect
* @property {number} x - Integer left, x-coordinate value.
* @property {number} y - Integer top, y-coordinate value.
* @property {number} width - Integer width of the rectangle.
* @property {number} height - Integer height of the rectangle.
*/
QVariant qRectToVariant(const QRect& rect) {
QVariantMap obj;
obj["x"] = rect.x();
@ -615,6 +607,61 @@ QRect qRectFromVariant(const QVariant& object) {
return qRectFromVariant(object, valid);
}
QScriptValue qRectFToScriptValue(QScriptEngine* engine, const QRectF& rect) {
QScriptValue obj = engine->newObject();
obj.setProperty("x", rect.x());
obj.setProperty("y", rect.y());
obj.setProperty("width", rect.width());
obj.setProperty("height", rect.height());
return obj;
}
void qRectFFromScriptValue(const QScriptValue &object, QRectF& rect) {
rect.setX(object.property("x").toVariant().toFloat());
rect.setY(object.property("y").toVariant().toFloat());
rect.setWidth(object.property("width").toVariant().toFloat());
rect.setHeight(object.property("height").toVariant().toFloat());
}
QVariant qRectFToVariant(const QRectF& rect) {
QVariantMap obj;
obj["x"] = rect.x();
obj["y"] = rect.y();
obj["width"] = rect.width();
obj["height"] = rect.height();
return obj;
}
QRectF qRectFFromVariant(const QVariant& objectVar, bool& valid) {
QVariantMap object = objectVar.toMap();
QRectF rect;
valid = false;
rect.setX(object["x"].toFloat(&valid));
if (valid) {
rect.setY(object["y"].toFloat(&valid));
}
if (valid) {
rect.setWidth(object["width"].toFloat(&valid));
}
if (valid) {
rect.setHeight(object["height"].toFloat(&valid));
}
return rect;
}
QRectF qRectFFromVariant(const QVariant& object) {
bool valid;
return qRectFFromVariant(object, valid);
}
QScriptValue xColorToScriptValue(QScriptEngine *engine, const xColor& color) {
QScriptValue obj = engine->newObject();
obj.setProperty("red", color.red);
obj.setProperty("green", color.green);
obj.setProperty("blue", color.blue);
return obj;
}
void xColorFromScriptValue(const QScriptValue &object, xColor& color) {
if (!object.isValid()) {

View file

@ -100,7 +100,11 @@ void qRectFromScriptValue(const QScriptValue& object, QRect& rect);
QRect qRectFromVariant(const QVariant& object, bool& isValid);
QRect qRectFromVariant(const QVariant& object);
QVariant qRectToVariant(const QRect& rect);
QScriptValue qRectFToScriptValue(QScriptEngine* engine, const QRectF& rect);
void qRectFFromScriptValue(const QScriptValue& object, QRectF& rect);
QRectF qRectFFromVariant(const QVariant& object, bool& isValid);
QRectF qRectFFromVariant(const QVariant& object);
QVariant qRectFToVariant(const QRectF& rect);
// xColor
QScriptValue xColorToScriptValue(QScriptEngine* engine, const xColor& color);

View file

@ -21,7 +21,7 @@
Q_DECLARE_LOGGING_CATEGORY(inputplugins)
Q_LOGGING_CATEGORY(inputplugins, "hifi.inputplugins")
const char* LeapMotionPlugin::NAME = "Leap Motion";
const char* LeapMotionPlugin::NAME = "LeapMotion";
const char* LeapMotionPlugin::LEAPMOTION_ID_STRING = "Leap Motion";
const bool DEFAULT_ENABLED = false;
@ -203,7 +203,6 @@ static const char* getControllerJointName(controller::StandardPoseChannel i) {
return "unknown";
}
void LeapMotionPlugin::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) {
if (!_enabled) {
return;
@ -312,13 +311,13 @@ void LeapMotionPlugin::InputDevice::update(float deltaTime, const controller::In
void LeapMotionPlugin::init() {
loadSettings();
auto preferences = DependencyManager::get<Preferences>();
static const QString LEAPMOTION_PLUGIN { "Leap Motion" };
{
auto getter = [this]()->bool { return _enabled; };
auto setter = [this](bool value) {
_enabled = value;
emit deviceStatusChanged(getName(), isRunning());
saveSettings();
if (!_enabled) {
auto userInputMapper = DependencyManager::get<controller::UserInputMapper>();
@ -406,6 +405,7 @@ void LeapMotionPlugin::loadSettings() {
settings.beginGroup(idString);
{
_enabled = settings.value(SETTINGS_ENABLED_KEY, QVariant(DEFAULT_ENABLED)).toBool();
emit deviceStatusChanged(getName(), isRunning());
_sensorLocation = settings.value(SETTINGS_SENSOR_LOCATION_KEY, QVariant(DEFAULT_SENSOR_LOCATION)).toString();
_desktopHeightOffset =
settings.value(SETTINGS_DESKTOP_HEIGHT_OFFSET_KEY, QVariant(DEFAULT_DESKTOP_HEIGHT_OFFSET)).toFloat();

View file

@ -30,7 +30,7 @@ public:
// Plugin methods
virtual const QString getName() const override { return NAME; }
const QString getID() const override { return LEAPMOTION_ID_STRING; }
bool isRunning() const override { return _active && _enabled; }
virtual void init() override;
virtual bool activate() override;
@ -43,8 +43,6 @@ protected:
static const char* NAME;
static const char* LEAPMOTION_ID_STRING;
const float DEFAULT_DESKTOP_HEIGHT_OFFSET = 0.2f;
bool _enabled { false };
QString _sensorLocation;
float _desktopHeightOffset { DEFAULT_DESKTOP_HEIGHT_OFFSET };

View file

@ -369,7 +369,11 @@ void NeuronPlugin::init() {
static const QString NEURON_PLUGIN { "Perception Neuron" };
{
auto getter = [this]()->bool { return _enabled; };
auto setter = [this](bool value) { _enabled = value; saveSettings(); };
auto setter = [this](bool value) {
_enabled = value;
saveSettings();
emit deviceStatusChanged(getName(), _enabled && _active);
};
auto preference = new CheckPreference(NEURON_PLUGIN, "Enabled", getter, setter);
preferences->addPreference(preference);
}
@ -493,7 +497,7 @@ void NeuronPlugin::loadSettings() {
{
// enabled
_enabled = settings.value("enabled", QVariant(DEFAULT_ENABLED)).toBool();
emit deviceStatusChanged(getName(), _enabled && _active);
// serverAddress
_serverAddress = settings.value("serverAddress", QVariant(DEFAULT_SERVER_ADDRESS)).toString();

View file

@ -30,7 +30,7 @@ public:
virtual bool isSupported() const override;
virtual const QString getName() const override { return NAME; }
const QString getID() const override { return NEURON_ID_STRING; }
bool isRunning() const override { return _active && _enabled; }
virtual bool activate() override;
virtual void deactivate() override;
@ -67,7 +67,6 @@ protected:
static const char* NAME;
static const char* NEURON_ID_STRING;
bool _enabled;
QString _serverAddress;
int _serverPort;
void* _socketRef;

View file

@ -79,10 +79,11 @@ bool SDL2Manager::activate() {
auto preferences = DependencyManager::get<Preferences>();
static const QString SDL2_PLUGIN { "Game Controller" };
{
auto getter = [this]()->bool { return _isEnabled; };
auto getter = [this]()->bool { return _enabled; };
auto setter = [this](bool value) {
_isEnabled = value;
_enabled = value;
saveSettings();
emit deviceStatusChanged(getName(), isRunning());
};
auto preference = new CheckPreference(SDL2_PLUGIN, "Enabled", getter, setter);
preferences->addPreference(preference);
@ -147,7 +148,7 @@ void SDL2Manager::saveSettings() const {
QString idString = getID();
settings.beginGroup(idString);
{
settings.setValue(QString(SETTINGS_ENABLED_KEY), _isEnabled);
settings.setValue(QString(SETTINGS_ENABLED_KEY), _enabled);
}
settings.endGroup();
}
@ -157,7 +158,8 @@ void SDL2Manager::loadSettings() {
QString idString = getID();
settings.beginGroup(idString);
{
_isEnabled = settings.value(SETTINGS_ENABLED_KEY, QVariant(DEFAULT_ENABLED)).toBool();
_enabled = settings.value(SETTINGS_ENABLED_KEY, QVariant(DEFAULT_ENABLED)).toBool();
emit deviceStatusChanged(getName(), isRunning());
}
settings.endGroup();
}
@ -173,7 +175,7 @@ void SDL2Manager::pluginFocusOutEvent() {
}
void SDL2Manager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) {
if (!_isEnabled) {
if (!_enabled) {
return;
}

View file

@ -26,7 +26,7 @@ public:
bool isSupported() const override;
const QString getName() const override { return NAME; }
const QString getID() const override { return SDL2_ID_STRING; }
bool isRunning() const override { return _active && _enabled; }
QStringList getSubdeviceNames() override;
void init() override;
@ -81,7 +81,6 @@ private:
int buttonRelease() const { return SDL_RELEASED; }
QMap<SDL_JoystickID, Joystick::Pointer> _openJoysticks;
bool _isEnabled { false };
bool _isInitialized { false };
static const char* NAME;
static const char* SDL2_ID_STRING;

View file

@ -171,3 +171,53 @@ void OculusBaseDisplayPlugin::updatePresentPose() {
_currentPresentFrameInfo.presentPose = ovr::toGlm(trackingState.HeadPose.ThePose);
_currentPresentFrameInfo.renderPose = _currentPresentFrameInfo.presentPose;
}
QRectF OculusBaseDisplayPlugin::getPlayAreaRect() {
if (!_session) {
return QRectF();
}
int floorPointsCount = 0;
auto result = ovr_GetBoundaryGeometry(_session, ovrBoundary_PlayArea, nullptr, &floorPointsCount);
if (!OVR_SUCCESS(result) || floorPointsCount != 4) {
return QRectF();
}
auto floorPoints = new ovrVector3f[floorPointsCount];
result = ovr_GetBoundaryGeometry(_session, ovrBoundary_PlayArea, floorPoints, nullptr);
if (!OVR_SUCCESS(result)) {
return QRectF();
}
auto minXZ = ovr::toGlm(floorPoints[0]);
auto maxXZ = minXZ;
for (int i = 1; i < floorPointsCount; i++) {
auto point = ovr::toGlm(floorPoints[i]);
minXZ.x = std::min(minXZ.x, point.x);
minXZ.z = std::min(minXZ.z, point.z);
maxXZ.x = std::max(maxXZ.x, point.x);
maxXZ.z = std::max(maxXZ.z, point.z);
}
glm::vec2 center = glm::vec2((minXZ.x + maxXZ.x) / 2, (minXZ.z + maxXZ.z) / 2);
glm::vec2 dimensions = glm::vec2(maxXZ.x - minXZ.x, maxXZ.z - minXZ.z);
return QRectF(center.x, center.y, dimensions.x, dimensions.y);
}
QVector<glm::vec3> OculusBaseDisplayPlugin::getSensorPositions() {
if (!_session) {
return QVector<glm::vec3>();
}
QVector<glm::vec3> result;
auto numTrackers = ovr_GetTrackerCount(_session);
for (uint i = 0; i < numTrackers; i++) {
auto trackerPose = ovr_GetTrackerPose(_session, i);
if (trackerPose.TrackerFlags & ovrTracker_PoseTracked) {
result.append(ovr::toGlm(trackerPose.Pose.Position));
}
}
return result;
}

View file

@ -27,6 +27,9 @@ public:
void resetSensors() override final;
bool beginFrameRender(uint32_t frameIndex) override;
float getTargetFrameRate() const override { return _hmdDesc.DisplayRefreshRate; }
QRectF getPlayAreaRect() override;
QVector<glm::vec3> getSensorPositions() override;
protected:
void customizeContext() override;

View file

@ -750,3 +750,37 @@ QString OpenVrDisplayPlugin::getPreferredAudioOutDevice() const {
}
return device;
}
QRectF OpenVrDisplayPlugin::getPlayAreaRect() {
auto chaperone = vr::VRChaperone();
if (!chaperone) {
qWarning() << "No chaperone";
return QRectF();
}
if (chaperone->GetCalibrationState() >= vr::ChaperoneCalibrationState_Error) {
qWarning() << "Chaperone status =" << chaperone->GetCalibrationState();
return QRectF();
}
vr::HmdQuad_t rect;
if (!chaperone->GetPlayAreaRect(&rect)) {
qWarning() << "Chaperone rect not obtained";
return QRectF();
}
auto minXZ = transformPoint(_sensorResetMat, toGlm(rect.vCorners[0]));
auto maxXZ = minXZ;
for (int i = 1; i < 4; i++) {
auto point = transformPoint(_sensorResetMat, toGlm(rect.vCorners[i]));
minXZ.x = std::min(minXZ.x, point.x);
minXZ.z = std::min(minXZ.z, point.z);
maxXZ.x = std::max(maxXZ.x, point.x);
maxXZ.z = std::max(maxXZ.z, point.z);
}
glm::vec2 center = glm::vec2((minXZ.x + maxXZ.x) / 2, (minXZ.z + maxXZ.z) / 2);
glm::vec2 dimensions = glm::vec2(maxXZ.x - minXZ.x, maxXZ.z - minXZ.z);
return QRectF(center.x, center.y, dimensions.x, dimensions.y);
}

View file

@ -64,6 +64,8 @@ public:
QString getPreferredAudioInDevice() const override;
QString getPreferredAudioOutDevice() const override;
QRectF getPlayAreaRect() override;
protected:
bool internalActivate() override;
void internalDeactivate() override;

Binary file not shown.

Binary file not shown.

View file

@ -21,14 +21,10 @@ Script.include("/~/system/libraries/controllers.js");
(function() { // BEGIN LOCAL_SCOPE
var TARGET_MODEL_URL = Script.resolvePath("../../assets/models/teleport-destination.fbx");
var TARGET_MODEL_URL = Script.resolvePath("../../assets/models/teleportationSpotBasev8.fbx");
var SEAT_MODEL_URL = Script.resolvePath("../../assets/models/teleport-seat.fbx");
var TARGET_MODEL_DIMENSIONS = {
x: 1.15,
y: 0.5,
z: 1.15
};
var TARGET_MODEL_DIMENSIONS = { x: 0.6552, y: 0.3063, z: 0.6552 };
var COLORS_TELEPORT_SEAT = {
red: 255,
@ -59,20 +55,23 @@ Script.include("/~/system/libraries/controllers.js");
var cancelPath = {
color: COLORS_TELEPORT_CANCEL,
alpha: 1,
width: 0.025
alpha: 0.3,
width: 0.025,
drawInFront: true
};
var teleportPath = {
color: COLORS_TELEPORT_CAN_TELEPORT,
alpha: 1,
width: 0.025
alpha: 0.7,
width: 0.025,
drawInFront: true
};
var seatPath = {
color: COLORS_TELEPORT_SEAT,
alpha: 1,
width: 0.025
alpha: 0.7,
width: 0.025,
drawInFront: true
};
var teleportEnd = {
@ -150,19 +149,149 @@ Script.include("/~/system/libraries/controllers.js");
this.teleportParabolaHeadVisuals;
this.teleportParabolaHeadCollisions;
this.PLAY_AREA_OVERLAY_MODEL = Script.resolvePath("../../assets/models/trackingSpacev18.fbx");
this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS = { x: 1.969, y: 0.001, z: 1.969 };
this.PLAY_AREA_FLOAT_ABOVE_FLOOR = 0.005;
this.PLAY_AREA_OVERLAY_OFFSET = // Offset from floor.
{ x: 0, y: this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y / 2 + this.PLAY_AREA_FLOAT_ABOVE_FLOOR, z: 0 };
this.PLAY_AREA_SENSOR_OVERLAY_MODEL = Script.resolvePath("../../assets/models/oculusSensorv11.fbx");
this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS = { x: 0.1198, y: 0.2981, z: 0.1198 };
this.PLAY_AREA_SENSOR_OVERLAY_ROTATION = Quat.fromVec3Degrees({ x: 0, y: -90, z: 0 });
this.PLAY_AREA_BOX_ALPHA = 1.0;
this.PLAY_AREA_SENSOR_ALPHA = 0.8;
this.playAreaSensorPositions = [];
this.playArea = { x: 0, y: 0 };
this.playAreaCenterOffset = this.PLAY_AREA_OVERLAY_OFFSET;
this.isPlayAreaVisible = false;
this.wasPlayAreaVisible = false;
this.isPlayAreaAvailable = false;
this.targetOverlayID = null;
this.playAreaOverlay = null;
this.playAreaSensorPositionOverlays = [];
this.TELEPORT_SCALE_DURATION = 130;
this.TELEPORT_SCALE_TIMEOUT = 25;
this.isTeleportVisible = false;
this.teleportScaleTimer = null;
this.teleportScaleStart = 0;
this.teleportScaleFactor = 0;
this.teleportScaleMode = "head";
this.TELEPORTED_FADE_DELAY_DURATION = 900;
this.TELEPORTED_FADE_DURATION = 200;
this.TELEPORTED_FADE_INTERVAL = 25;
this.TELEPORTED_FADE_DELAY_DELTA = this.TELEPORTED_FADE_INTERVAL / this.TELEPORTED_FADE_DELAY_DURATION;
this.TELEPORTED_FADE_DELTA = this.TELEPORTED_FADE_INTERVAL / this.TELEPORTED_FADE_DURATION;
this.teleportedFadeTimer = null;
this.teleportedFadeDelayFactor = 0;
this.teleportedFadeFactor = 0;
this.teleportedPosition = Vec3.ZERO;
this.TELEPORTED_TARGET_ALPHA = 1.0;
this.TELEPORTED_TARGET_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 });
this.teleportedTargetOverlay = null;
this.setPlayAreaDimensions = function () {
var avatarScale = MyAvatar.sensorToWorldScale;
var playAreaOverlayProperties = {
dimensions:
Vec3.multiply(this.teleportScaleFactor * avatarScale, {
x: this.playArea.width,
y: this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y,
z: this.playArea.height
})
};
if (this.teleportScaleFactor < 1) {
// Adjust position of playAreOverlay so that its base is at correct height.
// Always parenting to teleport target is good enough for this.
var sensorToWorldMatrix = MyAvatar.sensorToWorldMatrix;
var sensorToWorldRotation = Mat4.extractRotation(MyAvatar.sensorToWorldMatrix);
var worldToSensorMatrix = Mat4.inverse(sensorToWorldMatrix);
var avatarSensorPosition = Mat4.transformPoint(worldToSensorMatrix, MyAvatar.position);
avatarSensorPosition.y = 0;
var targetRotation = Overlays.getProperty(this.targetOverlayID, "rotation");
var relativePlayAreaCenterOffset =
Vec3.sum(this.playAreaCenterOffset, { x: 0, y: -TARGET_MODEL_DIMENSIONS.y / 2, z: 0 });
var localPosition = Vec3.multiplyQbyV(Quat.inverse(targetRotation),
Vec3.multiplyQbyV(sensorToWorldRotation,
Vec3.multiply(avatarScale, Vec3.subtract(relativePlayAreaCenterOffset, avatarSensorPosition))));
localPosition.y = this.teleportScaleFactor * localPosition.y;
playAreaOverlayProperties.parentID = this.targetOverlayID;
playAreaOverlayProperties.localPosition = localPosition;
}
Overlays.editOverlay(this.playAreaOverlay, playAreaOverlayProperties);
for (var i = 0; i < this.playAreaSensorPositionOverlays.length; i++) {
localPosition = this.playAreaSensorPositions[i];
localPosition = Vec3.multiply(avatarScale, localPosition);
// Position relative to the play area.
localPosition.y = avatarScale * (this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS.y / 2
- this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y / 2);
Overlays.editOverlay(this.playAreaSensorPositionOverlays[i], {
dimensions: Vec3.multiply(this.teleportScaleFactor * avatarScale, this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS),
parentID: this.playAreaOverlay,
localPosition: localPosition
});
}
};
this.updatePlayAreaScale = function () {
if (this.isPlayAreaAvailable) {
this.setPlayAreaDimensions();
}
};
this.teleporterSelectionName = "teleporterSelection" + hand.toString();
this.TELEPORTER_SELECTION_STYLE = {
outlineUnoccludedColor: { red: 0, green: 0, blue: 0 },
outlineUnoccludedAlpha: 0,
outlineOccludedColor: { red: 0, green: 0, blue: 0 },
outlineOccludedAlpha: 0,
fillUnoccludedColor: { red: 0, green: 0, blue: 0 },
fillUnoccludedAlpha: 0,
fillOccludedColor: { red: 0, green: 0, blue: 255 },
fillOccludedAlpha: 0.84,
outlineWidth: 0,
isOutlineSmooth: false
};
this.addToSelectedItemsList = function (properties) {
for (var i = 0, length = teleportRenderStates.length; i < length; i++) {
var state = properties.renderStates[teleportRenderStates[i].name];
if (state && state.end) {
Selection.addToSelectedItemsList(this.teleporterSelectionName, "overlay", state.end);
}
}
};
this.cleanup = function() {
Selection.removeListFromMap(_this.teleporterSelectionName);
Pointers.removePointer(_this.teleportParabolaHandVisuals);
Pointers.removePointer(_this.teleportParabolaHandCollisions);
Pointers.removePointer(_this.teleportParabolaHeadVisuals);
Pointers.removePointer(_this.teleportParabolaHeadCollisions);
Picks.removePick(_this.teleportHandCollisionPick);
Picks.removePick(_this.teleportHeadCollisionPick);
Overlays.deleteOverlay(_this.teleportedTargetOverlay);
Overlays.deleteOverlay(_this.playAreaOverlay);
for (var i = 0; i < _this.playAreaSensorPositionOverlays.length; i++) {
Overlays.deleteOverlay(_this.playAreaSensorPositionOverlays[i]);
}
_this.playAreaSensorPositionOverlays = [];
};
this.initPointers = function () {
this.initPointers = function() {
if (_this.init) {
_this.cleanup();
}
_this.teleportParabolaHandVisuals = Pointers.createPointer(PickType.Parabola, {
joint: (_this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND",
dirOffset: { x: 0, y: 1, z: 0.1 },
@ -221,6 +350,9 @@ Script.include("/~/system/libraries/controllers.js");
maxDistance: 8.0
});
_this.addToSelectedItemsList(Pointers.getPointerProperties(_this.teleportParabolaHandVisuals));
_this.addToSelectedItemsList(Pointers.getPointerProperties(_this.teleportParabolaHeadVisuals));
var capsuleData = MyAvatar.getCollisionCapsule();
@ -262,11 +394,264 @@ Script.include("/~/system/libraries/controllers.js");
position: { x: 0, y: offset + height * 0.5, z: 0 },
threshold: _this.capsuleThreshold
});
_this.playAreaOverlay = Overlays.addOverlay("model", {
url: _this.PLAY_AREA_OVERLAY_MODEL,
drawInFront: false,
visible: false
});
_this.teleportedTargetOverlay = Overlays.addOverlay("model", {
url: TARGET_MODEL_URL,
alpha: _this.TELEPORTED_TARGET_ALPHA,
visible: false
});
Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", _this.playAreaOverlay);
Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", _this.teleportedTargetOverlay);
_this.playArea = HMD.playArea;
_this.isPlayAreaAvailable = HMD.active && _this.playArea.width !== 0 && _this.playArea.height !== 0;
if (_this.isPlayAreaAvailable) {
_this.playAreaCenterOffset = Vec3.sum({ x: _this.playArea.x, y: 0, z: _this.playArea.y },
_this.PLAY_AREA_OVERLAY_OFFSET);
_this.playAreaSensorPositions = HMD.sensorPositions;
for (var i = 0; i < _this.playAreaSensorPositions.length; i++) {
if (i > _this.playAreaSensorPositionOverlays.length - 1) {
var overlay = Overlays.addOverlay("model", {
url: _this.PLAY_AREA_SENSOR_OVERLAY_MODEL,
dimensions: _this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS,
parentID: _this.playAreaOverlay,
localRotation: _this.PLAY_AREA_SENSOR_OVERLAY_ROTATION,
drawInFront: false,
visible: false
});
_this.playAreaSensorPositionOverlays.push(overlay);
Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", overlay);
}
}
_this.setPlayAreaDimensions();
}
_this.init = true;
}
};
_this.initPointers();
this.translateXAction = Controller.findAction("TranslateX");
this.translateYAction = Controller.findAction("TranslateY");
this.translateZAction = Controller.findAction("TranslateZ");
this.setPlayAreaVisible = function (visible, targetOverlayID, fade) {
if (!this.isPlayAreaAvailable || this.isPlayAreaVisible === visible) {
return;
}
this.wasPlayAreaVisible = this.isPlayAreaVisible;
this.isPlayAreaVisible = visible;
this.targetOverlayID = targetOverlayID;
if (this.teleportedFadeTimer !== null) {
Script.clearTimeout(this.teleportedFadeTimer);
this.teleportedFadeTimer = null;
}
if (visible || !fade) {
// Immediately make visible or invisible.
this.isPlayAreaVisible = visible;
Overlays.editOverlay(this.playAreaOverlay, {
dimensions: Vec3.ZERO,
alpha: this.PLAY_AREA_BOX_ALPHA,
visible: visible
});
for (var i = 0; i < this.playAreaSensorPositionOverlays.length; i++) {
Overlays.editOverlay(this.playAreaSensorPositionOverlays[i], {
dimensions: Vec3.ZERO,
alpha: this.PLAY_AREA_SENSOR_ALPHA,
visible: visible
});
}
Overlays.editOverlay(this.teleportedTargetOverlay, { visible: false });
} else {
// Fading out of overlays is initiated in setTeleportVisible().
}
};
this.updatePlayArea = function (position) {
var sensorToWorldMatrix = MyAvatar.sensorToWorldMatrix;
var sensorToWorldRotation = Mat4.extractRotation(MyAvatar.sensorToWorldMatrix);
var worldToSensorMatrix = Mat4.inverse(sensorToWorldMatrix);
var avatarSensorPosition = Mat4.transformPoint(worldToSensorMatrix, MyAvatar.position);
avatarSensorPosition.y = 0;
var targetXZPosition = { x: position.x, y: 0, z: position.z };
var avatarXZPosition = MyAvatar.position;
avatarXZPosition.y = 0;
var MIN_PARENTING_DISTANCE = 0.2; // Parenting under this distance results in the play area's rotation jittering.
if (Vec3.distance(targetXZPosition, avatarXZPosition) < MIN_PARENTING_DISTANCE) {
// Set play area position and rotation in world coordinates with no parenting.
Overlays.editOverlay(this.playAreaOverlay, {
parentID: Uuid.NULL,
position: Vec3.sum(position,
Vec3.multiplyQbyV(sensorToWorldRotation,
Vec3.multiply(MyAvatar.sensorToWorldScale,
Vec3.subtract(this.playAreaCenterOffset, avatarSensorPosition)))),
rotation: sensorToWorldRotation
});
} else {
// Set play area position and rotation in local coordinates with parenting.
var targetRotation = Overlays.getProperty(this.targetOverlayID, "rotation");
var sensorToTargetRotation = Quat.multiply(Quat.inverse(targetRotation), sensorToWorldRotation);
var relativePlayAreaCenterOffset =
Vec3.sum(this.playAreaCenterOffset, { x: 0, y: -TARGET_MODEL_DIMENSIONS.y / 2, z: 0 });
Overlays.editOverlay(this.playAreaOverlay, {
parentID: this.targetOverlayID,
localPosition: Vec3.multiplyQbyV(Quat.inverse(targetRotation),
Vec3.multiplyQbyV(sensorToWorldRotation,
Vec3.multiply(MyAvatar.sensorToWorldScale,
Vec3.subtract(relativePlayAreaCenterOffset, avatarSensorPosition)))),
localRotation: sensorToTargetRotation
});
}
};
this.scaleInTeleport = function () {
_this.teleportScaleFactor = Math.min((Date.now() - _this.teleportScaleStart) / _this.TELEPORT_SCALE_DURATION, 1);
Pointers.editRenderState(
_this.teleportScaleMode === "head" ? _this.teleportParabolaHeadVisuals : _this.teleportParabolaHandVisuals,
"teleport",
{
path: teleportPath, // Teleport beam disappears if not included.
end: { dimensions: Vec3.multiply(_this.teleportScaleFactor, TARGET_MODEL_DIMENSIONS) }
}
);
if (_this.isPlayAreaVisible) {
_this.setPlayAreaDimensions();
}
if (_this.teleportScaleFactor < 1) {
_this.teleportScaleTimer = Script.setTimeout(_this.scaleInTeleport, _this.TELEPORT_SCALE_TIMEOUT);
} else {
_this.teleportScaleTimer = null;
}
};
this.fadeOutTeleport = function () {
var isAvatarMoving,
i, length;
isAvatarMoving = Controller.getActionValue(_this.translateXAction) !== 0
|| Controller.getActionValue(_this.translateYAction) !== 0
|| Controller.getActionValue(_this.translateZAction) !== 0;
if (_this.teleportedFadeDelayFactor > 0 && !_this.isTeleportVisible && !isAvatarMoving) {
// Delay fade.
_this.teleportedFadeDelayFactor = _this.teleportedFadeDelayFactor - _this.TELEPORTED_FADE_DELAY_DELTA;
_this.teleportedFadeTimer = Script.setTimeout(_this.fadeOutTeleport, _this.TELEPORTED_FADE_INTERVAL);
} else if (_this.teleportedFadeFactor > 0 && !_this.isTeleportVisible && !isAvatarMoving) {
// Fade.
_this.teleportedFadeFactor = _this.teleportedFadeFactor - _this.TELEPORTED_FADE_DELTA;
Overlays.editOverlay(_this.teleportedTargetOverlay, {
alpha: _this.teleportedFadeFactor * _this.TELEPORTED_TARGET_ALPHA
});
if (_this.wasPlayAreaVisible) {
Overlays.editOverlay(_this.playAreaOverlay, {
alpha: _this.teleportedFadeFactor * _this.PLAY_AREA_BOX_ALPHA
});
var sensorAlpha = _this.teleportedFadeFactor * _this.PLAY_AREA_SENSOR_ALPHA;
for (i = 0, length = _this.playAreaSensorPositionOverlays.length; i < length; i++) {
Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { alpha: sensorAlpha });
}
}
_this.teleportedFadeTimer = Script.setTimeout(_this.fadeOutTeleport, _this.TELEPORTED_FADE_INTERVAL);
} else {
// Make invisible.
Overlays.editOverlay(_this.teleportedTargetOverlay, { visible: false });
if (_this.wasPlayAreaVisible) {
Overlays.editOverlay(_this.playAreaOverlay, { visible: false });
for (i = 0, length = _this.playAreaSensorPositionOverlays.length; i < length; i++) {
Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { visible: false });
}
}
_this.teleportedFadeTimer = null;
Selection.disableListHighlight(this.teleporterSelectionName);
}
};
this.cancelFade = function () {
// Other hand may call this to immediately hide fading overlays.
var i, length;
if (this.teleportedFadeTimer) {
Overlays.editOverlay(this.teleportedTargetOverlay, { visible: false });
if (this.wasPlayAreaVisible) {
Overlays.editOverlay(this.playAreaOverlay, { visible: false });
for (i = 0, length = this.playAreaSensorPositionOverlays.length; i < length; i++) {
Overlays.editOverlay(this.playAreaSensorPositionOverlays[i], { visible: false });
}
}
this.teleportedFadeTimer = null;
}
};
this.setTeleportVisible = function (visible, mode, fade) {
// Scales in teleport target and play area when start displaying them.
if (visible === this.isTeleportVisible) {
return;
}
if (visible) {
this.teleportScaleMode = mode;
Pointers.editRenderState(
mode === "head" ? _this.teleportParabolaHeadVisuals : _this.teleportParabolaHandVisuals,
"teleport",
{
path: teleportPath, // Teleport beam disappears if not included.
end: { dimensions: Vec3.ZERO }
}
);
this.getOtherModule().cancelFade();
this.teleportScaleStart = Date.now();
this.teleportScaleFactor = 0;
this.scaleInTeleport();
Selection.enableListHighlight(this.teleporterSelectionName, this.TELEPORTER_SELECTION_STYLE);
} else {
if (this.teleportScaleTimer !== null) {
Script.clearTimeout(this.teleportScaleTimer);
this.teleportScaleTimer = null;
}
if (fade) {
// Copy of target at teleported position for fading.
var avatarScale = MyAvatar.sensorToWorldScale;
Overlays.editOverlay(this.teleportedTargetOverlay, {
position: Vec3.sum(this.teleportedPosition, {
x: 0,
y: -getAvatarFootOffset() + avatarScale * TARGET_MODEL_DIMENSIONS.y / 2,
z: 0
}),
rotation: Quat.multiply(this.TELEPORTED_TARGET_ROTATION, MyAvatar.orientation),
dimensions: Vec3.multiply(avatarScale, TARGET_MODEL_DIMENSIONS),
alpha: this.TELEPORTED_TARGET_ALPHA,
visible: true
});
// Fade out over time.
this.teleportedFadeDelayFactor = 1.0;
this.teleportedFadeFactor = 1.0;
this.teleportedFadeTimer = Script.setTimeout(this.fadeOutTeleport, this.TELEPORTED_FADE_DELAY);
} else {
Selection.disableListHighlight(this.teleporterSelectionName);
}
}
this.isTeleportVisible = visible;
};
this.axisButtonStateX = 0; // Left/right axis button pressed.
this.axisButtonStateY = 0; // Up/down axis button pressed.
this.BUTTON_TRANSITION_DELAY = 100; // Allow time for transition from direction buttons to touch-pad.
@ -379,6 +764,7 @@ Script.include("/~/system/libraries/controllers.js");
this.setTeleportState(mode, "cancel", "collision");
} else if (teleportLocationType === TARGET.SURFACE || teleportLocationType === TARGET.DISCREPANCY) {
this.setTeleportState(mode, "teleport", "collision");
this.updatePlayArea(result.intersection);
} else if (teleportLocationType === TARGET.SEAT) {
this.setTeleportState(mode, "collision", "seat");
}
@ -387,6 +773,7 @@ Script.include("/~/system/libraries/controllers.js");
this.teleport = function(newResult, target) {
var result = newResult;
this.teleportedPosition = newResult.intersection;
if (_this.buttonValue !== 0) {
return makeRunningValues(true, [], []);
}
@ -410,6 +797,8 @@ Script.include("/~/system/libraries/controllers.js");
};
this.disableLasers = function() {
this.setPlayAreaVisible(false, null, true);
this.setTeleportVisible(false, null, true);
Pointers.disablePointer(_this.teleportParabolaHandVisuals);
Pointers.disablePointer(_this.teleportParabolaHandCollisions);
Pointers.disablePointer(_this.teleportParabolaHeadVisuals);
@ -418,14 +807,29 @@ Script.include("/~/system/libraries/controllers.js");
Picks.disablePick(_this.teleportHandCollisionPick);
};
this.setTeleportState = function(mode, visibleState, invisibleState) {
this.teleportState = "";
this.setTeleportState = function (mode, visibleState, invisibleState) {
var teleportState = mode + visibleState + invisibleState;
if (teleportState === this.teleportState) {
return;
}
this.teleportState = teleportState;
var pointerID;
if (mode === 'head') {
Pointers.setRenderState(_this.teleportParabolaHeadVisuals, visibleState);
Pointers.setRenderState(_this.teleportParabolaHeadCollisions, invisibleState);
pointerID = _this.teleportParabolaHeadVisuals;
} else {
Pointers.setRenderState(_this.teleportParabolaHandVisuals, visibleState);
Pointers.setRenderState(_this.teleportParabolaHandCollisions, invisibleState);
pointerID = _this.teleportParabolaHandVisuals;
}
var visible = visibleState === "teleport";
this.setPlayAreaVisible(visible && MyAvatar.showPlayArea,
Pointers.getPointerProperties(pointerID).renderStates.teleport.end, false);
this.setTeleportVisible(visible, mode, false);
};
this.setIgnoreEntities = function(entitiesToIgnore) {
@ -642,4 +1046,9 @@ Script.include("/~/system/libraries/controllers.js");
Messages.subscribe('Hifi-Teleport-Ignore-Remove');
Messages.messageReceived.connect(handleTeleportMessages);
MyAvatar.sensorToWorldScaleChanged.connect(function () {
leftTeleporter.updatePlayAreaScale();
rightTeleporter.updatePlayAreaScale();
});
}()); // END LOCAL_SCOPE

View file

@ -16,7 +16,9 @@
(function () {
var LEAP_MOTION_NAME = "LeapMotion";
var handTouchEnabled = true;
var leapMotionEnabled = Controller.getRunningInputDeviceNames().indexOf(LEAP_MOTION_NAME) >= 0;
var MSECONDS_AFTER_LOAD = 2000;
var updateFingerWithIndex = 0;
var untouchableEntities = [];
@ -870,6 +872,12 @@
handTouchEnabled = !shouldDisable;
});
Controller.inputDeviceRunningChanged.connect(function (deviceName, isEnabled) {
if (deviceName == LEAP_MOTION_NAME) {
leapMotionEnabled = isEnabled;
}
});
MyAvatar.disableHandTouchForIDChanged.connect(function (entityID, disable) {
var entityIndex = untouchableEntities.indexOf(entityID);
if (disable) {
@ -902,7 +910,7 @@
Script.update.connect(function () {
if (!handTouchEnabled) {
if (!handTouchEnabled || leapMotionEnabled) {
return;
}

View file

@ -15,6 +15,7 @@
<script type="text/javascript" src="js/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
<script type="text/javascript" src="js/spinButtons.js"></script>
<script type="text/javascript" src="js/listView.js"></script>
<script type="text/javascript" src="js/entityList.js"></script>
</head>
<body onload='loaded();'>

View file

@ -15,9 +15,31 @@ const VISIBLE_GLYPH = "&#xe007;";
const TRANSPARENCY_GLYPH = "&#xe00b;";
const BAKED_GLYPH = "&#xe01a;"
const SCRIPT_GLYPH = "k";
const BYTES_PER_MEGABYTE = 1024 * 1024;
const IMAGE_MODEL_NAME = 'default-image-model.fbx';
const COLLAPSE_EXTRA_INFO = "E";
const EXPAND_EXTRA_INFO = "D";
const FILTER_IN_VIEW_ATTRIBUTE = "pressed";
const WINDOW_NONVARIABLE_HEIGHT = 207;
const NUM_COLUMNS = 12;
const EMPTY_ENTITY_ID = "0";
const DELETE = 46; // Key code for the delete key.
const KEY_P = 80; // Key code for letter p used for Parenting hotkey.
const MAX_ITEMS = Number.MAX_VALUE; // Used to set the max length of the list of discovered entities.
const COLUMN_INDEX = {
TYPE: 0,
NAME: 1,
URL: 2,
LOCKED: 3,
VISIBLE: 4,
VERTICLES_COUNT: 5,
TEXTURES_COUNT: 6,
TEXTURES_SIZE: 7,
HAS_TRANSPARENT: 8,
IS_BAKED: 9,
DRAW_CALLS: 10,
HAS_SCRIPT: 11
};
const COMPARE_ASCENDING = function(a, b) {
let va = a[currentSortColumn];
@ -37,17 +59,21 @@ const COMPARE_DESCENDING = function(a, b) {
return COMPARE_ASCENDING(b, a);
}
// List of all entities
let entities = []
var entities = []
// List of all entities, indexed by Entity ID
var entitiesByID = {};
// The filtered and sorted list of entities
// The filtered and sorted list of entities passed to ListView
var visibleEntities = [];
// List of all entities that are currently selected
var selectedEntities = [];
var entityList = null; // The ListView
var currentSortColumn = 'type';
var currentSortOrder = ASCENDING_SORT;
var isFilterInView = false;
var showExtraInfo = false;
const ENABLE_PROFILING = false;
var profileIndent = '';
@ -56,19 +82,25 @@ const PROFILE_NOOP = function(_name, fn, args) {
} ;
const PROFILE = !ENABLE_PROFILING ? PROFILE_NOOP : function(name, fn, args) {
console.log("PROFILE-Web " + profileIndent + "(" + name + ") Begin");
var previousIndent = profileIndent;
let previousIndent = profileIndent;
profileIndent += ' ';
var before = Date.now();
let before = Date.now();
fn.apply(this, args);
var delta = Date.now() - before;
let delta = Date.now() - before;
profileIndent = previousIndent;
console.log("PROFILE-Web " + profileIndent + "(" + name + ") End " + delta + "ms");
};
debugPrint = function (message) {
console.log(message);
};
function loaded() {
openEventBridge(function() {
elEntityTable = document.getElementById("entity-table");
elEntityTableBody = document.getElementById("entity-table-body");
elEntityTableScroll = document.getElementById("entity-table-scroll");
elEntityTableHeaderRow = document.querySelectorAll("#entity-table thead th");
elRefresh = document.getElementById("refresh");
elToggleLocked = document.getElementById("locked");
elToggleVisible = document.getElementById("visible");
@ -78,15 +110,13 @@ function loaded() {
elRadius = document.getElementById("radius");
elExport = document.getElementById("export");
elPal = document.getElementById("pal");
elEntityTable = document.getElementById("entity-table");
elInfoToggle = document.getElementById("info-toggle");
elInfoToggleGlyph = elInfoToggle.firstChild;
elFooter = document.getElementById("footer-text");
elNoEntitiesMessage = document.getElementById("no-entities");
elNoEntitiesInView = document.getElementById("no-entities-in-view");
elNoEntitiesRadius = document.getElementById("no-entities-radius");
elEntityTableScroll = document.getElementById("entity-table-scroll");
document.getElementById("entity-name").onclick = function() {
setSortColumn('name');
};
@ -123,14 +153,46 @@ function loaded() {
document.getElementById("entity-hasScript").onclick = function () {
setSortColumn('hasScript');
};
elRefresh.onclick = function() {
refreshEntities();
}
elToggleLocked.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleLocked' }));
}
elToggleVisible.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleVisible' }));
}
elExport.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'export'}));
}
elPal.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'pal' }));
}
elDelete.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
}
elFilter.onkeyup = refreshEntityList;
elFilter.onpaste = refreshEntityList;
elFilter.onchange = onFilterChange;
elFilter.onblur = refreshFooter;
elInView.onclick = toggleFilterInView;
elRadius.onchange = onRadiusChange;
elInfoToggle.onclick = toggleInfo;
elNoEntitiesInView.style.display = "none";
entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow,
createRow, updateRow, clearRow, WINDOW_NONVARIABLE_HEIGHT);
function onRowClicked(clickEvent) {
let entityID = this.dataset.entityID;
let selection = [entityID];
if (clickEvent.ctrlKey) {
let selectedIndex = selectedEntities.indexOf(entityID);
if (selectedIndex >= 0) {
selection = selectedEntities;
selection = [];
selection = selection.concat(selectedEntities);
selection.splice(selectedIndex, 1)
} else {
selection = selection.concat(selectedEntities);
@ -145,35 +207,26 @@ function loaded() {
} else if (previousItemFound === -1 && selectedEntities[0] === entity.id) {
previousItemFound = i;
}
};
}
if (previousItemFound !== -1 && clickedItemFound !== -1) {
let betweenItems = [];
selection = [];
let toItem = Math.max(previousItemFound, clickedItemFound);
// skip first and last item in this loop, we add them to selection after the loop
for (let i = (Math.min(previousItemFound, clickedItemFound) + 1); i < toItem; i++) {
visibleEntities[i].el.className = 'selected';
betweenItems.push(visibleEntities[i].id);
for (let i = Math.min(previousItemFound, clickedItemFound); i <= toItem; i++) {
selection.push(visibleEntities[i].id);
}
if (previousItemFound > clickedItemFound) {
// always make sure that we add the items in the right order
betweenItems.reverse();
selection.reverse();
}
selection = selection.concat(betweenItems, selectedEntities);
}
} else if (!clickEvent.ctrlKey && !clickEvent.shiftKey && selectedEntities.length === 1) {
// if reselecting the same entity then deselect it
if (selectedEntities[0] === entityID) {
selection = [];
}
}
selectedEntities.forEach(function(entityID) {
if (selection.indexOf(entityID) === -1) {
let entity = entitiesByID[entityID];
if (entity !== undefined) {
entity.el.className = '';
}
}
});
selectedEntities = selection;
this.className = 'selected';
updateSelectedEntities(selection);
EventBridge.emitWebEvent(JSON.stringify({
type: "selectionUpdate",
@ -191,9 +244,7 @@ function loaded() {
entityIds: [this.dataset.entityID],
}));
}
const BYTES_PER_MEGABYTE = 1024 * 1024;
function decimalMegabytes(number) {
return number ? (number / BYTES_PER_MEGABYTE).toFixed(1) : "";
}
@ -206,15 +257,12 @@ function loaded() {
let urlParts = url.split('/');
return urlParts[urlParts.length - 1];
}
// Update the entity list with the new set of data sent from edit.js
function updateEntityList(entityData) {
const IMAGE_MODEL_NAME = 'default-image-model.fbx';
entities = []
function updateEntityData(entityData) {
entities = [];
entitiesByID = {};
visibleEntities = [];
visibleEntities.length = 0; // maintains itemData reference in ListView
PROFILE("map-data", function() {
entityData.forEach(function(entity) {
let type = entity.type;
@ -222,7 +270,7 @@ function loaded() {
if (filename === IMAGE_MODEL_NAME) {
type = "Image";
}
let entityData = {
id: entity.id,
name: entity.name,
@ -231,60 +279,25 @@ function loaded() {
fullUrl: entity.url,
locked: entity.locked,
visible: entity.visible,
verticesCount: entity.verticesCount,
texturesCount: entity.texturesCount,
texturesSize: entity.texturesSize,
verticesCount: displayIfNonZero(entity.verticesCount),
texturesCount: displayIfNonZero(entity.texturesCount),
texturesSize: decimalMegabytes(entity.texturesSize),
hasTransparent: entity.hasTransparent,
isBaked: entity.isBaked,
drawCalls: entity.drawCalls,
drawCalls: displayIfNonZero(entity.drawCalls),
hasScript: entity.hasScript,
elRow: null, // if this entity has a visible row element assigned to it
selected: false // if this entity is selected for edit regardless of having a visible row
}
entities.push(entityData);
entitiesByID[entityData.id] = entityData;
});
});
PROFILE("create-rows", function() {
entities.forEach(function(entity) {
let row = document.createElement('tr');
row.dataset.entityID = entity.id;
row.attributes.title = entity.fullUrl;
function addColumn(cls, text) {
let col = document.createElement('td');
col.className = cls;
col.innerText = text;
row.append(col);
}
function addColumnHTML(cls, text) {
let col = document.createElement('td');
col.className = cls;
col.innerHTML = text;
row.append(col);
}
addColumn('type', entity.type);
addColumn('name', entity.name);
addColumn('url', entity.url);
addColumnHTML('locked glyph', entity.locked ? LOCKED_GLYPH : null);
addColumnHTML('visible glyph', entity.visible ? VISIBLE_GLYPH : null);
addColumn('verticesCount', displayIfNonZero(entity.verticesCount));
addColumn('texturesCount', displayIfNonZero(entity.texturesCount));
addColumn('texturesSize', decimalMegabytes(entity.texturesSize));
addColumnHTML('hasTransparent glyph', entity.hasTransparent ? TRANSPARENCY_GLYPH : null);
addColumnHTML('isBaked glyph', entity.isBaked ? BAKED_GLYPH : null);
addColumn('drawCalls', displayIfNonZero(entity.drawCalls));
addColumn('hasScript glyph', entity.hasScript ? SCRIPT_GLYPH : null);
row.addEventListener('click', onRowClicked);
row.addEventListener('dblclick', onRowDoubleClicked);
entity.el = row;
});
});
refreshEntityList();
updateSelectedEntities(selectedEntities);
}
function refreshEntityList() {
PROFILE("refresh-entity-list", function() {
PROFILE("filter", function() {
@ -300,44 +313,95 @@ function loaded() {
});
}
});
PROFILE("sort", function() {
let cmp = currentSortOrder === ASCENDING_SORT ? COMPARE_ASCENDING : COMPARE_DESCENDING;
visibleEntities.sort(cmp);
});
PROFILE("update-dom", function() {
elEntityTableBody.innerHTML = '';
for (let i = 0, len = visibleEntities.length; i < len; ++i) {
elEntityTableBody.append(visibleEntities[i].el);
}
entityList.itemData = visibleEntities;
entityList.refresh();
});
refreshFooter();
refreshNoEntitiesMessage();
});
}
function removeEntities(deletedIDs) {
// Loop from the back so we can pop items off while iterating
// delete any entities matching deletedIDs list from entities and entitiesByID lists
// if the entity had an associated row element then ensure row is unselected and clear it's entity
for (let j = entities.length - 1; j >= 0; --j) {
let id = entities[j];
let id = entities[j].id;
for (let i = 0, length = deletedIDs.length; i < length; ++i) {
if (id === deletedIDs[i]) {
let elRow = entities[j].elRow;
if (elRow) {
elRow.className = '';
elRow.dataset.entityID = EMPTY_ENTITY_ID;
}
entities.splice(j, 1);
entitiesByID[id].el.remove();
delete entitiesByID[id];
break;
}
}
}
refreshEntities();
}
function clearEntities() {
entities = []
entitiesByID = {};
visibleEntities = [];
elEntityTableBody.innerHTML = '';
// delete any entities matching deletedIDs list from selectedEntities list
for (let j = selectedEntities.length - 1; j >= 0; --j) {
let id = selectedEntities[j].id;
for (let i = 0, length = deletedIDs.length; i < length; ++i) {
if (id === deletedIDs[i]) {
selectedEntities.splice(j, 1);
break;
}
}
}
// delete any entities matching deletedIDs list from visibleEntities list
// if this was a row that was above our current row offset (a hidden top row in the top buffer),
// then decrease row offset accordingly
let firstVisibleRow = entityList.getFirstVisibleRowIndex();
for (let j = visibleEntities.length - 1; j >= 0; --j) {
let id = visibleEntities[j].id;
for (let i = 0, length = deletedIDs.length; i < length; ++i) {
if (id === deletedIDs[i]) {
if (j < firstVisibleRow && entityList.rowOffset > 0) {
entityList.rowOffset--;
}
visibleEntities.splice(j, 1);
break;
}
}
}
entityList.refresh();
refreshFooter();
refreshNoEntitiesMessage();
}
function clearEntities() {
// clear the associated entity ID from all visible row elements
let firstVisibleRow = entityList.getFirstVisibleRowIndex();
let lastVisibleRow = entityList.getLastVisibleRowIndex();
for (let i = firstVisibleRow; i <= lastVisibleRow && i < visibleEntities.length; i++) {
let entity = visibleEntities[i];
entity.elRow.dataset.entityID = EMPTY_ENTITY_ID;
}
entities = [];
entitiesByID = {};
visibleEntities.length = 0; // maintains itemData reference in ListView
entityList.resetToTop();
entityList.clear();
refreshFooter();
refreshNoEntitiesMessage();
}
var elSortOrder = {
@ -363,17 +427,18 @@ function loaded() {
currentSortColumn = column;
currentSortOrder = ASCENDING_SORT;
}
elSortOrder[column].innerHTML = currentSortOrder === ASCENDING_SORT ? ASCENDING_STRING : DESCENDING_STRING;
refreshSortOrder();
refreshEntityList();
});
}
setSortColumn('type');
function refreshSortOrder() {
elSortOrder[currentSortColumn].innerHTML = currentSortOrder === ASCENDING_SORT ? ASCENDING_STRING : DESCENDING_STRING;
}
function refreshEntities() {
clearEntities();
EventBridge.emitWebEvent(JSON.stringify({ type: 'refresh' }));
}
function refreshFooter() {
if (selectedEntities.length > 1) {
elFooter.firstChild.nodeValue = selectedEntities.length + " entities selected";
@ -385,76 +450,142 @@ function loaded() {
elFooter.firstChild.nodeValue = visibleEntities.length + " entities found";
}
}
function refreshNoEntitiesMessage() {
if (visibleEntities.length > 0) {
elNoEntitiesMessage.style.display = "none";
} else {
elNoEntitiesMessage.style.display = "block";
}
}
function updateSelectedEntities(selectedIDs) {
let notFound = false;
// reset all currently selected entities and their rows first
selectedEntities.forEach(function(id) {
let entity = entitiesByID[id];
if (entity !== undefined) {
entity.el.className = '';
entity.selected = false;
if (entity.elRow) {
entity.elRow.className = '';
}
}
});
// then reset selected entities list with newly selected entities and set them selected
selectedEntities = [];
for (let i = 0; i < selectedIDs.length; i++) {
let id = selectedIDs[i];
selectedIDs.forEach(function(id) {
selectedEntities.push(id);
let entity = entitiesByID[id];
if (entity !== undefined) {
entity.el.className = 'selected';
entity.selected = true;
if (entity.elRow) {
entity.elRow.className = 'selected';
}
} else {
notFound = true;
}
}
});
refreshFooter();
return notFound;
}
elRefresh.onclick = function() {
refreshEntities();
function isGlyphColumn(columnIndex) {
return columnIndex === COLUMN_INDEX.LOCKED || columnIndex === COLUMN_INDEX.VISIBLE ||
columnIndex === COLUMN_INDEX.HAS_TRANSPARENT || columnIndex === COLUMN_INDEX.IS_BAKED ||
columnIndex === COLUMN_INDEX.HAS_SCRIPT;
}
elToggleLocked.onclick = function () {
EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleLocked' }));
}
elToggleVisible.onclick = function () {
EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleVisible' }));
}
elExport.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'export'}));
}
elPal.onclick = function () {
EventBridge.emitWebEvent(JSON.stringify({ type: 'pal' }));
}
elDelete.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
}
document.addEventListener("keydown", function (keyDownEvent) {
if (keyDownEvent.target.nodeName === "INPUT") {
return;
function createRow() {
let row = document.createElement("tr");
for (let i = 0; i < NUM_COLUMNS; i++) {
let column = document.createElement("td");
if (isGlyphColumn(i)) {
column.className = 'glyph';
}
row.appendChild(column);
}
var keyCode = keyDownEvent.keyCode;
if (keyCode === DELETE) {
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
refreshEntities();
row.onclick = onRowClicked;
row.ondblclick = onRowDoubleClicked;
return row;
}
function updateRow(elRow, itemData) {
// update all column texts and glyphs to this entity's data
let typeCell = elRow.childNodes[COLUMN_INDEX.TYPE];
typeCell.innerText = itemData.type;
let nameCell = elRow.childNodes[COLUMN_INDEX.NAME];
nameCell.innerText = itemData.name;
let urlCell = elRow.childNodes[COLUMN_INDEX.URL];
urlCell.innerText = itemData.url;
let lockedCell = elRow.childNodes[COLUMN_INDEX.LOCKED];
lockedCell.innerHTML = itemData.locked ? LOCKED_GLYPH : null;
let visibleCell = elRow.childNodes[COLUMN_INDEX.VISIBLE];
visibleCell.innerHTML = itemData.visible ? VISIBLE_GLYPH : null;
let verticesCountCell = elRow.childNodes[COLUMN_INDEX.VERTICLES_COUNT];
verticesCountCell.innerText = itemData.verticesCount;
let texturesCountCell = elRow.childNodes[COLUMN_INDEX.TEXTURES_COUNT];
texturesCountCell.innerText = itemData.texturesCount;
let texturesSizeCell = elRow.childNodes[COLUMN_INDEX.TEXTURES_SIZE];
texturesSizeCell.innerText = itemData.texturesSize;
let hasTransparentCell = elRow.childNodes[COLUMN_INDEX.HAS_TRANSPARENT];
hasTransparentCell.innerHTML = itemData.hasTransparent ? TRANSPARENCY_GLYPH : null;
let isBakedCell = elRow.childNodes[COLUMN_INDEX.IS_BAKED];
isBakedCell.innerHTML = itemData.isBaked ? BAKED_GLYPH : null;
let drawCallsCell = elRow.childNodes[COLUMN_INDEX.DRAW_CALLS];
drawCallsCell.innerText = itemData.drawCalls;
let hasScriptCell = elRow.childNodes[COLUMN_INDEX.HAS_SCRIPT];
hasScriptCell.innerHTML = itemData.hasScript ? SCRIPT_GLYPH : null;
// if this entity was previously selected flag it's row as selected
if (itemData.selected) {
elRow.className = 'selected';
} else {
elRow.className = '';
}
if (keyDownEvent.keyCode === KEY_P && keyDownEvent.ctrlKey) {
if (keyDownEvent.shiftKey) {
EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' }));
// if this row previously had an associated entity ID that wasn't the new entity ID then clear
// the ID from the row and the row element from the previous entity's data, then set the new
// entity ID to the row and the row element to the new entity's data
let prevEntityID = elRow.dataset.entityID;
let newEntityID = itemData.id;
let validPrevItemID = prevEntityID !== undefined && prevEntityID !== EMPTY_ENTITY_ID;
if (validPrevItemID && prevEntityID !== newEntityID && entitiesByID[prevEntityID].elRow === elRow) {
elRow.dataset.entityID = EMPTY_ENTITY_ID;
entitiesByID[prevEntityID].elRow = null;
}
if (!validPrevItemID || prevEntityID !== newEntityID) {
elRow.dataset.entityID = newEntityID;
entitiesByID[newEntityID].elRow = elRow;
}
}
function clearRow(elRow) {
// reset all texts and glyphs for each of the row's column
for (let i = 0; i < NUM_COLUMNS; i++) {
let cell = elRow.childNodes[i];
if (isGlyphColumn(i)) {
cell.innerHTML = "";
} else {
EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' }));
cell.innerText = "";
}
}
}, false);
var isFilterInView = false;
var FILTER_IN_VIEW_ATTRIBUTE = "pressed";
elNoEntitiesInView.style.display = "none";
elInView.onclick = function () {
// clear the row from any associated entity
let entityID = elRow.dataset.entityID;
if (entityID && entitiesByID[entityID]) {
entitiesByID[entityID].elRow = null;
}
// reset the row to hidden and clear the entity from the row
elRow.className = '';
elRow.dataset.entityID = EMPTY_ENTITY_ID;
}
function toggleFilterInView() {
isFilterInView = !isFilterInView;
if (isFilterInView) {
elInView.setAttribute(FILTER_IN_VIEW_ATTRIBUTE, FILTER_IN_VIEW_ATTRIBUTE);
@ -466,100 +597,20 @@ function loaded() {
EventBridge.emitWebEvent(JSON.stringify({ type: "filterInView", filterInView: isFilterInView }));
refreshEntities();
}
elRadius.onchange = function () {
function onFilterChange() {
refreshEntityList();
entityList.resize();
}
function onRadiusChange() {
elRadius.value = Math.max(elRadius.value, 0);
elNoEntitiesRadius.firstChild.nodeValue = elRadius.value;
elNoEntitiesMessage.style.display = "none";
EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elRadius.value }));
refreshEntities();
elNoEntitiesRadius.firstChild.nodeValue = elRadius.value;
}
if (window.EventBridge !== undefined) {
EventBridge.scriptEventReceived.connect(function(data) {
data = JSON.parse(data);
if (data.type === "clearEntityList") {
clearEntities();
} else if (data.type == "selectionUpdate") {
var notFound = updateSelectedEntities(data.selectedIDs);
if (notFound) {
refreshEntities();
}
} else if (data.type === "update" && data.selectedIDs !== undefined) {
PROFILE("update", function() {
var newEntities = data.entities;
if (newEntities && newEntities.length === 0) {
elNoEntitiesMessage.style.display = "block";
elFooter.firstChild.nodeValue = "0 entities found";
} else if (newEntities) {
elNoEntitiesMessage.style.display = "none";
updateEntityList(newEntities);
updateSelectedEntities(data.selectedIDs);
}
});
} else if (data.type === "removeEntities" && data.deletedIDs !== undefined && data.selectedIDs !== undefined) {
removeEntities(data.deletedIDs);
updateSelectedEntities(data.selectedIDs);
} else if (data.type === "deleted" && data.ids) {
removeEntities(data.ids);
refreshFooter();
}
});
setTimeout(refreshEntities, 1000);
}
function resize() {
// Take up available window space
elEntityTableScroll.style.height = window.innerHeight - 207;
var SCROLLABAR_WIDTH = 21;
var tds = document.querySelectorAll("#entity-table-body tr:first-child td");
var ths = document.querySelectorAll("#entity-table thead th");
if (tds.length >= ths.length) {
// Update the widths of the header cells to match the body
for (var i = 0; i < ths.length; i++) {
ths[i].width = tds[i].offsetWidth;
}
} else {
// Reasonable widths if nothing is displayed
var tableWidth = document.getElementById("entity-table").offsetWidth - SCROLLABAR_WIDTH;
if (showExtraInfo) {
ths[0].width = 0.10 * tableWidth;
ths[1].width = 0.20 * tableWidth;
ths[2].width = 0.20 * tableWidth;
ths[3].width = 0.04 * tableWidth;
ths[4].width = 0.04 * tableWidth;
ths[5].width = 0.08 * tableWidth;
ths[6].width = 0.08 * tableWidth;
ths[7].width = 0.10 * tableWidth;
ths[8].width = 0.04 * tableWidth;
ths[9].width = 0.08 * tableWidth;
ths[10].width = 0.04 * tableWidth + SCROLLABAR_WIDTH;
} else {
ths[0].width = 0.16 * tableWidth;
ths[1].width = 0.34 * tableWidth;
ths[2].width = 0.34 * tableWidth;
ths[3].width = 0.08 * tableWidth;
ths[4].width = 0.08 * tableWidth;
}
}
};
window.onresize = resize;
elFilter.onkeyup = refreshEntityList;
elFilter.onpaste = refreshEntityList;
elFilter.onchange = function() {
refreshEntityList();
resize();
};
elFilter.onblur = refreshFooter;
var showExtraInfo = false;
var COLLAPSE_EXTRA_INFO = "E";
var EXPAND_EXTRA_INFO = "D";
function toggleInfo(event) {
showExtraInfo = !showExtraInfo;
if (showExtraInfo) {
@ -569,15 +620,62 @@ function loaded() {
elEntityTable.className = "";
elInfoToggleGlyph.innerHTML = EXPAND_EXTRA_INFO;
}
resize();
entityList.resize();
event.stopPropagation();
}
elInfoToggle.addEventListener("click", toggleInfo, true);
resize();
document.addEventListener("keydown", function (keyDownEvent) {
if (keyDownEvent.target.nodeName === "INPUT") {
return;
}
let keyCode = keyDownEvent.keyCode;
if (keyCode === DELETE) {
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
}
if (keyDownEvent.keyCode === KEY_P && keyDownEvent.ctrlKey) {
if (keyDownEvent.shiftKey) {
EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' }));
} else {
EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' }));
}
}
}, false);
if (window.EventBridge !== undefined) {
EventBridge.scriptEventReceived.connect(function(data) {
data = JSON.parse(data);
if (data.type === "clearEntityList") {
clearEntities();
} else if (data.type == "selectionUpdate") {
let notFound = updateSelectedEntities(data.selectedIDs);
if (notFound) {
refreshEntities();
}
} else if (data.type === "update" && data.selectedIDs !== undefined) {
PROFILE("update", function() {
let newEntities = data.entities;
if (newEntities) {
if (newEntities.length === 0) {
clearEntities();
} else {
updateEntityData(newEntities);
updateSelectedEntities(data.selectedIDs);
}
}
});
} else if (data.type === "removeEntities" && data.deletedIDs !== undefined && data.selectedIDs !== undefined) {
removeEntities(data.deletedIDs);
updateSelectedEntities(data.selectedIDs);
} else if (data.type === "deleted" && data.ids) {
removeEntities(data.ids);
}
});
}
refreshSortOrder();
refreshEntities();
});
augmentSpinButtons();
// Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked

View file

@ -0,0 +1,307 @@
// listView.js
//
// Created by David Back on 27 Aug 2018
// Copyright 2018 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
const SCROLL_ROWS = 2; // number of rows used as scrolling buffer, each time we pass this number of rows we scroll
const FIRST_ROW_INDEX = 3; // the first elRow element's index in the child nodes of the table body
debugPrint = function (message) {
console.log(message);
};
function ListView(elTableBody, elTableScroll, elTableHeaderRow, createRowFunction,
updateRowFunction, clearRowFunction, WINDOW_NONVARIABLE_HEIGHT) {
this.elTableBody = elTableBody;
this.elTableScroll = elTableScroll;
this.elTableHeaderRow = elTableHeaderRow;
this.elTopBuffer = null;
this.elBottomBuffer = null;
this.createRowFunction = createRowFunction;
this.updateRowFunction = updateRowFunction;
this.clearRowFunction = clearRowFunction;
// the list of row elements created in the table up to max viewable height plus SCROLL_ROWS rows for scrolling buffer
this.elRows = [];
// the list of all row item data to show in the scrolling table, passed to updateRowFunction to set to each row
this.itemData = [];
// the current index within the itemData list that is set to the top most elRow element
this.rowOffset = 0;
// height of the elRow elements
this.rowHeight = 0;
// the previous elTableScroll.scrollTop value when the elRows were last shifted for scrolling
this.lastRowShiftScrollTop = 0;
this.initialize();
};
ListView.prototype = {
getNumRows: function() {
return this.elRows.length;
},
getScrollHeight: function() {
return this.rowHeight * SCROLL_ROWS;
},
getFirstVisibleRowIndex: function() {
return this.rowOffset;
},
getLastVisibleRowIndex: function() {
return this.getFirstVisibleRowIndex() + entityList.getNumRows() - 1;
},
resetToTop: function() {
this.rowOffset = 0;
this.lastRowShiftScrollTop = 0;
this.refreshBuffers();
this.elTableScroll.scrollTop = 0;
},
clear: function() {
for (let i = 0; i < this.getNumRows(); i++) {
let elRow = this.elTableBody.childNodes[i + FIRST_ROW_INDEX];
this.clearRowFunction(elRow);
elRow.style.display = "none"; // hide cleared rows
}
},
onScroll: function() {
var that = this.listView;
that.scroll();
},
scroll: function() {
let scrollTop = this.elTableScroll.scrollTop;
let scrollHeight = this.getScrollHeight();
let nextRowChangeScrollTop = this.lastRowShiftScrollTop + scrollHeight;
let totalItems = this.itemData.length;
let numRows = this.getNumRows();
// if the top of the scroll area has past the amount of scroll row space since the last point of scrolling and there
// are still more rows to scroll to then trigger a scroll down by the min of the scroll row space or number of
// remaining rows below
// if the top of the scroll area has gone back above the last point of scrolling then trigger a scroll up by min of
// the scroll row space or number of rows above
if (scrollTop >= nextRowChangeScrollTop && numRows + this.rowOffset < totalItems) {
let numScrolls = Math.ceil((scrollTop - nextRowChangeScrollTop) / scrollHeight);
let numScrollRows = numScrolls * SCROLL_ROWS;
if (numScrollRows + this.rowOffset + numRows > totalItems) {
numScrollRows = totalItems - this.rowOffset - numRows;
}
this.scrollRows(numScrollRows);
} else if (scrollTop < this.lastRowShiftScrollTop) {
let numScrolls = Math.ceil((this.lastRowShiftScrollTop - scrollTop) / scrollHeight);
let numScrollRows = numScrolls * SCROLL_ROWS;
if (this.rowOffset - numScrollRows < 0) {
numScrollRows = this.rowOffset;
}
this.scrollRows(-numScrollRows);
}
},
scrollRows: function(numScrollRows) {
let numScrollRowsAbsolute = Math.abs(numScrollRows);
if (numScrollRowsAbsolute === 0) {
return;
}
let scrollDown = numScrollRows > 0;
let prevTopHeight = parseInt(this.elTopBuffer.getAttribute("height"));
let prevBottomHeight = parseInt(this.elBottomBuffer.getAttribute("height"));
// if the number of rows to scroll at once is greater than the total visible number of row elements,
// then just advance the rowOffset accordingly and allow the refresh below to update all rows
if (numScrollRowsAbsolute > this.getNumRows()) {
this.rowOffset += numScrollRows;
} else {
// for each row to scroll down, move the top row element to the bottom of the
// table before the bottom buffer and reset it's row data to the new item
// for each row to scroll up, move the bottom row element to the top of
// the table before the top row and reset it's row data to the new item
for (let i = 0; i < numScrollRowsAbsolute; i++) {
let topRow = this.elTableBody.childNodes[FIRST_ROW_INDEX];
let rowToMove = scrollDown ? topRow : this.elTableBody.childNodes[FIRST_ROW_INDEX + this.getNumRows() - 1];
let rowIndex = scrollDown ? this.getNumRows() + this.rowOffset : this.rowOffset - 1;
let moveRowBefore = scrollDown ? this.elBottomBuffer : topRow;
this.elTableBody.removeChild(rowToMove);
this.elTableBody.insertBefore(rowToMove, moveRowBefore);
this.updateRowFunction(rowToMove, this.itemData[rowIndex]);
this.rowOffset += scrollDown ? 1 : -1;
}
}
// add/remove the row space that was scrolled away to the top buffer height and last scroll point
// add/remove the row space that was scrolled away to the bottom buffer height
let scrolledSpace = this.rowHeight * numScrollRows;
let newTopHeight = prevTopHeight + scrolledSpace;
let newBottomHeight = prevBottomHeight - scrolledSpace;
this.elTopBuffer.setAttribute("height", newTopHeight);
this.elBottomBuffer.setAttribute("height", newBottomHeight);
this.lastRowShiftScrollTop += scrolledSpace;
// if scrolling more than the total number of visible rows at once then refresh all row data
if (numScrollRowsAbsolute > this.getNumRows()) {
this.refresh();
}
},
refresh: function() {
// block refreshing before rows are initialized
let numRows = this.getNumRows();
if (numRows === 0) {
return;
}
let prevScrollTop = this.elTableScroll.scrollTop;
// start with all row data cleared and initially set to invisible
this.clear();
// if we are at the bottom of the list adjust row offset to make sure all rows stay in view
this.refreshRowOffset();
// update all row data and set rows visible until max visible items reached
for (let i = 0; i < numRows; i++) {
let rowIndex = i + this.rowOffset;
if (rowIndex >= this.itemData.length) {
break;
}
let rowElementIndex = i + FIRST_ROW_INDEX;
let elRow = this.elTableBody.childNodes[rowElementIndex];
let itemData = this.itemData[rowIndex];
this.updateRowFunction(elRow, itemData);
elRow.style.display = ""; // make sure the row is visible
}
// update the top and bottom buffer heights to adjust for above changes
this.refreshBuffers();
// adjust the last row shift scroll point based on how much the current scroll point changed
let scrollTopDifference = this.elTableScroll.scrollTop - prevScrollTop;
if (scrollTopDifference !== 0) {
this.lastRowShiftScrollTop += scrollTopDifference;
if (this.lastRowShiftScrollTop < 0) {
this.lastRowShiftScrollTop = 0;
}
}
},
refreshBuffers: function() {
// top buffer height is the number of hidden rows above the top row
let topHiddenRows = this.rowOffset;
let topBufferHeight = this.rowHeight * topHiddenRows;
this.elTopBuffer.setAttribute("height", topBufferHeight);
// bottom buffer height is the number of hidden rows below the bottom row (last scroll buffer row)
let bottomHiddenRows = this.itemData.length - this.getNumRows() - this.rowOffset;
let bottomBufferHeight = this.rowHeight * bottomHiddenRows;
if (bottomHiddenRows < 0) {
bottomBufferHeight = 0;
}
this.elBottomBuffer.setAttribute("height", bottomBufferHeight);
},
refreshRowOffset: function() {
// make sure the row offset isn't causing visible rows to pass the end of the item list and is clamped to 0
var numRows = this.getNumRows();
if (this.rowOffset + numRows > this.itemData.length) {
this.rowOffset = this.itemData.length - numRows;
}
if (this.rowOffset < 0) {
this.rowOffset = 0;
}
},
onResize: function() {
var that = this.listView;
that.resize();
},
resize: function() {
if (!this.elTableBody || !this.elTableScroll) {
debugPrint("ListView.resize - no valid table body or table scroll element");
return;
}
let prevScrollTop = this.elTableScroll.scrollTop;
// take up available window space
this.elTableScroll.style.height = window.innerHeight - WINDOW_NONVARIABLE_HEIGHT;
let viewableHeight = parseInt(this.elTableScroll.style.height) + 1;
// remove all existing row elements and clear row list
for (let i = 0; i < this.getNumRows(); i++) {
let elRow = this.elRows[i];
this.elTableBody.removeChild(elRow);
}
this.elRows = [];
// create new row elements inserted between the top and bottom buffers up until the max viewable scroll area
let usedHeight = 0;
while (usedHeight < viewableHeight) {
let newRow = this.createRowFunction();
this.elTableBody.insertBefore(newRow, this.elBottomBuffer);
this.rowHeight = newRow.offsetHeight;
usedHeight += this.rowHeight;
this.elRows.push(newRow);
}
// add SCROLL_ROWS extras rows for scrolling buffer purposes
for (let i = 0; i < SCROLL_ROWS; i++) {
let scrollRow = this.createRowFunction();
this.elTableBody.insertBefore(scrollRow, this.elBottomBuffer);
this.elRows.push(scrollRow);
}
let ths = this.elTableHeaderRow;
let tds = this.getNumRows() > 0 ? this.elRows[0].childNodes : [];
if (!ths) {
debugPrint("ListView.resize - no valid table header row");
} else if (tds.length !== ths.length) {
debugPrint("ListView.resize - td list size " + tds.length + " does not match th list size " + ths.length);
}
// update the widths of the header cells to match the body cells (using first body row)
for (let i = 0; i < ths.length; i++) {
ths[i].width = tds[i].offsetWidth;
}
// restore the scroll point to the same scroll point from before above changes
this.elTableScroll.scrollTop = prevScrollTop;
this.refresh();
},
initialize: function() {
if (!this.elTableBody || !this.elTableScroll) {
debugPrint("ListView.initialize - no valid table body or table scroll element");
return;
}
// delete initial blank row
this.elTableBody.deleteRow(0);
this.elTopBuffer = document.createElement("tr");
this.elTableBody.appendChild(this.elTopBuffer);
this.elTopBuffer.setAttribute("height", 0);
this.elBottomBuffer = document.createElement("tr");
this.elTableBody.appendChild(this.elBottomBuffer);
this.elBottomBuffer.setAttribute("height", 0);
this.elTableScroll.listView = this;
this.elTableScroll.onscroll = this.onScroll;
window.listView = this;
window.onresize = this.onResize;
// initialize all row elements
this.resize();
}
};