notificationSoundTablet{ QStringLiteral("play_notification_sounds_tablet"), true};
@@ -485,7 +490,11 @@ bool TabletProxy::isPathLoaded(const QVariant& path) {
}
void TabletProxy::setQmlTabletRoot(OffscreenQmlSurface* qmlOffscreenSurface) {
- Q_ASSERT(QThread::currentThread() == qApp->thread());
+ if (QThread::currentThread() != thread()) {
+ QMetaObject::invokeMethod(this, "setQmlTabletRoot", Q_ARG(OffscreenQmlSurface*, qmlOffscreenSurface));
+ return;
+ }
+
_qmlOffscreenSurface = qmlOffscreenSurface;
_qmlTabletRoot = qmlOffscreenSurface ? qmlOffscreenSurface->getRootItem() : nullptr;
if (_qmlTabletRoot && _qmlOffscreenSurface) {
@@ -654,6 +663,11 @@ void TabletProxy::loadQMLSource(const QVariant& path, bool resizable) {
}
void TabletProxy::stopQMLSource() {
+ if (QThread::currentThread() != thread()) {
+ QMetaObject::invokeMethod(this, "stopQMLSource");
+ return;
+ }
+
// For desktop toolbar mode dialogs.
if (!_toolbarMode || !_desktopWindow) {
qCDebug(uiLogging) << "tablet cannot clear QML because not desktop toolbar mode";
@@ -879,6 +893,12 @@ void TabletProxy::sendToQml(const QVariant& msg) {
OffscreenQmlSurface* TabletProxy::getTabletSurface() {
+ if (QThread::currentThread() != thread()) {
+ OffscreenQmlSurface* result = nullptr;
+ BLOCKING_INVOKE_METHOD(this, "getTabletSurface", Q_RETURN_ARG(OffscreenQmlSurface*, result));
+ return result;
+ }
+
return _qmlOffscreenSurface;
}
@@ -888,6 +908,11 @@ void TabletProxy::desktopWindowClosed() {
}
void TabletProxy::unfocus() {
+ if (QThread::currentThread() != thread()) {
+ QMetaObject::invokeMethod(this, "unfocus");
+ return;
+ }
+
if (_qmlOffscreenSurface) {
_qmlOffscreenSurface->lowerKeyboard();
}
diff --git a/scripts/+android_questInterface/defaultScripts.js b/scripts/+android_questInterface/defaultScripts.js
index d22716302c..c294537419 100644
--- a/scripts/+android_questInterface/defaultScripts.js
+++ b/scripts/+android_questInterface/defaultScripts.js
@@ -14,8 +14,8 @@
var DEFAULT_SCRIPTS_COMBINED = [
"system/request-service.js",
"system/progress.js",
- //"system/away.js",
- "system/hmd.js",
+ "system/away.js",
+ //"system/hmd.js",
"system/menu.js",
"system/bubble.js",
"system/pal.js", // "system/mod.js", // older UX, if you prefer
@@ -25,6 +25,7 @@ var DEFAULT_SCRIPTS_COMBINED = [
"system/notifications.js",
"system/commerce/wallet.js",
"system/dialTone.js",
+ "system/marketplaces/marketplaces.js",
"system/quickGoto.js",
"system/firstPersonHMD.js",
"system/tablet-ui/tabletUI.js",
diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml
index f5c0b8c5da..d147585212 100644
--- a/scripts/developer/utilities/render/deferredLighting.qml
+++ b/scripts/developer/utilities/render/deferredLighting.qml
@@ -148,6 +148,27 @@ Rectangle {
}
}
Separator {}
+ Column {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ spacing: 5
+ Repeater {
+ model: [ "MSAA:PrepareFramebuffer:numSamples:4:1"
+ ]
+ ConfigSlider {
+ label: qsTr(modelData.split(":")[0])
+ integral: true
+ config: render.mainViewTask.getConfig(modelData.split(":")[1])
+ property: modelData.split(":")[2]
+ max: modelData.split(":")[3]
+ min: modelData.split(":")[4]
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ }
+ }
+ }
+ Separator {}
Item {
height: childrenRect.height
diff --git a/scripts/system/away.js b/scripts/system/away.js
index 45b6f43b73..2af43b2055 100644
--- a/scripts/system/away.js
+++ b/scripts/system/away.js
@@ -65,7 +65,7 @@ var eventMappingName = "io.highfidelity.away"; // goActive on hand controller bu
var eventMapping = Controller.newMapping(eventMappingName);
var avatarPosition = MyAvatar.position;
var wasHmdMounted = HMD.mounted;
-
+var previousBubbleState = Users.getIgnoreRadiusEnabled();
// some intervals we may create/delete
var avatarMovedInterval;
@@ -154,7 +154,7 @@ function goAway(fromStartup) {
if (!isEnabled || isAway) {
return;
}
-
+
// If we're entering away mode from some other state than startup, then we create our move timer immediately.
// However if we're just stating up, we need to delay this process so that we don't think the initial teleport
// is actually a move.
@@ -166,7 +166,12 @@ function goAway(fromStartup) {
avatarMovedInterval = Script.setInterval(ifAvatarMovedGoActive, BASIC_TIMER_INTERVAL);
}, WAIT_FOR_MOVE_ON_STARTUP);
}
-
+
+ previousBubbleState = Users.getIgnoreRadiusEnabled();
+ if (!previousBubbleState) {
+ Users.toggleIgnoreRadius();
+ }
+ UserActivityLogger.bubbleToggled(Users.getIgnoreRadiusEnabled());
UserActivityLogger.toggledAway(true);
MyAvatar.isAway = true;
}
@@ -179,6 +184,11 @@ function goActive() {
UserActivityLogger.toggledAway(false);
MyAvatar.isAway = false;
+ if (Users.getIgnoreRadiusEnabled() !== previousBubbleState) {
+ Users.toggleIgnoreRadius();
+ UserActivityLogger.bubbleToggled(Users.getIgnoreRadiusEnabled());
+ }
+
if (!Window.hasFocus()) {
Window.setFocus();
}
diff --git a/scripts/system/controllers/controllerModules/farGrabEntity.js b/scripts/system/controllers/controllerModules/farGrabEntity.js
index 197a809e91..65a3671cae 100644
--- a/scripts/system/controllers/controllerModules/farGrabEntity.js
+++ b/scripts/system/controllers/controllerModules/farGrabEntity.js
@@ -60,6 +60,14 @@ Script.include("/~/system/libraries/controllers.js");
this.reticleMaxY = 0;
this.endedGrab = 0;
this.MIN_HAPTIC_PULSE_INTERVAL = 500; // ms
+ this.disabled = false;
+ var _this = this;
+ this.leftTrigger = 0.0;
+ this.rightTrigger = 0.0;
+ this.initialControllerRotation = Quat.IDENTITY;
+ this.currentControllerRotation = Quat.IDENTITY;
+ this.manipulating = false;
+ this.wasManipulating = false;
var FAR_GRAB_JOINTS = [65527, 65528]; // FARGRAB_LEFTHAND_INDEX, FARGRAB_RIGHTHAND_INDEX
@@ -75,6 +83,45 @@ Script.include("/~/system/libraries/controllers.js");
100,
makeLaserParams(this.hand, false));
+ this.getOtherModule = function () {
+ return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("LeftFarGrabEntity") : ("RightFarGrabEntity"));
+ };
+
+ // Get the rotation of the fargrabbed entity.
+ this.getTargetRotation = function () {
+ if (this.targetIsNull()) {
+ return null;
+ } else {
+ var props = Entities.getEntityProperties(this.targetEntityID, ["rotation"]);
+ return props.rotation;
+ }
+ };
+
+ this.getOffhand = function () {
+ return (this.hand === RIGHT_HAND ? LEFT_HAND : RIGHT_HAND);
+ }
+
+ this.getOffhandTrigger = function () {
+ return (_this.hand === RIGHT_HAND ? _this.leftTrigger : _this.rightTrigger);
+ }
+
+ // Activation criteria for rotating a fargrabbed entity. If we're changing the mapping, this is where to do it.
+ this.shouldManipulateTarget = function () {
+ return (_this.getOffhandTrigger() > TRIGGER_ON_VALUE) ? true : false;
+ };
+
+ // Get the delta between the current rotation and where the controller was when manipulation started.
+ this.calculateEntityRotationManipulation = function (controllerRotation) {
+ return Quat.multiply(controllerRotation, Quat.inverse(this.initialControllerRotation));
+ };
+
+ this.setJointTranslation = function (newTargetPosLocal) {
+ MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal);
+ };
+
+ this.setJointRotation = function (newTargetRotLocal) {
+ MyAvatar.setJointRotation(FAR_GRAB_JOINTS[this.hand], newTargetRotLocal);
+ };
this.handToController = function() {
return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
@@ -142,8 +189,9 @@ Script.include("/~/system/libraries/controllers.js");
Messages.sendLocalMessage('Hifi-unhighlight-entity', JSON.stringify(message));
var newTargetPosLocal = MyAvatar.worldToJointPoint(targetProps.position);
- MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal);
- MyAvatar.setJointRotation(FAR_GRAB_JOINTS[this.hand], { x: 0, y: 0, z: 0, w: 1 });
+ var newTargetRotLocal = targetProps.rotation;
+ this.setJointTranslation(newTargetPosLocal);
+ this.setJointRotation(newTargetRotLocal);
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(targetProps.id, "startDistanceGrab", args);
@@ -227,12 +275,44 @@ Script.include("/~/system/libraries/controllers.js");
newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition);
newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition);
- // MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], MyAvatar.worldToJointPoint(newTargetPosition));
-
- // var newTargetPosLocal = Mat4.transformPoint(MyAvatar.getSensorToWorldMatrix(), newTargetPosition);
var newTargetPosLocal = MyAvatar.worldToJointPoint(newTargetPosition);
- MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal);
- MyAvatar.setJointRotation(FAR_GRAB_JOINTS[this.hand], { x: 0, y: 0, z: 0, w: 1 });
+
+ // This block handles the user's ability to rotate the object they're FarGrabbing
+ if (this.shouldManipulateTarget()) {
+ // Get the pose of the controller that is not grabbing.
+ var pose = Controller.getPoseValue((this.getOffhand() ? Controller.Standard.RightHand : Controller.Standard.LeftHand));
+ if (pose.valid) {
+ // If we weren't manipulating the object yet, initialize the entity's original position.
+ if (!this.manipulating) {
+ // This will only be triggered if we've let go of the off-hand trigger and pulled it again without ending a grab.
+ // Need to poll the entity's rotation again here.
+ if (!this.wasManipulating) {
+ this.initialEntityRotation = this.getTargetRotation();
+ }
+ // Save the original controller orientation, we only care about the delta between this rotation and wherever
+ // the controller rotates, so that we can apply it to the entity's rotation.
+ this.initialControllerRotation = Quat.multiply(pose.rotation, MyAvatar.orientation);
+ this.manipulating = true;
+ }
+ }
+
+ var rot = Quat.multiply(pose.rotation, MyAvatar.orientation);
+ var rotBetween = this.calculateEntityRotationManipulation(rot);
+ var doubleRot = Quat.multiply(rotBetween, rotBetween);
+ this.lastJointRotation = Quat.multiply(doubleRot, this.initialEntityRotation);
+ this.setJointRotation(this.lastJointRotation);
+ } else {
+ // If we were manipulating but the user isn't currently expressing this intent, we want to know so we preserve the rotation
+ // between manipulations without ending the fargrab.
+ if (this.manipulating) {
+ this.initialEntityRotation = this.lastJointRotation;
+ this.wasManipulating = true;
+ }
+ this.manipulating = false;
+ // Reset the inital controller position.
+ this.initialControllerRotation = Quat.IDENTITY;
+ }
+ this.setJointTranslation(newTargetPosLocal);
this.previousRoomControllerPosition = roomControllerPosition;
};
@@ -254,9 +334,15 @@ Script.include("/~/system/libraries/controllers.js");
}));
unhighlightTargetEntity(this.targetEntityID);
this.grabbing = false;
- this.targetEntityID = null;
this.potentialEntityWithContextOverlay = false;
MyAvatar.clearJointData(FAR_GRAB_JOINTS[this.hand]);
+ this.initialEntityRotation = Quat.IDENTITY;
+ this.initialControllerRotation = Quat.IDENTITY;
+ this.targetEntityID = null;
+ this.manipulating = false;
+ this.wasManipulating = false;
+ var otherModule = this.getOtherModule();
+ otherModule.disabled = false;
};
this.updateRecommendedArea = function() {
@@ -326,7 +412,9 @@ Script.include("/~/system/libraries/controllers.js");
this.distanceHolding = false;
- if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) {
+ if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE && !this.disabled) {
+ var otherModule = this.getOtherModule();
+ otherModule.disabled = true;
return makeRunningValues(true, [], []);
} else {
this.destroyContextOverlay();
@@ -336,6 +424,8 @@ Script.include("/~/system/libraries/controllers.js");
};
this.run = function (controllerData) {
+ this.leftTrigger = controllerData.triggerValues[LEFT_HAND];
+ this.rightTrigger = controllerData.triggerValues[RIGHT_HAND];
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || this.targetIsNull()) {
this.endFarGrabEntity(controllerData);
return makeRunningValues(false, [], []);
diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js
index efbca66d72..f7d5b5a2dd 100644
--- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js
+++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js
@@ -30,6 +30,20 @@
100,
makeLaserParams((this.hand + HUD_LASER_OFFSET), false));
+ this.getFarGrab = function () {
+ return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("RightFarGrabEntity") : ("LeftFarGrabEntity"));
+ }
+
+ this.farGrabActive = function () {
+ var farGrab = this.getFarGrab();
+ // farGrab will be null if module isn't loaded.
+ if (farGrab) {
+ return farGrab.targetIsNull();
+ } else {
+ return false;
+ }
+ };
+
this.getOtherHandController = function() {
return (this.hand === RIGHT_HAND) ? Controller.Standard.LeftHand : Controller.Standard.RightHand;
};
@@ -79,7 +93,7 @@
this.isReady = function (controllerData) {
var otherModuleRunning = this.getOtherModule().running;
- if (!otherModuleRunning && HMD.active) {
+ if (!otherModuleRunning && HMD.active && !this.farGrabActive()) {
if (this.processLaser(controllerData)) {
this.running = true;
return ControllerDispatcherUtils.makeRunningValues(true, [], []);
diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
index ec35dfe081..4f21b44533 100644
--- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
+++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
@@ -37,6 +37,20 @@ Script.include("/~/system/libraries/controllers.js");
100,
makeLaserParams(hand, true));
+ this.getFarGrab = function () {
+ return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("RightFarGrabEntity") : ("LeftFarGrabEntity"));
+ };
+
+ this.farGrabActive = function () {
+ var farGrab = this.getFarGrab();
+ // farGrab will be null if module isn't loaded.
+ if (farGrab) {
+ return farGrab.targetIsNull();
+ } else {
+ return false;
+ }
+ };
+
this.grabModuleWantsNearbyOverlay = function(controllerData) {
if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE || controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) {
var nearGrabName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay";
@@ -184,7 +198,12 @@ Script.include("/~/system/libraries/controllers.js");
this.dominantHandOverride = false;
- this.isReady = function(controllerData) {
+ this.isReady = function (controllerData) {
+ // Trivial rejection for when FarGrab is active.
+ if (this.farGrabActive()) {
+ return makeRunningValues(false, [], []);
+ }
+
var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE &&
controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE;
var type = this.getInteractableType(controllerData, isTriggerPressed, false);
diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js
index 6f74b43a8e..508e8d46e3 100644
--- a/scripts/system/libraries/utils.js
+++ b/scripts/system/libraries/utils.js
@@ -418,13 +418,11 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride)
var HOME_BUTTON_Z_OFFSET = (tabletDepth / 1.9) * sensorScaleOffsetOverride;
Entities.editEntity(HMD.homeButtonID, {
localPosition: { x: HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
- localRotation: { x: 0, y: 1, z: 0, w: 0 },
dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
});
Entities.editEntity(HMD.homeButtonHighlightID, {
localPosition: { x: -HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
- localRotation: { x: 0, y: 1, z: 0, w: 0 },
dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
});
};
@@ -482,13 +480,11 @@ reparentAndScaleTablet = function(width, reparentProps) {
var HOME_BUTTON_Z_OFFSET = (tabletDepth / 1.9) * sensorScaleOffsetOverride;
Entities.editEntity(HMD.homeButtonID, {
localPosition: { x: HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
- localRotation: { x: 0, y: 1, z: 0, w: 0 },
dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
});
Entities.editEntity(HMD.homeButtonHighlightID, {
localPosition: { x: -HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
- localRotation: { x: 0, y: 1, z: 0, w: 0 },
dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
});
}
diff --git a/scripts/system/modules/createWindow.js b/scripts/system/modules/createWindow.js
index 0c4412abfb..7369cf91f8 100644
--- a/scripts/system/modules/createWindow.js
+++ b/scripts/system/modules/createWindow.js
@@ -125,9 +125,6 @@ module.exports = (function() {
Script.scriptEnding.connect(this, function() {
this.window.close();
- // FIXME: temp solution for reload crash (MS18269),
- // we should decide on proper object ownership strategy for InteractiveWindow API
- this.window = null;
});
},
setVisible: function(visible) {
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index 6cda67db2d..b9ae635a4f 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -20,7 +20,7 @@ endfunction()
if (BUILD_TOOLS)
# Allow different tools for stable builds
- if (RELEASE_TYPE STREQUAL "PRODUCTION")
+ if (STABLE_BUILD)
set(ALL_TOOLS
udt-test
vhacd-util
diff --git a/tools/nitpick/CMakeLists.txt b/tools/nitpick/CMakeLists.txt
index e69b16b866..44eace5e70 100644
--- a/tools/nitpick/CMakeLists.txt
+++ b/tools/nitpick/CMakeLists.txt
@@ -80,7 +80,9 @@ else ()
add_executable(${TARGET_NAME} ${NITPICK_SRCS} ${QM})
endif ()
-add_dependencies(${TARGET_NAME} resources)
+if (NOT UNIX)
+ add_dependencies(${TARGET_NAME} resources)
+endif()
# disable /OPT:REF and /OPT:ICF for the Debug builds
# This will prevent the following linker warnings
diff --git a/tools/nitpick/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp
index 7e11bd80fc..04b7e7980e 100644
--- a/tools/nitpick/src/AWSInterface.cpp
+++ b/tools/nitpick/src/AWSInterface.cpp
@@ -27,7 +27,10 @@ AWSInterface::AWSInterface(QObject* parent) : QObject(parent) {
void AWSInterface::createWebPageFromResults(const QString& testResults,
const QString& workingDirectory,
QCheckBox* updateAWSCheckBox,
- QLineEdit* urlLineEdit) {
+ QRadioButton* diffImageRadioButton,
+ QRadioButton* ssimImageRadionButton,
+ QLineEdit* urlLineEdit
+) {
_workingDirectory = workingDirectory;
// Verify filename is in correct format
@@ -52,6 +55,13 @@ void AWSInterface::createWebPageFromResults(const QString& testResults,
QString zipFilenameWithoutExtension = zipFilename.split('.')[0];
extractTestFailuresFromZippedFolder(_workingDirectory + "/" + zipFilenameWithoutExtension);
+
+ if (diffImageRadioButton->isChecked()) {
+ _comparisonImageFilename = "Difference Image.png";
+ } else {
+ _comparisonImageFilename = "SSIM Image.png";
+ }
+
createHTMLFile();
if (updateAWSCheckBox->isChecked()) {
@@ -353,7 +363,7 @@ void AWSInterface::openTable(QTextStream& stream, const QString& testResult, con
stream << "\t\t\t\tTest | \n";
stream << "\t\t\t\tActual Image | \n";
stream << "\t\t\t\tExpected Image | \n";
- stream << "\t\t\t\tDifference Image | \n";
+ stream << "\t\t\t\tComparison Image | \n";
stream << "\t\t\t\n";
}
}
@@ -378,12 +388,13 @@ void AWSInterface::createEntry(const int index, const QString& testResult, QText
QString folder;
bool differenceFileFound;
+
if (isFailure) {
folder = FAILURES_FOLDER;
- differenceFileFound = QFile::exists(_htmlFailuresFolder + "/" + resultName + "/Difference Image.png");
+ differenceFileFound = QFile::exists(_htmlFailuresFolder + "/" + resultName + "/" + _comparisonImageFilename);
} else {
folder = SUCCESSES_FOLDER;
- differenceFileFound = QFile::exists(_htmlSuccessesFolder + "/" + resultName + "/Difference Image.png");
+ differenceFileFound = QFile::exists(_htmlSuccessesFolder + "/" + resultName + "/" + _comparisonImageFilename);
}
if (textResultsFileFound) {
@@ -450,7 +461,7 @@ void AWSInterface::createEntry(const int index, const QString& testResult, QText
stream << "\t\t\t\t | \n";
if (differenceFileFound) {
- stream << "\t\t\t\t | \n";
+ stream << "\t\t\t\t | \n";
} else {
stream << "\t\t\t\tNo Image Found\n";
}
@@ -512,12 +523,12 @@ void AWSInterface::updateAWS() {
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n";
- if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) {
+ if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/" + _comparisonImageFilename)) {
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
- << "Difference Image.png"
+ << _comparisonImageFilename
<< "', 'rb')\n";
- stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n";
+ stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << _comparisonImageFilename << "', Body=data)\n\n";
}
}
}
@@ -555,12 +566,12 @@ void AWSInterface::updateAWS() {
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n";
- if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) {
+ if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/" + _comparisonImageFilename)) {
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
- << "Difference Image.png"
+ << _comparisonImageFilename
<< "', 'rb')\n";
- stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n";
+ stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << _comparisonImageFilename << "', Body=data)\n\n";
}
}
}
diff --git a/tools/nitpick/src/AWSInterface.h b/tools/nitpick/src/AWSInterface.h
index d95b8ecf2f..77d500fa7c 100644
--- a/tools/nitpick/src/AWSInterface.h
+++ b/tools/nitpick/src/AWSInterface.h
@@ -14,6 +14,7 @@
#include
#include
#include
+#include
#include
#include "BusyWindow.h"
@@ -28,6 +29,8 @@ public:
void createWebPageFromResults(const QString& testResults,
const QString& workingDirectory,
QCheckBox* updateAWSCheckBox,
+ QRadioButton* diffImageRadioButton,
+ QRadioButton* ssimImageRadionButton,
QLineEdit* urlLineEdit);
void extractTestFailuresFromZippedFolder(const QString& folderName);
@@ -67,6 +70,9 @@ private:
QString AWS_BUCKET{ "hifi-qa" };
QLineEdit* _urlLineEdit;
+
+
+ QString _comparisonImageFilename;
};
#endif // hifi_AWSInterface_h
\ No newline at end of file
diff --git a/tools/nitpick/src/ImageComparer.cpp b/tools/nitpick/src/ImageComparer.cpp
index fa73f97887..7e3e6eaf63 100644
--- a/tools/nitpick/src/ImageComparer.cpp
+++ b/tools/nitpick/src/ImageComparer.cpp
@@ -14,7 +14,7 @@
// Computes SSIM - see https://en.wikipedia.org/wiki/Structural_similarity
// The value is computed for the luminance component and the average value is returned
-double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) const {
+void ImageComparer::compareImages(const QImage& resultImage, const QImage& expectedImage) {
const int L = 255; // (2^number of bits per pixel) - 1
const double K1 { 0.01 };
@@ -39,8 +39,13 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co
double p[WIN_SIZE * WIN_SIZE];
double q[WIN_SIZE * WIN_SIZE];
+ _ssimResults.results.clear();
+
int windowCounter{ 0 };
double ssim{ 0.0 };
+ double min { 1.0 };
+ double max { -1.0 };
+
while (x < expectedImage.width()) {
int lastX = x + WIN_SIZE - 1;
if (lastX > expectedImage.width() - 1) {
@@ -96,7 +101,13 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co
double numerator = (2.0 * mP * mQ + c1) * (2.0 * sigPQ + c2);
double denominator = (mP * mP + mQ * mQ + c1) * (sigsqP + sigsqQ + c2);
- ssim += numerator / denominator;
+ double value { numerator / denominator };
+ _ssimResults.results.push_back(value);
+ ssim += value;
+
+ if (value < min) min = value;
+ if (value > max) max = value;
+
++windowCounter;
y += WIN_SIZE;
@@ -106,5 +117,17 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co
y = 0;
}
- return ssim / windowCounter;
-};
\ No newline at end of file
+ _ssimResults.width = (int)(expectedImage.width() / WIN_SIZE);
+ _ssimResults.height = (int)(expectedImage.height() / WIN_SIZE);
+ _ssimResults.min = min;
+ _ssimResults.max = max;
+ _ssimResults.ssim = ssim / windowCounter;
+};
+
+double ImageComparer::getSSIMValue() {
+ return _ssimResults.ssim;
+}
+
+SSIMResults ImageComparer::getSSIMResults() {
+ return _ssimResults;
+}
diff --git a/tools/nitpick/src/ImageComparer.h b/tools/nitpick/src/ImageComparer.h
index 7b7b8b0b74..fc14dab94d 100644
--- a/tools/nitpick/src/ImageComparer.h
+++ b/tools/nitpick/src/ImageComparer.h
@@ -10,12 +10,20 @@
#ifndef hifi_ImageComparer_h
#define hifi_ImageComparer_h
+#include "common.h"
+
#include
#include
class ImageComparer {
public:
- double compareImages(QImage resultImage, QImage expectedImage) const;
+ void compareImages(const QImage& resultImage, const QImage& expectedImage);
+ double getSSIMValue();
+
+ SSIMResults getSSIMResults();
+
+private:
+ SSIMResults _ssimResults;
};
#endif // hifi_ImageComparer_h
diff --git a/tools/nitpick/src/MismatchWindow.cpp b/tools/nitpick/src/MismatchWindow.cpp
index 58189b4795..fd5df0dd4e 100644
--- a/tools/nitpick/src/MismatchWindow.cpp
+++ b/tools/nitpick/src/MismatchWindow.cpp
@@ -21,7 +21,7 @@ MismatchWindow::MismatchWindow(QWidget *parent) : QDialog(parent) {
diffImage->setScaledContents(true);
}
-QPixmap MismatchWindow::computeDiffPixmap(QImage expectedImage, QImage resultImage) {
+QPixmap MismatchWindow::computeDiffPixmap(const QImage& expectedImage, const QImage& resultImage) {
// Create an empty difference image if the images differ in size
if (expectedImage.height() != resultImage.height() || expectedImage.width() != resultImage.width()) {
return QPixmap();
@@ -60,7 +60,7 @@ QPixmap MismatchWindow::computeDiffPixmap(QImage expectedImage, QImage resultIma
return resultPixmap;
}
-void MismatchWindow::setTestResult(TestResult testResult) {
+void MismatchWindow::setTestResult(const TestResult& testResult) {
errorLabel->setText("Similarity: " + QString::number(testResult._error));
imagePath->setText("Path to test: " + testResult._pathname);
@@ -99,3 +99,36 @@ void MismatchWindow::on_abortTestsButton_clicked() {
QPixmap MismatchWindow::getComparisonImage() {
return _diffPixmap;
}
+
+QPixmap MismatchWindow::getSSIMResultsImage(const SSIMResults& ssimResults) {
+ // This is an optimization, as QImage.setPixel() is embarrassingly slow
+ const int ELEMENT_SIZE { 8 };
+ const int WIDTH{ ssimResults.width * ELEMENT_SIZE };
+ const int HEIGHT{ ssimResults.height * ELEMENT_SIZE };
+
+ unsigned char* buffer = new unsigned char[WIDTH * HEIGHT * 3];
+
+
+ // loop over each SSIM result
+ for (int y = 0; y < ssimResults.height; ++y) {
+ for (int x = 0; x < ssimResults.width; ++x) {
+ double scaledResult = (ssimResults.results[x * ssimResults.height + y] + 1.0) / (2.0);
+ //double scaledResult = (ssimResults.results[x * ssimResults.height + y] - ssimResults.min) / (ssimResults.max - ssimResults.min);
+ // Create a square
+ for (int yy = 0; yy < ELEMENT_SIZE; ++yy) {
+ for (int xx = 0; xx < ELEMENT_SIZE; ++xx) {
+ buffer[(xx + yy * WIDTH + x * ELEMENT_SIZE + y * WIDTH * ELEMENT_SIZE) * 3 + 0] = 255 * (1.0 - scaledResult); // R
+ buffer[(xx + yy * WIDTH + x * ELEMENT_SIZE + y * WIDTH * ELEMENT_SIZE) * 3 + 1] = 255 * scaledResult; // G
+ buffer[(xx + yy * WIDTH + x * ELEMENT_SIZE + y * WIDTH * ELEMENT_SIZE) * 3 + 2] = 0; // B
+ }
+ }
+ }
+ }
+
+ QImage image(buffer, WIDTH, HEIGHT, QImage::Format_RGB888);
+ QPixmap pixmap = QPixmap::fromImage(image);
+
+ delete[] buffer;
+
+ return pixmap;
+}
diff --git a/tools/nitpick/src/MismatchWindow.h b/tools/nitpick/src/MismatchWindow.h
index 040e0b8bf1..116d35dfc5 100644
--- a/tools/nitpick/src/MismatchWindow.h
+++ b/tools/nitpick/src/MismatchWindow.h
@@ -20,12 +20,14 @@ class MismatchWindow : public QDialog, public Ui::MismatchWindow {
public:
MismatchWindow(QWidget *parent = Q_NULLPTR);
- void setTestResult(TestResult testResult);
+ void setTestResult(const TestResult& testResult);
UserResponse getUserResponse() { return _userResponse; }
- QPixmap computeDiffPixmap(QImage expectedImage, QImage resultImage);
+ QPixmap computeDiffPixmap(const QImage& expectedImage, const QImage& resultImage);
+
QPixmap getComparisonImage();
+ QPixmap getSSIMResultsImage(const SSIMResults& ssimResults);
private slots:
void on_passTestButton_clicked();
diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp
index bf9b9c11ba..d8a1ff486a 100644
--- a/tools/nitpick/src/Nitpick.cpp
+++ b/tools/nitpick/src/Nitpick.cpp
@@ -150,10 +150,6 @@ void Nitpick::on_tabWidget_currentChanged(int index) {
}
}
-void Nitpick::on_evaluateTestsPushbutton_clicked() {
- _testCreator->startTestsEvaluation(false, false);
-}
-
void Nitpick::on_createRecursiveScriptPushbutton_clicked() {
_testCreator->createRecursiveScript();
}
@@ -253,6 +249,10 @@ void Nitpick::on_showTaskbarPushbutton_clicked() {
#endif
}
+void Nitpick::on_evaluateTestsPushbutton_clicked() {
+ _testCreator->startTestsEvaluation(false, false);
+}
+
void Nitpick::on_closePushbutton_clicked() {
exit(0);
}
@@ -266,7 +266,7 @@ void Nitpick::on_createXMLScriptRadioButton_clicked() {
}
void Nitpick::on_createWebPagePushbutton_clicked() {
- _testCreator->createWebPage(_ui.updateAWSCheckBox, _ui.awsURLLineEdit);
+ _testCreator->createWebPage(_ui.updateAWSCheckBox, _ui.diffImageRadioButton, _ui.ssimImageRadioButton, _ui.awsURLLineEdit);
}
void Nitpick::about() {
diff --git a/tools/nitpick/src/Nitpick.h b/tools/nitpick/src/Nitpick.h
index 1e9d7bdee5..42f55ee8b2 100644
--- a/tools/nitpick/src/Nitpick.h
+++ b/tools/nitpick/src/Nitpick.h
@@ -51,7 +51,6 @@ private slots:
void on_tabWidget_currentChanged(int index);
- void on_evaluateTestsPushbutton_clicked();
void on_createRecursiveScriptPushbutton_clicked();
void on_createAllRecursiveScriptsPushbutton_clicked();
void on_createTestsPushbutton_clicked();
@@ -79,6 +78,8 @@ private slots:
void on_hideTaskbarPushbutton_clicked();
void on_showTaskbarPushbutton_clicked();
+ void on_evaluateTestsPushbutton_clicked();
+
void on_createPythonScriptRadioButton_clicked();
void on_createXMLScriptRadioButton_clicked();
diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp
index cf4fe86162..c548a63a83 100644
--- a/tools/nitpick/src/TestCreator.cpp
+++ b/tools/nitpick/src/TestCreator.cpp
@@ -91,23 +91,25 @@ int TestCreator::compareImageLists() {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Images are not the same size");
similarityIndex = -100.0;
} else {
- similarityIndex = _imageComparer.compareImages(resultImage, expectedImage);
+ _imageComparer.compareImages(resultImage, expectedImage);
+ similarityIndex = _imageComparer.getSSIMValue();
}
TestResult testResult = TestResult{
(float)similarityIndex,
_expectedImagesFullFilenames[i].left(_expectedImagesFullFilenames[i].lastIndexOf("/") + 1), // path to the test (including trailing /)
QFileInfo(_expectedImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of expected image
- QFileInfo(_resultImagesFullFilenames[i].toStdString().c_str()).fileName() // filename of result image
+ QFileInfo(_resultImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of result image
+ _imageComparer.getSSIMResults() // results of SSIM algoritm
};
_mismatchWindow.setTestResult(testResult);
-
+
if (similarityIndex < THRESHOLD) {
++numberOfFailures;
if (!isInteractiveMode) {
- appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), true);
+ appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true);
} else {
_mismatchWindow.exec();
@@ -115,7 +117,7 @@ int TestCreator::compareImageLists() {
case USER_RESPONSE_PASS:
break;
case USE_RESPONSE_FAIL:
- appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), true);
+ appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true);
break;
case USER_RESPONSE_ABORT:
keepOn = false;
@@ -126,7 +128,7 @@ int TestCreator::compareImageLists() {
}
}
} else {
- appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), false);
+ appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), false);
}
_progressBar->setValue(i);
@@ -158,8 +160,8 @@ int TestCreator::checkTextResults() {
return testsFailed.length();
}
-void TestCreator::appendTestResultsToFile(TestResult testResult, QPixmap comparisonImage, bool hasFailed) {
- // Critical error if TestCreator Results folder does not exist
+void TestCreator::appendTestResultsToFile(const TestResult& testResult, const QPixmap& comparisonImage, const QPixmap& ssimResultsImage, bool hasFailed) {
+ // Critical error if Test Results folder does not exist
if (!QDir().exists(_testResultsFolderPath)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Folder " + _testResultsFolderPath + " not found");
exit(-1);
@@ -219,6 +221,9 @@ void TestCreator::appendTestResultsToFile(TestResult testResult, QPixmap compari
}
comparisonImage.save(resultFolderPath + "/" + "Difference Image.png");
+
+ // Save the SSIM results image
+ ssimResultsImage.save(resultFolderPath + "/" + "SSIM Image.png");
}
void::TestCreator::appendTestResultsToFile(QString testResultFilename, bool hasFailed) {
@@ -1101,7 +1106,12 @@ void TestCreator::setTestRailCreateMode(TestRailCreateMode testRailCreateMode) {
_testRailCreateMode = testRailCreateMode;
}
-void TestCreator::createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit) {
+void TestCreator::createWebPage(
+ QCheckBox* updateAWSCheckBox,
+ QRadioButton* diffImageRadioButton,
+ QRadioButton* ssimImageRadionButton,
+ QLineEdit* urlLineEdit
+) {
QString testResults = QFileDialog::getOpenFileName(nullptr, "Please select the zipped test results to update from", nullptr,
"Zipped TestCreator Results (TestResults--*.zip)");
if (testResults.isNull()) {
@@ -1118,5 +1128,12 @@ void TestCreator::createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLine
_awsInterface = new AWSInterface;
}
- _awsInterface->createWebPageFromResults(testResults, workingDirectory, updateAWSCheckBox, urlLineEdit);
+ _awsInterface->createWebPageFromResults(
+ testResults,
+ workingDirectory,
+ updateAWSCheckBox,
+ diffImageRadioButton,
+ ssimImageRadionButton,
+ urlLineEdit
+ );
}
\ No newline at end of file
diff --git a/tools/nitpick/src/TestCreator.h b/tools/nitpick/src/TestCreator.h
index 59bbf7bbaf..cc32499967 100644
--- a/tools/nitpick/src/TestCreator.h
+++ b/tools/nitpick/src/TestCreator.h
@@ -88,7 +88,7 @@ public:
void includeTest(QTextStream& textStream, const QString& testPathname);
- void appendTestResultsToFile(TestResult testResult, QPixmap comparisonImage, bool hasFailed);
+ void appendTestResultsToFile(const TestResult& testResult, const QPixmap& comparisonImage, const QPixmap& ssimResultsImage, bool hasFailed);
void appendTestResultsToFile(QString testResultFilename, bool hasFailed);
bool createTestResultsFolderPath(const QString& directory);
@@ -103,7 +103,11 @@ public:
void setTestRailCreateMode(TestRailCreateMode testRailCreateMode);
- void createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit);
+ void createWebPage(
+ QCheckBox* updateAWSCheckBox,
+ QRadioButton* diffImageRadioButton,
+ QRadioButton* ssimImageRadionButton,
+ QLineEdit* urlLineEdit);
private:
QProgressBar* _progressBar;
@@ -117,7 +121,7 @@ private:
const QString TEST_RESULTS_FOLDER { "TestResults" };
const QString TEST_RESULTS_FILENAME { "TestResults.txt" };
- const double THRESHOLD{ 0.965 };
+ const double THRESHOLD{ 0.98 };
QDir _imageDirectory;
diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h
index 5df4e9c921..eb228ff2b3 100644
--- a/tools/nitpick/src/common.h
+++ b/tools/nitpick/src/common.h
@@ -10,21 +10,38 @@
#ifndef hifi_common_h
#define hifi_common_h
+#include
#include
+class SSIMResults {
+public:
+ int width;
+ int height;
+ std::vector results;
+ double ssim;
+
+ // Used for scaling
+ double min;
+ double max;
+};
+
class TestResult {
public:
- TestResult(float error, QString pathname, QString expectedImageFilename, QString actualImageFilename) :
+ TestResult(float error, const QString& pathname, const QString& expectedImageFilename, const QString& actualImageFilename, const SSIMResults& ssimResults) :
_error(error),
_pathname(pathname),
_expectedImageFilename(expectedImageFilename),
- _actualImageFilename(actualImageFilename)
+ _actualImageFilename(actualImageFilename),
+ _ssimResults(ssimResults)
{}
double _error;
+
QString _pathname;
QString _expectedImageFilename;
QString _actualImageFilename;
+
+ SSIMResults _ssimResults;
};
enum UserResponse {
diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui
index 4a5a18f8d4..a0f368863d 100644
--- a/tools/nitpick/ui/Nitpick.ui
+++ b/tools/nitpick/ui/Nitpick.ui
@@ -46,7 +46,7 @@
- 0
+ 5
@@ -910,7 +910,7 @@
190
- 180
+ 200
131
20
@@ -926,7 +926,7 @@
330
- 170
+ 190
181
51
@@ -1039,8 +1039,8 @@
- 270
- 30
+ 370
+ 20
160
51
@@ -1075,6 +1075,38 @@
true
+
+
+
+ 260
+ 50
+ 95
+ 20
+
+
+
+ Diff Image
+
+
+ false
+
+
+
+
+
+ 260
+ 30
+ 95
+ 20
+
+
+
+ SSIM Image
+
+
+ true
+
+
groupBox
updateTestRailRunResultsPushbutton
diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt
index b81a620406..c84cec2978 100644
--- a/tools/unity-avatar-exporter/Assets/README.txt
+++ b/tools/unity-avatar-exporter/Assets/README.txt
@@ -2,6 +2,7 @@ High Fidelity, Inc.
Avatar Exporter
Version 0.2
+
Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter.
To create a new avatar project:
|