diff --git a/CMakeGraphvizOptions.cmake b/CMakeGraphvizOptions.cmake new file mode 100644 index 0000000000..f1b73f1cae --- /dev/null +++ b/CMakeGraphvizOptions.cmake @@ -0,0 +1 @@ +set(GRAPHVIZ_EXTERNAL_LIBS FALSE) \ No newline at end of file diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp index bbee597797..0d62b8dcc7 100644 --- a/assignment-client/src/AssignmentClient.cpp +++ b/assignment-client/src/AssignmentClient.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include "AssignmentFactory.h" #include "AssignmentActionFactory.h" @@ -61,6 +62,7 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri DependencyManager::registerInheritance(); auto actionFactory = DependencyManager::set(); + DependencyManager::set(); // setup a thread for the NodeList and its PacketReceiver QThread* nodeThread = new QThread(this); diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index ed02bd3709..9a12550b43 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -23,14 +23,14 @@ var WANT_DEBUG = false; // these tune time-averaging and "on" value for analog trigger // -var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing -var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab -var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab +var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing +var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab +var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab var TRIGGER_OFF_VALUE = 0.15; var BUMPER_ON_VALUE = 0.5; -var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only use head for search/move. +var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only use head for search/move. // // distant manipulation @@ -421,7 +421,7 @@ function MyController(hand) { this.searchSphereOn = function(location, size, color) { if (this.searchSphere === null) { var sphereProperties = { - position: location, + position: location, size: size, color: color, alpha: SEARCH_SPHERE_ALPHA, @@ -429,10 +429,15 @@ function MyController(hand) { visible: true } this.searchSphere = Overlays.addOverlay("sphere", sphereProperties); - } else { - Overlays.editOverlay(this.searchSphere, { position: location, size: size, color: color, visible: true }); + } else { + Overlays.editOverlay(this.searchSphere, { + position: location, + size: size, + color: color, + visible: true + }); } - } + } this.overlayLineOn = function(closePoint, farPoint, color) { if (this.overlayLine === null) { @@ -765,7 +770,7 @@ function MyController(hand) { if (this.triggerSmoothedSqueezed() || this.bumperSqueezed()) { this.lastPickTime = 0; var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; - this.startingHandRotation = Controller.getPoseValue(controllerHandInput).rotation; + this.startingHandRotation = Controller.getPoseValue(controllerHandInput).rotation; if (this.triggerSmoothedSqueezed()) { this.setState(STATE_SEARCHING); } else { @@ -788,13 +793,19 @@ function MyController(hand) { var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; var currentHandRotation = Controller.getPoseValue(controllerHandInput).rotation; var handDeltaRotation = Quat.multiply(currentHandRotation, Quat.inverse(this.startingHandRotation)); - + var distantPickRay = { - origin: Camera.position, + origin: Camera.position, direction: Vec3.mix(Quat.getUp(this.getHandRotation()), Quat.getFront(Camera.orientation), HAND_HEAD_MIX_RATIO), length: PICK_MAX_DISTANCE }; + var searchVisualizationPickRay = { + origin: handPosition, + direction: Quat.getUp(this.getHandRotation()), + length: PICK_MAX_DISTANCE + }; + // Pick at some maximum rate, not always var pickRays = []; var now = Date.now(); @@ -1015,6 +1026,11 @@ function MyController(hand) { if (USE_PARTICLE_BEAM_FOR_SEARCHING === true) { this.handleParticleBeam(distantPickRay.origin, this.getHandRotation(), NO_INTERSECT_COLOR); } + + if (USE_OVERLAY_LINES_FOR_SEARCHING === true) { + this.overlayLineOn(searchVisualizationPickRay.origin, Vec3.sum(searchVisualizationPickRay.origin, Vec3.multiply(searchVisualizationPickRay.direction, LINE_LENGTH)), NO_INTERSECT_COLOR); + } + if (this.intersectionDistance > 0) { var SPHERE_INTERSECTION_SIZE = 0.011; var SEARCH_SPHERE_FOLLOW_RATE = 0.50; diff --git a/examples/directory.js b/examples/directory.js index d2a3768051..8d9993ffda 100644 --- a/examples/directory.js +++ b/examples/directory.js @@ -62,8 +62,13 @@ var directory = (function () { function setUp() { viewport = Controller.getViewportDimensions(); - directoryWindow = new OverlayWebWindow('Directory', DIRECTORY_URL, 900, 700, false); - directoryWindow.setVisible(false); + directoryWindow = new OverlayWebWindow({ + title: 'Directory', + source: DIRECTORY_URL, + width: 900, + height: 700, + visible: false + }); directoryButton = Overlays.addOverlay("image", { imageURL: DIRECTORY_BUTTON_URL, diff --git a/examples/edit.js b/examples/edit.js index 074b43c8c1..99219fcaa2 100644 --- a/examples/edit.js +++ b/examples/edit.js @@ -140,8 +140,13 @@ var importingSVOTextOverlay = Overlays.addOverlay("text", { }); var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; -var marketplaceWindow = new OverlayWebWindow('Marketplace', "about:blank", 900, 700, false); -marketplaceWindow.setVisible(false); +var marketplaceWindow = new OverlayWebWindow({ + title: 'Marketplace', + source: "about:blank", + width: 900, + height: 700, + visible: false +}); function showMarketplace(marketplaceID) { var url = MARKETPLACE_URL; diff --git a/examples/flowArts/raveStick/raveStick.js b/examples/flowArts/raveStick/raveStick.js index 5fb019bf97..d1180e2b34 100644 --- a/examples/flowArts/raveStick/raveStick.js +++ b/examples/flowArts/raveStick/raveStick.js @@ -24,6 +24,9 @@ RaveStick = function(spawnPosition) { green: 10, blue: 40 }]; + + + var stick = Entities.addEntity({ type: "Model", name: "raveStick", @@ -40,23 +43,25 @@ RaveStick = function(spawnPosition) { userData: JSON.stringify({ grabbableKey: { spatialKey: { - rightRelativePosition: { - x: 0.02, - y: 0, - z: 0 - }, - leftRelativePosition: { - x: -0.02, - y: 0, - z: 0 - }, - relativeRotation: Quat.fromPitchYawRollDegrees(90, 90, 0) + rightRelativePosition: { + x: 0.02, + y: 0, + z: 0 }, + leftRelativePosition: { + x: -0.02, + y: 0, + z: 0 + }, + relativeRotation: Quat.fromPitchYawRollDegrees(90, 90, 0) + }, invertSolidWhileHeld: true } }) }); + var glowEmitter = createGlowEmitter(); + var light = Entities.addEntity({ type: 'Light', name: "raveLight", @@ -82,14 +87,76 @@ RaveStick = function(spawnPosition) { green: 200, blue: 40 }; - function cleanup() { Entities.deleteEntity(stick); Entities.deleteEntity(light); + Entities.deleteEntity(glowEmitter); } this.cleanup = cleanup; -} \ No newline at end of file + + function createGlowEmitter() { + var props = Entities.getEntityProperties(stick, ["position", "rotation"]); + var forwardVec = Quat.getFront(props.rotation); + var forwardQuat = Quat.rotationBetween(Vec3.UNIT_Z, forwardVec); + var position = props.position; + var color = { + red: 150, + green: 20, + blue: 100 + } + var props = { + type: "ParticleEffect", + name: "Rave Stick Glow Emitter", + position: position, + parentID: stick, + isEmitting: true, + colorStart: color, + color: { + red: 200, + green: 200, + blue: 255 + }, + colorFinish: color, + maxParticles: 100000, + lifespan: 0.8, + emitRate: 1000, + emitOrientation: forwardQuat, + emitSpeed: 0.2, + speedSpread: 0.0, + emitDimensions: { + x: 0, + y: 0, + z: 0 + }, + polarStart: 0, + polarFinish: 0, + azimuthStart: 0.1, + azimuthFinish: 0.01, + emitAcceleration: { + x: 0, + y: 0, + z: 0 + }, + accelerationSpread: { + x: 0.00, + y: 0.00, + z: 0.00 + }, + radiusStart: 0.01, + radiusFinish: 0.005, + alpha: 0.7, + alphaSpread: 0.1, + alphaStart: 0.1, + alphaFinish: 0.1, + textures: "https://s3.amazonaws.com/hifi-public/eric/textures/particleSprites/beamParticle.png", + emitterShouldTrail: false + } + var glowEmitter = Entities.addEntity(props); + return glowEmitter; + + } +} diff --git a/examples/tests/qmlTest.js b/examples/tests/qmlTest.js new file mode 100644 index 0000000000..f1aa59cc09 --- /dev/null +++ b/examples/tests/qmlTest.js @@ -0,0 +1,40 @@ +print("Launching web window"); +qmlWindow = new OverlayWindow({ + title: 'Test Qml', + source: "https://s3.amazonaws.com/DreamingContent/qml/content.qml", + height: 240, + width: 320, + toolWindow: false, + visible: true +}); + +//qmlWindow.eventBridge.webEventReceived.connect(function(data) { +// print("JS Side event received: " + data); +//}); +// +//var titles = ["A", "B", "C"]; +//var titleIndex = 0; +// +//Script.setInterval(function() { +// qmlWindow.eventBridge.emitScriptEvent("JS Event sent"); +// var size = qmlWindow.size; +// var position = qmlWindow.position; +// print("Window visible: " + qmlWindow.visible) +// if (qmlWindow.visible) { +// print("Window size: " + size.x + "x" + size.y) +// print("Window pos: " + position.x + "x" + position.y) +// qmlWindow.setVisible(false); +// } else { +// qmlWindow.setVisible(true); +// qmlWindow.setTitle(titles[titleIndex]); +// qmlWindow.setSize(320 + Math.random() * 100, 240 + Math.random() * 100); +// titleIndex += 1; +// titleIndex %= titles.length; +// } +//}, 2 * 1000); +// +//Script.setTimeout(function() { +// print("Closing script"); +// qmlWindow.close(); +// Script.stop(); +//}, 15 * 1000) diff --git a/examples/tests/qmlWebTest.js b/examples/tests/qmlWebTest.js index f905e494dc..5faa68668d 100644 --- a/examples/tests/qmlWebTest.js +++ b/examples/tests/qmlWebTest.js @@ -1,6 +1,7 @@ print("Launching web window"); -webWindow = new OverlayWebWindow('Test Event Bridge', "file:///C:/Users/bdavis/Git/hifi/examples/html/qmlWebTest.html", 320, 240, false); +var htmlUrl = Script.resolvePath("..//html/qmlWebTest.html") +webWindow = new OverlayWebWindow('Test Event Bridge', htmlUrl, 320, 240, false); print("JS Side window: " + webWindow); print("JS Side bridge: " + webWindow.eventBridge); webWindow.eventBridge.webEventReceived.connect(function(data) { diff --git a/examples/toybox/pistol/pistol.js b/examples/toybox/pistol/pistol.js index 7fb05d992f..e0f063d463 100644 --- a/examples/toybox/pistol/pistol.js +++ b/examples/toybox/pistol/pistol.js @@ -37,6 +37,13 @@ this.bulletForce = 10; this.showLaser = false; + this.laserOffsets = { + y: 0.095 + }; + this.firingOffsets = { + z: 0.16 + } + }; Pistol.prototype = { @@ -272,46 +279,12 @@ }); }, 100); - Entities.editEntity(this.flash, { - isEmitting: true - }); - Script.setTimeout(function() { - Entities.editEntity(_this.flash, { - isEmitting: false - }); - }, 100) - - }, - - preload: function(entityID) { - this.entityID = entityID; - this.laser = Overlays.addOverlay("line3d", { - start: ZERO_VECTOR, - end: ZERO_VECTOR, - color: COLORS.RED, - alpha: 1, - visible: true, - lineWidth: 2 - }); - this.laserOffsets = { - y: 0.095 - }; - this.firingOffsets = { - z: 0.16 - } - var gunProps = Entities.getEntityProperties(this.entityID, ['position', 'rotation']); - var position = gunProps.position; - var rotation = Quat.fromPitchYawRollDegrees(0, 0, 0); - this.firingDirection = Quat.getFront(rotation); - var upVec = Quat.getUp(rotation); - this.barrelPoint = Vec3.sum(position, Vec3.multiply(upVec, this.laserOffsets.y)); - this.barrelPoint = Vec3.sum(this.barrelPoint, Vec3.multiply(this.firingDirection, this.firingOffsets.z)) - - this.flash = Entities.addEntity({ + var flash = Entities.addEntity({ type: "ParticleEffect", position: this.barrelPoint, "name": "Muzzle Flash", - isEmitting: false, + lifetime: 4, + parentID: this.entityID, "color": { red: 228, green: 128, @@ -363,14 +336,27 @@ "additiveBlending": true, "textures": "http://ericrius1.github.io/PartiArt/assets/star.png" }); + Script.setTimeout(function() { + Entities.editEntity(flash, { + isEmitting: false + }); + }, 100) - Script.setTimeout(function() { - Entities.editEntity(_this.flash, {parentID: _this.entityID}); - }, 500) + }, + preload: function(entityID) { + this.entityID = entityID; + this.laser = Overlays.addOverlay("line3d", { + start: ZERO_VECTOR, + end: ZERO_VECTOR, + color: COLORS.RED, + alpha: 1, + visible: true, + lineWidth: 2 + }); }, }; // entity scripts always need to return a newly constructed object of our type return new Pistol(); -}); +}); \ No newline at end of file diff --git a/interface/resources/controllers/standard_navigation.json b/interface/resources/controllers/standard_navigation.json new file mode 100644 index 0000000000..c3b30e8607 --- /dev/null +++ b/interface/resources/controllers/standard_navigation.json @@ -0,0 +1,61 @@ +{ + "name": "Standard to Action", + "when": "Application.NavigationFocused", + "channels": [ + { "disabled_from": { "makeAxis" : [ "Standard.DD", "Standard.DU" ] }, "to": "Actions.UiNavVertical" }, + { "disabled_from": { "makeAxis" : [ "Standard.DL", "Standard.DR" ] }, "to": "Actions.UiNavLateral" }, + { "disabled_from": { "makeAxis" : [ "Standard.LB", "Standard.RB" ] }, "to": "Actions.UiNavGroup" }, + { "from": "Standard.DU", "to": "Actions.UiNavVertical" }, + { "from": "Standard.DD", "to": "Actions.UiNavVertical", "filters": "invert" }, + { "from": "Standard.DL", "to": "Actions.UiNavLateral", "filters": "invert" }, + { "from": "Standard.DR", "to": "Actions.UiNavLateral" }, + { "from": "Standard.LB", "to": "Actions.UiNavGroup","filters": "invert" }, + { "from": "Standard.RB", "to": "Actions.UiNavGroup" }, + { "from": [ "Standard.A", "Standard.X", "Standard.RT", "Standard.LT" ], "to": "Actions.UiNavSelect" }, + { "from": [ "Standard.B", "Standard.Y", "Standard.RightPrimaryThumb", "Standard.LeftPrimaryThumb" ], "to": "Actions.UiNavBack" }, + { + "from": [ "Standard.RT", "Standard.LT" ], + "to": "Actions.UiNavSelect", + "filters": [ + { "type": "deadZone", "min": 0.5 }, + "constrainToInteger" + ] + }, + { + "from": "Standard.LX", "to": "Actions.UiNavLateral", + "filters": [ + { "type": "deadZone", "min": 0.95 }, + "constrainToInteger", + { "type": "pulse", "interval": 0.4 } + ] + }, + { + "from": "Standard.LY", "to": "Actions.UiNavVertical", + "filters": [ + "invert", + { "type": "deadZone", "min": 0.95 }, + "constrainToInteger", + { "type": "pulse", "interval": 0.4 } + ] + }, + { + "from": "Standard.RX", "to": "Actions.UiNavLateral", + "filters": [ + { "type": "deadZone", "min": 0.95 }, + "constrainToInteger", + { "type": "pulse", "interval": 0.4 } + ] + }, + { + "from": "Standard.RY", "to": "Actions.UiNavVertical", + "filters": [ + "invert", + { "type": "deadZone", "min": 0.95 }, + "constrainToInteger", + { "type": "pulse", "interval": 0.4 } + ] + } + ] +} + + diff --git a/interface/resources/qml/AvatarInputs.qml b/interface/resources/qml/AvatarInputs.qml index 7888ffd967..49aeee3074 100644 --- a/interface/resources/qml/AvatarInputs.qml +++ b/interface/resources/qml/AvatarInputs.qml @@ -84,6 +84,7 @@ Hifi.AvatarInputs { Item { width: root.mirrorWidth height: 44 + visible: !root.isHMD x: root.mirrorLeftPad y: root.mirrorVisible ? root.mirrorTopPad + root.mirrorHeight : 5 diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index 6e9502edb2..188351c113 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -1,9 +1,6 @@ - import QtQuick 2.3 import QtQuick.Controls 1.2 import QtWebEngine 1.1 -import QtWebChannel 1.0 -import QtWebSockets 1.0 import "controls" import "styles" @@ -13,6 +10,8 @@ VrDialog { HifiConstants { id: hifi } title: "WebWindow" resizable: true + enabled: false + visible: false // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false contentImplicitWidth: clientArea.implicitWidth @@ -24,23 +23,13 @@ VrDialog { function stop() { webview.stop(); } - Component.onCompleted: { - enabled = true - console.log("Web Window Created " + root); + // Ensure the JS from the web-engine makes it to our logging webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); }); - webview.loadingChanged.connect(handleWebviewLoading) - } - - function handleWebviewLoading(loadRequest) { - if (WebEngineView.LoadStartedStatus == loadRequest.status) { - var newUrl = loadRequest.url.toString(); - root.navigating(newUrl) - } } Item { @@ -56,13 +45,28 @@ VrDialog { id: webview url: root.source anchors.fill: parent + focus: true + onUrlChanged: { var currentUrl = url.toString(); - var newUrl = urlFixer.fixupUrl(currentUrl); + var newUrl = urlHandler.fixupUrl(currentUrl).toString(); if (newUrl != currentUrl) { url = newUrl; } } + + onLoadingChanged: { + // Required to support clicking on "hifi://" links + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + var url = loadRequest.url.toString(); + if (urlHandler.canHandleUrl(url)) { + if (urlHandler.handleUrl(url)) { + webview.stop(); + } + } + } + } + profile: WebEngineProfile { id: webviewProfile httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)" diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml new file mode 100644 index 0000000000..951aa24471 --- /dev/null +++ b/interface/resources/qml/QmlWindow.qml @@ -0,0 +1,55 @@ + +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtWebChannel 1.0 +import QtWebSockets 1.0 +import "qrc:///qtwebchannel/qwebchannel.js" as WebChannel + +import "controls" +import "styles" + +VrDialog { + id: root + objectName: "topLevelWindow" + HifiConstants { id: hifi } + title: "QmlWindow" + resizable: true + enabled: false + visible: false + focus: true + property var channel; + + // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer + destroyOnCloseButton: false + contentImplicitWidth: clientArea.implicitWidth + contentImplicitHeight: clientArea.implicitHeight + property alias source: pageLoader.source + + Item { + id: clientArea + implicitHeight: 600 + implicitWidth: 800 + x: root.clientX + y: root.clientY + width: root.clientWidth + height: root.clientHeight + focus: true + clip: true + + Loader { + id: pageLoader + objectName: "Loader" + anchors.fill: parent + focus: true + property var dialog: root + + onLoaded: { + forceActiveFocus() + } + + Keys.onPressed: { + console.log("QmlWindow pageLoader keypress") + } + } + } // item +} // dialog diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 56d4f9c14b..7048efe55c 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -258,14 +258,14 @@ Item { Text { color: root.fontColor; font.pixelSize: root.fontSize - visible: root.expanded + visible: root.showAcuity text: "LOD: " + root.lodStatus; } Text { color: root.fontColor; font.pixelSize: root.fontSize visible: root.expanded - text: "Renderable avatars: " + root.avatarRenderableCount + " w/in " + root.avatarRenderDistance + "m"; + text: root.lodStatsRenderText; } } } diff --git a/interface/resources/qml/VrMenu.qml b/interface/resources/qml/VrMenu.qml index 14a4a449fd..738ec34a02 100644 --- a/interface/resources/qml/VrMenu.qml +++ b/interface/resources/qml/VrMenu.qml @@ -1,7 +1,9 @@ import Hifi 1.0 as Hifi + import QtQuick 2.4 import QtQuick.Controls 1.3 import QtQuick.Controls.Styles 1.3 + import "controls" import "styles" @@ -21,15 +23,12 @@ Hifi.VrMenu { property var models: [] property var columns: [] - onEnabledChanged: { if (enabled && columns.length == 0) { pushColumn(rootMenu.items); } opacity = enabled ? 1.0 : 0.0 - if (enabled) { - forceActiveFocus() - } + offscreenFlags.navigationFocused = enabled; } // The actual animator @@ -49,13 +48,12 @@ Hifi.VrMenu { } property var menuBuilder: Component { - Border { - HifiConstants { id: hifi } - property int menuDepth + VrMenuView { + property int menuDepth: root.models.length - 1 + model: root.models[menuDepth] Component.onCompleted: { - menuDepth = root.models.length - 1 - if (menuDepth == 0) { + if (menuDepth === 0) { x = lastMousePosition.x - 20 y = lastMousePosition.y - 20 } else { @@ -65,48 +63,8 @@ Hifi.VrMenu { } } - border.color: hifi.colors.hifiBlue - color: hifi.colors.window - implicitHeight: listView.implicitHeight + 16 - implicitWidth: listView.implicitWidth + 16 - - Column { - id: listView - property real minWidth: 0 - anchors { - top: parent.top - topMargin: 8 - left: parent.left - leftMargin: 8 - right: parent.right - rightMargin: 8 - } - - Repeater { - model: root.models[menuDepth] - delegate: Loader { - id: loader - source: "VrMenuItem.qml" - Binding { - target: loader.item - property: "menuContainer" - value: root - when: loader.status == Loader.Ready - } - Binding { - target: loader.item - property: "source" - value: modelData - when: loader.status == Loader.Ready - } - Binding { - target: loader.item - property: "listView" - value: listView - when: loader.status == Loader.Ready - } - } - } + onSelected: { + root.selectItem(menuDepth, item) } } } @@ -116,14 +74,14 @@ Hifi.VrMenu { } function pushColumn(items) { - models.push(items) + models.push(itemsToModel(items)) if (columns.length) { var oldColumn = lastColumn(); //oldColumn.enabled = false } var newColumn = menuBuilder.createObject(root); columns.push(newColumn); - newColumn.forceActiveFocus(); + forceActiveFocus(); } function popColumn() { @@ -145,13 +103,39 @@ Hifi.VrMenu { curColumn.forceActiveFocus(); } - function selectItem(source) { + function itemsToModel(items) { + var newListModel = Qt.createQmlObject('import QtQuick 2.2; ListModel {}', root); + for (var i = 0; i < items.length; ++i) { + var item = items[i]; + switch (item.type) { + case 2: + newListModel.append({"type":item.type, "name": item.title, "item": item}) + break; + case 1: + newListModel.append({"type":item.type, "name": item.text, "item": item}) + break; + case 0: + newListModel.append({"type":item.type, "name": "-----", "item": item}) + break; + } + } + return newListModel; + } + + function selectItem(depth, source) { + var popped = false; + while (depth + 1 < columns.length) { + popColumn() + popped = true + } + switch (source.type) { case 2: + lastColumn().enabled = false pushColumn(source.items) break; case 1: - source.trigger() + if (!popped) source.trigger() enabled = false break; case 0: @@ -165,14 +149,6 @@ Hifi.VrMenu { } } - Keys.onPressed: { - switch (event.key) { - case Qt.Key_Escape: - root.popColumn() - event.accepted = true; - } - } - MouseArea { anchors.fill: parent id: mouseArea @@ -206,4 +182,36 @@ Hifi.VrMenu { function removeItem(menu, menuItem) { menu.removeItem(menuItem); } + + function previousItem() { + if (columns.length) { + lastColumn().incrementCurrentIndex() + } + } + + function nextItem() { + if (columns.length) { + lastColumn().decrementCurrentIndex() + } + } + + function selectCurrentItem() { + if (columns.length) { + var depth = columns.length - 1; + var index = lastColumn().currentIndex; + if (index >= 0) { + var model = models[depth]; + var item = model.get(index).item; + selectItem(depth, item); + } + } + } + + Keys.onDownPressed: previousItem(); + Keys.onUpPressed: nextItem(); + Keys.onSpacePressed: selectCurrentItem(); + Keys.onReturnPressed: selectCurrentItem(); + Keys.onRightPressed: selectCurrentItem(); + Keys.onLeftPressed: popColumn(); + Keys.onEscapePressed: popColumn(); } diff --git a/interface/resources/qml/VrMenuItem.qml b/interface/resources/qml/VrMenuItem.qml index fbde35059d..2b1a4a3b5a 100644 --- a/interface/resources/qml/VrMenuItem.qml +++ b/interface/resources/qml/VrMenuItem.qml @@ -6,57 +6,18 @@ import "styles" Item { id: root - HifiConstants { - id: hifi + HifiConstants { + id: hifi } + // The model object + property alias text: label.text property var source - property var menuContainer - property var listView - - MouseArea { - anchors.left: parent.left - anchors.right: tag.right - anchors.rightMargin: -4 - anchors.top: parent.top - anchors.bottom: parent.bottom - acceptedButtons: Qt.LeftButton - hoverEnabled: true - - Rectangle { - id: highlight - visible: false - anchors.fill: parent - color: "#7f0e7077" - } - - onEntered: { - //if (source.type == 2 && enabled) { - // timer.start() - //} - highlight.visible = source.enabled - } - - onExited: { - timer.stop() - highlight.visible = false - } - - onClicked: { - select() - } - } implicitHeight: source.visible ? label.implicitHeight * 1.5 : 0 - implicitWidth: label.implicitWidth + label.height * 2.5 + implicitWidth: label.width + label.height * 2.5 visible: source.visible - - Timer { - id: timer - interval: 1000 - onTriggered: parent.select() - } - + width: parent.width FontAwesome { clip: true @@ -84,32 +45,18 @@ Item { Text { id: label - text: typedText() anchors.left: check.right anchors.leftMargin: 4 anchors.verticalCenter: parent.verticalCenter verticalAlignment: Text.AlignVCenter color: source.enabled ? hifi.colors.text : hifi.colors.disabledText - enabled: source.enabled && source.visible + enabled: source.visible && (source.type !== 0 ? source.enabled : false) visible: source.visible - function typedText() { - if (source) { - switch (source.type) { - case 2: - return source.title - case 1: - return source.text - case 0: - return "-----" - } - } - return "" - } } FontAwesome { id: tag - x: listView.width - width - 4 + x: root.parent.width - width size: label.height width: implicitWidth visible: source.visible && (source.type == 2) @@ -117,17 +64,4 @@ Item { anchors.verticalCenter: parent.verticalCenter color: label.color } - - function select() { - //timer.stop(); - var popped = false - while (columns.length - 1 > listView.parent.menuDepth) { - popColumn() - popped = true - } - - if (!popped || source.type != 1) { - root.menuContainer.selectItem(source) - } - } } diff --git a/interface/resources/qml/VrMenuView.qml b/interface/resources/qml/VrMenuView.qml new file mode 100644 index 0000000000..b00e21ba93 --- /dev/null +++ b/interface/resources/qml/VrMenuView.qml @@ -0,0 +1,77 @@ +import QtQuick 2.4 +import QtQuick.Controls 1.3 +import QtQuick.Controls.Styles 1.3 + +import "styles" + +ListView { + id: root + HifiConstants { id: hifi } + width: 128 + height: count * 32 + onEnabledChanged: recalcSize(); + onVisibleChanged: recalcSize(); + onCountChanged: recalcSize(); + + signal selected(var item) + + highlight: Rectangle { + width: root.currentItem ? root.currentItem.width : 0 + height: root.currentItem ? root.currentItem.height : 0 + color: "lightsteelblue"; radius: 3 + } + + delegate: VrMenuItem { + text: name + source: item + onImplicitHeightChanged: root.recalcSize() + onImplicitWidthChanged: root.recalcSize() + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: root.currentIndex = index + onClicked: root.selected(item) + } + } + + function recalcSize() { + if (model.count !== count || !visible) { + return; + } + + var originalIndex = currentIndex; + var maxWidth = width; + var newHeight = 0; + for (var i = 0; i < count; ++i) { + currentIndex = i; + if (!currentItem) { + continue; + } + if (currentItem && currentItem.implicitWidth > maxWidth) { + maxWidth = currentItem.implicitWidth + } + if (currentItem.visible) { + newHeight += currentItem.implicitHeight + } + } + if (maxWidth > width) { + width = maxWidth; + } + if (newHeight > height) { + height = newHeight + } + currentIndex = originalIndex; + } + + Border { + id: border + anchors.fill: parent + anchors.margins: -8 + z: parent.z - 1 + border.color: hifi.colors.hifiBlue + color: hifi.colors.window + } +} + + diff --git a/interface/resources/qml/controls/VrDialog.qml b/interface/resources/qml/controls/VrDialog.qml index aa14e2fcba..411fdbbb0b 100644 --- a/interface/resources/qml/controls/VrDialog.qml +++ b/interface/resources/qml/controls/VrDialog.qml @@ -41,6 +41,11 @@ DialogBase { // modify the visibility onEnabledChanged: { opacity = enabled ? 1.0 : 0.0 + // If the dialog is initially invisible, setting opacity doesn't + // trigger making it visible. + if (enabled) { + visible = true; + } } // The actual animator diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e81aa7ec52..f6350cb8d5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -11,6 +11,8 @@ #include "Application.h" +#include + #include #include #include @@ -28,11 +30,14 @@ #include #include #include -#include #include #include #include +#include +#include +#include + #include #include #include @@ -47,6 +52,7 @@ #include #include +#include #include #include #include @@ -337,6 +343,8 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); + #if defined(Q_OS_MAC) || defined(Q_OS_WIN) DependencyManager::set(); @@ -682,6 +690,74 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // Setup the userInputMapper with the actions auto userInputMapper = DependencyManager::get(); connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) { + using namespace controller; + static auto offscreenUi = DependencyManager::get(); + if (offscreenUi->navigationFocused()) { + auto actionEnum = static_cast(action); + int key = Qt::Key_unknown; + static int lastKey = Qt::Key_unknown; + bool navAxis = false; + switch (actionEnum) { + case Action::UI_NAV_VERTICAL: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Up; + } else if (state < 0.0f) { + key = Qt::Key_Down; + } + break; + + case Action::UI_NAV_LATERAL: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Right; + } else if (state < 0.0f) { + key = Qt::Key_Left; + } + break; + + case Action::UI_NAV_GROUP: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Tab; + } else if (state < 0.0f) { + key = Qt::Key_Backtab; + } + break; + + case Action::UI_NAV_BACK: + key = Qt::Key_Escape; + break; + + case Action::UI_NAV_SELECT: + key = Qt::Key_Return; + break; + } + + if (navAxis) { + if (lastKey != Qt::Key_unknown) { + QKeyEvent event(QEvent::KeyRelease, lastKey, Qt::NoModifier); + sendEvent(offscreenUi->getWindow(), &event); + lastKey = Qt::Key_unknown; + } + + if (key != Qt::Key_unknown) { + QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); + sendEvent(offscreenUi->getWindow(), &event); + lastKey = key; + } + } else if (key != Qt::Key_unknown) { + if (state) { + QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); + sendEvent(offscreenUi->getWindow(), &event); + } else { + QKeyEvent event(QEvent::KeyRelease, key, Qt::NoModifier); + sendEvent(offscreenUi->getWindow(), &event); + } + return; + } + } + if (action == controller::toInt(controller::Action::RETICLE_CLICK)) { auto globalPos = QCursor::pos(); auto localPos = _glWidget->mapFromGlobal(globalPos); @@ -752,6 +828,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _applicationStateDevice->addInputVariant(QString("Grounded"), controller::StateController::ReadLambda([]() -> float { return (float)qApp->getMyAvatar()->getCharacterController()->onGround(); })); + _applicationStateDevice->addInputVariant(QString("NavigationFocused"), controller::StateController::ReadLambda([]() -> float { + static auto offscreenUi = DependencyManager::get(); + return offscreenUi->navigationFocused() ? 1.0 : 0.0; + })); userInputMapper->registerDevice(_applicationStateDevice); @@ -1094,9 +1174,59 @@ void Application::initializeUi() { offscreenUi->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); offscreenUi->load("Root.qml"); offscreenUi->load("RootMenu.qml"); - auto scriptingInterface = DependencyManager::get(); - offscreenUi->getRootContext()->setContextProperty("Controller", scriptingInterface.data()); - offscreenUi->getRootContext()->setContextProperty("MyAvatar", getMyAvatar()); + // FIXME either expose so that dialogs can set this themselves or + // do better detection in the offscreen UI of what has focus + offscreenUi->setNavigationFocused(false); + + auto rootContext = offscreenUi->getRootContext(); + auto engine = rootContext->engine(); + connect(engine, &QQmlEngine::quit, [] { + qApp->quit(); + }); + rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); + rootContext->setContextProperty("AnimationCache", DependencyManager::get().data()); + rootContext->setContextProperty("Controller", DependencyManager::get().data()); + rootContext->setContextProperty("Entities", DependencyManager::get().data()); + rootContext->setContextProperty("MyAvatar", getMyAvatar()); + rootContext->setContextProperty("Messages", DependencyManager::get().data()); + rootContext->setContextProperty("Recording", DependencyManager::get().data()); + + rootContext->setContextProperty("TREE_SCALE", TREE_SCALE); + rootContext->setContextProperty("Quat", new Quat()); + rootContext->setContextProperty("Vec3", new Vec3()); + rootContext->setContextProperty("Uuid", new ScriptUUID()); + + rootContext->setContextProperty("AvatarList", DependencyManager::get().data()); + + rootContext->setContextProperty("Camera", &_myCamera); + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + rootContext->setContextProperty("SpeechRecognizer", DependencyManager::get().data()); +#endif + + rootContext->setContextProperty("Overlays", &_overlays); + rootContext->setContextProperty("Desktop", DependencyManager::get().data()); + + rootContext->setContextProperty("Window", DependencyManager::get().data()); + rootContext->setContextProperty("Menu", MenuScriptingInterface::getInstance()); + rootContext->setContextProperty("Stats", Stats::getInstance()); + rootContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + rootContext->setContextProperty("AudioDevice", AudioDeviceScriptingInterface::getInstance()); + rootContext->setContextProperty("AnimationCache", DependencyManager::get().data()); + rootContext->setContextProperty("SoundCache", DependencyManager::get().data()); + rootContext->setContextProperty("Account", AccountScriptingInterface::getInstance()); + rootContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); + rootContext->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); + rootContext->setContextProperty("FaceTracker", DependencyManager::get().data()); + rootContext->setContextProperty("AvatarManager", DependencyManager::get().data()); + rootContext->setContextProperty("UndoStack", &_undoStackScriptingInterface); + rootContext->setContextProperty("LODManager", DependencyManager::get().data()); + rootContext->setContextProperty("Paths", DependencyManager::get().data()); + rootContext->setContextProperty("HMD", DependencyManager::get().data()); + rootContext->setContextProperty("Scene", DependencyManager::get().data()); + rootContext->setContextProperty("Render", DependencyManager::get().data()); + rootContext->setContextProperty("ScriptDiscoveryService", this->getRunningScriptsWidget()); + _glWidget->installEventFilter(offscreenUi.data()); VrMenu::load(); VrMenu::executeQueuedLambdas(); @@ -2860,7 +2990,11 @@ void Application::update(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::update()"); - updateLOD(); + if (DependencyManager::get()->getUseAcuity()) { + updateLOD(); + } else { + DependencyManager::get()->updatePIDRenderDistance(getTargetFrameRate(), getLastInstanteousFps(), deltaTime, isThrottleRendering()); + } { PerformanceTimer perfTimer("devices"); @@ -2999,8 +3133,7 @@ void Application::update(float deltaTime) { getEntities()->update(); // update the models... } - myAvatar->harvestResultsFromPhysicsSimulation(); - myAvatar->simulateAttachments(deltaTime); + myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); } } @@ -4111,6 +4244,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerFunction("WebWindow", WebWindowClass::constructor, 1); scriptEngine->registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); + scriptEngine->registerFunction("OverlayWindow", QmlWindowClass::constructor); scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); diff --git a/interface/src/LODManager.cpp b/interface/src/LODManager.cpp index 368143e36e..10a288a44e 100644 --- a/interface/src/LODManager.cpp +++ b/interface/src/LODManager.cpp @@ -9,6 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include @@ -20,9 +21,30 @@ Setting::Handle desktopLODDecreaseFPS("desktopLODDecreaseFPS", DEFAULT_DESKTOP_LOD_DOWN_FPS); Setting::Handle hmdLODDecreaseFPS("hmdLODDecreaseFPS", DEFAULT_HMD_LOD_DOWN_FPS); +// There are two different systems in use, based on lodPreference: +// pid: renderDistance is adjusted by a PID such that frame rate targets are met. +// acuity: a pseudo-acuity target is held, or adjusted to match minimum frame rates (and a PID controlls avatar rendering distance) +// If unspecified, acuity is used only if user has specified non-default minumum frame rates. +Setting::Handle lodPreference("lodPreference", (int)LODManager::LODPreference::unspecified); +const float SMALLEST_REASONABLE_HORIZON = 50.0f; // meters +Setting::Handle renderDistanceInverseHighLimit("renderDistanceInverseHighLimit", 1.0f / SMALLEST_REASONABLE_HORIZON); +void LODManager::setRenderDistanceInverseHighLimit(float newValue) { + renderDistanceInverseHighLimit.set(newValue); // persist it, and tell all the controllers that use it + _renderDistanceController.setControlledValueHighLimit(newValue); +} LODManager::LODManager() { calculateAvatarLODDistanceMultiplier(); + + setRenderDistanceInverseHighLimit(renderDistanceInverseHighLimit.get()); + setRenderDistanceInverseLowLimit(1.0f / (float)TREE_SCALE); + // Advice for tuning parameters: + // See PIDController.h. There's a section on tuning in the reference. + // Turn on logging with the following (or from js with LODManager.setRenderDistanceControllerHistory("render pid", 240)) + //setRenderDistanceControllerHistory("render pid", 60 * 4); + // Note that extra logging/hysteresis is turned off in Avatar.cpp when the above logging is on. + setRenderDistanceKP(0.000012f); // Usually about 0.6 of largest that doesn't oscillate when other parameters 0. + setRenderDistanceKI(0.00002f); // Big enough to bring us to target with the above KP. } float LODManager::getLODDecreaseFPS() { @@ -39,7 +61,6 @@ float LODManager::getLODIncreaseFPS() { return getDesktopLODIncreaseFPS(); } - void LODManager::autoAdjustLOD(float currentFPS) { // NOTE: our first ~100 samples at app startup are completely all over the place, and we don't @@ -217,15 +238,58 @@ QString LODManager::getLODFeedbackText() { return result; } +static float renderDistance = (float)TREE_SCALE; +static int renderedCount = 0; +static int lastRenderedCount = 0; +bool LODManager::getUseAcuity() { return lodPreference.get() == (int)LODManager::LODPreference::acuity; } +void LODManager::setUseAcuity(bool newValue) { lodPreference.set(newValue ? (int)LODManager::LODPreference::acuity : (int)LODManager::LODPreference::pid); } +float LODManager::getRenderDistance() { + return renderDistance; +} +int LODManager::getRenderedCount() { + return lastRenderedCount; +} +QString LODManager::getLODStatsRenderText() { + QString label = getUseAcuity() ? "Renderable avatars: " : "Rendered objects: "; + return label + QString::number(getRenderedCount()) + " w/in " + QString::number((int)getRenderDistance()) + "m"; +} +// compare audoAdjustLOD() +void LODManager::updatePIDRenderDistance(float targetFps, float measuredFps, float deltaTime, bool isThrottled) { + float distance; + if (!isThrottled) { + _renderDistanceController.setMeasuredValueSetpoint(targetFps / 2.0f); // No problem updating in flight. + // The PID controller raises the controlled value when the measured value goes up. + // The measured value is frame rate. When the controlled value (1 / render cutoff distance) + // goes up, the render cutoff distance gets closer, the number of rendered avatars is less, and frame rate + // goes up. + distance = 1.0f / _renderDistanceController.update(measuredFps, deltaTime); + } else { + // Here we choose to just use the maximum render cutoff distance if throttled. + distance = 1.0f / _renderDistanceController.getControlledValueLowLimit(); + } + _renderDistanceAverage.updateAverage(distance); + renderDistance = _renderDistanceAverage.getAverage(); // average only once per cycle + lastRenderedCount = renderedCount; + renderedCount = 0; +} + bool LODManager::shouldRender(const RenderArgs* args, const AABox& bounds) { + float distanceToCamera = glm::length(bounds.calcCenter() - args->_viewFrustum->getPosition()); + float largestDimension = bounds.getLargestDimension(); + if (!getUseAcuity()) { + const float scenerySize = 300; // meters + bool isRendered = (largestDimension > scenerySize) || // render scenery regardless of distance + (fabsf(distanceToCamera - largestDimension) < renderDistance); + renderedCount += isRendered ? 1 : 0; + return isRendered; + } + const float maxScale = (float)TREE_SCALE; const float octreeToMeshRatio = 4.0f; // must be this many times closer to a mesh than a voxel to see it. float octreeSizeScale = args->_sizeScale; int boundaryLevelAdjust = args->_boundaryLevelAdjust; float visibleDistanceAtMaxScale = boundaryDistanceForRenderLevel(boundaryLevelAdjust, octreeSizeScale) / octreeToMeshRatio; - float distanceToCamera = glm::length(bounds.calcCenter() - args->_viewFrustum->getPosition()); - float largestDimension = bounds.getLargestDimension(); - + static bool shouldRenderTableNeedsBuilding = true; static QMap shouldRenderTable; if (shouldRenderTableNeedsBuilding) { @@ -315,6 +379,12 @@ void LODManager::setBoundaryLevelAdjust(int boundaryLevelAdjust) { void LODManager::loadSettings() { setDesktopLODDecreaseFPS(desktopLODDecreaseFPS.get()); setHMDLODDecreaseFPS(hmdLODDecreaseFPS.get()); + + if (lodPreference.get() == (int)LODManager::LODPreference::unspecified) { + setUseAcuity((getDesktopLODDecreaseFPS() != DEFAULT_DESKTOP_LOD_DOWN_FPS) || (getHMDLODDecreaseFPS() != DEFAULT_HMD_LOD_DOWN_FPS)); + } + Menu::getInstance()->getActionForOption(MenuOption::LodTools)->setEnabled(getUseAcuity()); + Menu::getInstance()->getSubMenuFromName(MenuOption::RenderResolution, Menu::getInstance()->getSubMenuFromName("Render", Menu::getInstance()->getMenu("Developer")))->setEnabled(getUseAcuity()); } void LODManager::saveSettings() { diff --git a/interface/src/LODManager.h b/interface/src/LODManager.h index b0185b528f..6b141004e5 100644 --- a/interface/src/LODManager.h +++ b/interface/src/LODManager.h @@ -15,6 +15,7 @@ #include #include #include +#include #include const float DEFAULT_DESKTOP_LOD_DOWN_FPS = 15.0; @@ -81,6 +82,27 @@ public: Q_INVOKABLE float getLODDecreaseFPS(); Q_INVOKABLE float getLODIncreaseFPS(); + enum class LODPreference { + pid = 0, + acuity, + unspecified + }; + static bool getUseAcuity(); + static void setUseAcuity(bool newValue); + Q_INVOKABLE void setRenderDistanceKP(float newValue) { _renderDistanceController.setKP(newValue); } + Q_INVOKABLE void setRenderDistanceKI(float newValue) { _renderDistanceController.setKI(newValue); } + Q_INVOKABLE void setRenderDistanceKD(float newValue) { _renderDistanceController.setKD(newValue); } + Q_INVOKABLE bool getRenderDistanceControllerIsLogging() { return _renderDistanceController.getIsLogging(); } + Q_INVOKABLE void setRenderDistanceControllerHistory(QString label, int size) { return _renderDistanceController.setHistorySize(label, size); } + Q_INVOKABLE float getRenderDistanceInverseLowLimit() { return _renderDistanceController.getControlledValueLowLimit(); } + Q_INVOKABLE void setRenderDistanceInverseLowLimit(float newValue) { _renderDistanceController.setControlledValueLowLimit(newValue); } + Q_INVOKABLE float getRenderDistanceInverseHighLimit() { return _renderDistanceController.getControlledValueHighLimit(); } + Q_INVOKABLE void setRenderDistanceInverseHighLimit(float newValue); + void updatePIDRenderDistance(float targetFps, float measuredFps, float deltaTime, bool isThrottled); + float getRenderDistance(); + int getRenderedCount(); + QString getLODStatsRenderText(); + static bool shouldRender(const RenderArgs* args, const AABox& bounds); bool shouldRenderMesh(float largestDimension, float distanceToCamera); void autoAdjustLOD(float currentFPS); @@ -116,6 +138,9 @@ private: bool _shouldRenderTableNeedsRebuilding = true; QMap _shouldRenderTable; + + PIDController _renderDistanceController{}; + SimpleMovingAverage _renderDistanceAverage{ 10 }; }; #endif // hifi_LODManager_h diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 4b77e96f79..4d898caefc 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -64,6 +64,7 @@ public: void saveSettings(); MenuWrapper* getMenu(const QString& menuName); + MenuWrapper* getSubMenuFromName(const QString& menuName, MenuWrapper* menu); void triggerOption(const QString& menuOption); QAction* getActionForOption(const QString& menuOption); @@ -130,7 +131,6 @@ private: const QString& grouping = QString()); QAction* getActionFromName(const QString& menuName, MenuWrapper* menu); - MenuWrapper* getSubMenuFromName(const QString& menuName, MenuWrapper* menu); MenuWrapper* getMenuParent(const QString& menuName, QString& finalMenuPart); QAction* getMenuAction(const QString& menuName); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 03d18330e8..d0eaea8f23 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -42,6 +42,7 @@ #include "Util.h" #include "world.h" #include "InterfaceLogging.h" +#include "SoftAttachmentModel.h" #include using namespace std; @@ -108,9 +109,6 @@ Avatar::Avatar(RigPointer rig) : Avatar::~Avatar() { assert(_motionState == nullptr); - for(auto attachment : _unusedAttachments) { - delete attachment; - } } const float BILLBOARD_LOD_DISTANCE = 40.0f; @@ -186,26 +184,6 @@ void Avatar::simulate(float deltaTime) { qCDebug(interfaceapp) << "Billboarding" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for LOD" << getLODDistance(); } - const bool isControllerLogging = DependencyManager::get()->getRenderDistanceControllerIsLogging(); - float renderDistance = DependencyManager::get()->getRenderDistance(); - const float SKIP_HYSTERESIS_PROPORTION = isControllerLogging ? 0.0f : BILLBOARD_HYSTERESIS_PROPORTION; - float distance = glm::distance(qApp->getCamera()->getPosition(), getPosition()); - if (_shouldSkipRender) { - if (distance < renderDistance * (1.0f - SKIP_HYSTERESIS_PROPORTION)) { - _shouldSkipRender = false; - _skeletonModel.setVisibleInScene(true, qApp->getMain3DScene()); - if (!isControllerLogging) { // Test for isMyAvatar is prophylactic. Never occurs in current code. - qCDebug(interfaceapp) << "Rerendering" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for distance" << renderDistance; - } - } - } else if (distance > renderDistance * (1.0f + SKIP_HYSTERESIS_PROPORTION)) { - _shouldSkipRender = true; - _skeletonModel.setVisibleInScene(false, qApp->getMain3DScene()); - if (!isControllerLogging) { - qCDebug(interfaceapp) << "Unrendering" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for distance" << renderDistance; - } - } - // simple frustum check float boundingRadius = getBillboardSize(); bool inViewFrustum = qApp->getViewFrustum()->sphereInFrustum(getPosition(), boundingRadius) != @@ -257,6 +235,8 @@ void Avatar::simulate(float deltaTime) { // until velocity is included in AvatarData update message. //_position += _velocity * deltaTime; measureMotionDerivatives(deltaTime); + + simulateAttachments(deltaTime); } bool Avatar::isLookingAtMe(AvatarSharedPointer avatar) const { @@ -324,7 +304,7 @@ bool Avatar::addToScene(AvatarSharedPointer self, std::shared_ptr _skeletonModel.addToScene(scene, pendingChanges); getHead()->getFaceModel().addToScene(scene, pendingChanges); - for (auto attachmentModel : _attachmentModels) { + for (auto& attachmentModel : _attachmentModels) { attachmentModel->addToScene(scene, pendingChanges); } @@ -335,7 +315,7 @@ void Avatar::removeFromScene(AvatarSharedPointer self, std::shared_ptrgetFaceModel().removeFromScene(scene, pendingChanges); - for (auto attachmentModel : _attachmentModels) { + for (auto& attachmentModel : _attachmentModels) { attachmentModel->removeFromScene(scene, pendingChanges); } } @@ -565,15 +545,14 @@ void Avatar::fixupModelsInScene() { faceModel.removeFromScene(scene, pendingChanges); faceModel.addToScene(scene, pendingChanges); } - for (auto attachmentModel : _attachmentModels) { + for (auto& attachmentModel : _attachmentModels) { if (attachmentModel->isRenderable() && attachmentModel->needsFixupInScene()) { attachmentModel->removeFromScene(scene, pendingChanges); attachmentModel->addToScene(scene, pendingChanges); } } - for (auto attachmentModelToRemove : _attachmentsToRemove) { + for (auto& attachmentModelToRemove : _attachmentsToRemove) { attachmentModelToRemove->removeFromScene(scene, pendingChanges); - _unusedAttachments << attachmentModelToRemove; } _attachmentsToRemove.clear(); scene->enqueuePendingChanges(pendingChanges); @@ -603,21 +582,29 @@ bool Avatar::shouldRenderHead(const RenderArgs* renderArgs) const { return true; } +// virtual void Avatar::simulateAttachments(float deltaTime) { for (int i = 0; i < _attachmentModels.size(); i++) { const AttachmentData& attachment = _attachmentData.at(i); - Model* model = _attachmentModels.at(i); + auto& model = _attachmentModels.at(i); int jointIndex = getJointIndex(attachment.jointName); glm::vec3 jointPosition; glm::quat jointRotation; - if (_skeletonModel.getJointPositionInWorldFrame(jointIndex, jointPosition) && - _skeletonModel.getJointRotationInWorldFrame(jointIndex, jointRotation)) { - model->setTranslation(jointPosition + jointRotation * attachment.translation * getUniformScale()); - model->setRotation(jointRotation * attachment.rotation); - model->setScaleToFit(true, getUniformScale() * attachment.scale, true); // hack to force rescale - model->setSnapModelToCenter(false); // hack to force resnap - model->setSnapModelToCenter(true); + if (attachment.isSoft) { + // soft attachments do not have transform offsets + model->setTranslation(getPosition()); + model->setRotation(getOrientation() * Quaternions::Y_180); model->simulate(deltaTime); + } else { + if (_skeletonModel.getJointPositionInWorldFrame(jointIndex, jointPosition) && + _skeletonModel.getJointRotationInWorldFrame(jointIndex, jointRotation)) { + model->setTranslation(jointPosition + jointRotation * attachment.translation * getUniformScale()); + model->setRotation(jointRotation * attachment.rotation); + model->setScaleToFit(true, getUniformScale() * attachment.scale, true); // hack to force rescale + model->setSnapModelToCenter(false); // hack to force resnap + model->setSnapModelToCenter(true); + model->simulate(deltaTime); + } } } } @@ -940,13 +927,48 @@ void Avatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { _skeletonModel.setURL(_skeletonModelURL); } +// create new model, can return an instance of a SoftAttachmentModel rather then Model +static std::shared_ptr allocateAttachmentModel(bool isSoft, RigPointer rigOverride) { + if (isSoft) { + // cast to std::shared_ptr + return std::dynamic_pointer_cast(std::make_shared(std::make_shared(), nullptr, rigOverride)); + } else { + return std::make_shared(std::make_shared()); + } +} + void Avatar::setAttachmentData(const QVector& attachmentData) { - AvatarData::setAttachmentData(attachmentData); if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setAttachmentData", Qt::DirectConnection, Q_ARG(const QVector, attachmentData)); return; } + + auto oldAttachmentData = _attachmentData; + AvatarData::setAttachmentData(attachmentData); + + // if number of attachments has been reduced, remove excess models. + while (_attachmentModels.size() > attachmentData.size()) { + auto attachmentModel = _attachmentModels.back(); + _attachmentModels.pop_back(); + _attachmentsToRemove.push_back(attachmentModel); + } + + for (int i = 0; i < attachmentData.size(); i++) { + if (i == _attachmentModels.size()) { + // if number of attachments has been increased, we need to allocate a new model + _attachmentModels.push_back(allocateAttachmentModel(attachmentData[i].isSoft, _skeletonModel.getRig())); + } + else if (i < oldAttachmentData.size() && oldAttachmentData[i].isSoft != attachmentData[i].isSoft) { + // if the attachment has changed type, we need to re-allocate a new one. + _attachmentsToRemove.push_back(_attachmentModels[i]); + _attachmentModels[i] = allocateAttachmentModel(attachmentData[i].isSoft, _skeletonModel.getRig()); + } + _attachmentModels[i]->setURL(attachmentData[i].modelURL); + } + + // AJT: TODO REMOVE + /* // make sure we have as many models as attachments while (_attachmentModels.size() < attachmentData.size()) { Model* model = nullptr; @@ -959,16 +981,20 @@ void Avatar::setAttachmentData(const QVector& attachmentData) { _attachmentModels.append(model); } while (_attachmentModels.size() > attachmentData.size()) { - auto attachmentModel = _attachmentModels.takeLast(); - _attachmentsToRemove << attachmentModel; + auto attachmentModel = _attachmentModels.back(); + _attachmentModels.pop_back(); + _attachmentsToRemove.push_back(attachmentModel); } + */ + /* // update the urls for (int i = 0; i < attachmentData.size(); i++) { _attachmentModels[i]->setURL(attachmentData.at(i).modelURL); _attachmentModels[i]->setSnapModelToCenter(true); _attachmentModels[i]->setScaleToFit(true, getUniformScale() * _attachmentData.at(i).scale); } + */ } void Avatar::setBillboard(const QByteArray& billboard) { diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 03ab80044b..8f89027d2f 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -68,7 +68,7 @@ public: void init(); void simulate(float deltaTime); - void simulateAttachments(float deltaTime); + virtual void simulateAttachments(float deltaTime); virtual void render(RenderArgs* renderArgs, const glm::vec3& cameraPosition); @@ -179,9 +179,9 @@ protected: SkeletonModel _skeletonModel; glm::vec3 _skeletonOffset; - QVector _attachmentModels; - QVector _attachmentsToRemove; - QVector _unusedAttachments; + std::vector> _attachmentModels; + std::vector> _attachmentsToRemove; + float _bodyYawDelta; // degrees/sec // These position histories and derivatives are in the world-frame. diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 217cd28e61..7d8d5e4c9b 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -76,13 +76,6 @@ AvatarManager::AvatarManager(QObject* parent) : packetReceiver.registerListener(PacketType::AvatarBillboard, this, "processAvatarBillboardPacket"); } -const float SMALLEST_REASONABLE_HORIZON = 5.0f; // meters -Setting::Handle avatarRenderDistanceInverseHighLimit("avatarRenderDistanceHighLimit", 1.0f / SMALLEST_REASONABLE_HORIZON); -void AvatarManager::setRenderDistanceInverseHighLimit(float newValue) { - avatarRenderDistanceInverseHighLimit.set(newValue); - _renderDistanceController.setControlledValueHighLimit(newValue); -} - void AvatarManager::init() { _myAvatar->init(); { @@ -98,19 +91,6 @@ void AvatarManager::init() { _myAvatar->addToScene(_myAvatar, scene, pendingChanges); } scene->enqueuePendingChanges(pendingChanges); - - const float target_fps = qApp->getTargetFrameRate(); - _renderDistanceController.setMeasuredValueSetpoint(target_fps); - _renderDistanceController.setControlledValueHighLimit(avatarRenderDistanceInverseHighLimit.get()); - _renderDistanceController.setControlledValueLowLimit(1.0f / (float) TREE_SCALE); - // Advice for tuning parameters: - // See PIDController.h. There's a section on tuning in the reference. - // Turn on logging with the following (or from js with AvatarList.setRenderDistanceControllerHistory("avatar render", 300)) - //_renderDistanceController.setHistorySize("avatar render", target_fps * 4); - // Note that extra logging/hysteresis is turned off in Avatar.cpp when the above logging is on. - _renderDistanceController.setKP(0.0008f); // Usually about 0.6 of largest that doesn't oscillate when other parameters 0. - _renderDistanceController.setKI(0.0006f); // Big enough to bring us to target with the above KP. - _renderDistanceController.setKD(0.000001f); // A touch of kd increases the speed by which we get there. } void AvatarManager::updateMyAvatar(float deltaTime) { @@ -145,23 +125,6 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { PerformanceTimer perfTimer("otherAvatars"); - float distance; - if (!qApp->isThrottleRendering()) { - _renderDistanceController.setMeasuredValueSetpoint(qApp->getTargetFrameRate()); // No problem updating in flight. - // The PID controller raises the controlled value when the measured value goes up. - // The measured value is frame rate. When the controlled value (1 / render cutoff distance) - // goes up, the render cutoff distance gets closer, the number of rendered avatars is less, and frame rate - // goes up. - const float deduced = qApp->getLastUnsynchronizedFps(); - distance = 1.0f / _renderDistanceController.update(deduced, deltaTime); - } else { - // Here we choose to just use the maximum render cutoff distance if throttled. - distance = 1.0f / _renderDistanceController.getControlledValueLowLimit(); - } - _renderDistanceAverage.updateAverage(distance); - _renderDistance = _renderDistanceAverage.getAverage(); - int renderableCount = 0; - // simulate avatars auto hashCopy = getHashCopy(); @@ -179,14 +142,10 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { } else { avatar->startUpdate(); avatar->simulate(deltaTime); - if (avatar->getShouldRender()) { - renderableCount++; - } avatar->endUpdate(); ++avatarIterator; } } - _renderedAvatarCount = renderableCount; // simulate avatar fades simulateAvatarFades(deltaTime); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 1b165495c3..f34a784ba5 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -45,7 +45,6 @@ public: void clearOtherAvatars(); bool shouldShowReceiveStats() const { return _shouldShowReceiveStats; } - PIDController& getRenderDistanceController() { return _renderDistanceController; } class LocalLight { public: @@ -68,19 +67,6 @@ public: void addAvatarToSimulation(Avatar* avatar); - // Expose results and parameter-tuning operations to other systems, such as stats and javascript. - Q_INVOKABLE float getRenderDistance() { return _renderDistance; } - Q_INVOKABLE float getRenderDistanceInverseLowLimit() { return _renderDistanceController.getControlledValueLowLimit(); } - Q_INVOKABLE float getRenderDistanceInverseHighLimit() { return _renderDistanceController.getControlledValueHighLimit(); } - Q_INVOKABLE int getNumberInRenderRange() { return _renderedAvatarCount; } - Q_INVOKABLE bool getRenderDistanceControllerIsLogging() { return _renderDistanceController.getIsLogging(); } - Q_INVOKABLE void setRenderDistanceControllerHistory(QString label, int size) { return _renderDistanceController.setHistorySize(label, size); } - Q_INVOKABLE void setRenderDistanceKP(float newValue) { _renderDistanceController.setKP(newValue); } - Q_INVOKABLE void setRenderDistanceKI(float newValue) { _renderDistanceController.setKI(newValue); } - Q_INVOKABLE void setRenderDistanceKD(float newValue) { _renderDistanceController.setKD(newValue); } - Q_INVOKABLE void setRenderDistanceInverseLowLimit(float newValue) { _renderDistanceController.setControlledValueLowLimit(newValue); } - Q_INVOKABLE void setRenderDistanceInverseHighLimit(float newValue); - public slots: void setShouldShowReceiveStats(bool shouldShowReceiveStats) { _shouldShowReceiveStats = shouldShowReceiveStats; } void updateAvatarRenderStatus(bool shouldRenderAvatars); @@ -106,10 +92,6 @@ private: QVector _localLights; bool _shouldShowReceiveStats = false; - float _renderDistance { (float) TREE_SCALE }; - int _renderedAvatarCount { 0 }; - PIDController _renderDistanceController { }; - SimpleMovingAverage _renderDistanceAverage { 10 }; SetOfAvatarMotionStates _avatarMotionStates; SetOfMotionStates _motionStatesToAdd; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index c989f1dc71..000cd75784 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -201,6 +201,11 @@ MyAvatar::~MyAvatar() { _lookAtTargetAvatar.reset(); } +// virtual +void MyAvatar::simulateAttachments(float deltaTime) { + // don't update attachments here, do it in harvestResultsFromPhysicsSimulation() +} + QByteArray MyAvatar::toByteArray(bool cullSmallChanges, bool sendAll) { CameraMode mode = qApp->getCamera()->getMode(); _globalPosition = getPosition(); @@ -621,6 +626,7 @@ void MyAvatar::saveData() { settings.setValue("rotation_y", eulers.y); settings.setValue("rotation_z", eulers.z); settings.setValue("scale", attachment.scale); + settings.setValue("isSoft", attachment.isSoft); } settings.endArray(); @@ -702,6 +708,7 @@ void MyAvatar::loadData() { eulers.z = loadSetting(settings, "rotation_z", 0.0f); attachment.rotation = glm::quat(eulers); attachment.scale = loadSetting(settings, "scale", 1.0f); + attachment.isSoft = settings.value("isSoft").toBool(); attachmentData.append(attachment); } settings.endArray(); @@ -1057,7 +1064,7 @@ void MyAvatar::prepareForPhysicsSimulation() { _characterController.setFollowVelocity(_followVelocity); } -void MyAvatar::harvestResultsFromPhysicsSimulation() { +void MyAvatar::harvestResultsFromPhysicsSimulation(float deltaType) { glm::vec3 position = getPosition(); glm::quat orientation = getOrientation(); _characterController.getPositionAndOrientation(position, orientation); @@ -1068,6 +1075,9 @@ void MyAvatar::harvestResultsFromPhysicsSimulation() { } else { setVelocity(_characterController.getLinearVelocity()); } + + // now that physics has adjusted our position, we can update attachements. + Avatar::simulateAttachments(deltaType); } void MyAvatar::adjustSensorTransform() { @@ -1599,7 +1609,7 @@ void MyAvatar::maybeUpdateBillboard() { if (_billboardValid || !(_skeletonModel.isLoadedWithTextures() && getHead()->getFaceModel().isLoadedWithTextures())) { return; } - foreach (Model* model, _attachmentModels) { + for (auto& model : _attachmentModels) { if (!model->isLoadedWithTextures()) { return; } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 019ba0f992..1a5753d0a5 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -83,6 +83,8 @@ public: MyAvatar(RigPointer rig); ~MyAvatar(); + virtual void simulateAttachments(float deltaTime) override; + AudioListenerMode getAudioListenerModeHead() const { return FROM_HEAD; } AudioListenerMode getAudioListenerModeCamera() const { return FROM_CAMERA; } AudioListenerMode getAudioListenerModeCustom() const { return CUSTOM; } @@ -204,7 +206,7 @@ public: MyCharacterController* getCharacterController() { return &_characterController; } void prepareForPhysicsSimulation(); - void harvestResultsFromPhysicsSimulation(); + void harvestResultsFromPhysicsSimulation(float deltaTime); void adjustSensorTransform(); const QString& getCollisionSoundURL() { return _collisionSoundURL; } diff --git a/interface/src/avatar/SoftAttachmentModel.cpp b/interface/src/avatar/SoftAttachmentModel.cpp new file mode 100644 index 0000000000..92534f01ea --- /dev/null +++ b/interface/src/avatar/SoftAttachmentModel.cpp @@ -0,0 +1,84 @@ +// +// SoftAttachmentModel.cpp +// interface/src/avatar +// +// Created by Anthony J. Thibault on 12/17/15. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "SoftAttachmentModel.h" +#include "InterfaceLogging.h" + +SoftAttachmentModel::SoftAttachmentModel(RigPointer rig, QObject* parent, RigPointer rigOverride) : + Model(rig, parent), + _rigOverride(rigOverride) { + assert(_rig); + assert(_rigOverride); +} + +SoftAttachmentModel::~SoftAttachmentModel() { +} + +// virtual +void SoftAttachmentModel::updateRig(float deltaTime, glm::mat4 parentTransform) { + _needsUpdateClusterMatrices = true; +} + +int SoftAttachmentModel::getJointIndexOverride(int i) const { + QString name = _rig->nameOfJoint(i); + if (name.isEmpty()) { + return -1; + } + return _rigOverride->indexOfJoint(name); +} + +// virtual +// use the _rigOverride matrices instead of the Model::_rig +void SoftAttachmentModel::updateClusterMatrices(glm::vec3 modelPosition, glm::quat modelOrientation) { + if (!_needsUpdateClusterMatrices) { + return; + } + _needsUpdateClusterMatrices = false; + + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + + glm::mat4 modelToWorld = glm::mat4_cast(modelOrientation); + for (int i = 0; i < _meshStates.size(); i++) { + MeshState& state = _meshStates[i]; + const FBXMesh& mesh = geometry.meshes.at(i); + + for (int j = 0; j < mesh.clusters.size(); j++) { + const FBXCluster& cluster = mesh.clusters.at(j); + + // TODO: cache these look ups as an optimization + int jointIndexOverride = getJointIndexOverride(cluster.jointIndex); + glm::mat4 jointMatrix(glm::mat4::_null); + if (jointIndexOverride >= 0 && jointIndexOverride < _rigOverride->getJointStateCount()) { + jointMatrix = _rigOverride->getJointTransform(jointIndexOverride); + } else { + jointMatrix = _rig->getJointTransform(cluster.jointIndex); + } + state.clusterMatrices[j] = modelToWorld * jointMatrix * cluster.inverseBindMatrix; + } + + // Once computed the cluster matrices, update the buffer(s) + if (mesh.clusters.size() > 1) { + if (!state.clusterBuffer) { + state.clusterBuffer = std::make_shared(state.clusterMatrices.size() * sizeof(glm::mat4), + (const gpu::Byte*) state.clusterMatrices.constData()); + } else { + state.clusterBuffer->setSubData(0, state.clusterMatrices.size() * sizeof(glm::mat4), + (const gpu::Byte*) state.clusterMatrices.constData()); + } + } + } + + // post the blender if we're not currently waiting for one to finish + if (geometry.hasBlendedMeshes() && _blendshapeCoefficients != _blendedBlendshapeCoefficients) { + _blendedBlendshapeCoefficients = _blendshapeCoefficients; + DependencyManager::get()->noteRequiresBlend(this); + } +} diff --git a/interface/src/avatar/SoftAttachmentModel.h b/interface/src/avatar/SoftAttachmentModel.h new file mode 100644 index 0000000000..84a8b4cc66 --- /dev/null +++ b/interface/src/avatar/SoftAttachmentModel.h @@ -0,0 +1,42 @@ +// +// SoftAttachmentModel.h +// interface/src/avatar +// +// Created by Anthony J. Thibault on 12/17/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_SoftAttachmentModel_h +#define hifi_SoftAttachmentModel_h + +#include + +// A model that allows the creator to specify a secondary rig instance. +// When the cluster matrices are created for rendering, the +// cluster matrices will use the secondary rig for the joint poses +// instead of the primary rig. +// +// This is used by Avatar instances to wear clothing that follows the same +// animated pose as the SkeletonModel. + +class SoftAttachmentModel : public Model { + Q_OBJECT + +public: + + SoftAttachmentModel(RigPointer rig, QObject* parent, RigPointer rigOverride); + ~SoftAttachmentModel(); + + virtual void updateRig(float deltaTime, glm::mat4 parentTransform) override; + virtual void updateClusterMatrices(glm::vec3 modelPosition, glm::quat modelOrientation) override; + +protected: + int getJointIndexOverride(int i) const; + + RigPointer _rigOverride; +}; + +#endif // hifi_SoftAttachmentModel_h diff --git a/interface/src/scripting/AccountScriptingInterface.cpp b/interface/src/scripting/AccountScriptingInterface.cpp index 87ea3220a4..126cd53003 100644 --- a/interface/src/scripting/AccountScriptingInterface.cpp +++ b/interface/src/scripting/AccountScriptingInterface.cpp @@ -17,7 +17,6 @@ AccountScriptingInterface::AccountScriptingInterface() { AccountManager& accountManager = AccountManager::getInstance(); connect(&accountManager, &AccountManager::balanceChanged, this, &AccountScriptingInterface::updateBalance); - } AccountScriptingInterface* AccountScriptingInterface::getInstance() { @@ -39,3 +38,12 @@ void AccountScriptingInterface::updateBalance() { AccountManager& accountManager = AccountManager::getInstance(); emit balanceChanged(accountManager.getAccountInfo().getBalanceInSatoshis()); } + +QString AccountScriptingInterface::getUsername() { + AccountManager& accountManager = AccountManager::getInstance(); + if (accountManager.isLoggedIn()) { + return accountManager.getAccountInfo().getUsername(); + } else { + return "Unknown user"; + } +} diff --git a/interface/src/scripting/AccountScriptingInterface.h b/interface/src/scripting/AccountScriptingInterface.h index e9cf0ede5f..578a9d6728 100644 --- a/interface/src/scripting/AccountScriptingInterface.h +++ b/interface/src/scripting/AccountScriptingInterface.h @@ -24,6 +24,7 @@ signals: public slots: static AccountScriptingInterface* getInstance(); float getBalance(); + QString getUsername(); bool isLoggedIn(); void updateBalance(); }; diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 03d6432104..b11e4c7a95 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -86,7 +86,7 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) { // Now render the overlay components together into a single texture renderDomainConnectionStatusBorder(renderArgs); // renders the connected domain line - renderAudioScope(renderArgs); // audio scope in the very back + renderAudioScope(renderArgs); // audio scope in the very back - NOTE: this is the debug audio scope, not the VU meter renderRearView(renderArgs); // renders the mirror view selfie renderQmlUi(renderArgs); // renders a unit quad with the QML UI texture, and the text overlays from scripts renderOverlays(renderArgs); // renders Scripts Overlay and AudioScope @@ -158,7 +158,7 @@ void ApplicationOverlay::renderRearViewToFbo(RenderArgs* renderArgs) { } void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { - if (Menu::getInstance()->isOptionChecked(MenuOption::Mirror)) { + if (!qApp->isHMDMode() && Menu::getInstance()->isOptionChecked(MenuOption::Mirror)) { gpu::Batch& batch = *renderArgs->_batch; auto geometryCache = DependencyManager::get(); diff --git a/interface/src/ui/AttachmentsDialog.cpp b/interface/src/ui/AttachmentsDialog.cpp index 1d53eb3871..d718b52d6d 100644 --- a/interface/src/ui/AttachmentsDialog.cpp +++ b/interface/src/ui/AttachmentsDialog.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -27,13 +28,13 @@ AttachmentsDialog::AttachmentsDialog(QWidget* parent) : QDialog(parent) { - + setWindowTitle("Edit Attachments"); setAttribute(Qt::WA_DeleteOnClose); - + QVBoxLayout* layout = new QVBoxLayout(); setLayout(layout); - + QScrollArea* area = new QScrollArea(); layout->addWidget(area); area->setWidgetResizable(true); @@ -42,26 +43,26 @@ AttachmentsDialog::AttachmentsDialog(QWidget* parent) : container->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); area->setWidget(container); _attachments->addStretch(1); - + foreach (const AttachmentData& data, DependencyManager::get()->getMyAvatar()->getAttachmentData()) { addAttachment(data); } - + QPushButton* newAttachment = new QPushButton("New Attachment"); connect(newAttachment, SIGNAL(clicked(bool)), SLOT(addAttachment())); layout->addWidget(newAttachment); - + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok); layout->addWidget(buttons); connect(buttons, SIGNAL(accepted()), SLOT(deleteLater())); _ok = buttons->button(QDialogButtonBox::Ok); - + setMinimumSize(600, 600); } void AttachmentsDialog::setVisible(bool visible) { QDialog::setVisible(visible); - + // un-default the OK button if (visible) { _ok->setDefault(false); @@ -104,11 +105,11 @@ AttachmentPanel::AttachmentPanel(AttachmentsDialog* dialog, const AttachmentData _dialog(dialog), _applying(false) { setFrameStyle(QFrame::StyledPanel); - + QFormLayout* layout = new QFormLayout(); layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); setLayout(layout); - + QHBoxLayout* urlBox = new QHBoxLayout(); layout->addRow("Model URL:", urlBox); urlBox->addWidget(_modelURL = new QLineEdit(data.modelURL.toString()), 1); @@ -117,7 +118,7 @@ AttachmentPanel::AttachmentPanel(AttachmentsDialog* dialog, const AttachmentData QPushButton* chooseURL = new QPushButton("Choose"); urlBox->addWidget(chooseURL); connect(chooseURL, SIGNAL(clicked(bool)), SLOT(chooseModelURL())); - + layout->addRow("Joint:", _jointName = new QComboBox()); QSharedPointer geometry = DependencyManager::get()->getMyAvatar()->getSkeletonModel().getGeometry(); if (geometry && geometry->isLoaded()) { @@ -127,26 +128,30 @@ AttachmentPanel::AttachmentPanel(AttachmentsDialog* dialog, const AttachmentData } _jointName->setCurrentText(data.jointName); connect(_jointName, SIGNAL(currentIndexChanged(int)), SLOT(jointNameChanged())); - + QHBoxLayout* translationBox = new QHBoxLayout(); translationBox->addWidget(_translationX = createTranslationBox(this, data.translation.x)); translationBox->addWidget(_translationY = createTranslationBox(this, data.translation.y)); translationBox->addWidget(_translationZ = createTranslationBox(this, data.translation.z)); layout->addRow("Translation:", translationBox); - + QHBoxLayout* rotationBox = new QHBoxLayout(); glm::vec3 eulers = glm::degrees(safeEulerAngles(data.rotation)); rotationBox->addWidget(_rotationX = createRotationBox(this, eulers.x)); rotationBox->addWidget(_rotationY = createRotationBox(this, eulers.y)); rotationBox->addWidget(_rotationZ = createRotationBox(this, eulers.z)); layout->addRow("Rotation:", rotationBox); - + layout->addRow("Scale:", _scale = new QDoubleSpinBox()); _scale->setSingleStep(0.01); _scale->setMaximum(FLT_MAX); _scale->setValue(data.scale); connect(_scale, SIGNAL(valueChanged(double)), SLOT(updateAttachmentData())); - + + layout->addRow("Is Soft:", _isSoft = new QCheckBox()); + _isSoft->setChecked(data.isSoft); + connect(_isSoft, SIGNAL(stateChanged(int)), SLOT(updateAttachmentData())); + QPushButton* remove = new QPushButton("Delete"); layout->addRow(remove); connect(remove, SIGNAL(clicked(bool)), SLOT(deleteLater())); @@ -160,6 +165,7 @@ AttachmentData AttachmentPanel::getAttachmentData() const { data.translation = glm::vec3(_translationX->value(), _translationY->value(), _translationZ->value()); data.rotation = glm::quat(glm::radians(glm::vec3(_rotationX->value(), _rotationY->value(), _rotationZ->value()))); data.scale = _scale->value(); + data.isSoft = _isSoft->isChecked(); return data; } @@ -227,6 +233,7 @@ void AttachmentPanel::applyAttachmentData(const AttachmentData& attachment) { _rotationY->setValue(eulers.y); _rotationZ->setValue(eulers.z); _scale->setValue(attachment.scale); + _isSoft->setChecked(attachment.isSoft); _applying = false; _dialog->updateAttachmentData(); } diff --git a/interface/src/ui/AttachmentsDialog.h b/interface/src/ui/AttachmentsDialog.h index d3b9219a16..43ba5f8f3e 100644 --- a/interface/src/ui/AttachmentsDialog.h +++ b/interface/src/ui/AttachmentsDialog.h @@ -36,11 +36,11 @@ public slots: void updateAttachmentData(); private slots: - + void addAttachment(const AttachmentData& data = AttachmentData()); private: - + QVBoxLayout* _attachments; QPushButton* _ok; }; @@ -50,7 +50,7 @@ class AttachmentPanel : public QFrame { Q_OBJECT public: - + AttachmentPanel(AttachmentsDialog* dialog, const AttachmentData& data = AttachmentData()); AttachmentData getAttachmentData() const; @@ -64,9 +64,9 @@ private slots: void updateAttachmentData(); private: - + void applyAttachmentData(const AttachmentData& attachment); - + AttachmentsDialog* _dialog; QLineEdit* _modelURL; QComboBox* _jointName; @@ -77,6 +77,7 @@ private: QDoubleSpinBox* _rotationY; QDoubleSpinBox* _rotationZ; QDoubleSpinBox* _scale; + QCheckBox* _isSoft; bool _applying; }; diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index a9827a23c2..7b5f80ee80 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -66,6 +66,7 @@ void AvatarInputs::update() { && !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)); AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking)); AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking)); + AI_UPDATE(isHMD, qApp->isHMDMode()); auto audioIO = DependencyManager::get(); const float AUDIO_METER_AVERAGING = 0.5; diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index 8ed4e8f163..36d3027a42 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -30,6 +30,7 @@ class AvatarInputs : public QQuickItem { AI_PROPERTY(float, audioLevel, 0) AI_PROPERTY(bool, mirrorVisible, false) AI_PROPERTY(bool, mirrorZoomed, true) + AI_PROPERTY(bool, isHMD, false) public: static AvatarInputs* getInstance(); @@ -44,6 +45,7 @@ signals: void audioLevelChanged(); void mirrorVisibleChanged(); void mirrorZoomedChanged(); + void isHMDChanged(); protected: Q_INVOKABLE void resetSensors(); diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index a38cc13100..dfdfedb50d 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -48,6 +48,7 @@ PreferencesDialog::PreferencesDialog(QWidget* parent) : connect(ui.buttonChangeAppearance, &QPushButton::clicked, this, &PreferencesDialog::openFullAvatarModelBrowser); connect(ui.appearanceDescription, &QLineEdit::editingFinished, this, &PreferencesDialog::changeFullAvatarURL); + connect(ui.useAcuityCheckBox, &QCheckBox::clicked, this, &PreferencesDialog::changeUseAcuity); connect(qApp, &Application::fullAvatarURLChanged, this, &PreferencesDialog::fullAvatarURLChanged); @@ -58,6 +59,16 @@ PreferencesDialog::PreferencesDialog(QWidget* parent) : UIUtil::scaleWidgetFontSizes(this); } +void PreferencesDialog::changeUseAcuity() { + bool useAcuity = ui.useAcuityCheckBox->isChecked(); + ui.label_desktopMinimumFPSSpin->setEnabled(useAcuity); + ui.desktopMinimumFPSSpin->setEnabled(useAcuity); + ui.label_hmdMinimumFPSSpin->setEnabled(useAcuity); + ui.hmdMinimumFPSSpin->setEnabled(useAcuity); + ui.label_smallestReasonableRenderHorizon->setText(useAcuity ? "Minimum Avatar Display Distance (@half speed)" : "Minimum Display Distance (@half speed)"); + Menu::getInstance()->getActionForOption(MenuOption::LodTools)->setEnabled(useAcuity); + Menu::getInstance()->getSubMenuFromName(MenuOption::RenderResolution, Menu::getInstance()->getSubMenuFromName("Render", Menu::getInstance()->getMenu("Developer")))->setEnabled(useAcuity); +} void PreferencesDialog::changeFullAvatarURL() { DependencyManager::get()->getMyAvatar()->useFullAvatarURL(ui.appearanceDescription->text(), ""); this->fullAvatarURLChanged(ui.appearanceDescription->text(), ""); @@ -212,9 +223,11 @@ void PreferencesDialog::loadPreferences() { // LOD items auto lodManager = DependencyManager::get(); + ui.useAcuityCheckBox->setChecked(lodManager->getUseAcuity()); ui.desktopMinimumFPSSpin->setValue(lodManager->getDesktopLODDecreaseFPS()); ui.hmdMinimumFPSSpin->setValue(lodManager->getHMDLODDecreaseFPS()); - ui.avatarRenderSmallestReasonableHorizon->setValue(1.0f / DependencyManager::get()->getRenderDistanceInverseHighLimit()); + ui.smallestReasonableRenderHorizon->setValue(1.0f / lodManager->getRenderDistanceInverseHighLimit()); + changeUseAcuity(); } void PreferencesDialog::savePreferences() { @@ -303,7 +316,8 @@ void PreferencesDialog::savePreferences() { // LOD items auto lodManager = DependencyManager::get(); + lodManager->setUseAcuity(ui.useAcuityCheckBox->isChecked()); lodManager->setDesktopLODDecreaseFPS(ui.desktopMinimumFPSSpin->value()); lodManager->setHMDLODDecreaseFPS(ui.hmdMinimumFPSSpin->value()); - DependencyManager::get()->setRenderDistanceInverseHighLimit(1.0f / ui.avatarRenderSmallestReasonableHorizon->value()); + lodManager->setRenderDistanceInverseHighLimit(1.0f / ui.smallestReasonableRenderHorizon->value()); } diff --git a/interface/src/ui/PreferencesDialog.h b/interface/src/ui/PreferencesDialog.h index 1536eca3ee..a6c27dee08 100644 --- a/interface/src/ui/PreferencesDialog.h +++ b/interface/src/ui/PreferencesDialog.h @@ -51,6 +51,7 @@ private slots: void openScriptsLocationBrowser(); void changeFullAvatarURL(); void fullAvatarURLChanged(const QString& newValue, const QString& modelName); + void changeUseAcuity(); }; #endif // hifi_PreferencesDialog_h diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 6acacee41d..b82ab93068 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -116,8 +116,6 @@ void Stats::updateStats(bool force) { auto avatarManager = DependencyManager::get(); // we need to take one avatar out so we don't include ourselves STAT_UPDATE(avatarCount, avatarManager->size() - 1); - STAT_UPDATE(avatarRenderableCount, avatarManager->getNumberInRenderRange()); - STAT_UPDATE(avatarRenderDistance, (int) round(avatarManager->getRenderDistance())); // deliberately truncating STAT_UPDATE(serverCount, (int)nodeList->size()); STAT_UPDATE(renderrate, (int)qApp->getFps()); if (qApp->getActiveDisplayPlugin()) { @@ -285,7 +283,9 @@ void Stats::updateStats(bool force) { STAT_UPDATE(localLeaves, (int)OctreeElement::getLeafNodeCount()); // LOD Details STAT_UPDATE(lodStatus, "You can see " + DependencyManager::get()->getLODFeedbackText()); + STAT_UPDATE(lodStatsRenderText, DependencyManager::get()->getLODStatsRenderText()); } + STAT_UPDATE(showAcuity, (_expanded || force) && DependencyManager::get()->getUseAcuity()); bool performanceTimerIsActive = PerformanceTimer::isActive(); bool displayPerf = _expanded && Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index eb28883001..5e948ce0f0 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -30,6 +30,7 @@ class Stats : public QQuickItem { Q_PROPERTY(QString monospaceFont READ monospaceFont CONSTANT) Q_PROPERTY(float audioPacketlossUpstream READ getAudioPacketLossUpstream) Q_PROPERTY(float audioPacketlossDownstream READ getAudioPacketLossDownstream) + Q_PROPERTY(bool showAcuity READ getShowAcuity WRITE setShowAcuity NOTIFY showAcuityChanged) STATS_PROPERTY(int, serverCount, 0) STATS_PROPERTY(int, renderrate, 0) @@ -37,8 +38,6 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, simrate, 0) STATS_PROPERTY(int, avatarSimrate, 0) STATS_PROPERTY(int, avatarCount, 0) - STATS_PROPERTY(int, avatarRenderableCount, 0) - STATS_PROPERTY(int, avatarRenderDistance, 0) STATS_PROPERTY(int, packetInCount, 0) STATS_PROPERTY(int, packetOutCount, 0) STATS_PROPERTY(float, mbpsIn, 0) @@ -77,6 +76,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(QString, packetStats, QString()) STATS_PROPERTY(QString, lodStatus, QString()) STATS_PROPERTY(QString, timingStats, QString()) + STATS_PROPERTY(QString, lodStatsRenderText, QString()) STATS_PROPERTY(int, serverElements, 0) STATS_PROPERTY(int, serverInternal, 0) STATS_PROPERTY(int, serverLeaves, 0) @@ -108,12 +108,15 @@ public: emit expandedChanged(); } } + bool getShowAcuity() { return _showAcuity; } + void setShowAcuity(bool newValue) { _showAcuity = newValue; } public slots: void forceUpdateStats() { updateStats(true); } signals: void expandedChanged(); + void showAcuityChanged(); void timingExpandedChanged(); void serverCountChanged(); void renderrateChanged(); @@ -121,8 +124,7 @@ signals: void simrateChanged(); void avatarSimrateChanged(); void avatarCountChanged(); - void avatarRenderableCountChanged(); - void avatarRenderDistanceChanged(); + void lodStatsRenderTextChanged(); void packetInCountChanged(); void packetOutCountChanged(); void mbpsInChanged(); @@ -172,6 +174,7 @@ private: int _recentMaxPackets{ 0 } ; // recent max incoming voxel packets to process bool _resetRecentMaxPacketsSoon{ true }; bool _expanded{ false }; + bool _showAcuity{ false }; bool _timingExpanded{ false }; QString _monospaceFont; const AudioIOStats* _audioStats; diff --git a/interface/ui/preferencesDialog.ui b/interface/ui/preferencesDialog.ui index e6a5e2228d..a66c4bf7ee 100644 --- a/interface/ui/preferencesDialog.ui +++ b/interface/ui/preferencesDialog.ui @@ -742,6 +742,43 @@ + + + + + 0 + 0 + + + + + 32 + 28 + + + + + 0 + 0 + + + + + Arial + + + + Render based on visual acuity + + + + 0 + 0 + + + + + @@ -757,7 +794,7 @@ 7 - + Arial @@ -842,7 +879,7 @@ 7 - + Arial @@ -927,7 +964,7 @@ 7 - + Arial @@ -963,7 +1000,7 @@ - + 100 diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 4dd091f1d6..c6060d791a 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -245,6 +245,14 @@ int Rig::indexOfJoint(const QString& jointName) const { } } +QString Rig::nameOfJoint(int jointIndex) const { + if (_animSkeleton) { + return _animSkeleton->getJointName(jointIndex); + } else { + return ""; + } +} + void Rig::setModelOffset(const glm::mat4& modelOffsetMat) { AnimPose newModelOffset = AnimPose(modelOffsetMat); if (!isEqual(_modelOffset.trans, newModelOffset.trans) || diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 9faf93e40b..669af2ea64 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -91,6 +91,7 @@ public: bool jointStatesEmpty(); int getJointStateCount() const; int indexOfJoint(const QString& jointName) const; + QString nameOfJoint(int jointIndex) const; void setModelOffset(const glm::mat4& modelOffsetMat); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 8942699070..d955808e34 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1269,6 +1269,7 @@ void AvatarData::updateJointMappings() { static const QString JSON_ATTACHMENT_URL = QStringLiteral("modelUrl"); static const QString JSON_ATTACHMENT_JOINT_NAME = QStringLiteral("jointName"); static const QString JSON_ATTACHMENT_TRANSFORM = QStringLiteral("transform"); +static const QString JSON_ATTACHMENT_IS_SOFT = QStringLiteral("isSoft"); QJsonObject AttachmentData::toJson() const { QJsonObject result; @@ -1287,6 +1288,7 @@ QJsonObject AttachmentData::toJson() const { if (!transform.isIdentity()) { result[JSON_ATTACHMENT_TRANSFORM] = Transform::toJson(transform); } + result[JSON_ATTACHMENT_IS_SOFT] = isSoft; return result; } @@ -1311,21 +1313,25 @@ void AttachmentData::fromJson(const QJsonObject& json) { rotation = transform.getRotation(); scale = transform.getScale().x; } + + if (json.contains(JSON_ATTACHMENT_IS_SOFT)) { + isSoft = json[JSON_ATTACHMENT_IS_SOFT].toBool(); + } } bool AttachmentData::operator==(const AttachmentData& other) const { return modelURL == other.modelURL && jointName == other.jointName && translation == other.translation && - rotation == other.rotation && scale == other.scale; + rotation == other.rotation && scale == other.scale && isSoft == other.isSoft; } QDataStream& operator<<(QDataStream& out, const AttachmentData& attachment) { return out << attachment.modelURL << attachment.jointName << - attachment.translation << attachment.rotation << attachment.scale; + attachment.translation << attachment.rotation << attachment.scale << attachment.isSoft; } QDataStream& operator>>(QDataStream& in, AttachmentData& attachment) { return in >> attachment.modelURL >> attachment.jointName >> - attachment.translation >> attachment.rotation >> attachment.scale; + attachment.translation >> attachment.rotation >> attachment.scale >> attachment.isSoft; } void AttachmentDataObject::setModelURL(const QString& modelURL) const { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index ee1139b364..42ecf098b3 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -439,9 +439,10 @@ public: glm::vec3 translation; glm::quat rotation; float scale { 1.0f }; - + bool isSoft { false }; + bool isValid() const { return modelURL.isValid(); } - + bool operator==(const AttachmentData& other) const; QJsonObject toJson() const; diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 0bda52b237..7bee5101b1 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -70,6 +70,12 @@ namespace controller { makeAxisPair(Action::RETICLE_UP, "ReticleUp"), makeAxisPair(Action::RETICLE_DOWN, "ReticleDown"), + makeAxisPair(Action::UI_NAV_LATERAL, "UiNavLateral"), + makeAxisPair(Action::UI_NAV_VERTICAL, "UiNavVertical"), + makeAxisPair(Action::UI_NAV_GROUP, "UiNavGroup"), + makeAxisPair(Action::UI_NAV_SELECT, "UiNavSelect"), + makeAxisPair(Action::UI_NAV_BACK, "UiNavBack"), + // Aliases and bisected versions makeAxisPair(Action::LONGITUDINAL_BACKWARD, "Backward"), makeAxisPair(Action::LONGITUDINAL_FORWARD, "Forward"), diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index 56dd9660d9..812d3b8df3 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -55,6 +55,12 @@ enum class Action { SHIFT, + UI_NAV_LATERAL, + UI_NAV_VERTICAL, + UI_NAV_GROUP, + UI_NAV_SELECT, + UI_NAV_BACK, + // Pointer/Reticle control RETICLE_CLICK, RETICLE_X, @@ -90,6 +96,7 @@ enum class Action { BOOM_IN, BOOM_OUT, + NUM_ACTIONS, }; diff --git a/libraries/controllers/src/controllers/InputDevice.h b/libraries/controllers/src/controllers/InputDevice.h index fc3477b41a..3add7d236f 100644 --- a/libraries/controllers/src/controllers/InputDevice.h +++ b/libraries/controllers/src/controllers/InputDevice.h @@ -83,6 +83,7 @@ protected: friend class UserInputMapper; virtual Input::NamedVector getAvailableInputs() const = 0; + virtual QStringList getDefaultMappingConfigs() const { return QStringList() << getDefaultMappingConfig(); } virtual QString getDefaultMappingConfig() const { return QString(); } virtual EndpointPointer createEndpoint(const Input& input) const; diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp index fadbeee326..e101c5f4ff 100644 --- a/libraries/controllers/src/controllers/StandardController.cpp +++ b/libraries/controllers/src/controllers/StandardController.cpp @@ -131,10 +131,10 @@ EndpointPointer StandardController::createEndpoint(const Input& input) const { return std::make_shared(input); } -QString StandardController::getDefaultMappingConfig() const { +QStringList StandardController::getDefaultMappingConfigs() const { static const QString DEFAULT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard.json"; - return DEFAULT_MAPPING_JSON; + static const QString DEFAULT_NAV_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_navigation.json"; + return QStringList() << DEFAULT_NAV_MAPPING_JSON << DEFAULT_MAPPING_JSON; } - } diff --git a/libraries/controllers/src/controllers/StandardController.h b/libraries/controllers/src/controllers/StandardController.h index 6c18c76371..57bd0faba5 100644 --- a/libraries/controllers/src/controllers/StandardController.h +++ b/libraries/controllers/src/controllers/StandardController.h @@ -27,7 +27,7 @@ class StandardController : public QObject, public InputDevice { public: virtual EndpointPointer createEndpoint(const Input& input) const override; virtual Input::NamedVector getAvailableInputs() const override; - virtual QString getDefaultMappingConfig() const override; + virtual QStringList getDefaultMappingConfigs() const override; virtual void update(float deltaTime, bool jointsCaptured) override; virtual void focusOutEvent() override; diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index 9251a663ba..fe64566b29 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -100,7 +100,7 @@ void UserInputMapper::registerDevice(InputDevice::Pointer device) { } _registeredDevices[deviceID] = device; - auto mapping = loadMapping(device->getDefaultMappingConfig()); + auto mapping = loadMappings(device->getDefaultMappingConfigs()); if (mapping) { _mappingsByDevice[deviceID] = mapping; enableMapping(mapping); @@ -139,7 +139,7 @@ void UserInputMapper::loadDefaultMapping(uint16 deviceID) { } - auto mapping = loadMapping(proxyEntry->second->getDefaultMappingConfig()); + auto mapping = loadMappings(proxyEntry->second->getDefaultMappingConfigs()); if (mapping) { auto prevMapping = _mappingsByDevice[deviceID]; disableMapping(prevMapping); @@ -710,6 +710,21 @@ Mapping::Pointer UserInputMapper::loadMapping(const QString& jsonFile) { return parseMapping(json); } +MappingPointer UserInputMapper::loadMappings(const QStringList& jsonFiles) { + Mapping::Pointer result; + for (const QString& jsonFile : jsonFiles) { + auto subMapping = loadMapping(jsonFile); + if (subMapping) { + if (!result) { + result = subMapping; + } else { + auto& routes = result->routes; + routes.insert(routes.end(), subMapping->routes.begin(), subMapping->routes.end()); + } + } + } + return result; +} static const QString JSON_NAME = QStringLiteral("name"); @@ -888,7 +903,7 @@ Endpoint::Pointer UserInputMapper::parseDestination(const QJsonValue& value) { Endpoint::Pointer UserInputMapper::parseAxis(const QJsonValue& value) { if (value.isObject()) { - auto object = value.toObject(); + auto object = value.toObject(); if (object.contains("makeAxis")) { auto axisValue = object.value("makeAxis"); if (axisValue.isArray()) { @@ -985,6 +1000,20 @@ Route::Pointer UserInputMapper::parseRoute(const QJsonValue& value) { return result; } +void injectConditional(Route::Pointer& route, Conditional::Pointer& conditional) { + if (!conditional) { + return; + } + + if (!route->conditional) { + route->conditional = conditional; + return; + } + + route->conditional = std::make_shared(conditional, route->conditional); +} + + Mapping::Pointer UserInputMapper::parseMapping(const QJsonValue& json) { if (!json.isObject()) { return Mapping::Pointer(); @@ -994,12 +1023,24 @@ Mapping::Pointer UserInputMapper::parseMapping(const QJsonValue& json) { auto mapping = std::make_shared("default"); mapping->name = obj[JSON_NAME].toString(); const auto& jsonChannels = obj[JSON_CHANNELS].toArray(); + Conditional::Pointer globalConditional; + if (obj.contains(JSON_CHANNEL_WHEN)) { + auto conditionalsValue = obj[JSON_CHANNEL_WHEN]; + globalConditional = parseConditional(conditionalsValue); + } + for (const auto& channelIt : jsonChannels) { Route::Pointer route = parseRoute(channelIt); + if (!route) { qWarning() << "Couldn't parse route"; continue; } + + if (globalConditional) { + injectConditional(route, globalConditional); + } + mapping->routes.push_back(route); } _mappingsByName[mapping->name] = mapping; diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index d93a93016c..98a85a2a44 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -107,6 +107,7 @@ namespace controller { MappingPointer newMapping(const QString& mappingName); MappingPointer parseMapping(const QString& json); MappingPointer loadMapping(const QString& jsonFile); + MappingPointer loadMappings(const QStringList& jsonFiles); void loadDefaultMapping(uint16 deviceID); void enableMapping(const QString& mappingName, bool enable = true); diff --git a/libraries/controllers/src/controllers/impl/conditionals/AndConditional.h b/libraries/controllers/src/controllers/impl/conditionals/AndConditional.h index c60e4b15df..2299843a24 100644 --- a/libraries/controllers/src/controllers/impl/conditionals/AndConditional.h +++ b/libraries/controllers/src/controllers/impl/conditionals/AndConditional.h @@ -18,7 +18,11 @@ class AndConditional : public Conditional { public: using Pointer = std::shared_ptr; - AndConditional(Conditional::List children) : _children(children) { } + AndConditional(Conditional::List children) + : _children(children) {} + + AndConditional(Conditional::Pointer& first, Conditional::Pointer& second) + : _children({ first, second }) {} virtual bool satisfied() override; diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 26a564e20b..f89f1f5b72 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -254,10 +254,33 @@ private: _quit = true; } + static const uint64_t MAX_SHUTDOWN_WAIT_SECS = 5; void stop() { - QMutexLocker lock(&_mutex); - post(STOP); - _cond.wait(&_mutex); + if (_thread.isRunning()) { + qDebug() << "Stopping QML render thread " << _thread.currentThreadId(); + { + QMutexLocker lock(&_mutex); + post(STOP); + } + auto start = usecTimestampNow(); + auto now = usecTimestampNow(); + bool shutdownClean = false; + while (now - start < (MAX_SHUTDOWN_WAIT_SECS * USECS_PER_SECOND)) { + QMutexLocker lock(&_mutex); + if (_cond.wait(&_mutex, MSECS_PER_SECOND)) { + shutdownClean = true; + break; + } + now = usecTimestampNow(); + } + + if (!shutdownClean) { + qWarning() << "Failed to shut down the QML render thread"; + } + + } else { + qDebug() << "QML render thread already completed"; + } } bool allowNewFrame(uint8_t fps) { diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index d66cbeb285..608e811b4b 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -37,7 +37,7 @@ public: using MouseTranslator = std::function; - void create(QOpenGLContext* context); + virtual void create(QOpenGLContext* context); void resize(const QSize& size); QSize size() const; Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); diff --git a/libraries/model/src/model/Geometry.h b/libraries/model/src/model/Geometry.h index fa29faff5f..f9d9b0eeb4 100755 --- a/libraries/model/src/model/Geometry.h +++ b/libraries/model/src/model/Geometry.h @@ -58,7 +58,7 @@ public: const gpu::Stream::FormatPointer getVertexFormat() const { return _vertexFormat; } // BufferStream on the mesh vertices and attributes matching the vertex format - const gpu::BufferStream getVertexStream() const { return _vertexStream; } + const gpu::BufferStream& getVertexStream() const { return _vertexStream; } // Index Buffer void setIndexBuffer(const BufferView& buffer); diff --git a/libraries/networking/src/ResourceManager.cpp b/libraries/networking/src/ResourceManager.cpp index fd465a0aed..f195011290 100644 --- a/libraries/networking/src/ResourceManager.cpp +++ b/libraries/networking/src/ResourceManager.cpp @@ -23,16 +23,27 @@ QMutex ResourceManager::_prefixMapLock; void ResourceManager::setUrlPrefixOverride(const QString& prefix, const QString& replacement) { QMutexLocker locker(&_prefixMapLock); - _prefixMap[prefix] = replacement; + if (replacement.isEmpty()) { + _prefixMap.erase(prefix); + } else { + _prefixMap[prefix] = replacement; + } } QString ResourceManager::normalizeURL(const QString& urlString) { QString result = urlString; - QMutexLocker locker(&_prefixMapLock); - foreach(const auto& entry, _prefixMap) { + PrefixMap copy; + + { + QMutexLocker locker(&_prefixMapLock); + copy = _prefixMap; + } + + foreach(const auto& entry, copy) { const auto& prefix = entry.first; const auto& replacement = entry.second; if (result.startsWith(prefix)) { + qDebug() << "Replacing " << prefix << " with " << replacement; result.replace(0, prefix.size(), replacement); } } diff --git a/libraries/networking/src/ResourceScriptingInterface.cpp b/libraries/networking/src/ResourceScriptingInterface.cpp new file mode 100644 index 0000000000..38be49049c --- /dev/null +++ b/libraries/networking/src/ResourceScriptingInterface.cpp @@ -0,0 +1,15 @@ +// +// Created by Bradley Austin Davis on 2015/12/29 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ResourceScriptingInterface.h" + +#include "ResourceManager.h" + +void ResourceScriptingInterface::overrideUrlPrefix(const QString& prefix, const QString& replacement) { + ResourceManager::setUrlPrefixOverride(prefix, replacement); +} diff --git a/libraries/networking/src/ResourceScriptingInterface.h b/libraries/networking/src/ResourceScriptingInterface.h new file mode 100644 index 0000000000..d9777e7514 --- /dev/null +++ b/libraries/networking/src/ResourceScriptingInterface.h @@ -0,0 +1,31 @@ +// +// AssetClient.h +// libraries/networking/src +// +// Created by Ryan Huffman on 2015/07/21 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#ifndef hifi_networking_ResourceScriptingInterface_h +#define hifi_networking_ResourceScriptingInterface_h + +#include + +#include + +class ResourceScriptingInterface : public QObject, public Dependency { + Q_OBJECT +public: + Q_INVOKABLE void overrideUrlPrefix(const QString& prefix, const QString& replacement); + + Q_INVOKABLE void restoreUrlPrefix(const QString& prefix) { + overrideUrlPrefix(prefix, ""); + } +}; + + +#endif diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 090fd1ae5c..42ee77b02b 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -44,7 +44,7 @@ PacketVersion versionForPacketType(PacketType packetType) { return VERSION_ENTITITES_HAVE_QUERY_BOX; case PacketType::AvatarData: case PacketType::BulkAvatarData: - return 17; + return static_cast(AvatarMixerPacketVersion::SoftAttachmentSupport); default: return 17; } diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 671d61f831..81cc80e9d5 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -164,4 +164,9 @@ const PacketVersion VERSION_ENTITIES_HAVE_PARENTS = 51; const PacketVersion VERSION_ENTITITES_REMOVED_START_AUTOMATICALLY_FROM_ANIMATION_PROPERTY_GROUP = 52; const PacketVersion VERSION_ENTITITES_HAVE_QUERY_BOX = 53; +enum class AvatarMixerPacketVersion : PacketVersion { + TranslationSupport = 17, + SoftAttachmentSupport +}; + #endif // hifi_PacketHeaders_h diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 27e326fcba..4429f49346 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -79,6 +79,7 @@ const DisplayPluginList& PluginManager::getDisplayPlugins() { auto& container = PluginContainer::getInstance(); for (auto plugin : displayPlugins) { plugin->setContainer(&container); + plugin->init(); } }); @@ -104,6 +105,7 @@ const InputPluginList& PluginManager::getInputPlugins() { auto& container = PluginContainer::getInstance(); for (auto plugin : inputPlugins) { plugin->setContainer(&container); + plugin->init(); } }); return inputPlugins; diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index db0e47de5e..e6892b95f4 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -819,8 +819,6 @@ model::MeshPointer DeferredLightingEffect::getSpotLightMesh() { //DEBUG: model::Mesh::Part part(0, indices, 0, model::Mesh::LINE_STRIP); _spotLightMesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(sizeof(part), (gpu::Byte*) &part), gpu::Element::PART_DRAWCALL)); - - _spotLightMesh->getVertexStream(); } return _spotLightMesh; } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 6f63a91a7d..ef9a837b27 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -974,11 +974,14 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { _needsUpdateClusterMatrices = true; _rig->updateAnimations(deltaTime, parentTransform); } + void Model::simulateInternal(float deltaTime) { // update the world space transforms for all joints glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset); updateRig(deltaTime, parentTransform); } + +// virtual void Model::updateClusterMatrices(glm::vec3 modelPosition, glm::quat modelOrientation) { PerformanceTimer perfTimer("Model::updateClusterMatrices"); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index a93338e41a..3416a9b71e 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -112,7 +112,7 @@ public: bool getSnapModelToRegistrationPoint() { return _snapModelToRegistrationPoint; } virtual void simulate(float deltaTime, bool fullUpdate = true); - void updateClusterMatrices(glm::vec3 modelPosition, glm::quat modelOrientation); + virtual void updateClusterMatrices(glm::vec3 modelPosition, glm::quat modelOrientation); /// Returns a reference to the shared geometry. const QSharedPointer& getGeometry() const { return _geometry; } @@ -312,7 +312,7 @@ protected: // hook for derived classes to be notified when setUrl invalidates the current model. virtual void onInvalidate() {}; -private: +protected: void deleteGeometry(); void initJointTransforms(); @@ -370,7 +370,6 @@ private: bool _showCollisionHull = false; friend class ModelMeshPartPayload; -protected: RigPointer _rig; }; diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index ef448e93f0..6aa48a532d 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -391,7 +392,7 @@ void ScriptEngine::init() { registerGlobalObject("Recording", recordingInterface.data()); registerGlobalObject("Assets", &_assetScriptingInterface); - + registerGlobalObject("Resources", DependencyManager::get().data()); } void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) { @@ -1296,4 +1297,4 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS entityScript.property(methodName).call(entityScript, args); } } -} \ No newline at end of file +} diff --git a/libraries/shared/src/PIDController.h b/libraries/shared/src/PIDController.h index 0b2411530a..0a376872cc 100644 --- a/libraries/shared/src/PIDController.h +++ b/libraries/shared/src/PIDController.h @@ -31,7 +31,7 @@ public: float update(float measuredValue, float dt, bool resetAccumulator = false); // returns the new computedValue void setHistorySize(QString label = QString(""), int size = 0) { _history.reserve(size); _history.resize(0); _label = label; } // non-empty does logging - bool getIsLogging() { return _history.capacity(); } + bool getIsLogging() { return !_label.isEmpty(); } float getMeasuredValueSetpoint() const { return _measuredValueSetpoint; } // In normal operation (where we can easily reach setpoint), controlledValue is typcially pinned at max. // Defaults to [0, max float], but for 1/LODdistance, it might be, say, [0, 0.2 or 0.1] diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index a1f00ab5ad..db5a9a6009 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -9,10 +9,13 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "OffscreenUi.h" -#include -#include -#include -#include + +#include +#include + +#include +#include + #include "ErrorDialog.h" #include "MessageDialog.h" @@ -27,7 +30,62 @@ public: } }; +class OffscreenFlags : public QObject { + Q_OBJECT + Q_PROPERTY(bool navigationFocused READ isNavigationFocused WRITE setNavigationFocused NOTIFY navigationFocusedChanged) +public: + + OffscreenFlags(QObject* parent = nullptr) : QObject(parent) {} + bool isNavigationFocused() const { return _navigationFocused; } + void setNavigationFocused(bool focused) { + if (_navigationFocused != focused) { + _navigationFocused = focused; + emit navigationFocusedChanged(); + } + } + +signals: + void navigationFocusedChanged(); + +private: + bool _navigationFocused { false }; +}; + +class UrlHandler : public QObject { + Q_OBJECT +public: + Q_INVOKABLE bool canHandleUrl(const QString& url) { + static auto handler = dynamic_cast(qApp); + return handler->canAcceptURL(url); + } + + Q_INVOKABLE bool handleUrl(const QString& url) { + static auto handler = dynamic_cast(qApp); + return handler->acceptURL(url); + } + + // FIXME hack for authentication, remove when we migrate to Qt 5.6 + Q_INVOKABLE QString fixupUrl(const QString& originalUrl) { + static const QString ACCESS_TOKEN_PARAMETER = "access_token"; + static const QString ALLOWED_HOST = "metaverse.highfidelity.com"; + QString result = originalUrl; + QUrl url(originalUrl); + QUrlQuery query(url); + if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) { + qDebug() << "Updating URL with auth token"; + AccountManager& accountManager = AccountManager::getInstance(); + query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager.getAccountInfo().getAccessToken().token); + url.setQuery(query.query()); + result = url.toString(); + } + + return result; + } +}; + +static UrlHandler * urlHandler { nullptr }; +static OffscreenFlags* offscreenFlags { nullptr }; // This hack allows the QML UI to work with keys that are also bound as // shortcuts at the application level. However, it seems as though the @@ -58,9 +116,15 @@ OffscreenUi::OffscreenUi() { ::qmlRegisterType("Hifi", 1, 0, "Root"); } -OffscreenUi::~OffscreenUi() { -} +void OffscreenUi::create(QOpenGLContext* context) { + OffscreenQmlSurface::create(context); + auto rootContext = getRootContext(); + offscreenFlags = new OffscreenFlags(); + rootContext->setContextProperty("offscreenFlags", offscreenFlags); + urlHandler = new UrlHandler(); + rootContext->setContextProperty("urlHandler", urlHandler); +} void OffscreenUi::show(const QUrl& url, const QString& name, std::function f) { QQuickItem* item = getRootItem()->findChild(name); @@ -139,7 +203,14 @@ void OffscreenUi::error(const QString& text) { pDialog->setEnabled(true); } - OffscreenUi::ButtonCallback OffscreenUi::NO_OP_CALLBACK = [](QMessageBox::StandardButton) {}; +bool OffscreenUi::navigationFocused() { + return offscreenFlags->isNavigationFocused(); +} + +void OffscreenUi::setNavigationFocused(bool focused) { + offscreenFlags->setNavigationFocused(focused); +} + #include "OffscreenUi.moc" diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index be7f6b5e2e..d6845c1d37 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -25,10 +25,12 @@ class OffscreenUi : public OffscreenQmlSurface, public Dependency { public: OffscreenUi(); - virtual ~OffscreenUi(); + virtual void create(QOpenGLContext* context) override; void show(const QUrl& url, const QString& name, std::function f = [](QQmlContext*, QObject*) {}); void toggle(const QUrl& url, const QString& name, std::function f = [](QQmlContext*, QObject*) {}); bool shouldSwallowShortcut(QEvent* event); + bool navigationFocused(); + void setNavigationFocused(bool focused); // Messagebox replacement functions using ButtonCallback = std::function; diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 781b8c1b76..940ba121f3 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -8,155 +8,42 @@ #include "QmlWebWindowClass.h" -#include - -#include -#include -#include #include #include #include + #include + #include #include -#include -#include -#include + +#include #include +#include #include #include #include "OffscreenUi.h" -QWebSocketServer* QmlWebWindowClass::_webChannelServer { nullptr }; -static QWebChannel webChannel; -static const uint16_t WEB_CHANNEL_PORT = 51016; -static std::atomic nextWindowId; static const char* const URL_PROPERTY = "source"; -static const char* const TITLE_PROPERTY = "title"; -static const QRegExp HIFI_URL_PATTERN { "^hifi://" }; - -void QmlScriptEventBridge::emitWebEvent(const QString& data) { - QMetaObject::invokeMethod(this, "webEventReceived", Qt::QueuedConnection, Q_ARG(QString, data)); -} - -void QmlScriptEventBridge::emitScriptEvent(const QString& data) { - QMetaObject::invokeMethod(this, "scriptEventReceived", Qt::QueuedConnection, - Q_ARG(int, _webWindow->getWindowId()), Q_ARG(QString, data)); -} - -class QmlWebTransport : public QWebChannelAbstractTransport { - Q_OBJECT -public: - QmlWebTransport(QWebSocket* webSocket) : _webSocket(webSocket) { - // Translate from the websocket layer to the webchannel layer - connect(webSocket, &QWebSocket::textMessageReceived, [this](const QString& message) { - QJsonParseError error; - QJsonDocument document = QJsonDocument::fromJson(message.toUtf8(), &error); - if (error.error || !document.isObject()) { - qWarning() << "Unable to parse incoming JSON message" << message; - return; - } - emit messageReceived(document.object(), this); - }); - } - - virtual void sendMessage(const QJsonObject &message) override { - // Translate from the webchannel layer to the websocket layer - _webSocket->sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact)); - } - -private: - QWebSocket* const _webSocket; -}; - - -void QmlWebWindowClass::setupServer() { - if (!_webChannelServer) { - _webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode); - if (!_webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) { - qFatal("Failed to open web socket server."); - } - - QObject::connect(_webChannelServer, &QWebSocketServer::newConnection, [] { - webChannel.connectTo(new QmlWebTransport(_webChannelServer->nextPendingConnection())); - }); - } -} - -class UrlFixer : public QObject { - Q_OBJECT -public: - Q_INVOKABLE QString fixupUrl(const QString& originalUrl) { - static const QString ACCESS_TOKEN_PARAMETER = "access_token"; - static const QString ALLOWED_HOST = "metaverse.highfidelity.com"; - QString result = originalUrl; - QUrl url(originalUrl); - QUrlQuery query(url); - if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) { - qDebug() << "Updating URL with auth token"; - AccountManager& accountManager = AccountManager::getInstance(); - query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager.getAccountInfo().getAccessToken().token); - url.setQuery(query.query()); - result = url.toString(); - } - - return result; - } -}; - -static UrlFixer URL_FIXER; // Method called by Qt scripts to create a new web window in the overlay QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { - QmlWebWindowClass* retVal { nullptr }; - const QString title = context->argument(0).toString(); - QString url = context->argument(1).toString(); - if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:")) { - url = QUrl::fromLocalFile(url).toString(); - } - const int width = std::max(100, std::min(1280, context->argument(2).toInt32()));; - const int height = std::max(100, std::min(720, context->argument(3).toInt32()));; - - - // Build the event bridge and wrapper on the main thread - QMetaObject::invokeMethod(DependencyManager::get().data(), "load", Qt::BlockingQueuedConnection, - Q_ARG(const QString&, "QmlWebWindow.qml"), - Q_ARG(std::function, [&](QQmlContext* context, QObject* object) { - setupServer(); - retVal = new QmlWebWindowClass(object); - webChannel.registerObject(url.toLower(), retVal); - context->setContextProperty("urlFixer", &URL_FIXER); - retVal->setTitle(title); - retVal->setURL(url); - retVal->setSize(width, height); - })); - connect(engine, &QScriptEngine::destroyed, retVal, &QmlWebWindowClass::deleteLater); - return engine->newQObject(retVal); + return QmlWindowClass::internalConstructor("QmlWebWindow.qml", context, engine, + [&](QQmlContext* context, QObject* object) { return new QmlWebWindowClass(object); }); } -QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) - : _windowId(++nextWindowId), _qmlWindow(qmlWindow) -{ - qDebug() << "Created window with ID " << _windowId; - Q_ASSERT(_qmlWindow); - Q_ASSERT(dynamic_cast(_qmlWindow)); +QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) : QmlWindowClass(qmlWindow) { QObject::connect(_qmlWindow, SIGNAL(navigating(QString)), this, SLOT(handleNavigation(QString))); } void QmlWebWindowClass::handleNavigation(const QString& url) { bool handled = false; - - if (url.contains(HIFI_URL_PATTERN)) { - DependencyManager::get()->handleLookupString(url); - handled = true; - } else { - static auto handler = dynamic_cast(qApp); - if (handler) { - if (handler->canAcceptURL(url)) { - handled = handler->acceptURL(url); - } + static auto handler = dynamic_cast(qApp); + if (handler) { + if (handler->canAcceptURL(url)) { + handled = handler->acceptURL(url); } } @@ -165,80 +52,6 @@ void QmlWebWindowClass::handleNavigation(const QString& url) { } } -void QmlWebWindowClass::setVisible(bool visible) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); - return; - } - - auto qmlWindow = asQuickItem(); - if (qmlWindow->isEnabled() != visible) { - qmlWindow->setEnabled(visible); - emit visibilityChanged(visible); - } -} - -QQuickItem* QmlWebWindowClass::asQuickItem() const { - return dynamic_cast(_qmlWindow); -} - -bool QmlWebWindowClass::isVisible() const { - if (QThread::currentThread() != thread()) { - bool result; - QMetaObject::invokeMethod(const_cast(this), "isVisible", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result)); - return result; - } - - return asQuickItem()->isEnabled(); -} - - -glm::vec2 QmlWebWindowClass::getPosition() const { - if (QThread::currentThread() != thread()) { - glm::vec2 result; - QMetaObject::invokeMethod(const_cast(this), "getPosition", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); - return result; - } - - return glm::vec2(asQuickItem()->x(), asQuickItem()->y()); -} - - -void QmlWebWindowClass::setPosition(const glm::vec2& position) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setPosition", Qt::QueuedConnection, Q_ARG(glm::vec2, position)); - return; - } - - asQuickItem()->setPosition(QPointF(position.x, position.y)); -} - -void QmlWebWindowClass::setPosition(int x, int y) { - setPosition(glm::vec2(x, y)); -} - -glm::vec2 QmlWebWindowClass::getSize() const { - if (QThread::currentThread() != thread()) { - glm::vec2 result; - QMetaObject::invokeMethod(const_cast(this), "getSize", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); - return result; - } - - return glm::vec2(asQuickItem()->width(), asQuickItem()->height()); -} - -void QmlWebWindowClass::setSize(const glm::vec2& size) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size)); - } - - asQuickItem()->setSize(QSizeF(size.x, size.y)); -} - -void QmlWebWindowClass::setSize(int width, int height) { - setSize(glm::vec2(width, height)); -} - QString QmlWebWindowClass::getURL() const { if (QThread::currentThread() != thread()) { QString result; @@ -254,31 +67,4 @@ void QmlWebWindowClass::setURL(const QString& urlString) { QMetaObject::invokeMethod(this, "setURL", Qt::QueuedConnection, Q_ARG(QString, urlString)); } _qmlWindow->setProperty(URL_PROPERTY, urlString); -} - - -void QmlWebWindowClass::setTitle(const QString& title) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setTitle", Qt::QueuedConnection, Q_ARG(QString, title)); - } - - _qmlWindow->setProperty(TITLE_PROPERTY, title); -} - -void QmlWebWindowClass::close() { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection); - } - _qmlWindow->setProperty("destroyOnInvisible", true); - _qmlWindow->setProperty("visible", false); - _qmlWindow->deleteLater(); -} - -void QmlWebWindowClass::hasClosed() { -} - -void QmlWebWindowClass::raise() { - // FIXME -} - -#include "QmlWebWindowClass.moc" +} \ No newline at end of file diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h index 0f2531deb8..14e533c7b4 100644 --- a/libraries/ui/src/QmlWebWindowClass.h +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -9,97 +9,26 @@ #ifndef hifi_ui_QmlWebWindowClass_h #define hifi_ui_QmlWebWindowClass_h -#include -#include -#include -#include - -#include - -class QScriptEngine; -class QScriptContext; -class QmlWebWindowClass; -class QWebSocketServer; -class QWebSocket; - -class QmlScriptEventBridge : public QObject { - Q_OBJECT -public: - QmlScriptEventBridge(const QmlWebWindowClass* webWindow) : _webWindow(webWindow) {} - -public slots : - void emitWebEvent(const QString& data); - void emitScriptEvent(const QString& data); - -signals: - void webEventReceived(const QString& data); - void scriptEventReceived(int windowId, const QString& data); - -private: - const QmlWebWindowClass* _webWindow { nullptr }; - QWebSocket *_socket { nullptr }; -}; +#include "QmlWindowClass.h" // FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping -class QmlWebWindowClass : public QObject { +class QmlWebWindowClass : public QmlWindowClass { Q_OBJECT - Q_PROPERTY(QObject* eventBridge READ getEventBridge CONSTANT) - Q_PROPERTY(int windowId READ getWindowId CONSTANT) Q_PROPERTY(QString url READ getURL CONSTANT) - Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition) - Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize) - Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibilityChanged) public: static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); QmlWebWindowClass(QObject* qmlWindow); public slots: - bool isVisible() const; - void setVisible(bool visible); - - glm::vec2 getPosition() const; - void setPosition(const glm::vec2& position); - void setPosition(int x, int y); - - glm::vec2 getSize() const; - void setSize(const glm::vec2& size); - void setSize(int width, int height); - QString getURL() const; void setURL(const QString& url); - void setTitle(const QString& title); - - // Ugh.... do not want to do - Q_INVOKABLE void raise(); - Q_INVOKABLE void close(); - Q_INVOKABLE int getWindowId() const { return _windowId; }; - Q_INVOKABLE QmlScriptEventBridge* getEventBridge() const { return _eventBridge; }; - signals: - void visibilityChanged(bool visible); // Tool window void urlChanged(); - void moved(glm::vec2 position); - void resized(QSizeF size); - void closed(); private slots: - void hasClosed(); void handleNavigation(const QString& url); - -private: - static void setupServer(); - static QWebSocketServer* _webChannelServer; - - QQuickItem* asQuickItem() const; - QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) }; - - // FIXME needs to be initialized in the ctor once we have support - // for tool window panes in QML - const bool _isToolWindow { false }; - const int _windowId; - QObject* const _qmlWindow; }; #endif diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp new file mode 100644 index 0000000000..956a5a42c7 --- /dev/null +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -0,0 +1,280 @@ +// +// Created by Bradley Austin Davis on 2015-12-15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "QmlWindowClass.h" + +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include "OffscreenUi.h" + +QWebSocketServer* QmlWindowClass::_webChannelServer { nullptr }; +static QWebChannel webChannel; +static const uint16_t WEB_CHANNEL_PORT = 51016; +static std::atomic nextWindowId; +static const char* const SOURCE_PROPERTY = "source"; +static const char* const TITLE_PROPERTY = "title"; +static const char* const WIDTH_PROPERTY = "width"; +static const char* const HEIGHT_PROPERTY = "height"; +static const char* const VISIBILE_PROPERTY = "visible"; + +void QmlScriptEventBridge::emitWebEvent(const QString& data) { + QMetaObject::invokeMethod(this, "webEventReceived", Qt::QueuedConnection, Q_ARG(QString, data)); +} + +void QmlScriptEventBridge::emitScriptEvent(const QString& data) { + QMetaObject::invokeMethod(this, "scriptEventReceived", Qt::QueuedConnection, + Q_ARG(int, _webWindow->getWindowId()), Q_ARG(QString, data)); +} + +class QmlWebTransport : public QWebChannelAbstractTransport { + Q_OBJECT +public: + QmlWebTransport(QWebSocket* webSocket) : _webSocket(webSocket) { + // Translate from the websocket layer to the webchannel layer + connect(webSocket, &QWebSocket::textMessageReceived, [this](const QString& message) { + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(message.toUtf8(), &error); + if (error.error || !document.isObject()) { + qWarning() << "Unable to parse incoming JSON message" << message; + return; + } + emit messageReceived(document.object(), this); + }); + } + + virtual void sendMessage(const QJsonObject &message) override { + // Translate from the webchannel layer to the websocket layer + _webSocket->sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact)); + } + +private: + QWebSocket* const _webSocket; +}; + + +void QmlWindowClass::setupServer() { + if (!_webChannelServer) { + _webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode); + if (!_webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) { + qFatal("Failed to open web socket server."); + } + + QObject::connect(_webChannelServer, &QWebSocketServer::newConnection, [] { + webChannel.connectTo(new QmlWebTransport(_webChannelServer->nextPendingConnection())); + }); + } +} + +QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource, + QScriptContext* context, QScriptEngine* engine, + std::function function) +{ + const auto argumentCount = context->argumentCount(); + QString url; + QString title; + int width = 100, height = 100; + bool isToolWindow = false; + bool visible = true; + if (argumentCount > 1) { + + if (!context->argument(0).isUndefined()) { + title = context->argument(0).toString(); + } + if (!context->argument(1).isUndefined()) { + url = context->argument(1).toString(); + } + if (context->argument(2).isNumber()) { + width = context->argument(2).toInt32(); + } + if (context->argument(3).isNumber()) { + height = context->argument(3).toInt32(); + } + } else { + auto argumentObject = context->argument(0); + qDebug() << argumentObject.toString(); + if (!argumentObject.property(TITLE_PROPERTY).isUndefined()) { + title = argumentObject.property(TITLE_PROPERTY).toString(); + } + if (!argumentObject.property(SOURCE_PROPERTY).isUndefined()) { + url = argumentObject.property(SOURCE_PROPERTY).toString(); + } + if (argumentObject.property(WIDTH_PROPERTY).isNumber()) { + width = argumentObject.property(WIDTH_PROPERTY).toInt32(); + } + if (argumentObject.property(HEIGHT_PROPERTY).isNumber()) { + height = argumentObject.property(HEIGHT_PROPERTY).toInt32(); + } + if (argumentObject.property(VISIBILE_PROPERTY).isBool()) { + visible = argumentObject.property(VISIBILE_PROPERTY).toBool(); + } + } + + if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:")) { + url = QUrl::fromLocalFile(url).toString(); + } + + width = std::max(100, std::min(1280, width)); + height = std::max(100, std::min(720, height)); + + QmlWindowClass* retVal{ nullptr }; + + // Build the event bridge and wrapper on the main thread + QMetaObject::invokeMethod(DependencyManager::get().data(), "load", Qt::BlockingQueuedConnection, + Q_ARG(const QString&, qmlSource), + Q_ARG(std::function, [&](QQmlContext* context, QObject* object) { + setupServer(); + retVal = function(context, object); + registerObject(url.toLower(), retVal); + if (!title.isEmpty()) { + retVal->setTitle(title); + } + retVal->setSize(width, height); + object->setProperty(SOURCE_PROPERTY, url); + if (visible) { + object->setProperty("enabled", true); + } + })); + connect(engine, &QScriptEngine::destroyed, retVal, &QmlWindowClass::deleteLater); + return engine->newQObject(retVal); +} + + +// Method called by Qt scripts to create a new web window in the overlay +QScriptValue QmlWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { + return internalConstructor("QmlWindow.qml", context, engine, [&](QQmlContext* context, QObject* object){ + return new QmlWindowClass(object); + }); +} + +QmlWindowClass::QmlWindowClass(QObject* qmlWindow) + : _windowId(++nextWindowId), _qmlWindow(qmlWindow) +{ + qDebug() << "Created window with ID " << _windowId; + Q_ASSERT(_qmlWindow); + Q_ASSERT(dynamic_cast(_qmlWindow)); +} + +void QmlWindowClass::registerObject(const QString& name, QObject* object) { + webChannel.registerObject(name, object); +} + +void QmlWindowClass::deregisterObject(QObject* object) { + webChannel.deregisterObject(object); +} + +void QmlWindowClass::setVisible(bool visible) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); + return; + } + + auto qmlWindow = asQuickItem(); + if (qmlWindow->isEnabled() != visible) { + qmlWindow->setEnabled(visible); + emit visibilityChanged(visible); + } +} + +QQuickItem* QmlWindowClass::asQuickItem() const { + return dynamic_cast(_qmlWindow); +} + +bool QmlWindowClass::isVisible() const { + if (QThread::currentThread() != thread()) { + bool result; + QMetaObject::invokeMethod(const_cast(this), "isVisible", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result)); + return result; + } + + return asQuickItem()->isEnabled(); +} + + +glm::vec2 QmlWindowClass::getPosition() const { + if (QThread::currentThread() != thread()) { + glm::vec2 result; + QMetaObject::invokeMethod(const_cast(this), "getPosition", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + return glm::vec2(asQuickItem()->x(), asQuickItem()->y()); +} + + +void QmlWindowClass::setPosition(const glm::vec2& position) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPosition", Qt::QueuedConnection, Q_ARG(glm::vec2, position)); + return; + } + + asQuickItem()->setPosition(QPointF(position.x, position.y)); +} + +void QmlWindowClass::setPosition(int x, int y) { + setPosition(glm::vec2(x, y)); +} + +glm::vec2 QmlWindowClass::getSize() const { + if (QThread::currentThread() != thread()) { + glm::vec2 result; + QMetaObject::invokeMethod(const_cast(this), "getSize", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + return glm::vec2(asQuickItem()->width(), asQuickItem()->height()); +} + +void QmlWindowClass::setSize(const glm::vec2& size) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size)); + } + + asQuickItem()->setSize(QSizeF(size.x, size.y)); +} + +void QmlWindowClass::setSize(int width, int height) { + setSize(glm::vec2(width, height)); +} + +void QmlWindowClass::setTitle(const QString& title) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setTitle", Qt::QueuedConnection, Q_ARG(QString, title)); + } + + _qmlWindow->setProperty(TITLE_PROPERTY, title); +} + +void QmlWindowClass::close() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection); + } + _qmlWindow->setProperty("destroyOnInvisible", true); + _qmlWindow->setProperty("visible", false); + _qmlWindow->deleteLater(); +} + +void QmlWindowClass::hasClosed() { +} + +void QmlWindowClass::raise() { + // FIXME +} + +#include "QmlWindowClass.moc" diff --git a/libraries/ui/src/QmlWindowClass.h b/libraries/ui/src/QmlWindowClass.h new file mode 100644 index 0000000000..41572b448d --- /dev/null +++ b/libraries/ui/src/QmlWindowClass.h @@ -0,0 +1,103 @@ +// +// Created by Bradley Austin Davis on 2015-12-15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ui_QmlWindowClass_h +#define hifi_ui_QmlWindowClass_h + +#include +#include +#include +#include +#include + +class QScriptEngine; +class QScriptContext; +class QmlWindowClass; +class QWebSocketServer; +class QWebSocket; + +class QmlScriptEventBridge : public QObject { + Q_OBJECT +public: + QmlScriptEventBridge(const QmlWindowClass* webWindow) : _webWindow(webWindow) {} + +public slots : + void emitWebEvent(const QString& data); + void emitScriptEvent(const QString& data); + +signals: + void webEventReceived(const QString& data); + void scriptEventReceived(int windowId, const QString& data); + +private: + const QmlWindowClass* _webWindow { nullptr }; + QWebSocket *_socket { nullptr }; +}; + +// FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping +class QmlWindowClass : public QObject { + Q_OBJECT + Q_PROPERTY(QObject* eventBridge READ getEventBridge CONSTANT) + Q_PROPERTY(int windowId READ getWindowId CONSTANT) + Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition) + Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize) + Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibilityChanged) + +public: + static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); + QmlWindowClass(QObject* qmlWindow); + +public slots: + bool isVisible() const; + void setVisible(bool visible); + + glm::vec2 getPosition() const; + void setPosition(const glm::vec2& position); + void setPosition(int x, int y); + + glm::vec2 getSize() const; + void setSize(const glm::vec2& size); + void setSize(int width, int height); + + void setTitle(const QString& title); + + // Ugh.... do not want to do + Q_INVOKABLE void raise(); + Q_INVOKABLE void close(); + Q_INVOKABLE int getWindowId() const { return _windowId; }; + Q_INVOKABLE QmlScriptEventBridge* getEventBridge() const { return _eventBridge; }; + +signals: + void visibilityChanged(bool visible); // Tool window + void moved(glm::vec2 position); + void resized(QSizeF size); + void closed(); + +protected slots: + void hasClosed(); + +protected: + static QScriptValue internalConstructor(const QString& qmlSource, + QScriptContext* context, QScriptEngine* engine, + std::function function); + static void setupServer(); + static void registerObject(const QString& name, QObject* object); + static void deregisterObject(QObject* object); + static QWebSocketServer* _webChannelServer; + + QQuickItem* asQuickItem() const; + QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) }; + + // FIXME needs to be initialized in the ctor once we have support + // for tool window panes in QML + const bool _isToolWindow { false }; + const int _windowId; + QObject* const _qmlWindow; +}; + +#endif diff --git a/unpublishedScripts/hiddenEntityReset.js b/unpublishedScripts/hiddenEntityReset.js index a53d6e721f..4352a5d562 100644 --- a/unpublishedScripts/hiddenEntityReset.js +++ b/unpublishedScripts/hiddenEntityReset.js @@ -170,10 +170,12 @@ function createRaveStick(position) { var modelURL = "http://hifi-content.s3.amazonaws.com/eric/models/raveStick.fbx"; + var rotation = Quat.fromPitchYawRollDegrees(0, 0, 0); var stick = Entities.addEntity({ type: "Model", name: "raveStick", modelURL: modelURL, + rotation: rotation, position: position, shapeType: 'box', collisionsWillMove: true, @@ -206,6 +208,66 @@ }) }); + var forwardVec = Quat.getFront(rotation); + var forwardQuat = Quat.rotationBetween(Vec3.UNIT_Z, forwardVec); + var color = { + red: 150, + green: 20, + blue: 100 + } + var raveGlowEmitter = Entities.addEntity({ + type: "ParticleEffect", + name: "Rave Stick Glow Emitter", + position: position, + parentID: stick, + isEmitting: true, + colorStart: color, + color: { + red: 200, + green: 200, + blue: 255 + }, + colorFinish: color, + maxParticles: 100000, + lifespan: 0.8, + emitRate: 1000, + emitOrientation: forwardQuat, + emitSpeed: 0.2, + speedSpread: 0.0, + emitDimensions: { + x: 0, + y: 0, + z: 0 + }, + polarStart: 0, + polarFinish: 0, + azimuthStart: 0.1, + azimuthFinish: 0.01, + emitAcceleration: { + x: 0, + y: 0, + z: 0 + }, + accelerationSpread: { + x: 0.00, + y: 0.00, + z: 0.00 + }, + radiusStart: 0.01, + radiusFinish: 0.005, + alpha: 0.7, + alphaSpread: 0.1, + alphaStart: 0.1, + alphaFinish: 0.1, + textures: "https://s3.amazonaws.com/hifi-public/eric/textures/particleSprites/beamParticle.png", + emitterShouldTrail: false, + userData: JSON.stringify({ + resetMe: { + resetMe: true + } + }) + }); + } function createGun(position) { diff --git a/unpublishedScripts/masterReset.js b/unpublishedScripts/masterReset.js index 2d6d9a0d01..8aedd1c650 100644 --- a/unpublishedScripts/masterReset.js +++ b/unpublishedScripts/masterReset.js @@ -149,10 +149,12 @@ MasterReset = function() { function createRaveStick(position) { var modelURL = "http://hifi-content.s3.amazonaws.com/eric/models/raveStick.fbx"; + var rotation = Quat.fromPitchYawRollDegrees(0, 0, 0); var stick = Entities.addEntity({ type: "Model", name: "raveStick", modelURL: modelURL, + rotation: rotation, position: position, shapeType: 'box', collisionsWillMove: true, @@ -189,6 +191,66 @@ MasterReset = function() { } }) }); + + var forwardVec = Quat.getFront(rotation); + var forwardQuat = Quat.rotationBetween(Vec3.UNIT_Z, forwardVec); + var color = { + red: 150, + green: 20, + blue: 100 + } + var raveGlowEmitter = Entities.addEntity({ + type: "ParticleEffect", + name: "Rave Stick Glow Emitter", + position: position, + parentID: stick, + isEmitting: true, + colorStart: color, + color: { + red: 200, + green: 200, + blue: 255 + }, + colorFinish: color, + maxParticles: 100000, + lifespan: 0.8, + emitRate: 1000, + emitOrientation: forwardQuat, + emitSpeed: 0.2, + speedSpread: 0.0, + emitDimensions: { + x: 0, + y: 0, + z: 0 + }, + polarStart: 0, + polarFinish: 0, + azimuthStart: 0.1, + azimuthFinish: 0.01, + emitAcceleration: { + x: 0, + y: 0, + z: 0 + }, + accelerationSpread: { + x: 0.00, + y: 0.00, + z: 0.00 + }, + radiusStart: 0.01, + radiusFinish: 0.005, + alpha: 0.7, + alphaSpread: 0.1, + alphaStart: 0.1, + alphaFinish: 0.1, + textures: "https://s3.amazonaws.com/hifi-public/eric/textures/particleSprites/beamParticle.png", + emitterShouldTrail: false, + userData: JSON.stringify({ + resetMe: { + resetMe: true + } + }) + }); } function createGun(position) { @@ -1084,7 +1146,7 @@ MasterReset = function() { y: 0, z: 0.06 }, - relativeRotation: Quat.fromPitchYawRollDegrees(0,-90, -90) + relativeRotation: Quat.fromPitchYawRollDegrees(0, -90, -90) }, invertSolidWhileHeld: true }