From 143a315f1d1ee4a8bc5ded4add1c89744d8745e7 Mon Sep 17 00:00:00 2001 From: Olivier Prat Date: Tue, 20 Mar 2018 15:29:50 -0700 Subject: [PATCH 01/21] Fade edited objects are now selected with ray picking --- .../scripting/SelectionScriptingInterface.cpp | 22 ++++- .../scripting/SelectionScriptingInterface.h | 27 ++++- libraries/render-utils/src/FadeEffectJobs.cpp | 99 ++++++++----------- libraries/render-utils/src/FadeEffectJobs.h | 1 - .../utilities/render/debugTransition.js | 50 +++++++++- 5 files changed, 137 insertions(+), 62 deletions(-) diff --git a/interface/src/scripting/SelectionScriptingInterface.cpp b/interface/src/scripting/SelectionScriptingInterface.cpp index 233e61c8ae..f8a62e848c 100644 --- a/interface/src/scripting/SelectionScriptingInterface.cpp +++ b/interface/src/scripting/SelectionScriptingInterface.cpp @@ -110,7 +110,6 @@ bool SelectionScriptingInterface::enableListHighlight(const QString& listName, c } if (!(*highlightStyle).isBoundToList()) { - setupHandler(listName); (*highlightStyle).setBoundToList(true); } @@ -172,6 +171,18 @@ render::HighlightStyle SelectionScriptingInterface::getHighlightStyle(const QStr } } +bool SelectionScriptingInterface::enableListToScene(const QString& listName) { + setupHandler(listName); + + return true; +} + +bool SelectionScriptingInterface::disableListToScene(const QString& listName) { + removeHandler(listName); + + return true; +} + template bool SelectionScriptingInterface::addToGameplayObjects(const QString& listName, T idToAdd) { { QWriteLocker lock(&_selectionListsLock); @@ -303,6 +314,15 @@ void SelectionScriptingInterface::setupHandler(const QString& selectionName) { (*handler)->initialize(selectionName); } +void SelectionScriptingInterface::removeHandler(const QString& selectionName) { + QWriteLocker lock(&_selectionHandlersLock); + auto handler = _handlerMap.find(selectionName); + if (handler != _handlerMap.end()) { + delete handler.value(); + _handlerMap.erase(handler); + } +} + void SelectionScriptingInterface::onSelectedItemsListChanged(const QString& listName) { { QWriteLocker lock(&_selectionHandlersLock); diff --git a/interface/src/scripting/SelectionScriptingInterface.h b/interface/src/scripting/SelectionScriptingInterface.h index 8295375870..3046ac371e 100644 --- a/interface/src/scripting/SelectionScriptingInterface.h +++ b/interface/src/scripting/SelectionScriptingInterface.h @@ -160,13 +160,14 @@ public: * If the Selection doesn't exist, it will be created. * All objects in the list will be displayed with the highlight effect as specified from the highlightStyle. * The function can be called several times with different values in the style to modify it. - * + * * @function Selection.enableListHighlight * @param listName {string} name of the selection * @param highlightStyle {jsObject} highlight style fields (see Selection.getListHighlightStyle for a detailed description of the highlightStyle). * @returns {bool} true if the selection was successfully enabled for highlight. */ Q_INVOKABLE bool enableListHighlight(const QString& listName, const QVariantMap& highlightStyle); + /**jsdoc * Disable highlighting for the named selection. * If the Selection doesn't exist or wasn't enabled for highliting then nothing happens simply returning false. @@ -175,7 +176,27 @@ public: * @param listName {string} name of the selection * @returns {bool} true if the selection was successfully disabled for highlight, false otherwise. */ - Q_INVOKABLE bool disableListHighlight(const QString& listName); + Q_INVOKABLE bool disableListHighlight(const QString& listName); + /**jsdoc + * Enable scene selection for the named selection. + * If the Selection doesn't exist, it will be created. + * All objects in the list will be sent to a scene selection. + * + * @function Selection.enableListToScene + * @param listName {string} name of the selection + * @returns {bool} true if the selection was successfully enabled on the scene. + */ + Q_INVOKABLE bool enableListToScene(const QString& listName); + /**jsdoc + * Disable scene selection for the named selection. + * If the Selection doesn't exist or wasn't enabled on the scene then nothing happens simply returning false. + * + * @function Selection.disableListToScene + * @param listName {string} name of the selection + * @returns {bool} true if the selection was successfully disabled on the scene, false otherwise. + */ + Q_INVOKABLE bool disableListToScene(const QString& listName); + /**jsdoc * Query the highlight style values for the named selection. * If the Selection doesn't exist or hasn't been highlight enabled yet, it will return an empty object. @@ -223,7 +244,7 @@ private: template bool removeFromGameplayObjects(const QString& listName, T idToRemove); void setupHandler(const QString& selectionName); - + void removeHandler(const QString& selectionName); }; diff --git a/libraries/render-utils/src/FadeEffectJobs.cpp b/libraries/render-utils/src/FadeEffectJobs.cpp index da3f8dddc0..e6de306c6d 100644 --- a/libraries/render-utils/src/FadeEffectJobs.cpp +++ b/libraries/render-utils/src/FadeEffectJobs.cpp @@ -39,43 +39,52 @@ void FadeEditJob::run(const render::RenderContextPointer& renderContext, const F auto scene = renderContext->_scene; if (_isEditEnabled) { - float minIsectDistance = std::numeric_limits::max(); - auto& itemBounds = inputs.get0(); - auto editedItem = findNearestItem(renderContext, itemBounds, minIsectDistance); - render::Transaction transaction; - bool hasTransaction{ false }; + static const std::string selectionName("TransitionEdit"); + auto scene = renderContext->_scene; + if (!scene->isSelectionEmpty(selectionName)) { + auto selection = scene->getSelection(selectionName); + auto editedItem = selection.getItems().front(); + render::Transaction transaction; + bool hasTransaction{ false }; - if (editedItem != _editedItem && render::Item::isValidID(_editedItem)) { - // Remove transition from previously edited item as we've changed edited item - hasTransaction = true; + if (editedItem != _editedItem && render::Item::isValidID(_editedItem)) { + // Remove transition from previously edited item as we've changed edited item + hasTransaction = true; + transaction.removeTransitionFromItem(_editedItem); + } + _editedItem = editedItem; + + if (render::Item::isValidID(_editedItem)) { + static const render::Transition::Type categoryToTransition[FADE_CATEGORY_COUNT] = { + render::Transition::ELEMENT_ENTER_DOMAIN, + render::Transition::BUBBLE_ISECT_OWNER, + render::Transition::BUBBLE_ISECT_TRESPASSER, + render::Transition::USER_ENTER_DOMAIN, + render::Transition::AVATAR_CHANGE + }; + + auto transitionType = categoryToTransition[inputs.get1()]; + + transaction.queryTransitionOnItem(_editedItem, [transitionType, scene](render::ItemID id, const render::Transition* transition) { + if (transition == nullptr || transition->isFinished || transition->eventType != transitionType) { + // Relaunch transition + render::Transaction transaction; + transaction.addTransitionToItem(id, transitionType); + scene->enqueueTransaction(transaction); + } + }); + hasTransaction = true; + } + + if (hasTransaction) { + scene->enqueueTransaction(transaction); + } + } else if (render::Item::isValidID(_editedItem)) { + // Remove transition from previously edited item as we've disabled fade edition + render::Transaction transaction; transaction.removeTransitionFromItem(_editedItem); - } - _editedItem = editedItem; - - if (render::Item::isValidID(_editedItem)) { - static const render::Transition::Type categoryToTransition[FADE_CATEGORY_COUNT] = { - render::Transition::ELEMENT_ENTER_DOMAIN, - render::Transition::BUBBLE_ISECT_OWNER, - render::Transition::BUBBLE_ISECT_TRESPASSER, - render::Transition::USER_ENTER_DOMAIN, - render::Transition::AVATAR_CHANGE - }; - - auto transitionType = categoryToTransition[inputs.get1()]; - - transaction.queryTransitionOnItem(_editedItem, [transitionType, scene](render::ItemID id, const render::Transition* transition) { - if (transition == nullptr || transition->isFinished || transition->eventType!=transitionType) { - // Relaunch transition - render::Transaction transaction; - transaction.addTransitionToItem(id, transitionType); - scene->enqueueTransaction(transaction); - } - }); - hasTransaction = true; - } - - if (hasTransaction) { scene->enqueueTransaction(transaction); + _editedItem = render::Item::INVALID_ITEM_ID; } } else if (render::Item::isValidID(_editedItem)) { @@ -87,28 +96,6 @@ void FadeEditJob::run(const render::RenderContextPointer& renderContext, const F } } -render::ItemID FadeEditJob::findNearestItem(const render::RenderContextPointer& renderContext, const render::ItemBounds& inputs, float& minIsectDistance) const { - const glm::vec3 rayOrigin = renderContext->args->getViewFrustum().getPosition(); - const glm::vec3 rayDirection = renderContext->args->getViewFrustum().getDirection(); - BoxFace face; - glm::vec3 normal; - float isectDistance; - render::ItemID nearestItem = render::Item::INVALID_ITEM_ID; - const float minDistance = 1.f; - const float maxDistance = 50.f; - - for (const auto& itemBound : inputs) { - if (!itemBound.bound.contains(rayOrigin) && itemBound.bound.findRayIntersection(rayOrigin, rayDirection, isectDistance, face, normal)) { - auto& item = renderContext->_scene->getItem(itemBound.id); - if (item.getKey().isWorldSpace() && isectDistance>minDistance && isectDistance < minIsectDistance && isectDistance Date: Tue, 20 Mar 2018 17:48:41 -0700 Subject: [PATCH 02/21] Proposal to improve fade editing with some extra visual cues --- libraries/render-utils/src/FadeEffectJobs.cpp | 2 +- .../utilities/render/debugTransition.js | 80 ++++++++++++++++++- .../developer/utilities/render/transition.qml | 5 +- 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/libraries/render-utils/src/FadeEffectJobs.cpp b/libraries/render-utils/src/FadeEffectJobs.cpp index e6de306c6d..a55fb304dd 100644 --- a/libraries/render-utils/src/FadeEffectJobs.cpp +++ b/libraries/render-utils/src/FadeEffectJobs.cpp @@ -581,7 +581,7 @@ void FadeJob::run(const render::RenderContextPointer& renderContext, FadeJob::Ou if (update(*jobConfig, scene, transaction, state, deltaTime)) { hasTransaction = true; } - if (isFirstItem && jobConfig->manualFade && (state.threshold != jobConfig->threshold)) { + if (isFirstItem && (state.threshold != jobConfig->threshold)) { jobConfig->setProperty("threshold", state.threshold); isFirstItem = false; } diff --git a/scripts/developer/utilities/render/debugTransition.js b/scripts/developer/utilities/render/debugTransition.js index 2cb48b7e2a..cef6e84268 100644 --- a/scripts/developer/utilities/render/debugTransition.js +++ b/scripts/developer/utilities/render/debugTransition.js @@ -66,7 +66,81 @@ wireEventBridge(onScreen); } + var isEditEnabled = false + var noiseSphere + var gradientSphere + var selectedEntity + var editedCategory + + var FADE_MIN_SCALE = 0.001 + var FADE_MAX_SCALE = 10000.0 + + function parameterToValuePow(parameter, minValue, maxOverMinValue) { + return minValue * Math.pow(maxOverMinValue, parameter); + //return parameter + } + + function update(dt) { + var gradientProperties = Entities.getEntityProperties(selectedEntity, ["position", "dimensions"]); + if (gradientProperties!=undefined) { + var pos = gradientProperties.position + if (pos!=undefined) { + var config = Render.getConfig("RenderMainView.Fade") + //print("Center at "+pos.x+" "+pos.y+" "+pos.z) + var dim = {x:config.baseSizeX, y:config.baseSizeY, z:config.baseSizeZ} + dim.x = parameterToValuePow(dim.x, FADE_MIN_SCALE, FADE_MAX_SCALE/ FADE_MIN_SCALE) + dim.y = parameterToValuePow(dim.y, FADE_MIN_SCALE, FADE_MAX_SCALE/ FADE_MIN_SCALE) + dim.z = parameterToValuePow(dim.z, FADE_MIN_SCALE, FADE_MAX_SCALE/ FADE_MIN_SCALE) + if (editedCategory==4 || editedCategory==5) { + dim.y = gradientProperties.dimensions.y + pos.y = pos.y - gradientProperties.dimensions.y/2 + } + Overlays.editOverlay(gradientSphere, { position: pos, dimensions: dim, alpha: config.baseLevel }) + dim.x = parameterToValuePow(config.noiseSizeX, FADE_MIN_SCALE, FADE_MAX_SCALE/ FADE_MIN_SCALE) + dim.y = parameterToValuePow(config.noiseSizeY, FADE_MIN_SCALE, FADE_MAX_SCALE/ FADE_MIN_SCALE) + dim.z = parameterToValuePow(config.noiseSizeZ, FADE_MIN_SCALE, FADE_MAX_SCALE/ FADE_MIN_SCALE) + Overlays.editOverlay(noiseSphere, { position: pos, dimensions: dim, alpha: config.noiseLevel }) + } + } + } + + Script.update.connect(update); + function fromQml(message) { + tokens = message.split(' ') + print("Received '"+message+"' from transition.qml") + if (tokens[0]=="edit") { + isEditEnabled = (tokens[1]=="true") + if (isEditEnabled) { + if (gradientSphere==undefined) { + gradientSphere = Overlays.addOverlay("sphere", { + position: MyAvatar.position, + rotation: Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0), + dimensions: { x: 1.0, y: 1.0, z: 1.0 }, + color: { red: 100, green: 150, blue: 255}, + alpha: 0.2, + solid: false + }); + } + if (noiseSphere==undefined) { + noiseSphere = Overlays.addOverlay("sphere", { + position: MyAvatar.position, + rotation: Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0), + dimensions: { x: 1.0, y: 1.0, z: 1.0 }, + color: { red: 255, green: 150, blue: 100}, + alpha: 0.2, + solid: false + }); + } + } else if (!isEditEnabled) { + Overlays.deleteOverlay(noiseSphere); + Overlays.deleteOverlay(gradientSphere); + noiseSphere = undefined + gradientSphere = undefined + } + } else if (tokens[0]=="category") { + editedCategory = parseInt(tokens[1]) + } } button.clicked.connect(onClicked); @@ -109,7 +183,6 @@ var currentSelectionName = "" var SelectionList = "TransitionEdit" - var selectedEntity = null Pointers.enablePointer(laser) Selection.enableListToScene(SelectionList) Selection.clearSelectedItemsList(SelectionList) @@ -120,6 +193,7 @@ } selectedEntity = id Selection.addToSelectedItemsList(SelectionList, "entity", selectedEntity) + update() }) function cleanup() { @@ -127,6 +201,8 @@ Pointers.removePointer(ray); Selection.removeListFromMap(SelectionList) Selection.disableListToScene(SelectionList); + Overlays.deleteOverlay(noiseSphere); + Overlays.deleteOverlay(gradientSphere); } - + Script.scriptEnding.connect(cleanup); }()); \ No newline at end of file diff --git a/scripts/developer/utilities/render/transition.qml b/scripts/developer/utilities/render/transition.qml index e83a85f8ed..a8737dfa6b 100644 --- a/scripts/developer/utilities/render/transition.qml +++ b/scripts/developer/utilities/render/transition.qml @@ -22,6 +22,8 @@ Rectangle { id: root anchors.margins: hifi.dimensions.contentMargin.x + signal sendToScript(var message); + color: hifi.colors.baseGray; property var config: Render.getConfig("RenderMainView.Fade"); @@ -48,7 +50,7 @@ Rectangle { checked: root.configEdit["editFade"] onCheckedChanged: { root.configEdit["editFade"] = checked; - Render.getConfig("RenderMainView.DrawFadedOpaqueBounds").enabled = checked; + root.sendToScript("edit "+checked); } } HifiControls.ComboBox { @@ -70,6 +72,7 @@ Rectangle { paramWidgetLoader.sourceComponent = undefined; postpone.interval = 100 postpone.start() + root.sendToScript("category "+currentIndex) } } } From 0c6f5bb04d525c7649469517492835a05bcf959a Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Mon, 26 Mar 2018 10:51:25 -0700 Subject: [PATCH 03/21] adding python baking scripts --- tools/bake-tools/bake.py | 91 ++++++++++++++++++++++ tools/bake-tools/convertToRelativePaths.py | 82 +++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 tools/bake-tools/bake.py create mode 100644 tools/bake-tools/convertToRelativePaths.py diff --git a/tools/bake-tools/bake.py b/tools/bake-tools/bake.py new file mode 100644 index 0000000000..0c8d5e1048 --- /dev/null +++ b/tools/bake-tools/bake.py @@ -0,0 +1,91 @@ +import os, json, sys, shutil, subprocess, shlex, time +EXE = os.environ['HIFI_OVEN'] + +def listFiles(directory, extension): + items = os.listdir(directory) + fileList = [] + for f in items: + if f.endswith('.' + extension): + fileList.append(f) + return fileList + +def camelCaseString(string): + string = string.replace('-', ' ') + return ''.join(x for x in string.title() if not x.isspace()) + +def groupFiles(originalDirectory, newDirectory, files): + for file in files: + newPath = os.sep.join([newDirectory, file]) + originalPath = os.sep.join([originalDirectory, file]) + shutil.move(originalPath, newPath) + +def groupKTXFiles(directory, filePath): + baseFile = os.path.basename(filePath) + filename = os.path.splitext(baseFile)[0] + camelCaseFileName = camelCaseString(filename) + path = os.sep.join([directory, camelCaseFileName]) + files = listFiles(directory, 'ktx') + if len(files) > 0: + createDirectory(path) + groupFiles(directory, path, files) + + newFilePath = os.sep.join([path, baseFile+'.baked.fbx']) + originalFilePath = os.sep.join([directory, baseFile+'.baked.fbx']) + originalFilePath.strip() + shutil.move(originalFilePath, newFilePath) + +def bakeFile(filePath, outputDirectory): + createDirectory(outputDirectory) + cmd = EXE + ' -i ' + filePath + ' -o ' + outputDirectory + ' -t fbx' + args = shlex.split(cmd) + process = subprocess.Popen(cmd, stdout=False, stderr=False) + process.wait() + bakedFile = os.path.splitext(filePath)[0] + groupKTXFiles(outputDirectory, bakedFile) + +def bakeFilesInDirectory(directory, outputDirectory): + for root, subFolders, filenames in os.walk(directory): + for filename in filenames: + if filename.endswith('.fbx'): + filePath = os.sep.join([root, filename]) + absFilePath = os.path.abspath(filePath) + outputFolder = os.path.join(outputDirectory, os.path.relpath(root)) + print "Baking file: " + filename + bakeFile(absFilePath, outputFolder) + else: + filePath = os.sep.join([root, filename]) + absFilePath = os.path.abspath(filePath) + outputFolder = os.path.join(outputDirectory, os.path.relpath(root)) + newFilePath = os.sep.join([outputFolder, filename]) + createDirectory(outputFolder) + print "moving file: " + filename + " to: " + outputFolder + shutil.copy(absFilePath, newFilePath) + +def createDirectory(directory): + if not os.path.exists(directory): + os.makedirs(directory) + +def checkIfExeExists(): + if not os.path.isfile(EXE) and os.access(EXE, os.X_OK): + print 'HIFI_OVEN evironment variable is not set' + sys.exit() + +def handleOptions(): + option = sys.argv[1] + if option == '--help' or option == '-h': + print 'Usage: bake.py INPUT_DIRECTORY[directory to bake] OUTPUT_DIRECTORY[directory to place backed files]' + print 'Note: Output directory will be created if directory does not exist' + sys.exit() + +def main(): + argsLength = len(sys.argv) + if argsLength == 3: + checkIfExeExists() + rootDirectory = sys.argv[1] + outputDirectory = os.path.abspath(sys.argv[2]) + createDirectory(outputDirectory) + bakeFilesInDirectory(rootDirectory, outputDirectory) + elif argsLength == 2: + handleOptions() + +main() diff --git a/tools/bake-tools/convertToRelativePaths.py b/tools/bake-tools/convertToRelativePaths.py new file mode 100644 index 0000000000..27a7b7ac02 --- /dev/null +++ b/tools/bake-tools/convertToRelativePaths.py @@ -0,0 +1,82 @@ +import json, os, sys, gzip + +prefix = 'file:///~/' +MAP = {} +def createAssetMapping(assetDirectory): + baseDirectory = os.path.basename(os.path.normpath(assetDirectory)) + for root, subfolder, filenames in os.walk(assetDirectory): + for filename in filenames: + if not filename.endswith('.ktx'): + substring = os.path.commonprefix([assetDirectory, root]) + newPath = root.replace(substring, ''); + filePath = os.sep.join([newPath, filename]) + if filePath[0] == '\\': + filePath = filePath[1:] + finalPath = prefix + baseDirectory + '/' + filePath + finalPath = finalPath.replace('\\', '/') + file = os.path.splitext(filename)[0] + file = os.path.splitext(file)[0] + MAP[file] = finalPath + +def hasURL(prop): + if "URL" in prop: + return True + return False + + +def handleURL(url): + newUrl = url + if "atp:" in url: + baseFilename = os.path.basename(url) + filename = os.path.splitext(baseFilename)[0] + newUrl = MAP[filename] + print newUrl + return newUrl + +def handleOptions(): + option = sys.argv[1] + if option == '--help' or option == '-h': + print 'Usage: convertToRelativePaths.py INPUT[json file you want to update the urls] INPUT[directory that the baked files are located in]' + sys.exit() + +def main(): + argsLength = len(sys.argv) + if argsLength == 3: + jsonFile = sys.argv[1] + gzipFile = jsonFile + '.gz' + assetDirectory = sys.argv[2] + createAssetMapping(assetDirectory) + f = open(jsonFile) + data = json.load(f) + f.close() + for entity in data['Entities']: + for prop in entity: + value = entity[prop] + if hasURL(prop): + value = handleURL(value) + if prop == "script": + value = handleURL(value) + if prop == "textures": + try: + tmp = json.loads(value) + for index in tmp: + tmp[index] = handleURL(tmp[index]) + value = json.dumps(tmp) + except: + value = handleURL(value) + + if prop == "serverScripts": + value = handleURL(value) + + entity[prop] = value + + + jsonString = json.dumps(data) + jsonBytes= jsonString.encode('utf-8') + with gzip.GzipFile(gzipFile, 'w') as fout: # 4. gzip + fout.write(jsonBytes) + + elif argsLength == 2: + handleOptions() + +main() From 194b00d732a477fe0a6270a356023407618fccc8 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 26 Mar 2018 16:31:58 -0700 Subject: [PATCH 04/21] fix translate arrow normals --- scripts/system/libraries/entitySelectionTool.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index fced5fc4e9..1fb2f59a19 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1653,11 +1653,11 @@ SelectionDisplay = (function() { mode: mode, onBegin: function(event, pickRay, pickResult) { if (direction === TRANSLATE_DIRECTION.X) { - pickNormal = { x:0, y:0, z:1 }; + pickNormal = { x:0, y:1, z:1 }; } else if (direction === TRANSLATE_DIRECTION.Y) { - pickNormal = { x:1, y:0, z:0 }; + pickNormal = { x:1, y:0, z:1 }; } else if (direction === TRANSLATE_DIRECTION.Z) { - pickNormal = { x:0, y:1, z:0 }; + pickNormal = { x:1, y:1, z:0 }; } var rotation = spaceMode === SPACE_LOCAL ? SelectionManager.localRotation : SelectionManager.worldRotation; @@ -1701,7 +1701,6 @@ SelectionDisplay = (function() { onMove: function(event) { pickRay = generalComputePickRay(event.x, event.y); - // translate mode left/right based on view toward entity var newIntersection = rayPlaneIntersection(pickRay, SelectionManager.worldPosition, pickNormal); var vector = Vec3.subtract(newIntersection, lastPick); @@ -1719,7 +1718,7 @@ SelectionDisplay = (function() { var dotVector = Vec3.dot(vector, projectionVector); vector = Vec3.multiply(dotVector, projectionVector); vector = grid.snapToGrid(vector); - + var wantDebug = false; if (wantDebug) { print("translateUpDown... "); @@ -2017,10 +2016,10 @@ SelectionDisplay = (function() { vector = grid.snapToSpacing(vector); var changeInDimensions = Vec3.multiply(NEGATE_VECTOR, vec3Mult(localSigns, vector)); - if (directionEnum === STRETCH_DIRECTION.ALL) { - var toCameraDistance = getDistanceToCamera(position); - var dimensionsMultiple = toCameraDistance * STRETCH_DIRECTION_ALL_CAMERA_DISTANCE_MULTIPLE; - changeInDimensions = Vec3.multiply(changeInDimensions, dimensionsMultiple); + if (directionEnum === STRETCH_DIRECTION.ALL) { + var toCameraDistance = getDistanceToCamera(position); + var dimensionsMultiple = toCameraDistance * STRETCH_DIRECTION_ALL_CAMERA_DISTANCE_MULTIPLE; + changeInDimensions = Vec3.multiply(changeInDimensions, dimensionsMultiple); } var newDimensions; From e4416db06fff64863614792d5cf5d4537f46596b Mon Sep 17 00:00:00 2001 From: Olivier Prat Date: Wed, 28 Mar 2018 16:54:53 -0400 Subject: [PATCH 05/21] Added save/file dialog for transition configurations in transition editor --- libraries/render-utils/src/FadeEffectJobs.cpp | 7 +-- libraries/render-utils/src/FadeEffectJobs.h | 4 +- .../utilities/render/debugTransition.js | 48 ++++++++++++++++--- .../developer/utilities/render/transition.qml | 36 ++++++++++---- 4 files changed, 72 insertions(+), 23 deletions(-) diff --git a/libraries/render-utils/src/FadeEffectJobs.cpp b/libraries/render-utils/src/FadeEffectJobs.cpp index a55fb304dd..1e2ea2bc15 100644 --- a/libraries/render-utils/src/FadeEffectJobs.cpp +++ b/libraries/render-utils/src/FadeEffectJobs.cpp @@ -340,11 +340,9 @@ QString FadeConfig::eventNames[FADE_CATEGORY_COUNT] = { "avatar_change", }; -void FadeConfig::save() const { - // Save will only work if the HIFI_USE_SOURCE_TREE_RESOURCES environment variable is set +void FadeConfig::save(const QString& configFilePath) const { assert(editedCategory < FADE_CATEGORY_COUNT); QJsonObject lProperties; - const QString configFilePath = PathUtils::resourcesPath() + "config/" + eventNames[editedCategory] + ".json"; QFile file(configFilePath); if (!file.open(QFile::WriteOnly | QFile::Text)) { qWarning() << "Fade event configuration file " << configFilePath << " cannot be opened"; @@ -369,8 +367,7 @@ void FadeConfig::save() const { } } -void FadeConfig::load() { - const QString configFilePath = PathUtils::resourcesPath() + "config/" + eventNames[editedCategory] + ".json"; +void FadeConfig::load(const QString& configFilePath) { QFile file(configFilePath); if (!file.exists()) { qWarning() << "Fade event configuration file " << configFilePath << " does not exist"; diff --git a/libraries/render-utils/src/FadeEffectJobs.h b/libraries/render-utils/src/FadeEffectJobs.h index f1026b6c2e..449995dba5 100644 --- a/libraries/render-utils/src/FadeEffectJobs.h +++ b/libraries/render-utils/src/FadeEffectJobs.h @@ -160,8 +160,8 @@ public: float manualThreshold{ 0.f }; bool manualFade{ false }; - Q_INVOKABLE void save() const; - Q_INVOKABLE void load(); + Q_INVOKABLE void save(const QString& filePath) const; + Q_INVOKABLE void load(const QString& filePath); static QString eventNames[FADE_CATEGORY_COUNT]; diff --git a/scripts/developer/utilities/render/debugTransition.js b/scripts/developer/utilities/render/debugTransition.js index cef6e84268..27b04bd32b 100644 --- a/scripts/developer/utilities/render/debugTransition.js +++ b/scripts/developer/utilities/render/debugTransition.js @@ -1,5 +1,3 @@ -"use strict"; - // // debugTransition.js // developer/utilities/render @@ -12,12 +10,17 @@ // (function() { + "use strict"; + var TABLET_BUTTON_NAME = "Transition"; var QMLAPP_URL = Script.resolvePath("./transition.qml"); var ICON_URL = Script.resolvePath("../../../system/assets/images/transition-i.svg"); var ACTIVE_ICON_URL = Script.resolvePath("../../../system/assets/images/transition-a.svg"); - + Script.include([ + Script.resolvePath("../../../system/libraries/stringHelpers.js"), + ]); + var onScreen = false; function onClicked() { @@ -106,10 +109,21 @@ Script.update.connect(update); + function loadConfiguration(fileUrl) { + var config = Render.getConfig("RenderMainView.Fade") + config.load(fileUrl) + } + + function saveConfiguration(fileUrl) { + var config = Render.getConfig("RenderMainView.Fade") + config.save(fileUrl) + } + function fromQml(message) { - tokens = message.split(' ') + tokens = message.split('*') print("Received '"+message+"' from transition.qml") - if (tokens[0]=="edit") { + command = tokens[0].toLowerCase() + if (command=="edit") { isEditEnabled = (tokens[1]=="true") if (isEditEnabled) { if (gradientSphere==undefined) { @@ -138,9 +152,28 @@ noiseSphere = undefined gradientSphere = undefined } - } else if (tokens[0]=="category") { + } else if (command=="category") { editedCategory = parseInt(tokens[1]) - } + } else if (command=="save") { + var filePath = tokens[1] + print("Raw token = "+filePath) + if (filePath.startsWith("file:///")) { + filePath = filePath.substr(8) + print("Saving configuration to "+filePath) + saveConfiguration(filePath) + } else { + print("Configurations can only be saved to local files") + } + } else if (command=="load") { + var filePath = tokens[1] + if (filePath.startsWith("file:///")) { + filePath = filePath.substr(8) + print("Loading configuration from "+filePath) + loadConfiguration(filePath) + } else { + print("Configurations can only be loaded from local files") + } + } } button.clicked.connect(onClicked); @@ -172,6 +205,7 @@ color: COLOR2, ignoreRayIntersection: true } + var laser = Pointers.createPointer(PickType.Ray, { joint: "Mouse", filter: Picks.PICK_ENTITIES, diff --git a/scripts/developer/utilities/render/transition.qml b/scripts/developer/utilities/render/transition.qml index a8737dfa6b..564090e2d6 100644 --- a/scripts/developer/utilities/render/transition.qml +++ b/scripts/developer/utilities/render/transition.qml @@ -11,6 +11,7 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 +import QtQuick.Dialogs 1.0 import "qrc:///qml/styles-uit" import "qrc:///qml/controls-uit" as HifiControls @@ -29,6 +30,24 @@ Rectangle { property var config: Render.getConfig("RenderMainView.Fade"); property var configEdit: Render.getConfig("RenderMainView.FadeEdit"); + FileDialog { + id: fileDialog + title: "Please choose a file" + folder: shortcuts.documents + nameFilters: [ "JSON files (*.json)", "All files (*)" ] + onAccepted: { + root.sendToScript(title.split(" ")[0]+"*"+fileUrl.toString()) + // This is a hack to be sure the widgets below properly reflect the change of category: delete the Component + // by setting the loader source to Null and then recreate it 500ms later + paramWidgetLoader.sourceComponent = undefined; + postpone.interval = 500 + postpone.start() + } + onRejected: { + } + Component.onCompleted: visible = false + } + ColumnLayout { spacing: 3 anchors.left: parent.left @@ -50,7 +69,7 @@ Rectangle { checked: root.configEdit["editFade"] onCheckedChanged: { root.configEdit["editFade"] = checked; - root.sendToScript("edit "+checked); + root.sendToScript("edit*"+checked); } } HifiControls.ComboBox { @@ -72,7 +91,7 @@ Rectangle { paramWidgetLoader.sourceComponent = undefined; postpone.interval = 100 postpone.start() - root.sendToScript("category "+currentIndex) + root.sendToScript("category*"+currentIndex) } } } @@ -107,19 +126,18 @@ Rectangle { id: saveAction text: "Save" onTriggered: { - root.config.save() + fileDialog.title = "Save configuration..." + fileDialog.selectExisting = false + fileDialog.open() } } Action { id: loadAction text: "Load" onTriggered: { - root.config.load() - // This is a hack to be sure the widgets below properly reflect the change of category: delete the Component - // by setting the loader source to Null and then recreate it 500ms later - paramWidgetLoader.sourceComponent = undefined; - postpone.interval = 500 - postpone.start() + fileDialog.title = "Load configuration..." + fileDialog.selectExisting = true + fileDialog.open() } } From 6639a245df244435f87c0093d964646227842ac4 Mon Sep 17 00:00:00 2001 From: Olivier Prat Date: Wed, 28 Mar 2018 18:20:10 -0400 Subject: [PATCH 06/21] Object picking / edition for transitions are activate / deactivated when transition editor window is shown / hidden --- .../utilities/render/debugTransition.js | 133 ++++++++++-------- .../developer/utilities/render/transition.qml | 10 -- 2 files changed, 73 insertions(+), 70 deletions(-) diff --git a/scripts/developer/utilities/render/debugTransition.js b/scripts/developer/utilities/render/debugTransition.js index 27b04bd32b..450b2e3ac9 100644 --- a/scripts/developer/utilities/render/debugTransition.js +++ b/scripts/developer/utilities/render/debugTransition.js @@ -40,6 +40,71 @@ var hasEventBridge = false; + function enableSphereVisualization() { + if (gradientSphere==undefined) { + gradientSphere = Overlays.addOverlay("sphere", { + position: MyAvatar.position, + rotation: Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0), + dimensions: { x: 1.0, y: 1.0, z: 1.0 }, + color: { red: 100, green: 150, blue: 255}, + alpha: 0.2, + solid: false + }); + } + if (noiseSphere==undefined) { + noiseSphere = Overlays.addOverlay("sphere", { + position: MyAvatar.position, + rotation: Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0), + dimensions: { x: 1.0, y: 1.0, z: 1.0 }, + color: { red: 255, green: 150, blue: 100}, + alpha: 0.2, + solid: false + }); + } + } + + function disableSphereVisualization() { + Overlays.deleteOverlay(noiseSphere); + Overlays.deleteOverlay(gradientSphere); + noiseSphere = undefined + gradientSphere = undefined + } + + // Create a Laser pointer used to pick and add objects to selections + var END_DIMENSIONS = { x: 0.05, y: 0.05, z: 0.05 }; + var COLOR1 = {red: 255, green: 0, blue: 0}; + var COLOR2 = {red: 0, green: 255, blue: 0}; + var end1 = { + type: "sphere", + dimensions: END_DIMENSIONS, + color: COLOR1, + ignoreRayIntersection: true + } + var end2 = { + type: "sphere", + dimensions: END_DIMENSIONS, + color: COLOR2, + ignoreRayIntersection: true + } + var laser + + function enablePointer() { + laser = Pointers.createPointer(PickType.Ray, { + joint: "Mouse", + filter: Picks.PICK_ENTITIES, + renderStates: [{name: "one", end: end1}], + defaultRenderStates: [{name: "one", end: end2, distance: 2.0}], + enabled: true + }); + Pointers.setRenderState(laser, "one"); + Pointers.enablePointer(laser) + } + + function disablePointer() { + Pointers.disablePointer(laser) + Pointers.removePointer(laser); + } + function wireEventBridge(on) { if (!tablet) { print("Warning in wireEventBridge(): 'tablet' undefined!"); @@ -49,11 +114,15 @@ if (!hasEventBridge) { tablet.fromQml.connect(fromQml); hasEventBridge = true; + enablePointer(); + Render.getConfig("RenderMainView.FadeEdit")["editFade"] = true } } else { if (hasEventBridge) { tablet.fromQml.disconnect(fromQml); hasEventBridge = false; + disablePointer(); + Render.getConfig("RenderMainView.FadeEdit")["editFade"] = false } } } @@ -121,38 +190,9 @@ function fromQml(message) { tokens = message.split('*') - print("Received '"+message+"' from transition.qml") + //print("Received '"+message+"' from transition.qml") command = tokens[0].toLowerCase() - if (command=="edit") { - isEditEnabled = (tokens[1]=="true") - if (isEditEnabled) { - if (gradientSphere==undefined) { - gradientSphere = Overlays.addOverlay("sphere", { - position: MyAvatar.position, - rotation: Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0), - dimensions: { x: 1.0, y: 1.0, z: 1.0 }, - color: { red: 100, green: 150, blue: 255}, - alpha: 0.2, - solid: false - }); - } - if (noiseSphere==undefined) { - noiseSphere = Overlays.addOverlay("sphere", { - position: MyAvatar.position, - rotation: Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0), - dimensions: { x: 1.0, y: 1.0, z: 1.0 }, - color: { red: 255, green: 150, blue: 100}, - alpha: 0.2, - solid: false - }); - } - } else if (!isEditEnabled) { - Overlays.deleteOverlay(noiseSphere); - Overlays.deleteOverlay(gradientSphere); - noiseSphere = undefined - gradientSphere = undefined - } - } else if (command=="category") { + if (command=="category") { editedCategory = parseInt(tokens[1]) } else if (command=="save") { var filePath = tokens[1] @@ -189,35 +229,9 @@ }); - // Create a Laser pointer used to pick and add objects to selections - var END_DIMENSIONS = { x: 0.05, y: 0.05, z: 0.05 }; - var COLOR1 = {red: 255, green: 0, blue: 0}; - var COLOR2 = {red: 0, green: 255, blue: 0}; - var end1 = { - type: "sphere", - dimensions: END_DIMENSIONS, - color: COLOR1, - ignoreRayIntersection: true - } - var end2 = { - type: "sphere", - dimensions: END_DIMENSIONS, - color: COLOR2, - ignoreRayIntersection: true - } - - var laser = Pointers.createPointer(PickType.Ray, { - joint: "Mouse", - filter: Picks.PICK_ENTITIES, - renderStates: [{name: "one", end: end1}], - defaultRenderStates: [{name: "one", end: end2, distance: 2.0}], - enabled: true - }); - Pointers.setRenderState(laser, "one"); - var currentSelectionName = "" var SelectionList = "TransitionEdit" - Pointers.enablePointer(laser) + Selection.enableListToScene(SelectionList) Selection.clearSelectedItemsList(SelectionList) @@ -231,8 +245,7 @@ }) function cleanup() { - Pointers.disablePointer(laser) - Pointers.removePointer(ray); + disablePointer(); Selection.removeListFromMap(SelectionList) Selection.disableListToScene(SelectionList); Overlays.deleteOverlay(noiseSphere); diff --git a/scripts/developer/utilities/render/transition.qml b/scripts/developer/utilities/render/transition.qml index 564090e2d6..342b2b533e 100644 --- a/scripts/developer/utilities/render/transition.qml +++ b/scripts/developer/utilities/render/transition.qml @@ -62,16 +62,6 @@ Rectangle { Layout.fillWidth: true id: root_col - HifiControls.CheckBox { - anchors.verticalCenter: parent.verticalCenter - boxSize: 20 - text: "Edit" - checked: root.configEdit["editFade"] - onCheckedChanged: { - root.configEdit["editFade"] = checked; - root.sendToScript("edit*"+checked); - } - } HifiControls.ComboBox { anchors.verticalCenter: parent.verticalCenter Layout.fillWidth: true From 9a7f88cd783315a01c78fc462d93331cacd00c0e Mon Sep 17 00:00:00 2001 From: Olivier Prat Date: Wed, 28 Mar 2018 19:23:09 -0400 Subject: [PATCH 07/21] Moved save / load buttons to top --- .../developer/utilities/render/transition.qml | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/scripts/developer/utilities/render/transition.qml b/scripts/developer/utilities/render/transition.qml index 342b2b533e..b002de30aa 100644 --- a/scripts/developer/utilities/render/transition.qml +++ b/scripts/developer/utilities/render/transition.qml @@ -58,7 +58,7 @@ Rectangle { } RowLayout { - spacing: 20 + spacing: 8 Layout.fillWidth: true id: root_col @@ -84,6 +84,18 @@ Rectangle { root.sendToScript("category*"+currentIndex) } } + HifiControls.Button { + action: saveAction + Layout.fillWidth: true + anchors.top: parent.top + anchors.bottom: parent.bottom + } + HifiControls.Button { + action: loadAction + Layout.fillWidth: true + anchors.top: parent.top + anchors.bottom: parent.bottom + } } RowLayout { @@ -375,20 +387,6 @@ Rectangle { id: paramWidgetLoader sourceComponent: paramWidgets } - - Row { - anchors.left: parent.left - anchors.right: parent.right - - Button { - action: saveAction - } - Button { - action: loadAction - } - } - - } } \ No newline at end of file From 07cb1c1f9e9d13a2f04dfd17fb8a01433eedd945 Mon Sep 17 00:00:00 2001 From: Olivier Prat Date: Thu, 29 Mar 2018 10:19:24 -0400 Subject: [PATCH 08/21] Moved gradient invert checkbox --- .../developer/utilities/render/transition.qml | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/scripts/developer/utilities/render/transition.qml b/scripts/developer/utilities/render/transition.qml index b002de30aa..a996c3a198 100644 --- a/scripts/developer/utilities/render/transition.qml +++ b/scripts/developer/utilities/render/transition.qml @@ -152,12 +152,7 @@ Rectangle { spacing: 3 width: root_col.width - HifiControls.CheckBox { - text: "Invert" - boxSize: 20 - checked: root.config["isInverted"] - onCheckedChanged: { root.config["isInverted"] = checked } - } + RowLayout { Layout.fillWidth: true @@ -219,16 +214,32 @@ Rectangle { } } - - ConfigSlider { + RowLayout { + spacing: 20 height: 36 - label: "Edge Width" - integral: false - config: root.config - property: "edgeWidth" - max: 1.0 - min: 0.0 + + HifiControls.CheckBox { + text: "Invert gradient" + anchors.verticalCenter: parent.verticalCenter + boxSize: 20 + checked: root.config["isInverted"] + onCheckedChanged: { root.config["isInverted"] = checked } + } + ConfigSlider { + anchors.left: undefined + anchors.verticalCenter: parent.verticalCenter + height: 36 + width: 300 + label: "Edge Width" + integral: false + config: root.config + property: "edgeWidth" + max: 1.0 + min: 0.0 + } } + + RowLayout { Layout.fillWidth: true From 648e6a6c9b524c6eed330599af2e7e94cfc69bc6 Mon Sep 17 00:00:00 2001 From: Olivier Prat Date: Thu, 29 Mar 2018 10:47:18 -0400 Subject: [PATCH 09/21] Added description for each event --- scripts/developer/utilities/render/transition.qml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/developer/utilities/render/transition.qml b/scripts/developer/utilities/render/transition.qml index a996c3a198..8811a15e82 100644 --- a/scripts/developer/utilities/render/transition.qml +++ b/scripts/developer/utilities/render/transition.qml @@ -75,6 +75,14 @@ Rectangle { } } onCurrentIndexChanged: { + var descriptions = [ + "Time based threshold, gradients centered on object", + "Fixed threshold, gradients centered on owner avatar", + "Position based threshold (increases when trespasser moves closer to avatar), gradients centered on trespasser avatar", + "Time based threshold, gradients centered on bottom of object", + "UNSUPPORTED" + ] + description.text = descriptions[currentIndex] root.config["editedCategory"] = currentIndex; // This is a hack to be sure the widgets below properly reflect the change of category: delete the Component // by setting the loader source to Null and then recreate it 100ms later @@ -98,6 +106,13 @@ Rectangle { } } + HifiControls.Label { + id: description + text: "..." + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + RowLayout { spacing: 20 height: 36 From ef63651b2bba29ed901883d23b6503e7100b6630 Mon Sep 17 00:00:00 2001 From: Olivier Prat Date: Thu, 29 Mar 2018 12:07:56 -0400 Subject: [PATCH 10/21] Duration and timing controls are disabled when event is not time based --- scripts/developer/utilities/render/transition.qml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/developer/utilities/render/transition.qml b/scripts/developer/utilities/render/transition.qml index 8811a15e82..65f4f4354a 100644 --- a/scripts/developer/utilities/render/transition.qml +++ b/scripts/developer/utilities/render/transition.qml @@ -72,6 +72,8 @@ Rectangle { interval: 100; running: false; repeat: false onTriggered: { paramWidgetLoader.sourceComponent = paramWidgets + var isTimeBased = categoryBox.currentIndex==0 || categoryBox.currentIndex==3 + paramWidgetLoader.item.isTimeBased = isTimeBased } } onCurrentIndexChanged: { @@ -82,6 +84,7 @@ Rectangle { "Time based threshold, gradients centered on bottom of object", "UNSUPPORTED" ] + description.text = descriptions[currentIndex] root.config["editedCategory"] = currentIndex; // This is a hack to be sure the widgets below properly reflect the change of category: delete the Component @@ -166,7 +169,7 @@ Rectangle { ColumnLayout { spacing: 3 width: root_col.width - + property bool isTimeBased RowLayout { Layout.fillWidth: true @@ -327,6 +330,8 @@ Rectangle { Layout.fillWidth: true ConfigSlider { + enabled: isTimeBased + opacity: isTimeBased ? 1.0 : 0.0 anchors.left: undefined anchors.right: undefined Layout.fillWidth: true @@ -339,6 +344,8 @@ Rectangle { min: 0.1 } HifiControls.ComboBox { + enabled: isTimeBased + opacity: isTimeBased ? 1.0 : 0.0 Layout.fillWidth: true model: ["Linear", "Ease In", "Ease Out", "Ease In / Out"] currentIndex: root.config["timing"] From e5da59a62f53e15bae36d524e208f249f6723719 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 16 Mar 2018 15:54:42 +0300 Subject: [PATCH 11/21] FB12870 - Tablet UI ->Asset Browser unresponsive when enabling/disabling "use baked version" --- interface/resources/qml/hifi/AssetServer.qml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index 1ff954feff..526ea6aad0 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -39,6 +39,7 @@ Windows.ScrollingWindow { property var assetMappingsModel: Assets.mappingModel; property var currentDirectory; property var selectedItemCount: treeView.selection.selectedIndexes.length; + property int updatesCount: 0; // this is used for notifying model-dependent bindings about model updates Settings { category: "Overlay.AssetServer" @@ -51,6 +52,9 @@ Windows.ScrollingWindow { ApplicationInterface.uploadRequest.connect(uploadClicked); assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError); assetMappingsModel.autoRefreshEnabled = true; + assetMappingsModel.updated.connect(function() { + ++updatesCount; + }); reload(); } @@ -852,12 +856,17 @@ Windows.ScrollingWindow { checked = Qt.binding(isChecked); } + function getStatus() { + // kind of hack for ensuring getStatus() will be re-evaluated on updatesCount changes + return updatesCount, assetProxyModel.data(treeView.selection.currentIndex, 0x105); + } + function isEnabled() { if (!treeView.selection.hasSelection) { return false; } - var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); + var status = getStatus(); if (status === "--") { return false; } @@ -882,9 +891,9 @@ Windows.ScrollingWindow { return false; } - var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); - return isEnabled() && status !== "Not Baked"; - } + var status = getStatus(); + return isEnabled() && status !== "Not Baked"; + } } Item { From 814cfa4587fd9eeb88b5ac13c9a7a12ec22b2d44 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 30 Mar 2018 00:09:28 +0300 Subject: [PATCH 12/21] fix TabletAssetServer --- .../qml/hifi/dialogs/TabletAssetServer.qml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 138eb5c6f8..6bf8f8a5d5 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -40,6 +40,7 @@ Rectangle { property var assetMappingsModel: Assets.mappingModel; property var currentDirectory; property var selectedItemCount: treeView.selection.selectedIndexes.length; + property int updatesCount: 0; // this is used for notifying model-dependent bindings about model updates Settings { category: "Overlay.AssetServer" @@ -51,6 +52,9 @@ Rectangle { ApplicationInterface.uploadRequest.connect(uploadClicked); assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError); assetMappingsModel.autoRefreshEnabled = true; + assetMappingsModel.updated.connect(function() { + ++updatesCount; + }); reload(); } @@ -850,12 +854,17 @@ Rectangle { checked = Qt.binding(isChecked); } + function getStatus() { + // kind of hack for ensuring getStatus() will be re-evaluated on updatesCount changes + return updatesCount, assetProxyModel.data(treeView.selection.currentIndex, 0x105); + } + function isEnabled() { if (!treeView.selection.hasSelection) { return false; } - var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); + var status = getStatus(); if (status === "--") { return false; } @@ -880,7 +889,7 @@ Rectangle { return false; } - var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); + var status = getStatus(); return isEnabled() && status !== "Not Baked"; } } From 0c2c09e77b83cfa1b98cf2dbef2e47032a1e5253 Mon Sep 17 00:00:00 2001 From: Olivier Prat Date: Fri, 30 Mar 2018 15:22:48 -0400 Subject: [PATCH 13/21] Fixed layout in HMD --- scripts/developer/utilities/render/transition.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/developer/utilities/render/transition.qml b/scripts/developer/utilities/render/transition.qml index 65f4f4354a..f74468a273 100644 --- a/scripts/developer/utilities/render/transition.qml +++ b/scripts/developer/utilities/render/transition.qml @@ -64,7 +64,8 @@ Rectangle { HifiControls.ComboBox { anchors.verticalCenter: parent.verticalCenter - Layout.fillWidth: true + anchors.left : parent.left + width: 300 id: categoryBox model: ["Elements enter/leave domain", "Bubble isect. - Owner POV", "Bubble isect. - Trespasser POV", "Another user leaves/arrives", "Changing an avatar"] Timer { From d4bc61fb3de6941ea3e470cea684eaa397ae6310 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 2 Apr 2018 13:53:30 -0700 Subject: [PATCH 14/21] Remove notifications and print outs for LOD changes because it was too much noise --- interface/src/LODManager.cpp | 10 +++++---- scripts/system/notifications.js | 38 ++++++--------------------------- 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/interface/src/LODManager.cpp b/interface/src/LODManager.cpp index 73408377c0..2e8d3011d7 100644 --- a/interface/src/LODManager.cpp +++ b/interface/src/LODManager.cpp @@ -86,10 +86,11 @@ void LODManager::autoAdjustLOD(float realTimeDelta) { if (_octreeSizeScale < ADJUST_LOD_MIN_SIZE_SCALE) { _octreeSizeScale = ADJUST_LOD_MIN_SIZE_SCALE; } - qCDebug(interfaceapp) << "adjusting LOD DOWN" + // DEBUG: Less is more, avoid logging all the time + /* qCDebug(interfaceapp) << "adjusting LOD DOWN" << "fps =" << currentFPS << "targetFPS =" << getLODDecreaseFPS() - << "octreeSizeScale =" << _octreeSizeScale; + << "octreeSizeScale =" << _octreeSizeScale; */ emit LODDecreased(); // Assuming the LOD adjustment will work: we optimistically reset _avgRenderTime // to provide an FPS just above the decrease threshold. It will drift close to its @@ -111,10 +112,11 @@ void LODManager::autoAdjustLOD(float realTimeDelta) { if (_octreeSizeScale > ADJUST_LOD_MAX_SIZE_SCALE) { _octreeSizeScale = ADJUST_LOD_MAX_SIZE_SCALE; } - qCDebug(interfaceapp) << "adjusting LOD UP" + // DEBUG: Less is more, avoid logging all the time + /* qCDebug(interfaceapp) << "adjusting LOD UP" << "fps =" << currentFPS << "targetFPS =" << getLODDecreaseFPS() - << "octreeSizeScale =" << _octreeSizeScale; + << "octreeSizeScale =" << _octreeSizeScale; */ emit LODIncreased(); // Assuming the LOD adjustment will work: we optimistically reset _avgRenderTime // to provide an FPS just below the increase threshold. It will drift close to its diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 728760c1e7..94a8e1adec 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -1,6 +1,6 @@ "use strict"; /*jslint vars:true, plusplus:true, forin:true*/ -/*global Script, Settings, Window, Controller, Overlays, SoundArray, LODManager, MyAvatar, Tablet, Camera, HMD, Menu, Quat, Vec3*/ +/*global Script, Settings, Window, Controller, Overlays, SoundArray, MyAvatar, Tablet, Camera, HMD, Menu, Quat, Vec3*/ // // notifications.js // Version 0.801 @@ -84,21 +84,18 @@ var NOTIFICATION_MENU_ITEM_POST = " Notifications"; var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; - var lodTextID = false; - var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications" + var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications" var NotificationType = { UNKNOWN: 0, SNAPSHOT: 1, - LOD_WARNING: 2, - CONNECTION_REFUSED: 3, - EDIT_ERROR: 4, - TABLET: 5, - CONNECTION: 6, - WALLET: 7, + CONNECTION_REFUSED: 2, + EDIT_ERROR: 3, + TABLET: 4, + CONNECTION: 5, + WALLET: 6, properties: [ { text: "Snapshot" }, - { text: "Level of Detail" }, { text: "Connection Refused" }, { text: "Edit error" }, { text: "Tablet" }, @@ -153,10 +150,6 @@ // This handles the final dismissal of a notification after fading function dismiss(firstNoteOut, firstButOut, firstOut) { - if (firstNoteOut === lodTextID) { - lodTextID = false; - } - Overlays.deleteOverlay(firstNoteOut); Overlays.deleteOverlay(firstButOut); notifications.splice(firstOut, 1); @@ -418,9 +411,6 @@ function deleteNotification(index) { var notificationTextID = notifications[index]; - if (notificationTextID === lodTextID) { - lodTextID = false; - } Overlays.deleteOverlay(notificationTextID); Overlays.deleteOverlay(buttons[index]); notifications.splice(index, 1); @@ -674,20 +664,6 @@ } } - LODManager.LODDecreased.connect(function () { - var warningText = "\n" + - "Due to the complexity of the content, the \n" + - "level of detail has been decreased. " + - "You can now see: \n" + - LODManager.getLODFeedbackText(); - - if (lodTextID === false) { - lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); - } else { - Overlays.editOverlay(lodTextID, { text: warningText }); - } - }); - Controller.keyPressEvent.connect(keyPressEvent); Controller.mousePressEvent.connect(mousePressEvent); Controller.keyReleaseEvent.connect(keyReleaseEvent); From 1f3063193a8e09409ffe5293a20a4ea75d68271f Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 2 Apr 2018 14:24:51 -0700 Subject: [PATCH 15/21] Tweak right-click to inspect --- .../ui/overlays/ContextOverlayInterface.cpp | 29 ++++++++++++++++++- .../src/ui/overlays/ContextOverlayInterface.h | 6 ++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index dd05e5c6a8..aca186a589 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -50,7 +50,9 @@ ContextOverlayInterface::ContextOverlayInterface() { _entityPropertyFlags += PROP_OWNING_AVATAR_ID; auto entityScriptingInterface = DependencyManager::get().data(); - connect(entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity, this, &ContextOverlayInterface::createOrDestroyContextOverlay); + connect(entityScriptingInterface, &EntityScriptingInterface::clickDownOnEntity, this, &ContextOverlayInterface::clickDownOnEntity); + connect(entityScriptingInterface, &EntityScriptingInterface::holdingClickOnEntity, this, &ContextOverlayInterface::holdingClickOnEntity); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity, this, &ContextOverlayInterface::mouseReleaseOnEntity); connect(entityScriptingInterface, &EntityScriptingInterface::hoverEnterEntity, this, &ContextOverlayInterface::contextOverlays_hoverEnterEntity); connect(entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity, this, &ContextOverlayInterface::contextOverlays_hoverLeaveEntity); connect(_tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"), &TabletProxy::tabletShownChanged, this, [&]() { @@ -97,6 +99,31 @@ void ContextOverlayInterface::setEnabled(bool enabled) { _enabled = enabled; } +void ContextOverlayInterface::clickDownOnEntity(const EntityItemID& entityItemID, const PointerEvent& event) { + if (_enabled && event.getButton() == PointerEvent::SecondaryButton && contextOverlayFilterPassed(entityItemID)) { + _mouseDownEntity = entityItemID; + _mouseDownEntityTimestamp = usecTimestampNow(); + } else { + if (!_currentEntityWithContextOverlay.isNull()) { + disableEntityHighlight(_currentEntityWithContextOverlay); + destroyContextOverlay(_currentEntityWithContextOverlay, event); + } + } +} + +static const float CONTEXT_OVERLAY_CLICK_HOLD_TIME_MSEC = 400.0f; +void ContextOverlayInterface::holdingClickOnEntity(const EntityItemID& entityItemID, const PointerEvent& event) { + if (!_mouseDownEntity.isNull() && ((usecTimestampNow() - _mouseDownEntityTimestamp) > (CONTEXT_OVERLAY_CLICK_HOLD_TIME_MSEC * USECS_PER_MSEC))) { + _mouseDownEntity = EntityItemID(); + } +} + +void ContextOverlayInterface::mouseReleaseOnEntity(const EntityItemID& entityItemID, const PointerEvent& event) { + if (_enabled && event.getButton() == PointerEvent::SecondaryButton && contextOverlayFilterPassed(entityItemID) && _mouseDownEntity == entityItemID) { + createOrDestroyContextOverlay(entityItemID, event); + } +} + bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { if (_enabled && event.getButton() == PointerEvent::SecondaryButton) { if (contextOverlayFilterPassed(entityItemID)) { diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h index fcdf2d5820..b80a3a70fb 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.h +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -64,6 +64,10 @@ signals: void contextOverlayClicked(const QUuid& currentEntityWithContextOverlay); public slots: + void clickDownOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); + void holdingClickOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); + void mouseReleaseOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); + bool createOrDestroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event); bool destroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event); bool destroyContextOverlay(const EntityItemID& entityItemID); @@ -84,6 +88,8 @@ private: }; bool _verboseLogging{ true }; bool _enabled { true }; + EntityItemID _mouseDownEntity{}; + quint64 _mouseDownEntityTimestamp; EntityItemID _currentEntityWithContextOverlay{}; EntityItemID _lastInspectedEntity{}; QString _entityMarketplaceID; From 5a1eac563ba352cbbe1dc480c67d327ce489052b Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 2 Apr 2018 15:50:18 -0700 Subject: [PATCH 16/21] Code review fixes --- interface/src/LODManager.cpp | 10 ---------- scripts/system/notifications.js | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/interface/src/LODManager.cpp b/interface/src/LODManager.cpp index 2e8d3011d7..d7d73e962a 100644 --- a/interface/src/LODManager.cpp +++ b/interface/src/LODManager.cpp @@ -86,11 +86,6 @@ void LODManager::autoAdjustLOD(float realTimeDelta) { if (_octreeSizeScale < ADJUST_LOD_MIN_SIZE_SCALE) { _octreeSizeScale = ADJUST_LOD_MIN_SIZE_SCALE; } - // DEBUG: Less is more, avoid logging all the time - /* qCDebug(interfaceapp) << "adjusting LOD DOWN" - << "fps =" << currentFPS - << "targetFPS =" << getLODDecreaseFPS() - << "octreeSizeScale =" << _octreeSizeScale; */ emit LODDecreased(); // Assuming the LOD adjustment will work: we optimistically reset _avgRenderTime // to provide an FPS just above the decrease threshold. It will drift close to its @@ -112,11 +107,6 @@ void LODManager::autoAdjustLOD(float realTimeDelta) { if (_octreeSizeScale > ADJUST_LOD_MAX_SIZE_SCALE) { _octreeSizeScale = ADJUST_LOD_MAX_SIZE_SCALE; } - // DEBUG: Less is more, avoid logging all the time - /* qCDebug(interfaceapp) << "adjusting LOD UP" - << "fps =" << currentFPS - << "targetFPS =" << getLODDecreaseFPS() - << "octreeSizeScale =" << _octreeSizeScale; */ emit LODIncreased(); // Assuming the LOD adjustment will work: we optimistically reset _avgRenderTime // to provide an FPS just below the increase threshold. It will drift close to its diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 94a8e1adec..8b68a9355e 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -84,7 +84,7 @@ var NOTIFICATION_MENU_ITEM_POST = " Notifications"; var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; - var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications" + var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications"; var NotificationType = { UNKNOWN: 0, From 077b2dab30d527136dfbd5b09ce9b38514af12cd Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 2 Apr 2018 16:09:43 -0700 Subject: [PATCH 17/21] Code review fixes --- scripts/system/notifications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 8b68a9355e..ba37f6ee4e 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -84,7 +84,7 @@ var NOTIFICATION_MENU_ITEM_POST = " Notifications"; var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; - var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications"; + var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications"; var NotificationType = { UNKNOWN: 0, From 4f5895cb16e52b908ccd2337928aee8912168264 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 3 Apr 2018 13:17:37 -0700 Subject: [PATCH 18/21] Added MD file to autoTester. --- tools/auto-tester/src/Test.cpp | 264 +++++++++++++++++++++--- tools/auto-tester/src/Test.h | 30 ++- tools/auto-tester/src/ui/AutoTester.cpp | 6 +- tools/auto-tester/src/ui/AutoTester.h | 5 +- tools/auto-tester/src/ui/AutoTester.ui | 23 ++- 5 files changed, 280 insertions(+), 48 deletions(-) diff --git a/tools/auto-tester/src/Test.cpp b/tools/auto-tester/src/Test.cpp index 347cfd90dc..98d4ae463c 100644 --- a/tools/auto-tester/src/Test.cpp +++ b/tools/auto-tester/src/Test.cpp @@ -24,15 +24,11 @@ extern AutoTester* autoTester; #include Test::Test() { - QString regex(EXPECTED_IMAGE_PREFIX + QString("\\\\d").repeated(NUM_DIGITS) + ".png"); - - expectedImageFilenameFormat = QRegularExpression(regex); - mismatchWindow.setModal(true); } bool Test::createTestResultsFolderPath(QString directory) { - QDateTime now = QDateTime::currentDateTime(); + QDateTime now = QDateTime::currentDateTime(); testResultsFolderPath = directory + "/" + TEST_RESULTS_FOLDER + "--" + now.toString(DATETIME_FORMAT); QDir testResultsFolder(testResultsFolderPath); @@ -76,7 +72,7 @@ bool Test::compareImageLists(bool isInteractiveMode, QProgressBar* progressBar) QImage expectedImage(expectedImagesFullFilenames[i]); if (resultImage.width() != expectedImage.width() || resultImage.height() != expectedImage.height()) { - messageBox.critical(0, "Internal error #1", "Images are not the same size"); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Images are not the same size"); exit(-1); } @@ -84,7 +80,7 @@ bool Test::compareImageLists(bool isInteractiveMode, QProgressBar* progressBar) try { similarityIndex = imageComparer.compareImages(resultImage, expectedImage); } catch (...) { - messageBox.critical(0, "Internal error #2", "Image not in expected format"); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Image not in expected format"); exit(-1); } @@ -131,20 +127,20 @@ bool Test::compareImageLists(bool isInteractiveMode, QProgressBar* progressBar) void Test::appendTestResultsToFile(QString testResultsFolderPath, TestFailure testFailure, QPixmap comparisonImage) { if (!QDir().exists(testResultsFolderPath)) { - messageBox.critical(0, "Internal error #3", "Folder " + testResultsFolderPath + " not found"); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Folder " + testResultsFolderPath + " not found"); exit(-1); } QString failureFolderPath { testResultsFolderPath + "/" + "Failure_" + QString::number(index) }; if (!QDir().mkdir(failureFolderPath)) { - messageBox.critical(0, "Internal error #4", "Failed to create folder " + failureFolderPath); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create folder " + failureFolderPath); exit(-1); } ++index; QFile descriptionFile(failureFolderPath + "/" + TEST_RESULTS_FILENAME); if (!descriptionFile.open(QIODevice::ReadWrite)) { - messageBox.critical(0, "Internal error #5", "Failed to create file " + TEST_RESULTS_FILENAME); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create file " + TEST_RESULTS_FILENAME); exit(-1); } @@ -164,14 +160,14 @@ void Test::appendTestResultsToFile(QString testResultsFolderPath, TestFailure te sourceFile = testFailure._pathname + testFailure._expectedImageFilename; destinationFile = failureFolderPath + "/" + "Expected Image.jpg"; if (!QFile::copy(sourceFile, destinationFile)) { - messageBox.critical(0, "Internal error #6", "Failed to copy " + sourceFile + " to " + destinationFile); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to copy " + sourceFile + " to " + destinationFile); exit(-1); } sourceFile = testFailure._pathname + testFailure._actualImageFilename; destinationFile = failureFolderPath + "/" + "Actual Image.jpg"; if (!QFile::copy(sourceFile, destinationFile)) { - messageBox.critical(0, "Internal error #7", "Failed to copy " + sourceFile + " to " + destinationFile); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to copy " + sourceFile + " to " + destinationFile); exit(-1); } @@ -209,11 +205,6 @@ void Test::startTestsEvaluation() { QStringList sortedTestResultsFilenames = createListOfAll_imagesInDirectory("png", pathToTestResultsDirectory); QStringList expectedImagesURLs; - const QString URLPrefix("https://raw.githubusercontent.com"); - const QString githubUser("NissimHadar"); - const QString testsRepo("hifi_tests"); - const QString branch("addRecursionToAutotester"); - resultImagesFullFilenames.clear(); expectedImagesFilenames.clear(); expectedImagesFullFilenames.clear(); @@ -226,16 +217,16 @@ void Test::startTestsEvaluation() { QString expectedImagePartialSourceDirectory = getExpectedImagePartialSourceDirectory(currentFilename); // Images are stored on GitHub as ExpectedImage_ddddd.png - // Extract the digits at the end of the filename (exluding the file extension) + // Extract the digits at the end of the filename (excluding the file extension) QString expectedImageFilenameTail = currentFilename.left(currentFilename.length() - 4).right(NUM_DIGITS); QString expectedImageStoredFilename = EXPECTED_IMAGE_PREFIX + expectedImageFilenameTail + ".png"; - QString imageURLString(URLPrefix + "/" + githubUser + "/" + testsRepo + "/" + branch + "/" + - expectedImagePartialSourceDirectory + "/" + expectedImageStoredFilename); + QString imageURLString("https://github.com/" + githubUser + "/hifi_tests/blob/" + gitHubBranch + "/" + + expectedImagePartialSourceDirectory + "/" + expectedImageStoredFilename + "?raw=true"); expectedImagesURLs << imageURLString; - // The image retrieved from Github needs a unique name + // The image retrieved from GitHub needs a unique name QString expectedImageFilename = currentFilename.replace("/", "_").replace(".", "_EI."); expectedImagesFilenames << expectedImageFilename; @@ -273,25 +264,31 @@ bool Test::isAValidDirectory(QString pathname) { return true; } -void Test::importTest(QTextStream& textStream, const QString& testPathname) { - // `testPathname` includes the full path to the test. We need the portion below (and including) `tests` - QStringList filenameParts = testPathname.split('/'); +QString Test::extractPathFromTestsDown(QString fullPath) { + // `fullPath` includes the full path to the test. We need the portion below (and including) `tests` + QStringList pathParts = fullPath.split('/'); int i{ 0 }; - while (i < filenameParts.length() && filenameParts[i] != "tests") { + while (i < pathParts.length() && pathParts[i] != "tests") { ++i; } - if (i == filenameParts.length()) { - messageBox.critical(0, "Internal error #10", "Bad testPathname"); + if (i == pathParts.length()) { + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Bad testPathname"); exit(-1); } - QString filename; - for (int j = i; j < filenameParts.length(); ++j) { - filename += "/" + filenameParts[j]; + QString partialPath; + for (int j = i; j < pathParts.length(); ++j) { + partialPath += "/" + pathParts[j]; } - textStream << "Script.include(\"" << "https://raw.githubusercontent.com/" << user << "/hifi_tests/" << branch << filename + "\");" << endl; + return partialPath; +} + +void Test::importTest(QTextStream& textStream, const QString& testPathname) { + QString partialPath = extractPathFromTestsDown(testPathname); + textStream << "Script.include(\"" << "https://github.com/" << githubUser + << "/hifi_tests/blob/" << gitHubBranch << partialPath + "?raw=true\");" << endl; } // Creates a single script in a user-selected folder. @@ -353,7 +350,7 @@ void Test::createRecursiveScript(QString topLevelDirectory, bool interactiveMode QFile allTestsFilename(topLevelDirectory + "/" + recursiveTestsFilename); if (!allTestsFilename.open(QIODevice::WriteOnly | QIODevice::Text)) { messageBox.critical(0, - "Internal Error #8", + "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create \"" + recursiveTestsFilename + "\" in directory \"" + topLevelDirectory + "\"" ); @@ -363,7 +360,9 @@ void Test::createRecursiveScript(QString topLevelDirectory, bool interactiveMode QTextStream textStream(&allTestsFilename); textStream << "// This is an automatically generated file, created by auto-tester" << endl << endl; - textStream << "var autoTester = Script.require(\"https://raw.githubusercontent.com/" + user + "/hifi_tests/" + branch + "/tests/utils/autoTester.js\");" << endl; + textStream << "var autoTester = Script.require(\"https://github.com/" + githubUser + "/hifi_tests/blob/" + + gitHubBranch + "/tests/utils/autoTester.js?raw=true\");" << endl; + textStream << "autoTester.enableRecursive();" << endl << endl; QVector testPathnames; @@ -454,6 +453,203 @@ void Test::createTest() { messageBox.information(0, "Success", "Test images have been created"); } +ExtractedText Test::getTestScriptLines(QString testFileName) { + ExtractedText relevantTextFromTest; + + QFile inputFile(testFileName); + inputFile.open(QIODevice::ReadOnly); + if (!inputFile.isOpen()) { + messageBox.critical(0, + "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Failed to open \"" + testFileName + ); + } + + QTextStream stream(&inputFile); + QString line = stream.readLine(); + + // Name of test is the string in the following line: + // autoTester.perform("Apply Material Entities to Avatars", Script.resolvePath("."), function(testType) {... + const QString ws("\\h*"); //white-space character + const QString functionPerformName(ws + "autoTester" + ws + "\\." + ws + "perform"); + const QString quotedString("\\\".+\\\""); + const QString ownPath("Script" + ws + "\\." + ws + "resolvePath" + ws + "\\(" + ws + "\\\"\\.\\\"" + ws + "\\)"); + const QString functionParameter("function" + ws + "\\(testType" + ws + "\\)"); + QString regexTestTitle(ws + functionPerformName + "\\(" + quotedString + "\\," + ws + ownPath + "\\," + ws + functionParameter + ws + "{" + ".*"); + QRegularExpression lineContainingTitle = QRegularExpression(regexTestTitle); + + // Assert platform checks that test is running on the correct OS + const QString functionAssertPlatform(ws + "autoTester" + ws + "\\." + ws + "assertPlatform"); + const QString regexAssertPlatform(ws + functionAssertPlatform + ws + "\\(" + ws + quotedString + ".*"); + const QRegularExpression lineAssertPlatform = QRegularExpression(regexAssertPlatform); + + // Assert display checks that test is running on the correct display + const QString functionAssertDisplay(ws + "autoTester" + ws + "\\." + ws + "assertDisplay"); + const QString regexAssertDisplay(ws + functionAssertDisplay + ws + "\\(" + ws + quotedString + ".*"); + const QRegularExpression lineAssertDisplay = QRegularExpression(regexAssertDisplay); + + // Assert CPU checks that test is running on the correct type of CPU + const QString functionAssertCPU(ws + "autoTester" + ws + "\\." + ws + "assertCPU"); + const QString regexAssertCPU(ws + functionAssertCPU + ws + "\\(" + ws + quotedString + ".*"); + const QRegularExpression lineAssertCPU = QRegularExpression(regexAssertCPU); + + // Assert GPU checks that test is running on the correct type of GPU + const QString functionAssertGPU(ws + "autoTester" + ws + "\\." + ws + "assertGPU"); + const QString regexAssertGPU(ws + functionAssertGPU + ws + "\\(" + ws + quotedString + ".*"); + const QRegularExpression lineAssertGPU = QRegularExpression(regexAssertGPU); + + // Each step is either of the following forms: + // autoTester.addStepSnapshot("Take snapshot"... + // autoTester.addStep("Clean up after test"... + const QString functionAddStepSnapshotName(ws + "autoTester" + ws + "\\." + ws + "addStepSnapshot"); + const QString regexStepSnapshot(ws + functionAddStepSnapshotName + ws + "\\(" + ws + quotedString + ".*"); + const QRegularExpression lineStepSnapshot = QRegularExpression(regexStepSnapshot); + + const QString functionAddStepName(ws + "autoTester" + ws + "\\." + ws + "addStep"); + const QString regexStep(ws + functionAddStepName + ws + "\\(" + ws + quotedString + ws + "\\)" + ".*"); + const QRegularExpression lineStep = QRegularExpression(regexStep); + + while (!line.isNull()) { + line = stream.readLine(); + if (lineContainingTitle.match(line).hasMatch()) { + QStringList tokens = line.split('"'); + relevantTextFromTest.title = tokens[1]; + } else if (lineAssertPlatform.match(line).hasMatch()) { + QStringList platforms = line.split('"'); + relevantTextFromTest.platform = platforms[1]; + } else if (lineAssertDisplay.match(line).hasMatch()) { + QStringList displays = line.split('"'); + relevantTextFromTest.display = displays[1]; + } else if (lineAssertCPU.match(line).hasMatch()) { + QStringList cpus = line.split('"'); + relevantTextFromTest.cpu = cpus[1]; + } else if (lineAssertGPU.match(line).hasMatch()) { + QStringList gpus = line.split('"'); + relevantTextFromTest.gpu = gpus[1]; + } else if (lineStepSnapshot.match(line).hasMatch()) { + QStringList tokens = line.split('"'); + QString nameOfStep = tokens[1]; + + Step *step = new Step(); + step->text = nameOfStep; + step->takeSnapshot = true; + relevantTextFromTest.stepList.emplace_back(step); + } else if (lineStep.match(line).hasMatch()) { + QStringList tokens = line.split('"'); + QString nameOfStep = tokens[1]; + + Step *step = new Step(); + step->text = nameOfStep; + step->takeSnapshot = false; + relevantTextFromTest.stepList.emplace_back(step); + } + } + + inputFile.close(); + + return relevantTextFromTest; +} + +// Create an MD file for a user-selected test. +// The folder selected must contain a script named "test.js", the file produced is named "test.md" +void Test::createMDFile() { + // Folder selection + QString testDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images", ".", QFileDialog::ShowDirsOnly); + if (testDirectory == "") { + return; + } + + // Verify folder contains test.js file + QString testFileName(testDirectory + "/" + TEST_FILENAME); + QFileInfo testFileInfo(testFileName); + if (!testFileInfo.exists()) { + messageBox.critical(0, "Error", "Could not find file: " + TEST_FILENAME); + return; + } + + ExtractedText testScriptLines = getTestScriptLines(testFileName); + + QString mdFilename(testDirectory + "/" + "test.md"); + QFile mdFile(mdFilename); + if (!mdFile.open(QIODevice::ReadWrite)) { + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create file " + mdFilename); + exit(-1); + } + + QTextStream stream(&mdFile); + + //Test title + QString testName = testScriptLines.title; + stream << "# " << testName << "\n"; + + // Find the relevant part of the path to the test (i.e. from "tests" down + QString partialPath = extractPathFromTestsDown(testDirectory); + + stream << "## Run this script URL: [Manual](./test.js?raw=true) [Auto](./testAuto.js?raw=true)(from menu/Edit/Open and Run scripts from URL...)." << "\n\n"; + + stream << "## Preconditions" << "\n"; + stream << "- In an empty region of a domain with editing rights." << "\n\n"; + + // Platform + QStringList platforms = testScriptLines.platform.split(" ");; + stream << "## Platforms\n"; + stream << "Run the test on each of the following platforms\n"; + for (int i = 0; i < platforms.size(); ++i) { + // Note that the platforms parameter may include extra spaces, these appear as empty strings in the list + if (platforms[i] != QString()) { + stream << " - " << platforms[i] << "\n"; + } + } + + // Display + QStringList displays = testScriptLines.display.split(" "); + stream << "## Displays\n"; + stream << "Run the test on each of the following displays\n"; + for (int i = 0; i < displays.size(); ++i) { + // Note that the displays parameter may include extra spaces, these appear as empty strings in the list + if (displays[i] != QString()) { + stream << " - " << displays[i] << "\n"; + } + } + + // CPU + QStringList cpus = testScriptLines.cpu.split(" "); + stream << "## Processors\n"; + stream << "Run the test on each of the following processors\n"; + for (int i = 0; i < cpus.size(); ++i) { + // Note that the cpus parameter may include extra spaces, these appear as empty strings in the list + if (cpus[i] != QString()) { + stream << " - " << cpus[i] << "\n"; + } + } + + // GPU + QStringList gpus = testScriptLines.gpu.split(" "); + stream << "## Graphics Cards\n"; + stream << "Run the test on graphics cards from each of the following vendors\n"; + for (int i = 0; i < gpus.size(); ++i) { + // Note that the gpus parameter may include extra spaces, these appear as empty strings in the list + if (gpus[i] != QString()) { + stream << " - " << gpus[i] << "\n"; + } + } + + stream << "## Steps\n"; + stream << "Press space bar to advance step by step\n\n"; + + int snapShotIndex { 0 }; + for (int i = 0; i < testScriptLines.stepList.size(); ++i) { + stream << "### Step " << QString::number(i) << "\n"; + stream << "- " << testScriptLines.stepList[i]->text << "\n"; + if (testScriptLines.stepList[i]->takeSnapshot) { + stream << "- ![](./ExpectedImage_" << QString::number(snapShotIndex).rightJustified(5, '0') << ".png)\n"; + ++snapShotIndex; + } + } + + mdFile.close(); +} + void Test::copyJPGtoPNG(QString sourceJPGFullFilename, QString destinationPNGFullFilename) { QFile::remove(destinationPNGFullFilename); @@ -526,7 +722,7 @@ QString Test::getExpectedImagePartialSourceDirectory(QString filename) { } if (i < 0) { - messageBox.critical(0, "Internal error #9", "Bad filename"); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Bad filename"); exit(-1); } diff --git a/tools/auto-tester/src/Test.h b/tools/auto-tester/src/Test.h index cd5075002a..3d04b00df9 100644 --- a/tools/auto-tester/src/Test.h +++ b/tools/auto-tester/src/Test.h @@ -19,6 +19,24 @@ #include "ImageComparer.h" #include "ui/MismatchWindow.h" +class Step { +public: + QString text; + bool takeSnapshot; +}; + +using StepList = std::vector; + +class ExtractedText { +public: + QString title; + QString platform; + QString display; + QString cpu; + QString gpu; + StepList stepList; +}; + class Test { public: Test(); @@ -31,7 +49,7 @@ public: void createRecursiveScript(QString topLevelDirectory, bool interactiveMode); void createTest(); - void deleteOldSnapshots(); + void createMDFile(); bool compareImageLists(bool isInteractiveMode, QProgressBar* progressBar); @@ -47,7 +65,7 @@ public: void zipAndDeleteTestResultsFolder(); bool isAValidDirectory(QString pathname); - + QString extractPathFromTestsDown(QString fullPath); QString getExpectedImageDestinationDirectory(QString filename); QString getExpectedImagePartialSourceDirectory(QString filename); @@ -62,8 +80,6 @@ private: QDir imageDirectory; - QRegularExpression expectedImageFilenameFormat; - MismatchWindow mismatchWindow; ImageComparer imageComparer; @@ -81,9 +97,11 @@ private: QStringList resultImagesFullFilenames; // Used for accessing GitHub - const QString user { "NissimHadar" }; - const QString branch { "addRecursionToAutotester" }; + const QString githubUser{ "highfidelity" }; + const QString gitHubBranch { "master" }; const QString DATETIME_FORMAT { "yyyy-MM-dd_hh-mm-ss" }; + + ExtractedText getTestScriptLines(QString testFileName); }; #endif // hifi_test_h \ No newline at end of file diff --git a/tools/auto-tester/src/ui/AutoTester.cpp b/tools/auto-tester/src/ui/AutoTester.cpp index a5e13331dd..9153365184 100644 --- a/tools/auto-tester/src/ui/AutoTester.cpp +++ b/tools/auto-tester/src/ui/AutoTester.cpp @@ -33,7 +33,11 @@ void AutoTester::on_createRecursiveScriptsRecursivelyButton_clicked() { } void AutoTester::on_createTestButton_clicked() { - test->createTest(); + test->createTest(); +} + +void AutoTester::on_createMDFileButton_clicked() { + test->createMDFile(); } void AutoTester::on_closeButton_clicked() { diff --git a/tools/auto-tester/src/ui/AutoTester.h b/tools/auto-tester/src/ui/AutoTester.h index 938e7ca2d2..82ff3780e3 100644 --- a/tools/auto-tester/src/ui/AutoTester.h +++ b/tools/auto-tester/src/ui/AutoTester.h @@ -29,8 +29,9 @@ private slots: void on_evaluateTestsButton_clicked(); void on_createRecursiveScriptButton_clicked(); void on_createRecursiveScriptsRecursivelyButton_clicked(); - void on_createTestButton_clicked(); - void on_closeButton_clicked(); + void on_createTestButton_clicked(); + void on_createMDFileButton_clicked(); + void on_closeButton_clicked(); void saveImage(int index); diff --git a/tools/auto-tester/src/ui/AutoTester.ui b/tools/auto-tester/src/ui/AutoTester.ui index 55c3897e58..600de283ad 100644 --- a/tools/auto-tester/src/ui/AutoTester.ui +++ b/tools/auto-tester/src/ui/AutoTester.ui @@ -7,7 +7,7 @@ 0 0 607 - 395 + 514 @@ -18,7 +18,7 @@ 20 - 300 + 420 220 40 @@ -44,7 +44,7 @@ 20 - 135 + 255 220 40 @@ -70,7 +70,7 @@ 23 - 100 + 220 131 20 @@ -86,7 +86,7 @@ 20 - 190 + 310 255 23 @@ -108,6 +108,19 @@ Create Recursive Scripts Recursively + + + + 20 + 90 + 220 + 40 + + + + Create MD file + + From f20eddefc8f6e345b6ce2859eaf1b4d76179b86d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 3 Apr 2018 13:46:02 -0700 Subject: [PATCH 19/21] Fix 'Private' HMD preview texture enable/disable...again --- .../qml/hifi/commerce/wallet/PassphraseChange.qml | 12 ++++++++++++ .../qml/hifi/commerce/wallet/PassphraseModal.qml | 4 ---- .../qml/hifi/commerce/wallet/PassphraseSelection.qml | 3 --- .../qml/hifi/commerce/wallet/SecurityImageChange.qml | 11 +++++++++++ .../hifi/commerce/wallet/SecurityImageSelection.qml | 12 ------------ .../resources/qml/hifi/commerce/wallet/Wallet.qml | 2 +- .../qml/hifi/commerce/wallet/WalletSetup.qml | 11 +++++++---- 7 files changed, 31 insertions(+), 24 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml index 91d2ab9f7f..e74b0fa9dc 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml @@ -24,6 +24,18 @@ Item { HifiConstants { id: hifi; } id: root; + + // This will cause a bug -- if you bring up passphrase selection in HUD mode while + // in HMD while having HMD preview enabled, then move, then finish passphrase selection, + // HMD preview will stay off. + // TODO: Fix this unlikely bug + onVisibleChanged: { + if (visible) { + sendSignalToWallet({method: 'disableHmdPreview'}); + } else { + sendSignalToWallet({method: 'maybeEnableHmdPreview'}); + } + } // Username Text RalewayRegular { diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml index 1fa9054d69..7c38406697 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml @@ -68,10 +68,6 @@ Item { propagateComposedEvents: false; hoverEnabled: true; } - - Component.onDestruction: { - sendSignalToParent({method: 'maybeEnableHmdPreview'}); - } // This will cause a bug -- if you bring up passphrase selection in HUD mode while // in HMD while having HMD preview enabled, then move, then finish passphrase selection, diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml index 50bea2a3cf..5fd6b01d18 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -61,9 +61,6 @@ Item { if (root.shouldImmediatelyFocus) { focusFirstTextField(); } - sendMessageToLightbox({method: 'disableHmdPreview'}); - } else { - sendMessageToLightbox({method: 'maybeEnableHmdPreview'}); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml index 86a4220b74..0d7fe9ed18 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml @@ -44,6 +44,17 @@ Item { } } + // This will cause a bug -- if you bring up security image selection in HUD mode while + // in HMD while having HMD preview enabled, then move, then finish passphrase selection, + // HMD preview will stay off. + // TODO: Fix this unlikely bug + onVisibleChanged: { + if (visible) { + sendSignalToWallet({method: 'disableHmdPreview'}); + } else { + sendSignalToWallet({method: 'maybeEnableHmdPreview'}); + } + } // Security Image Item { diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml index 56b78c5865..85fc0db3be 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml @@ -25,18 +25,6 @@ Item { id: root; property alias currentIndex: securityImageGrid.currentIndex; - - // This will cause a bug -- if you bring up security image selection in HUD mode while - // in HMD while having HMD preview enabled, then move, then finish passphrase selection, - // HMD preview will stay off. - // TODO: Fix this unlikely bug - onVisibleChanged: { - if (visible) { - sendSignalToWallet({method: 'disableHmdPreview'}); - } else { - sendSignalToWallet({method: 'maybeEnableHmdPreview'}); - } - } SecurityImageModel { id: gridModel; diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index b8b34dc395..b2e7daa066 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -237,7 +237,7 @@ Rectangle { } else { sendToScript(msg); } - } else if (msg.method === 'maybeEnableHmdPreview') { + } else { sendToScript(msg); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml index bad592067c..6956252ee0 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml @@ -76,6 +76,12 @@ Item { var currentStepNumber = root.activeView.substring(5); UserActivityLogger.commerceWalletSetupProgress(timestamp, root.setupAttemptID, Math.round((timestamp - root.startingTimestamp)/1000), currentStepNumber, root.setupStepNames[currentStepNumber - 1]); + + if (root.activeView === "step_2" || root.activeView === "step_3") { + sendSignalToWallet({method: 'disableHmdPreview'}); + } else { + sendSignalToWallet({method: 'maybeEnableHmdPreview'}); + } } // @@ -441,7 +447,7 @@ Item { } Item { id: choosePassphraseContainer; - visible: root.hasShownSecurityImageTip && root.activeView === "step_3"; + visible: root.activeView === "step_3"; // Anchors anchors.top: titleBarContainer.bottom; anchors.topMargin: 30; @@ -451,10 +457,7 @@ Item { onVisibleChanged: { if (visible) { - sendSignalToWallet({method: 'disableHmdPreview'}); Commerce.getWalletAuthenticatedStatus(); - } else { - sendSignalToWallet({method: 'maybeEnableHmdPreview'}); } } From 5f47d511718595422f93ff859fedc8bfcddaa74d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 3 Apr 2018 15:35:46 -0700 Subject: [PATCH 20/21] Prevent Context Overlay from flashing when briefly hovering over certified entity --- .../system/controllers/controllerModules/farActionGrabEntity.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index 07450e54ba..d8e2a217a4 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -295,6 +295,7 @@ Script.include("/~/system/libraries/Xform.js"); this.actionID = null; this.grabbedThingID = null; this.targetObject = null; + this.potentialEntityWithContextOverlay = false; }; this.updateRecommendedArea = function() { From 235996911a64002c94448a8f3f0d46295ee91cfe Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Wed, 4 Apr 2018 11:38:14 -0700 Subject: [PATCH 21/21] Fixed gcc warning. --- tools/auto-tester/src/Test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/auto-tester/src/Test.cpp b/tools/auto-tester/src/Test.cpp index 98d4ae463c..0bbd35e280 100644 --- a/tools/auto-tester/src/Test.cpp +++ b/tools/auto-tester/src/Test.cpp @@ -638,7 +638,7 @@ void Test::createMDFile() { stream << "Press space bar to advance step by step\n\n"; int snapShotIndex { 0 }; - for (int i = 0; i < testScriptLines.stepList.size(); ++i) { + for (size_t i = 0; i < testScriptLines.stepList.size(); ++i) { stream << "### Step " << QString::number(i) << "\n"; stream << "- " << testScriptLines.stepList[i]->text << "\n"; if (testScriptLines.stepList[i]->takeSnapshot) {