From 84e11db772c30cc3f926619067127639187a0953 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 29 Jun 2017 16:58:56 +1200 Subject: [PATCH 001/722] VR edit app script and button --- scripts/vr-edit/vr-edit.js | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 scripts/vr-edit/vr-edit.js diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js new file mode 100644 index 0000000000..3a81495708 --- /dev/null +++ b/scripts/vr-edit/vr-edit.js @@ -0,0 +1,61 @@ +"use strict"; + +// +// vr-edit.js +// +// Created by David Rowe on 27 Jun 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function () { + + var APP_NAME = "VR EDIT", // TODO: App name. + APP_ICON_INACTIVE = "icons/tablet-icons/edit-i.svg", // TODO: App icons. + APP_ICON_ACTIVE = "icons/tablet-icons/edit-a.svg", + tablet, + button, + isAppActive = false; + + function onButtonClicked() { + isAppActive = !isAppActive; + button.editProperties({ isActive: isAppActive }); + } + + function setUp() { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + if (!tablet) { + return; + } + + // Tablet/toolbar button. + button = tablet.addButton({ + icon: APP_ICON_INACTIVE, + activeIcon: APP_ICON_ACTIVE, + text: APP_NAME, + isActive: isAppActive + }); + if (button) { + button.clicked.connect(onButtonClicked); + } + } + + function tearDown() { + if (!tablet) { + return; + } + + if (button) { + button.clicked.disconnect(onButtonClicked); + tablet.removeButton(button); + button = null; + } + + tablet = null; + } + + setUp(); + Script.scriptEnding.connect(tearDown); +}()); From a863e37eb5a1482b9383d861ed9a4ce9a9ce71c3 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 30 Jun 2017 12:11:06 +1200 Subject: [PATCH 002/722] Communicate VR edit enabled state to hand controller script --- scripts/system/controllers/handControllerGrab.js | 2 +- scripts/system/edit.js | 2 +- scripts/system/libraries/utils.js | 10 +++++++--- scripts/vr-edit/vr-edit.js | 8 +++++++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 04921fe14d..77042e1ac6 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -14,7 +14,7 @@ /* global getEntityCustomData, flatten, Xform, Script, Quat, Vec3, MyAvatar, Entities, Overlays, Settings, Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset, - setGrabCommunications, Menu, HMD, isInEditMode, AvatarList */ + setGrabCommunications, Menu, HMD, isInEditMode, isInVREditMode, AvatarList */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE diff --git a/scripts/system/edit.js b/scripts/system/edit.js index a83d2159bb..4dc91ac024 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -228,7 +228,7 @@ var GRABBABLE_ENTITIES_MENU_CATEGORY = "Edit"; var GRABBABLE_ENTITIES_MENU_ITEM = "Create Entities As Grabbable"; var toolBar = (function () { - var EDIT_SETTING = "io.highfidelity.isEditting"; // for communication with other scripts + var EDIT_SETTING = "io.highfidelity.isEditing"; // for communication with other scripts var that = {}, toolBar, activeButton = null, diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index a5e97d8949..0f367e0cfe 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -6,12 +6,16 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -// note: this constant is currently duplicated in edit.js -EDIT_SETTING = "io.highfidelity.isEditting"; -isInEditMode = function isInEditMode() { +EDIT_SETTING = "io.highfidelity.isEditing"; // Note: This constant is duplicated in edit.js. +isInEditMode = function () { return Settings.getValue(EDIT_SETTING); }; +VR_EDIT_SETTING = "io.highfidelity.isVREditing"; // Note: This constant is duplicated in vr-edit.js. +isInVREditMode = function () { + return HMD.active && Settings.getValue(VR_EDIT_SETTING); +} + if (!Function.prototype.bind) { Function.prototype.bind = function(oThis) { if (typeof this !== 'function') { diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 3a81495708..6ceb4ff7ac 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -17,14 +17,20 @@ APP_ICON_ACTIVE = "icons/tablet-icons/edit-a.svg", tablet, button, - isAppActive = false; + isAppActive = false, + + VR_EDIT_SETTING = "io.highfidelity.isVREditing"; // Note: This constant is duplicated in utils.js. + function onButtonClicked() { isAppActive = !isAppActive; + Settings.setValue(VR_EDIT_SETTING, isAppActive); button.editProperties({ isActive: isAppActive }); } function setUp() { + Settings.setValue(VR_EDIT_SETTING, isAppActive); + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); if (!tablet) { return; From 83a78aa4074c631d23a02c592d95d329e0a261c8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 4 Jul 2017 15:54:33 +1200 Subject: [PATCH 003/722] No handControllerGrab.js lasers while in VR edit mode --- scripts/system/controllers/handControllerGrab.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 77042e1ac6..cbed0b1007 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -537,7 +537,7 @@ function storeAttachPointForHotspotInSettings(hotspot, hand, offsetPosition, off var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; function isEditing() { - return EXTERNALLY_MANAGED_2D_MINOR_MODE && isInEditMode(); + return EXTERNALLY_MANAGED_2D_MINOR_MODE && (isInEditMode() || isInVREditMode()); } function isIn2DMode() { @@ -1220,7 +1220,7 @@ function MyController(hand) { }; this.setState = function(newState, reason) { - if ((isInEditMode() && this.grabbedThingID !== HMD.tabletID) && + if (((isInEditMode() || isInVREditMode()) && this.grabbedThingID !== HMD.tabletID) && (newState !== STATE_OFF && newState !== STATE_SEARCHING && newState !== STATE_STYLUS_TOUCHING && @@ -1799,8 +1799,9 @@ function MyController(hand) { this.processStylus(); - if (isInEditMode() && !this.isNearStylusTarget && HMD.isHandControllerAvailable()) { + if (isInEditMode() && !isInVREditMode() && !this.isNearStylusTarget && HMD.isHandControllerAvailable()) { // Always showing lasers while in edit mode and hands/stylus is not active. + // But don't show lasers while in VR edit mode. var rayPickInfo = this.calcRayPickInfo(this.hand); this.intersectionDistance = (rayPickInfo.entityID || rayPickInfo.overlayID) ? rayPickInfo.distance : 0; this.searchIndicatorOn(rayPickInfo.searchRay); @@ -2263,7 +2264,7 @@ function MyController(hand) { return aDistance - bDistance; }); entity = grabbableEntities[0]; - if (!isInEditMode() || entity == HMD.tabletID) { // tablet is grabbable, even when editing + if ((!isInEditMode() && !isInVREditMode()) || entity == HMD.tabletID) { // tablet is grabbable, even when editing name = entityPropertiesCache.getProps(entity).name; this.grabbedThingID = entity; this.grabbedIsOverlay = false; @@ -2371,7 +2372,7 @@ function MyController(hand) { equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); } - if (farGrabEnabled && farSearching) { + if (farGrabEnabled && farSearching && !isInVREditMode()) { this.searchIndicatorOn(rayPickInfo.searchRay); } Reticle.setVisible(false); From cadd4685ee2fe899ed35b743a8f404a8411c5c2c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 4 Jul 2017 15:59:04 +1200 Subject: [PATCH 004/722] No haptic pulse in edit modes when hand enters near-grab distance --- scripts/system/controllers/handControllerGrab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index cbed0b1007..be073742b0 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1786,7 +1786,7 @@ function MyController(hand) { var nonTabletEntities = grabbableEntities.filter(function(entityID) { return entityID != HMD.tabletID && entityID != HMD.homeButtonID; }); - if (nonTabletEntities.length > 0) { + if (nonTabletEntities.length > 0 && !isInEditMode() && !isInVREditMode()) { Controller.triggerHapticPulse(1, 20, this.hand); } this.grabPointIntersectsEntity = true; From a24a0265f99a015c12718b921434f9dd5c9a1c4e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 4 Jul 2017 16:24:17 +1200 Subject: [PATCH 005/722] Improve communication with hand controller script --- scripts/vr-edit/vr-edit.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 6ceb4ff7ac..14d350803e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -22,14 +22,19 @@ VR_EDIT_SETTING = "io.highfidelity.isVREditing"; // Note: This constant is duplicated in utils.js. + function updateHandControllerGrab() { + // Communicate status to handControllerGrab.js. + Settings.setValue(VR_EDIT_SETTING, isAppActive); + } + function onButtonClicked() { isAppActive = !isAppActive; - Settings.setValue(VR_EDIT_SETTING, isAppActive); + updateHandControllerGrab(); button.editProperties({ isActive: isAppActive }); } function setUp() { - Settings.setValue(VR_EDIT_SETTING, isAppActive); + updateHandControllerGrab(); tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); if (!tablet) { @@ -49,6 +54,9 @@ } function tearDown() { + isAppActive = false; + updateHandControllerGrab(); + if (!tablet) { return; } From dcbf3ceeb9b7d31ab55a82847e10c55f8f1d52bd Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Jul 2017 12:40:45 +1200 Subject: [PATCH 006/722] Update loop --- scripts/vr-edit/vr-edit.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 14d350803e..5616fd12b5 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -21,6 +21,15 @@ VR_EDIT_SETTING = "io.highfidelity.isVREditing"; // Note: This constant is duplicated in utils.js. + UPDATE_LOOP_TIMEOUT = 16, + updateTimer = null; + + function update() { + // Main update loop. + updateTimer = null; + + updateTimer = Script.setTimeout(update, UPDATE_LOOP_TIMEOUT); + } function updateHandControllerGrab() { // Communicate status to handControllerGrab.js. @@ -31,6 +40,13 @@ isAppActive = !isAppActive; updateHandControllerGrab(); button.editProperties({ isActive: isAppActive }); + + if (isAppActive) { + update(); + } else { + Script.clearTimeout(updateTimer); + updateTimer = null; + } } function setUp() { @@ -51,9 +67,17 @@ if (button) { button.clicked.connect(onButtonClicked); } + + if (isAppActive) { + update(); + } } function tearDown() { + if (updateTimer) { + Script.clearTimeout(updateTimer); + } + isAppActive = false; updateHandControllerGrab(); From bf722f084efd06dfc409d29c7290a0bc6facd911 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Jul 2017 12:49:36 +1200 Subject: [PATCH 007/722] A controller object for each hand --- scripts/vr-edit/vr-edit.js | 114 ++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 5616fd12b5..d474974982 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -21,13 +21,112 @@ VR_EDIT_SETTING = "io.highfidelity.isVREditing"; // Note: This constant is duplicated in utils.js. + hands = [], + LEFT_HAND = 0, + RIGHT_HAND = 1, + UPDATE_LOOP_TIMEOUT = 16, - updateTimer = null; + updateTimer = null, + + Hand; + + Hand = function (side) { + var hand, + handController, + controllerTrigger, + controllerTriggerClicked, + + TRIGGER_ON_VALUE = 0.15, // Per handControllerGrab.js. + TRIGGER_OFF_VALUE = 0.1, // Per handControllerGrab.js. + GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. + + PICK_MAX_DISTANCE = 500, // Per handControllerGrab.js. + PRECISION_PICKING = true, + NO_INCLUDE_IDS = [], + NO_EXCLUDE_IDS = [], + VISIBLE_ONLY = true, + + isLaserOn = false; + + hand = side; + if (hand === LEFT_HAND) { + handController = Controller.Standard.LeftHand; + controllerTrigger = Controller.Standard.LT; + controllerTriggerClicked = Controller.Standard.LTClick; + GRAB_POINT_SPHERE_OFFSET.x = -GRAB_POINT_SPHERE_OFFSET.x; + } else { + handController = Controller.Standard.RightHand; + controllerTrigger = Controller.Standard.RT; + controllerTriggerClicked = Controller.Standard.RTClick; + } + + function update() { + var wasLaserOn, + handPose, + handPosition, + handOrientation, + deltaOrigin, + pickRay, + intersection, + distance; + + // Controller trigger. + wasLaserOn = isLaserOn; + isLaserOn = Controller.getValue(controllerTrigger) > (isLaserOn ? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE); + if (!isLaserOn) { + if (wasLaserOn) { + // Clear laser + } + return; + } + + // Hand position and orientation. + handPose = Controller.getPoseValue(handController); + if (!handPose.valid) { + isLaserOn = false; + if (wasLaserOn) { + // Clear laser + } + return; + } + handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); + handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); + + // Entity intersection, if any. + deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); + pickRay = { + origin: Vec3.sum(handPosition, deltaOrigin), // Add a bit to ... + direction: Quat.getUp(handOrientation), + length: PICK_MAX_DISTANCE + }; + intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + distance = intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE; + + // Update laser. + } + + function destroy() { + } + + if (!this instanceof Hand) { + return new Hand(); + } + + return { + update: update, + destroy: destroy + }; + }; + function update() { // Main update loop. updateTimer = null; + hands[LEFT_HAND].update(); + hands[RIGHT_HAND].update(); + updateTimer = Script.setTimeout(update, UPDATE_LOOP_TIMEOUT); } @@ -68,6 +167,10 @@ button.clicked.connect(onButtonClicked); } + // Hands, each with a laser, selection, etc. + hands[LEFT_HAND] = new Hand(LEFT_HAND); + hands[RIGHT_HAND] = new Hand(RIGHT_HAND); + if (isAppActive) { update(); } @@ -91,6 +194,15 @@ button = null; } + if (hands[LEFT_HAND]) { + hands[LEFT_HAND].destroy(); + hands[LEFT_HAND] = null; + } + if (hands[RIGHT_HAND]) { + hands[RIGHT_HAND].destroy(); + hands[RIGHT_HAND] = null; + } + tablet = null; } From f9ee21525d3c88333134326ea0e54f8f240bef2c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Jul 2017 12:53:54 +1200 Subject: [PATCH 008/722] A laser for each hand --- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 1 + scripts/vr-edit/vr-edit.js | 127 +++++++++++++++++- 2 files changed, 123 insertions(+), 5 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index ea91890f33..eb2306154e 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -410,6 +410,7 @@ void HmdDisplayPlugin::updateFrameData() { vec3 castDirection = glm::quat_cast(model) * laserDirection; // this offset needs to match GRAB_POINT_SPHERE_OFFSET in scripts/system/libraries/controllers.js:19 + // and in vr-edit.js static const vec3 GRAB_POINT_SPHERE_OFFSET(0.04f, 0.13f, 0.039f); // x = upward, y = forward, z = lateral // swizzle grab point so that (x = upward, y = lateral, z = forward) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index d474974982..ff2f070984 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -19,7 +19,7 @@ button, isAppActive = false, - VR_EDIT_SETTING = "io.highfidelity.isVREditing"; // Note: This constant is duplicated in utils.js. + VR_EDIT_SETTING = "io.highfidelity.isVREditing", // Note: This constant is duplicated in utils.js. hands = [], LEFT_HAND = 0, @@ -28,7 +28,115 @@ UPDATE_LOOP_TIMEOUT = 16, updateTimer = null, - Hand; + Hand, + Laser, + + AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}"; + + Laser = function (side) { + // May intersect with entities or bounding box of other hand's selection. + + var hand, + laserLine = null, + laserSphere = null, + + searchDistance = 0.0, + + SEARCH_SPHERE_SIZE = 0.013, // Per handControllerGrab.js multiplied by 1.2 per handControllerGrab.js. + SEARCH_SPHERE_FOLLOW_RATE = 0.5, // Per handControllerGrab.js. + COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 }, // Per handControllgerGrab.js. + COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }; // Per handControllgerGrab.js. + + hand = side; + laserLine = Overlays.addOverlay("line3d", { + lineWidth: 5, + alpha: 1.0, + glow: 1.0, + ignoreRayIntersection: true, + drawInFront: true, + parentID: AVATAR_SELF_ID, + parentJointIndex: MyAvatar.getJointIndex(hand === LEFT_HAND + ? "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND" + : "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"), + visible: false + }); + laserSphere = Overlays.addOverlay("circle3d", { + innerAlpha: 1.0, + outerAlpha: 0.0, + solid: true, + ignoreRayIntersection: true, + drawInFront: true, + visible: false + }); + + function colorPow(color, power) { + return { + red: Math.pow(color.red / 255, power) * 255, + green: Math.pow(color.green / 255, power) * 255, + blue: Math.pow(color.blue / 255, power) * 255 + }; + } + + function updateLine(start, end, color) { + Overlays.editOverlay(laserLine, { + start: start, + end: end, + color: color, + visible: true + }); + } + + function updateSphere(location, size, color) { + var rotation, + brightColor; + + rotation = Quat.lookAt(location, Camera.getPosition(), Vec3.UP); + brightColor = colorPow(color, 0.06); + + Overlays.editOverlay(laserSphere, { + position: location, + rotation: rotation, + innerColor: brightColor, + outerColor: color, + outerRadius: size, + visible: true + }); + } + + function update(origin, direction, distance, isClicked) { + var searchTarget, + sphereSize, + color; + + searchDistance = SEARCH_SPHERE_FOLLOW_RATE * searchDistance + (1.0 - SEARCH_SPHERE_FOLLOW_RATE) * distance; + searchTarget = Vec3.sum(origin, Vec3.multiply(searchDistance, direction)); + sphereSize = SEARCH_SPHERE_SIZE * searchDistance; + color = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE : COLORS_GRAB_SEARCHING_HALF_SQUEEZE; + + updateLine(origin, searchTarget, color); + updateSphere(searchTarget, sphereSize, color); + } + + function clear() { + Overlays.editOverlay(laserLine, { visible: false }); + Overlays.editOverlay(laserSphere, { visible: false }); + } + + function destroy() { + Overlays.deleteOverlay(laserLine); + Overlays.deleteOverlay(laserSphere); + } + + if (!this instanceof Laser) { + return new Laser(); + } + + return { + update: update, + clear: clear, + destroy: destroy + }; + }; Hand = function (side) { var hand, @@ -46,7 +154,9 @@ NO_EXCLUDE_IDS = [], VISIBLE_ONLY = true, - isLaserOn = false; + isLaserOn = false, + + laser; hand = side; if (hand === LEFT_HAND) { @@ -60,6 +170,8 @@ controllerTriggerClicked = Controller.Standard.RTClick; } + laser = new Laser(hand); + function update() { var wasLaserOn, handPose, @@ -75,7 +187,7 @@ isLaserOn = Controller.getValue(controllerTrigger) > (isLaserOn ? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE); if (!isLaserOn) { if (wasLaserOn) { - // Clear laser + laser.clear(); } return; } @@ -85,7 +197,7 @@ if (!handPose.valid) { isLaserOn = false; if (wasLaserOn) { - // Clear laser + laser.clear(); } return; } @@ -104,9 +216,14 @@ distance = intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE; // Update laser. + laser.update(pickRay.origin, pickRay.direction, distance, Controller.getValue(controllerTriggerClicked)); } function destroy() { + if (laser) { + laser.destroy(); + laser = null; + } } if (!this instanceof Hand) { From dd1116092e82a2bd5e0aa2ecb3f1de41a1ce7f10 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Jul 2017 14:20:29 +1200 Subject: [PATCH 009/722] Collect entities in parent-child tree that hovered entity belongs to --- scripts/vr-edit/vr-edit.js | 137 +++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 5 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index ff2f070984..65472d33f6 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -28,12 +28,114 @@ UPDATE_LOOP_TIMEOUT = 16, updateTimer = null, - Hand, + Selection, Laser, + Hand, - AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}"; + AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", + NULL_UUID = "{00000000-0000-0000-0000-000000000000}"; + + Selection = function () { + // Manages set of selected entities. Currently supports just one set of linked entities. + + var selection = [], + selectedEntityID = null, + selectionPosition = null, + selectionOrientation, + rootEntityID; + + function traverseEntityTree(id, result) { + // Recursively traverses tree of entities and their children, gather IDs and properties. + var children, + properties, + SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions"], + i, + length; + + properties = Entities.getEntityProperties(id, SELECTION_PROPERTIES); + result.push({ + id: id, + position: properties.position, + registrationPoint: properties.registrationPoint, + rotation: properties.rotation, + dimensions: properties.dimensions + }); + + children = Entities.getChildrenIDs(id); + for (i = 0, length = children.length; i < length; i += 1) { + traverseEntityTree(children[i], result); + } + } + + function select(entityID) { + var entityProperties, + PARENT_PROPERTIES = ["parentID", "position", "rotation"]; + + // Find root parent. + rootEntityID = entityID; + entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); + while (entityProperties.parentID !== NULL_UUID) { + rootEntityID = entityProperties.parentID; + entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); + } + + // Selection position and orientation is that of the root entity. + selectionPosition = entityProperties.position; + selectionOrientation = entityProperties.rotation; + + // Find all children. + selection = []; + traverseEntityTree(rootEntityID, selection); + + selectedEntityID = entityID; + } + + function getSelection() { + return selection; + } + + function getPositionAndOrientation() { + return { + position: selectionPosition, + orientation: selectionOrientation + }; + } + + function setPositionAndOrientation(position, orientation) { + selectionPosition = position; + selectionOrientation = orientation; + Entities.editEntity(rootEntityID, { + position: position, + rotation: orientation + }); + } + + function clear() { + selection = []; + selectedEntityID = null; + rootEntityID = null; + } + + function destroy() { + clear(); + } + + if (!this instanceof Selection) { + return new Selection(); + } + + return { + select: select, + selection: getSelection, + getPositionAndOrientation: getPositionAndOrientation, + setPositionAndOrientation: setPositionAndOrientation, + clear: clear, + destroy: destroy + }; + }; Laser = function (side) { + // Draws hand lasers. // May intersect with entities or bounding box of other hand's selection. var hand, @@ -139,6 +241,9 @@ }; Hand = function (side) { + // Hand controller input. + // Each hand has a laser, an entity selection, and entity highlighter. + var hand, handController, controllerTrigger, @@ -155,8 +260,10 @@ VISIBLE_ONLY = true, isLaserOn = false, + hoveredEntityID = null, - laser; + laser, + selection; hand = side; if (hand === LEFT_HAND) { @@ -171,6 +278,7 @@ } laser = new Laser(hand); + selection = new Selection(); function update() { var wasLaserOn, @@ -188,6 +296,8 @@ if (!isLaserOn) { if (wasLaserOn) { laser.clear(); + selection.clear(); + hoveredEntityID = null; } return; } @@ -198,6 +308,8 @@ isLaserOn = false; if (wasLaserOn) { laser.clear(); + selection.clear(); + hoveredEntityID = null; } return; } @@ -215,8 +327,19 @@ VISIBLE_ONLY); distance = intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE; - // Update laser. + // Hover entities. laser.update(pickRay.origin, pickRay.direction, distance, Controller.getValue(controllerTriggerClicked)); + if (intersection.intersects) { + if (intersection.entityID !== hoveredEntityID) { + hoveredEntityID = intersection.entityID; + selection.select(hoveredEntityID); + } + } else { + if (hoveredEntityID) { + selection.clear(); + hoveredEntityID = null; + } + } } function destroy() { @@ -224,6 +347,10 @@ laser.destroy(); laser = null; } + if (selection) { + selection.destroy(); + selection = null; + } } if (!this instanceof Hand) { @@ -248,7 +375,7 @@ } function updateHandControllerGrab() { - // Communicate status to handControllerGrab.js. + // Communicate app status to handControllerGrab.js. Settings.setValue(VR_EDIT_SETTING, isAppActive); } From c85df15badf0a432c746dca4e0acb61a4fd7dd71 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Jul 2017 14:21:47 +1200 Subject: [PATCH 010/722] Display bounding boxes as highlights when hovering --- scripts/vr-edit/vr-edit.js | 85 +++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 65472d33f6..063734d99a 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -28,6 +28,7 @@ UPDATE_LOOP_TIMEOUT = 16, updateTimer = null, + Highlights, Selection, Laser, Hand, @@ -35,6 +36,78 @@ AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", NULL_UUID = "{00000000-0000-0000-0000-000000000000}"; + Highlights = function () { + // Draws highlights on selected entities. + + var overlays = [], + HIGHLIGHT_COLOR = { red: 240, green: 240, blue: 0 }, + HIGHLIGHT_ALPHA = 0.8; + + function maybeAddOverlay(index) { + if (index >= overlays.length) { + overlays.push(Overlays.addOverlay("cube", { + color: HIGHLIGHT_COLOR, + alpha: HIGHLIGHT_ALPHA, + solid: false, + drawInFront: true, + ignoreRayIntersection: true, + visible: false + })); + } + } + + function editOverlay(index, details) { + Overlays.editOverlay(overlays[index], { + position: details.position, + registrationPoint: details.registrationPoint, + rotation: details.rotation, + dimensions: details.dimensions, + visible: true + }); + } + + function display(selection) { + var i, + length; + + // Add/edit overlay. + for (i = 0, length = selection.length; i < length; i += 1) { + maybeAddOverlay(i); + editOverlay(i, selection[i]); + } + + // Delete extra overlays. + for (i = overlays.length - 1, length = selection.length; i >= length; i -= 1) { + Overlays.deleteOverlay(overlays[i]); + overlays.splice(i, 1); + } + } + + function clear() { + var i, + length; + + for (i = 0, length = overlays.length; i < length; i += 1) { + Overlays.deleteOverlay(overlays[i]); + } + overlays = []; + } + + function destroy() { + clear(); + } + + if (!this instanceof Highlights) { + return new Highlights(); + } + + return { + display: display, + clear: clear, + destroy: destroy + }; + }; + Selection = function () { // Manages set of selected entities. Currently supports just one set of linked entities. @@ -263,7 +336,8 @@ hoveredEntityID = null, laser, - selection; + selection, + highlights; hand = side; if (hand === LEFT_HAND) { @@ -279,6 +353,7 @@ laser = new Laser(hand); selection = new Selection(); + highlights = new Highlights(); function update() { var wasLaserOn, @@ -297,6 +372,7 @@ if (wasLaserOn) { laser.clear(); selection.clear(); + highlights.clear(); hoveredEntityID = null; } return; @@ -309,6 +385,7 @@ if (wasLaserOn) { laser.clear(); selection.clear(); + highlights.clear(); hoveredEntityID = null; } return; @@ -333,10 +410,12 @@ if (intersection.entityID !== hoveredEntityID) { hoveredEntityID = intersection.entityID; selection.select(hoveredEntityID); + highlights.display(selection.selection()); } } else { if (hoveredEntityID) { selection.clear(); + highlights.clear(); hoveredEntityID = null; } } @@ -351,6 +430,10 @@ selection.destroy(); selection = null; } + if (highlights) { + highlights.destroy(); + highlights = null; + } } if (!this instanceof Hand) { From 75bc0f8b3f820be6a2c1a67c5a5b0bbe46a3c3f0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Jul 2017 15:47:08 +1200 Subject: [PATCH 011/722] Translate and rotate selected entities --- scripts/vr-edit/vr-edit.js | 98 ++++++++++++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 15 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 063734d99a..8cea7ed614 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -333,7 +333,13 @@ VISIBLE_ONLY = true, isLaserOn = false, - hoveredEntityID = null, + laseredEntityID = null, + + isEditing = false, + initialHandPosition, + initialHandOrientationInverse, + initialHandToSelectionVector, + initialSelectionOrientation, laser, selection, @@ -355,6 +361,40 @@ selection = new Selection(); highlights = new Highlights(); + function startEditing(handPose) { + var selectionPositionAndOrientation; + + initialHandPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); + initialHandOrientationInverse = Quat.inverse(Quat.multiply(MyAvatar.orientation, handPose.rotation)); + + selectionPositionAndOrientation = selection.getPositionAndOrientation(); + initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, initialHandPosition); + initialSelectionOrientation = selectionPositionAndOrientation.orientation; + + isEditing = true; + } + + function applyEdit(handPose) { + var handPosition, + handOrientation, + deltaOrientation, + selectionPosition, + selectionOrientation; + + handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); + handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); + + deltaOrientation = Quat.multiply(handOrientation, initialHandOrientationInverse); + selectionPosition = Vec3.sum(handPosition, Vec3.multiplyQbyV(deltaOrientation, initialHandToSelectionVector)); + selectionOrientation = Quat.multiply(deltaOrientation, initialSelectionOrientation); + + selection.setPositionAndOrientation(selectionPosition, selectionOrientation); + } + + function stopEditing() { + isEditing = false; + } + function update() { var wasLaserOn, handPose, @@ -363,7 +403,8 @@ deltaOrigin, pickRay, intersection, - distance; + distance, + isTriggerClicked; // Controller trigger. wasLaserOn = isLaserOn; @@ -373,7 +414,7 @@ laser.clear(); selection.clear(); highlights.clear(); - hoveredEntityID = null; + laseredEntityID = null; } return; } @@ -386,7 +427,7 @@ laser.clear(); selection.clear(); highlights.clear(); - hoveredEntityID = null; + laseredEntityID = null; } return; } @@ -404,20 +445,47 @@ VISIBLE_ONLY); distance = intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE; - // Hover entities. - laser.update(pickRay.origin, pickRay.direction, distance, Controller.getValue(controllerTriggerClicked)); - if (intersection.intersects) { - if (intersection.entityID !== hoveredEntityID) { - hoveredEntityID = intersection.entityID; - selection.select(hoveredEntityID); - highlights.display(selection.selection()); + // Laser, hover, edit. + isTriggerClicked = Controller.getValue(controllerTriggerClicked); + laser.update(pickRay.origin, pickRay.direction, distance, isTriggerClicked); + + if (isTriggerClicked) { + if (isEditing) { + // Perform edit. + applyEdit(handPose); + } else if (intersection.intersects) { + // Start editing. + if (intersection.entityID !== laseredEntityID) { + laseredEntityID = intersection.entityID; + selection.select(laseredEntityID); + } else { + highlights.clear(); + } + startEditing(handPose); } } else { - if (hoveredEntityID) { - selection.clear(); - highlights.clear(); - hoveredEntityID = null; + if (isEditing) { + // Stop editing. + stopEditing(); + laseredEntityID = null; // Force highlighting entities at their new position. } + + if (intersection.intersects) { + // Hover entities. + if (intersection.entityID !== laseredEntityID) { + laseredEntityID = intersection.entityID; + selection.select(laseredEntityID); + highlights.display(selection.selection()); + } + } else { + // Unhover entities. + if (laseredEntityID) { + selection.clear(); + highlights.clear(); + laseredEntityID = null; + } + } + } } From 1e76729d592daf3775ee58409da46fa6acca1bef Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Jul 2017 15:52:22 +1200 Subject: [PATCH 012/722] Lock laser distance while editing --- scripts/vr-edit/vr-edit.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 8cea7ed614..3022b0242c 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -340,6 +340,7 @@ initialHandOrientationInverse, initialHandToSelectionVector, initialSelectionOrientation, + editingDistance, laser, selection, @@ -443,7 +444,7 @@ }; intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, VISIBLE_ONLY); - distance = intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE; + distance = isEditing ? editingDistance : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); // Laser, hover, edit. isTriggerClicked = Controller.getValue(controllerTriggerClicked); @@ -461,6 +462,7 @@ } else { highlights.clear(); } + editingDistance = distance; startEditing(handPose); } } else { From 24b19632575b8c3e28776f85756181dcfb162ad1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Jul 2017 16:00:42 +1200 Subject: [PATCH 013/722] Tidying --- scripts/vr-edit/vr-edit.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 3022b0242c..d00e82b16b 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -396,6 +396,13 @@ isEditing = false; } + function clearLaser() { + laser.clear(); + selection.clear(); + highlights.clear(); + laseredEntityID = null; + } + function update() { var wasLaserOn, handPose, @@ -412,10 +419,7 @@ isLaserOn = Controller.getValue(controllerTrigger) > (isLaserOn ? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE); if (!isLaserOn) { if (wasLaserOn) { - laser.clear(); - selection.clear(); - highlights.clear(); - laseredEntityID = null; + clearLaser(); } return; } @@ -425,10 +429,7 @@ if (!handPose.valid) { isLaserOn = false; if (wasLaserOn) { - laser.clear(); - selection.clear(); - highlights.clear(); - laseredEntityID = null; + clearLaser(); } return; } @@ -449,7 +450,6 @@ // Laser, hover, edit. isTriggerClicked = Controller.getValue(controllerTriggerClicked); laser.update(pickRay.origin, pickRay.direction, distance, isTriggerClicked); - if (isTriggerClicked) { if (isEditing) { // Perform edit. @@ -487,7 +487,6 @@ laseredEntityID = null; } } - } } @@ -516,7 +515,6 @@ }; }; - function update() { // Main update loop. updateTimer = null; From e6e619f0d874db046d33f9272fbde68c6aa2ad8f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Jul 2017 22:36:18 +1200 Subject: [PATCH 014/722] Hover only editable entities --- scripts/vr-edit/vr-edit.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index d00e82b16b..619a15924d 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -335,6 +335,9 @@ isLaserOn = false, laseredEntityID = null, + EDITIBLE_ENTITY_QUERY_PROPERTYES = ["parentID", "visible", "locked", "type"], + NONEDITABLE_ENTITY_TYPES = ["Unknown", "Zone", "Light"], + isEditing = false, initialHandPosition, initialHandOrientationInverse, @@ -403,6 +406,15 @@ laseredEntityID = null; } + function isEditableEntity(entityID) { + // Entity trees are moved as a group so check the root entity. + var properties = Entities.getEntityProperties(entityID, EDITIBLE_ENTITY_QUERY_PROPERTYES); + while (properties.parentID && properties.parentID !== NULL_UUID) { + properties = Entities.getEntityProperties(properties.parentID, EDITIBLE_ENTITY_QUERY_PROPERTYES); + } + return properties.visible && !properties.locked && NONEDITABLE_ENTITY_TYPES.indexOf(properties.type) === -1; + } + function update() { var wasLaserOn, handPose, @@ -446,6 +458,7 @@ intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, VISIBLE_ONLY); distance = isEditing ? editingDistance : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); + intersection.intersects = isEditableEntity(intersection.entityID); // Laser, hover, edit. isTriggerClicked = Controller.getValue(controllerTriggerClicked); From 38e7ee309603541e06f5c7f546c74577b3d771e6 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 5 Jul 2017 22:38:59 +1200 Subject: [PATCH 015/722] Hover and select entities that hand intersects --- scripts/vr-edit/vr-edit.js | 140 +++++++++++++++++++++++++------------ 1 file changed, 94 insertions(+), 46 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 619a15924d..70e55f4e02 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -34,7 +34,8 @@ Hand, AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", - NULL_UUID = "{00000000-0000-0000-0000-000000000000}"; + NULL_UUID = "{00000000-0000-0000-0000-000000000000}", + HALF_TREE_SCALE = 16384; Highlights = function () { // Draws highlights on selected entities. @@ -326,14 +327,19 @@ TRIGGER_OFF_VALUE = 0.1, // Per handControllerGrab.js. GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. + NEAR_GRAB_RADIUS = 0.1, // Per handControllerGrab.js. + PICK_MAX_DISTANCE = 500, // Per handControllerGrab.js. PRECISION_PICKING = true, NO_INCLUDE_IDS = [], NO_EXCLUDE_IDS = [], VISIBLE_ONLY = true, + isTriggerPressed = false, + + isHandHover = false, isLaserOn = false, - laseredEntityID = null, + hoveredEntityID = null, EDITIBLE_ENTITY_QUERY_PROPERTYES = ["parentID", "visible", "locked", "type"], NONEDITABLE_ENTITY_TYPES = ["Unknown", "Zone", "Light"], @@ -343,7 +349,7 @@ initialHandOrientationInverse, initialHandToSelectionVector, initialSelectionOrientation, - editingDistance, + laserEditingDistance, laser, selection, @@ -416,88 +422,130 @@ } function update() { - var wasLaserOn, + var palmPosition, + entityID, + entityIDs, + entitySize, + size, + wasLaserOn, handPose, handPosition, handOrientation, deltaOrigin, pickRay, intersection, - distance, - isTriggerClicked; - - // Controller trigger. - wasLaserOn = isLaserOn; - isLaserOn = Controller.getValue(controllerTrigger) > (isLaserOn ? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE); - if (!isLaserOn) { - if (wasLaserOn) { - clearLaser(); - } - return; - } + laserLength, + isTriggerClicked, + wasEditing, + i, + length; // Hand position and orientation. handPose = Controller.getPoseValue(handController); if (!handPose.valid) { isLaserOn = false; if (wasLaserOn) { - clearLaser(); + laser.clear(); + selection.clear(); + highlights.clear(); + hoveredEntityID = null; } return; } - handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); - handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); - // Entity intersection, if any. - deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); - pickRay = { - origin: Vec3.sum(handPosition, deltaOrigin), // Add a bit to ... - direction: Quat.getUp(handOrientation), - length: PICK_MAX_DISTANCE - }; - intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, - VISIBLE_ONLY); - distance = isEditing ? editingDistance : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); - intersection.intersects = isEditableEntity(intersection.entityID); - - // Laser, hover, edit. + // Controller trigger. + isTriggerPressed = Controller.getValue(controllerTrigger) > (isTriggerPressed + ? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE); isTriggerClicked = Controller.getValue(controllerTriggerClicked); - laser.update(pickRay.origin, pickRay.direction, distance, isTriggerClicked); + + // Hand-entity intersection, if any. + entityID = null; + palmPosition = hand === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); + entityIDs = Entities.findEntities(palmPosition, NEAR_GRAB_RADIUS); + if (entityIDs.length > 0) { + // Find smallest, editable entity. + entitySize = HALF_TREE_SCALE; + for (i = 0, length = entityIDs.length; i < length; i += 1) { + if (isEditableEntity(entityIDs[i])) { + size = Vec3.length(Entities.getEntityProperties(entityIDs[i], "dimensions").dimensions); + if (size < entitySize) { + entityID = entityIDs[i]; + entitySize = size; + } + } + } + } + intersection = { + intersects: entityID !== null, + entityID: entityID + }; + isHandHover = intersection.intersects; + + // Laser-entity intersection, if any. + wasLaserOn = isLaserOn; + if (!isHandHover && isTriggerPressed) { + handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); + handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); + deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); + pickRay = { + origin: Vec3.sum(handPosition, deltaOrigin), // Add a bit to ... + direction: Quat.getUp(handOrientation), + length: PICK_MAX_DISTANCE + }; + intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + laserLength = isEditing + ? laserEditingDistance + : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); + intersection.intersects = isEditableEntity(intersection.entityID); + isLaserOn = true; + } else { + isLaserOn = false; + } + + // Laser update. + if (isLaserOn) { + laser.update(pickRay.origin, pickRay.direction, laserLength, isTriggerClicked); + } else if (wasLaserOn) { + laser.clear(); + } + + // Highlight / edit. if (isTriggerClicked) { if (isEditing) { // Perform edit. applyEdit(handPose); } else if (intersection.intersects) { // Start editing. - if (intersection.entityID !== laseredEntityID) { - laseredEntityID = intersection.entityID; - selection.select(laseredEntityID); - } else { - highlights.clear(); + if (intersection.entityID !== hoveredEntityID) { + hoveredEntityID = intersection.entityID; + selection.select(hoveredEntityID); + } + highlights.clear(); + if (isLaserOn) { + laserEditingDistance = laserLength; } - editingDistance = distance; startEditing(handPose); } } else { + wasEditing = isEditing; if (isEditing) { // Stop editing. stopEditing(); - laseredEntityID = null; // Force highlighting entities at their new position. } - if (intersection.intersects) { // Hover entities. - if (intersection.entityID !== laseredEntityID) { - laseredEntityID = intersection.entityID; - selection.select(laseredEntityID); + if (wasEditing || intersection.entityID !== hoveredEntityID) { + hoveredEntityID = intersection.entityID; + selection.select(hoveredEntityID); highlights.display(selection.selection()); } } else { // Unhover entities. - if (laseredEntityID) { + if (hoveredEntityID) { selection.clear(); highlights.clear(); - laseredEntityID = null; + hoveredEntityID = null; } } } From 72529cf90ba3be1d13a7b037e155b12112d29bbe Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 6 Jul 2017 10:49:17 +1200 Subject: [PATCH 016/722] Highlight hand when it intersects an entity --- scripts/vr-edit/vr-edit.js | 85 +++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 33 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 70e55f4e02..b3f0370c92 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -37,18 +37,37 @@ NULL_UUID = "{00000000-0000-0000-0000-000000000000}", HALF_TREE_SCALE = 16384; - Highlights = function () { + Highlights = function (hand) { // Draws highlights on selected entities. - var overlays = [], + var handOverlay, + entityOverlays = [], HIGHLIGHT_COLOR = { red: 240, green: 240, blue: 0 }, - HIGHLIGHT_ALPHA = 0.8; + HAND_HIGHLIGHT_ALPHA = 0.35, + ENTITY_HIGHLIGHT_ALPHA = 0.8, + HAND_HIGHLIGHT_DIMENSIONS = { x: 0.2, y: 0.2, z: 0.2 }, + HAND_HIGHLIGHT_OFFSET = { x: 0.0, y: 0.11, z: 0.02 }; - function maybeAddOverlay(index) { - if (index >= overlays.length) { - overlays.push(Overlays.addOverlay("cube", { + handOverlay = Overlays.addOverlay("sphere", { + dimensions: HAND_HIGHLIGHT_DIMENSIONS, + parentID: AVATAR_SELF_ID, + parentJointIndex: MyAvatar.getJointIndex(hand === LEFT_HAND + ? "_CONTROLLER_LEFTHAND" + : "_CONTROLLER_RIGHTHAND"), + localPosition: HAND_HIGHLIGHT_OFFSET, + color: HIGHLIGHT_COLOR, + alpha: HAND_HIGHLIGHT_ALPHA, + solid: true, + drawInFront: true, + ignoreRayIntersection: true, + visible: false + }); + + function maybeAddEntityOverlay(index) { + if (index >= entityOverlays.length) { + entityOverlays.push(Overlays.addOverlay("cube", { color: HIGHLIGHT_COLOR, - alpha: HIGHLIGHT_ALPHA, + alpha: ENTITY_HIGHLIGHT_ALPHA, solid: false, drawInFront: true, ignoreRayIntersection: true, @@ -57,8 +76,8 @@ } } - function editOverlay(index, details) { - Overlays.editOverlay(overlays[index], { + function editEntityOverlay(index, details) { + Overlays.editOverlay(entityOverlays[index], { position: details.position, registrationPoint: details.registrationPoint, rotation: details.rotation, @@ -67,20 +86,23 @@ }); } - function display(selection) { + function display(handSelected, selection) { var i, length; - // Add/edit overlay. + // Show/hide hand overlay. + Overlays.editOverlay(handOverlay, { visible: handSelected }); + + // Add/edit entity overlay. for (i = 0, length = selection.length; i < length; i += 1) { - maybeAddOverlay(i); - editOverlay(i, selection[i]); + maybeAddEntityOverlay(i); + editEntityOverlay(i, selection[i]); } - // Delete extra overlays. - for (i = overlays.length - 1, length = selection.length; i >= length; i -= 1) { - Overlays.deleteOverlay(overlays[i]); - overlays.splice(i, 1); + // Delete extra entity overlays. + for (i = entityOverlays.length - 1, length = selection.length; i >= length; i -= 1) { + Overlays.deleteOverlay(entityOverlays[i]); + entityOverlays.splice(i, 1); } } @@ -88,14 +110,19 @@ var i, length; - for (i = 0, length = overlays.length; i < length; i += 1) { - Overlays.deleteOverlay(overlays[i]); + // Hide hand overlay. + Overlays.editOverlay(handOverlay, { visible: false }); + + // Delete entity overlays. + for (i = 0, length = entityOverlays.length; i < length; i += 1) { + Overlays.deleteOverlay(entityOverlays[i]); } - overlays = []; + entityOverlays = []; } function destroy() { clear(); + Overlays.deleteOverlay(handOverlay); } if (!this instanceof Highlights) { @@ -337,7 +364,6 @@ isTriggerPressed = false, - isHandHover = false, isLaserOn = false, hoveredEntityID = null, @@ -369,7 +395,7 @@ laser = new Laser(hand); selection = new Selection(); - highlights = new Highlights(); + highlights = new Highlights(hand); function startEditing(handPose) { var selectionPositionAndOrientation; @@ -405,13 +431,6 @@ isEditing = false; } - function clearLaser() { - laser.clear(); - selection.clear(); - highlights.clear(); - laseredEntityID = null; - } - function isEditableEntity(entityID) { // Entity trees are moved as a group so check the root entity. var properties = Entities.getEntityProperties(entityID, EDITIBLE_ENTITY_QUERY_PROPERTYES); @@ -477,13 +496,13 @@ } intersection = { intersects: entityID !== null, - entityID: entityID + entityID: entityID, + handSelected: true }; - isHandHover = intersection.intersects; // Laser-entity intersection, if any. wasLaserOn = isLaserOn; - if (!isHandHover && isTriggerPressed) { + if (!intersection.intersects && isTriggerPressed) { handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); @@ -538,7 +557,7 @@ if (wasEditing || intersection.entityID !== hoveredEntityID) { hoveredEntityID = intersection.entityID; selection.select(hoveredEntityID); - highlights.display(selection.selection()); + highlights.display(intersection.handSelected, selection.selection()); } } else { // Unhover entities. From 3e96df5436acc378984b1377b5fa54b609ff76f0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 6 Jul 2017 16:24:15 +1200 Subject: [PATCH 017/722] Precalculate laser sphere bright color --- scripts/vr-edit/vr-edit.js | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index b3f0370c92..056c46e21d 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -248,7 +248,21 @@ SEARCH_SPHERE_SIZE = 0.013, // Per handControllerGrab.js multiplied by 1.2 per handControllerGrab.js. SEARCH_SPHERE_FOLLOW_RATE = 0.5, // Per handControllerGrab.js. COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 }, // Per handControllgerGrab.js. - COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }; // Per handControllgerGrab.js. + COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }, // Per handControllgerGrab.js. + COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT, + COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT, + BRIGHT_POW = 0.06; // Per handControllgerGrab.js. + + function colorPow(color, power) { // Per handControllerGrab.js. + return { + red: Math.pow(color.red / 255, power) * 255, + green: Math.pow(color.green / 255, power) * 255, + blue: Math.pow(color.blue / 255, power) * 255 + }; + } + + COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_HALF_SQUEEZE, BRIGHT_POW); + COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_FULL_SQUEEZE, BRIGHT_POW); hand = side; laserLine = Overlays.addOverlay("line3d", { @@ -272,14 +286,6 @@ visible: false }); - function colorPow(color, power) { - return { - red: Math.pow(color.red / 255, power) * 255, - green: Math.pow(color.green / 255, power) * 255, - blue: Math.pow(color.blue / 255, power) * 255 - }; - } - function updateLine(start, end, color) { Overlays.editOverlay(laserLine, { start: start, @@ -289,12 +295,10 @@ }); } - function updateSphere(location, size, color) { - var rotation, - brightColor; + function updateSphere(location, size, color, brightColor) { + var rotation; rotation = Quat.lookAt(location, Camera.getPosition(), Vec3.UP); - brightColor = colorPow(color, 0.06); Overlays.editOverlay(laserSphere, { position: location, @@ -309,15 +313,17 @@ function update(origin, direction, distance, isClicked) { var searchTarget, sphereSize, - color; + color, + brightColor; searchDistance = SEARCH_SPHERE_FOLLOW_RATE * searchDistance + (1.0 - SEARCH_SPHERE_FOLLOW_RATE) * distance; searchTarget = Vec3.sum(origin, Vec3.multiply(searchDistance, direction)); sphereSize = SEARCH_SPHERE_SIZE * searchDistance; color = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE : COLORS_GRAB_SEARCHING_HALF_SQUEEZE; + brightColor = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT : COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT; updateLine(origin, searchTarget, color); - updateSphere(searchTarget, sphereSize, color); + updateSphere(searchTarget, sphereSize, color, brightColor); } function clear() { From b042f8615b8e96d7bda7ddab7fac2ada028fd096 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 6 Jul 2017 16:36:22 +1200 Subject: [PATCH 018/722] Fix highlight position for entity with non-center registration point --- scripts/vr-edit/vr-edit.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 056c46e21d..4ee6b5e9cc 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -77,8 +77,11 @@ } function editEntityOverlay(index, details) { + var offset = Vec3.multiplyQbyV(details.rotation, + Vec3.multiplyVbyV(Vec3.subtract(Vec3.HALF, details.registrationPoint), details.dimensions)); + Overlays.editOverlay(entityOverlays[index], { - position: details.position, + position: Vec3.sum(details.position, offset), registrationPoint: details.registrationPoint, rotation: details.rotation, dimensions: details.dimensions, From c2159bc52a67dafdf1c990f5f56891b5008f27c4 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 6 Jul 2017 16:44:58 +1200 Subject: [PATCH 019/722] Fix laser not disappearing when lose hand tracking --- scripts/vr-edit/vr-edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 4ee6b5e9cc..11733f3c0f 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -470,6 +470,7 @@ // Hand position and orientation. handPose = Controller.getPoseValue(handController); + wasLaserOn = isLaserOn; if (!handPose.valid) { isLaserOn = false; if (wasLaserOn) { @@ -510,7 +511,6 @@ }; // Laser-entity intersection, if any. - wasLaserOn = isLaserOn; if (!intersection.intersects && isTriggerPressed) { handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); From cab2caaf276ea7babb625e1dce3e629ec01a43e9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 7 Jul 2017 14:45:49 +1200 Subject: [PATCH 020/722] Apply edit and highlight after calculating both hands' inputs --- scripts/vr-edit/vr-edit.js | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 11733f3c0f..6c27cc9ca2 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -371,7 +371,8 @@ NO_EXCLUDE_IDS = [], VISIBLE_ONLY = true, - isTriggerPressed = false, + handPose, + intersection = {}, isLaserOn = false, hoveredEntityID = null, @@ -386,6 +387,9 @@ initialSelectionOrientation, laserEditingDistance, + doEdit, + doHighlight, + laser, selection, highlights; @@ -406,7 +410,7 @@ selection = new Selection(); highlights = new Highlights(hand); - function startEditing(handPose) { + function startEditing() { var selectionPositionAndOrientation; initialHandPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); @@ -419,7 +423,7 @@ isEditing = true; } - function applyEdit(handPose) { + function applyEdit() { var handPosition, handOrientation, deltaOrientation, @@ -456,13 +460,13 @@ entitySize, size, wasLaserOn, - handPose, handPosition, handOrientation, deltaOrigin, pickRay, intersection, laserLength, + isTriggerPressed, isTriggerClicked, wasEditing, i, @@ -539,10 +543,12 @@ } // Highlight / edit. + doEdit = false; + doHighlight = false; if (isTriggerClicked) { if (isEditing) { // Perform edit. - applyEdit(handPose); + doEdit = true; } else if (intersection.intersects) { // Start editing. if (intersection.entityID !== hoveredEntityID) { @@ -553,7 +559,7 @@ if (isLaserOn) { laserEditingDistance = laserLength; } - startEditing(handPose); + startEditing(); } } else { wasEditing = isEditing; @@ -566,7 +572,7 @@ if (wasEditing || intersection.entityID !== hoveredEntityID) { hoveredEntityID = intersection.entityID; selection.select(hoveredEntityID); - highlights.display(intersection.handSelected, selection.selection()); + doHighlight = true; } } else { // Unhover entities. @@ -579,6 +585,14 @@ } } + function apply() { + if (doEdit) { + applyEdit(); + } else if (doHighlight) { + highlights.display(intersection.handSelected, selection.selection()); + } + } + function destroy() { if (laser) { laser.destroy(); @@ -600,6 +614,7 @@ return { update: update, + apply: apply, destroy: destroy }; }; @@ -610,6 +625,8 @@ hands[LEFT_HAND].update(); hands[RIGHT_HAND].update(); + hands[LEFT_HAND].apply(); + hands[RIGHT_HAND].apply(); updateTimer = Script.setTimeout(update, UPDATE_LOOP_TIMEOUT); } From 0ba11ffdc93b78c432bab1723da27ae9bda216f7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 7 Jul 2017 14:51:20 +1200 Subject: [PATCH 021/722] Make each hand aware of other --- scripts/vr-edit/vr-edit.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 6c27cc9ca2..6b729df3af 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -390,6 +390,8 @@ doEdit, doHighlight, + otherHand, + laser, selection, highlights; @@ -410,6 +412,10 @@ selection = new Selection(); highlights = new Highlights(hand); + function setOtherhand(hand) { + otherHand = hand; + } + function startEditing() { var selectionPositionAndOrientation; @@ -613,6 +619,7 @@ } return { + setOtherHand: setOtherhand, update: update, apply: apply, destroy: destroy @@ -671,6 +678,8 @@ // Hands, each with a laser, selection, etc. hands[LEFT_HAND] = new Hand(LEFT_HAND); hands[RIGHT_HAND] = new Hand(RIGHT_HAND); + hands[LEFT_HAND].setOtherHand(hands[RIGHT_HAND]); + hands[RIGHT_HAND].setOtherHand(hands[LEFT_HAND]); if (isAppActive) { update(); From 998d27d66d5a4272f88f2ee02f9dc68d14698be8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 7 Jul 2017 15:06:18 +1200 Subject: [PATCH 022/722] Logic and stub for grab versus scale --- scripts/vr-edit/vr-edit.js | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 6b729df3af..76e5ac4f1e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -194,6 +194,10 @@ selectedEntityID = entityID; } + function getRootEntityID() { + return rootEntityID; + } + function getSelection() { return selection; } @@ -231,6 +235,7 @@ return { select: select, selection: getSelection, + rootEntityID: getRootEntityID, getPositionAndOrientation: getPositionAndOrientation, setPositionAndOrientation: setPositionAndOrientation, clear: clear, @@ -412,10 +417,14 @@ selection = new Selection(); highlights = new Highlights(hand); - function setOtherhand(hand) { + function setOtherHand(hand) { otherHand = hand; } + function getIsEditing(rootEntityID) { + return isEditing && rootEntityID === selection.rootEntityID(); + } + function startEditing() { var selectionPositionAndOrientation; @@ -429,7 +438,7 @@ isEditing = true; } - function applyEdit() { + function applyGrab() { var handPosition, handOrientation, deltaOrientation, @@ -446,6 +455,10 @@ selection.setPositionAndOrientation(selectionPosition, selectionOrientation); } + function applyScale() { + // TODO + } + function stopEditing() { isEditing = false; } @@ -593,7 +606,13 @@ function apply() { if (doEdit) { - applyEdit(); + if (otherHand.isEditing(selection.rootEntityID())) { + if (hand === LEFT_HAND) { + applyScale(); + } + } else { + applyGrab(); + } } else if (doHighlight) { highlights.display(intersection.handSelected, selection.selection()); } @@ -619,7 +638,8 @@ } return { - setOtherHand: setOtherhand, + setOtherHand: setOtherHand, + isEditing: getIsEditing, update: update, apply: apply, destroy: destroy From 5da8fe19deb4db2e7d74465d95b8a75a61897e73 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 7 Jul 2017 15:10:07 +1200 Subject: [PATCH 023/722] Fix highlights not moving when entities being moved by other hand --- scripts/vr-edit/vr-edit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 76e5ac4f1e..616b91334d 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -81,6 +81,7 @@ Vec3.multiplyVbyV(Vec3.subtract(Vec3.HALF, details.registrationPoint), details.dimensions)); Overlays.editOverlay(entityOverlays[index], { + parentID: details.id, position: Vec3.sum(details.position, offset), registrationPoint: details.registrationPoint, rotation: details.rotation, From a35d5fe128665f0664855397e0eba591d3a43410 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 7 Jul 2017 15:49:52 +1200 Subject: [PATCH 024/722] Different color highlight for about-to-scale --- scripts/vr-edit/vr-edit.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 616b91334d..668fceb015 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -42,7 +42,8 @@ var handOverlay, entityOverlays = [], - HIGHLIGHT_COLOR = { red: 240, green: 240, blue: 0 }, + GRAB_HIGHLIGHT_COLOR = { red: 240, green: 240, blue: 0 }, + SCALE_HIGHLIGHT_COLOR = { red: 0, green: 240, blue: 240 }, HAND_HIGHLIGHT_ALPHA = 0.35, ENTITY_HIGHLIGHT_ALPHA = 0.8, HAND_HIGHLIGHT_DIMENSIONS = { x: 0.2, y: 0.2, z: 0.2 }, @@ -55,7 +56,6 @@ ? "_CONTROLLER_LEFTHAND" : "_CONTROLLER_RIGHTHAND"), localPosition: HAND_HIGHLIGHT_OFFSET, - color: HIGHLIGHT_COLOR, alpha: HAND_HIGHLIGHT_ALPHA, solid: true, drawInFront: true, @@ -66,7 +66,6 @@ function maybeAddEntityOverlay(index) { if (index >= entityOverlays.length) { entityOverlays.push(Overlays.addOverlay("cube", { - color: HIGHLIGHT_COLOR, alpha: ENTITY_HIGHLIGHT_ALPHA, solid: false, drawInFront: true, @@ -76,7 +75,7 @@ } } - function editEntityOverlay(index, details) { + function editEntityOverlay(index, details, overlayColor) { var offset = Vec3.multiplyQbyV(details.rotation, Vec3.multiplyVbyV(Vec3.subtract(Vec3.HALF, details.registrationPoint), details.dimensions)); @@ -86,21 +85,26 @@ registrationPoint: details.registrationPoint, rotation: details.rotation, dimensions: details.dimensions, + color: overlayColor, visible: true }); } - function display(handSelected, selection) { - var i, + function display(handSelected, selection, isScale) { + var overlayColor = isScale ? SCALE_HIGHLIGHT_COLOR : GRAB_HIGHLIGHT_COLOR, + i, length; // Show/hide hand overlay. - Overlays.editOverlay(handOverlay, { visible: handSelected }); + Overlays.editOverlay(handOverlay, { + color: overlayColor, + visible: handSelected + }); // Add/edit entity overlay. for (i = 0, length = selection.length; i < length; i += 1) { maybeAddEntityOverlay(i); - editEntityOverlay(i, selection[i]); + editEntityOverlay(i, selection[i], overlayColor); } // Delete extra entity overlays. @@ -397,6 +401,7 @@ doHighlight, otherHand, + otherHandWasEditing, laser, selection, @@ -484,11 +489,11 @@ handOrientation, deltaOrigin, pickRay, - intersection, laserLength, isTriggerPressed, isTriggerClicked, wasEditing, + otherHandIsEditing, i, length; @@ -589,9 +594,11 @@ } if (intersection.intersects) { // Hover entities. - if (wasEditing || intersection.entityID !== hoveredEntityID) { + otherHandIsEditing = otherHand.isEditing(selection.rootEntityID()); + if (wasEditing || intersection.entityID !== hoveredEntityID || otherHandIsEditing !== otherHandWasEditing) { hoveredEntityID = intersection.entityID; selection.select(hoveredEntityID); + otherHandWasEditing = otherHandIsEditing; doHighlight = true; } } else { @@ -615,7 +622,8 @@ applyGrab(); } } else if (doHighlight) { - highlights.display(intersection.handSelected, selection.selection()); + highlights.display(intersection.handSelected, selection.selection(), + otherHand.isEditing(selection.rootEntityID())); } } From 73c6414f93dc5d16b8d4aefec44341504a7f1761 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 8 Jul 2017 12:07:20 +1200 Subject: [PATCH 025/722] Natural scale of entity selection with hands / lasers --- scripts/vr-edit/vr-edit.js | 111 +++++++++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 10 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 668fceb015..6211d5f0dc 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -151,13 +151,15 @@ selectedEntityID = null, selectionPosition = null, selectionOrientation, - rootEntityID; + rootEntityID, + scaleCenter, + scaleRootOffset; function traverseEntityTree(id, result) { // Recursively traverses tree of entities and their children, gather IDs and properties. var children, properties, - SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions"], + SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions", "localPosition"], i, length; @@ -165,6 +167,7 @@ result.push({ id: id, position: properties.position, + localPosition: properties.localPosition, registrationPoint: properties.registrationPoint, rotation: properties.rotation, dimensions: properties.dimensions @@ -223,6 +226,33 @@ }); } + function startScaling(center) { + scaleCenter = center; + scaleRootOffset = Vec3.subtract(selection[0].position, center); + } + + function scale(factor, center) { + var position, + i, + length; + + // Position root. + position = Vec3.sum(center, Vec3.multiply(factor, scaleRootOffset)); + selectionPosition = position; + Entities.editEntity(selection[0].id, { + dimensions: Vec3.multiply(factor, selection[0].dimensions), + position: position + }); + + // Scale and position children. + for (i = 1, length = selection.length; i < length; i += 1) { + Entities.editEntity(selection[i].id, { + dimensions: Vec3.multiply(factor, selection[i].dimensions), + localPosition: Vec3.multiply(factor, selection[i].localPosition) + }); + } + } + function clear() { selection = []; selectedEntityID = null; @@ -243,6 +273,8 @@ rootEntityID: getRootEntityID, getPositionAndOrientation: getPositionAndOrientation, setPositionAndOrientation: setPositionAndOrientation, + startScaling: startScaling, + scale: scale, clear: clear, destroy: destroy }; @@ -391,12 +423,17 @@ NONEDITABLE_ENTITY_TYPES = ["Unknown", "Zone", "Light"], isEditing = false, - initialHandPosition, + isEditingWithHand = false, initialHandOrientationInverse, initialHandToSelectionVector, initialSelectionOrientation, laserEditingDistance, + isScaling = false, + initialTargetPosition, + initialTargetsCenter, + initialTargetsSeparation, + doEdit, doHighlight, @@ -431,19 +468,52 @@ return isEditing && rootEntityID === selection.rootEntityID(); } - function startEditing() { - var selectionPositionAndOrientation; + function getHandPosition() { + return Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); + } + + function getTargetPosition() { + if (isEditingWithHand) { + return hand === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); + } + return Vec3.sum(getHandPosition(), Vec3.multiply(laserEditingDistance, + Quat.getUp(Quat.multiply(MyAvatar.orientation, handPose.rotation)))); + } + + function startEditing() { + var selectionPositionAndOrientation, + initialOtherTargetPosition; - initialHandPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); initialHandOrientationInverse = Quat.inverse(Quat.multiply(MyAvatar.orientation, handPose.rotation)); + selection.select(hoveredEntityID); // Entity may have been moved by other hand so refresh position and orientation. selectionPositionAndOrientation = selection.getPositionAndOrientation(); - initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, initialHandPosition); + initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, getHandPosition()); initialSelectionOrientation = selectionPositionAndOrientation.orientation; + isEditingWithHand = intersection.handSelected; + + if (otherHand.isEditing(selection.rootEntityID())) { + // Store initial values for use in scaling. + initialTargetPosition = getTargetPosition(); + initialOtherTargetPosition = otherHand.getTargetPosition(); + initialTargetsCenter = Vec3.multiply(0.5, Vec3.sum(initialTargetPosition, initialOtherTargetPosition)); + initialTargetsSeparation = Vec3.distance(initialTargetPosition, initialOtherTargetPosition); + selection.startScaling(initialTargetsCenter); + isScaling = true; + } else { + isScaling = false; + } + isEditing = true; } + function updateGrabOffset(selectionPositionAndOrientation) { + initialHandOrientationInverse = Quat.inverse(Quat.multiply(MyAvatar.orientation, handPose.rotation)); + initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, getHandPosition()); + initialSelectionOrientation = selectionPositionAndOrientation.orientation; + } + function applyGrab() { var handPosition, handOrientation, @@ -451,7 +521,7 @@ selectionPosition, selectionOrientation; - handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); + handPosition = getHandPosition(); handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); deltaOrientation = Quat.multiply(handOrientation, initialHandOrientationInverse); @@ -462,10 +532,29 @@ } function applyScale() { - // TODO + var targetPosition, + otherTargetPosition, + targetsSeparation, + center, + scale, + selectionPositionAndOrientation; + + // Scale selection. + targetPosition = getTargetPosition(); + otherTargetPosition = otherHand.getTargetPosition(); + targetsSeparation = Vec3.distance(targetPosition, otherTargetPosition); + scale = targetsSeparation / initialTargetsSeparation; + center = Vec3.multiply(0.5, Vec3.sum(targetPosition, otherTargetPosition)); + selection.scale(scale, center); + + // Update grab offsets. + selectionPositionAndOrientation = selection.getPositionAndOrientation(); + updateGrabOffset(selectionPositionAndOrientation); + otherHand.updateGrabOffset(selectionPositionAndOrientation); } function stopEditing() { + isScaling = false; isEditing = false; } @@ -615,7 +704,7 @@ function apply() { if (doEdit) { if (otherHand.isEditing(selection.rootEntityID())) { - if (hand === LEFT_HAND) { + if (isScaling) { applyScale(); } } else { @@ -649,6 +738,8 @@ return { setOtherHand: setOtherHand, isEditing: getIsEditing, + getTargetPosition: getTargetPosition, + updateGrabOffset: updateGrabOffset, update: update, apply: apply, destroy: destroy From e8d8a5c0a27e4a9ee5353b7e491d4c8450dee472 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 8 Jul 2017 12:27:48 +1200 Subject: [PATCH 026/722] Rotate entity selection while scaling --- scripts/vr-edit/vr-edit.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 6211d5f0dc..4eeab88241 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -153,7 +153,8 @@ selectionOrientation, rootEntityID, scaleCenter, - scaleRootOffset; + scaleRootOffset, + scaleRootOrientation; function traverseEntityTree(id, result) { // Recursively traverses tree of entities and their children, gather IDs and properties. @@ -229,19 +230,24 @@ function startScaling(center) { scaleCenter = center; scaleRootOffset = Vec3.subtract(selection[0].position, center); + scaleRootOrientation = selectionOrientation; } - function scale(factor, center) { + function scale(factor, rotation, center) { var position, + orientation, i, length; // Position root. - position = Vec3.sum(center, Vec3.multiply(factor, scaleRootOffset)); + position = Vec3.sum(center, Vec3.multiply(factor, Vec3.multiplyQbyV(rotation, scaleRootOffset))); + orientation = Quat.multiply(rotation, scaleRootOrientation); selectionPosition = position; + selectionOrientation = orientation; Entities.editEntity(selection[0].id, { dimensions: Vec3.multiply(factor, selection[0].dimensions), - position: position + position: position, + rotation: orientation }); // Scale and position children. @@ -433,6 +439,7 @@ initialTargetPosition, initialTargetsCenter, initialTargetsSeparation, + initialtargetsDirection, doEdit, doHighlight, @@ -499,6 +506,7 @@ initialOtherTargetPosition = otherHand.getTargetPosition(); initialTargetsCenter = Vec3.multiply(0.5, Vec3.sum(initialTargetPosition, initialOtherTargetPosition)); initialTargetsSeparation = Vec3.distance(initialTargetPosition, initialOtherTargetPosition); + initialtargetsDirection = Vec3.subtract(initialOtherTargetPosition, initialTargetPosition); selection.startScaling(initialTargetsCenter); isScaling = true; } else { @@ -535,8 +543,9 @@ var targetPosition, otherTargetPosition, targetsSeparation, - center, scale, + rotation, + center, selectionPositionAndOrientation; // Scale selection. @@ -544,8 +553,9 @@ otherTargetPosition = otherHand.getTargetPosition(); targetsSeparation = Vec3.distance(targetPosition, otherTargetPosition); scale = targetsSeparation / initialTargetsSeparation; + rotation = Quat.rotationBetween(initialtargetsDirection, Vec3.subtract(otherTargetPosition, targetPosition)); center = Vec3.multiply(0.5, Vec3.sum(targetPosition, otherTargetPosition)); - selection.scale(scale, center); + selection.scale(scale, rotation, center); // Update grab offsets. selectionPositionAndOrientation = selection.getPositionAndOrientation(); From ed3c0cdced01e7cfa5a5b586c340876d8c1e6c07 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 11 Jul 2017 17:16:38 +1200 Subject: [PATCH 027/722] Use grip click to toggle scale-with-hands / scale-with-handles --- scripts/vr-edit/vr-edit.js | 41 ++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 4eeab88241..b8e1182ae6 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -18,6 +18,7 @@ tablet, button, isAppActive = false, + isScaleWithHandles = false, VR_EDIT_SETTING = "io.highfidelity.isVREditing", // Note: This constant is duplicated in utils.js. @@ -398,7 +399,7 @@ }; }; - Hand = function (side) { + Hand = function (side, gripPressedCallback) { // Hand controller input. // Each hand has a laser, an entity selection, and entity highlighter. @@ -406,6 +407,11 @@ handController, controllerTrigger, controllerTriggerClicked, + controllerGrip, + + isGripPressed = false, + GRIP_ON_VALUE = 0.99, + GRIP_OFF_VALUE = 0.95, TRIGGER_ON_VALUE = 0.15, // Per handControllerGrab.js. TRIGGER_OFF_VALUE = 0.1, // Per handControllerGrab.js. @@ -422,6 +428,8 @@ handPose, intersection = {}, + isScaleWithHandles = false, + isLaserOn = false, hoveredEntityID = null, @@ -456,11 +464,13 @@ handController = Controller.Standard.LeftHand; controllerTrigger = Controller.Standard.LT; controllerTriggerClicked = Controller.Standard.LTClick; + controllerGrip = Controller.Standard.LeftGrip; GRAB_POINT_SPHERE_OFFSET.x = -GRAB_POINT_SPHERE_OFFSET.x; } else { handController = Controller.Standard.RightHand; controllerTrigger = Controller.Standard.RT; controllerTriggerClicked = Controller.Standard.RTClick; + controllerGrip = Controller.Standard.RightGrip; } laser = new Laser(hand); @@ -471,6 +481,10 @@ otherHand = hand; } + function setScaleWithHandles(value) { + isScaleWithHandles = value; + } + function getIsEditing(rootEntityID) { return isEditing && rootEntityID === selection.rootEntityID(); } @@ -578,7 +592,8 @@ } function update() { - var palmPosition, + var gripValue, + palmPosition, entityID, entityIDs, entitySize, @@ -615,6 +630,17 @@ ? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE); isTriggerClicked = Controller.getValue(controllerTriggerClicked); + // Controller grip. + gripValue = Controller.getValue(controllerGrip); + if (isGripPressed) { + isGripPressed = gripValue > GRIP_OFF_VALUE; + } else { + isGripPressed = gripValue > GRIP_ON_VALUE; + if (isGripPressed) { + gripPressedCallback(); + } + } + // Hand-entity intersection, if any. entityID = null; palmPosition = hand === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); @@ -747,6 +773,7 @@ return { setOtherHand: setOtherHand, + setScaleWithHandles: setScaleWithHandles, isEditing: getIsEditing, getTargetPosition: getTargetPosition, updateGrabOffset: updateGrabOffset, @@ -786,6 +813,12 @@ } } + function onGripClicked() { + isScaleWithHandles = !isScaleWithHandles; + hands[LEFT_HAND].setScaleWithHandles(isScaleWithHandles); + hands[RIGHT_HAND].setScaleWithHandles(isScaleWithHandles); + } + function setUp() { updateHandControllerGrab(); @@ -806,8 +839,8 @@ } // Hands, each with a laser, selection, etc. - hands[LEFT_HAND] = new Hand(LEFT_HAND); - hands[RIGHT_HAND] = new Hand(RIGHT_HAND); + hands[LEFT_HAND] = new Hand(LEFT_HAND, onGripClicked); + hands[RIGHT_HAND] = new Hand(RIGHT_HAND, onGripClicked); hands[LEFT_HAND].setOtherHand(hands[RIGHT_HAND]); hands[RIGHT_HAND].setOtherHand(hands[LEFT_HAND]); From 3450d64bede55d4a87c3fc28d5056c14713b38b8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 11 Jul 2017 17:37:09 +1200 Subject: [PATCH 028/722] Hover with "scale" color if in scale-with-handles mode --- scripts/vr-edit/vr-edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index b8e1182ae6..9b4ea750a0 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -748,7 +748,7 @@ } } else if (doHighlight) { highlights.display(intersection.handSelected, selection.selection(), - otherHand.isEditing(selection.rootEntityID())); + otherHand.isEditing(selection.rootEntityID()) || isScaleWithHandles); } } From 657ac1aaeb57049150c80561db2e3f1af491caca Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 11 Jul 2017 21:16:21 +1200 Subject: [PATCH 029/722] Fix enumeration of entity tree --- scripts/vr-edit/vr-edit.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 9b4ea750a0..27dd7a429d 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -177,7 +177,9 @@ children = Entities.getChildrenIDs(id); for (i = 0, length = children.length; i < length; i += 1) { - traverseEntityTree(children[i], result); + if (Entities.getNestableType(children[i]) === ENTITY_TYPE) { + traverseEntityTree(children[i], result); + } } } From 68cdc235309a6801244db5d4716dd955f73c4404 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 12 Jul 2017 11:04:02 +1200 Subject: [PATCH 030/722] Display selection bounding box for scaling with handles --- scripts/vr-edit/vr-edit.js | 152 ++++++++++++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 27dd7a429d..2481d15aed 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -30,6 +30,7 @@ updateTimer = null, Highlights, + Handles, Selection, Laser, Hand, @@ -38,6 +39,18 @@ NULL_UUID = "{00000000-0000-0000-0000-000000000000}", HALF_TREE_SCALE = 16384; + if (typeof Vec3.min !== "function") { + Vec3.min = function (a, b) { + return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), z: Math.min(a.z, b.z) }; + }; + } + + if (typeof Vec3.max !== "function") { + Vec3.max = function (a, b) { + return { x: Math.max(a.x, b.x), y: Math.max(a.y, b.y), z: Math.max(a.z, b.z) }; + }; + } + Highlights = function (hand) { // Draws highlights on selected entities. @@ -145,9 +158,54 @@ }; }; + Handles = function () { + var boundingBoxOverlay, + //HIGHLIGHT_COLOR = { red: 0, green: 240, blue: 240 }, + HIGHLIGHT_COLOR = { red: 255, green: 0, blue: 255 }, + BOUNDING_BOX_ALPHA = 0.8; + + boundingBoxOverlay = Overlays.addOverlay("cube", { + color: HIGHLIGHT_COLOR, + alpha: BOUNDING_BOX_ALPHA, + solid: false, + drawInFront: true, + ignoreRayIntersection: true, + visible: false + }); + + function display(rootEntityID, boundingBox) { + // Selection bounding box. + Overlays.editOverlay(boundingBoxOverlay, { + parentID: rootEntityID, + position: boundingBox.center, + rotation: boundingBox.orientation, + dimensions: boundingBox.dimensions, + visible: true + }); + } + + function clear() { + Overlays.editOverlay(boundingBoxOverlay, { visible: false }); + } + + function destroy() { + clear(); + Overlays.deleteOverlay(boundingBoxOverlay); + } + + if (!this instanceof Handles) { + return new Handles(); + } + + return { + display: display, + clear: clear, + destroy: destroy + }; + }; + Selection = function () { // Manages set of selected entities. Currently supports just one set of linked entities. - var selection = [], selectedEntityID = null, selectionPosition = null, @@ -155,7 +213,8 @@ rootEntityID, scaleCenter, scaleRootOffset, - scaleRootOrientation; + scaleRootOrientation, + ENTITY_TYPE = "entity"; function traverseEntityTree(id, result) { // Recursively traverses tree of entities and their children, gather IDs and properties. @@ -214,6 +273,81 @@ return selection; } + function getBoundingBox() { + var center, + orientation, + inverseOrientation, + dimensions, + min, + max, + i, + j, + length, + registration, + position, + rotation, + corners = [], + NUM_CORNERS = 8; + + if (selection.length === 1) { + if (Vec3.equal(selection[0].registrationPoint, Vec3.HALF)) { + center = selectionPosition; + } else { + center = Vec3.sum(selectionPosition, + Vec3.multiplyQbyV(selectionOrientation, + Vec3.multiplyVbyV(selection[0].dimensions, + Vec3.subtract(Vec3.HALF, selection[0].registrationPoint)))); + } + orientation = selectionOrientation; + dimensions = selection[0].dimensions; + } else if (selection.length > 1) { + // Find min & max x, y, z values of entities' dimension box corners in root entity coordinate system. + // Note: Don't use entities' bounding boxes because they're in world coordinates and may make the calculated + // bounding box be larger than necessary. + min = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ZERO, selection[0].registrationPoint), selection[0].dimensions); + max = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ONE, selection[0].registrationPoint), selection[0].dimensions); + inverseOrientation = Quat.inverse(selectionOrientation); + for (i = 1, length = selection.length; i < length; i += 1) { + + registration = selection[i].registrationPoint; + corners[0] = { x: -registration.x, y: -registration.y, z: -registration.z }; + corners[1] = { x: -registration.x, y: -registration.y, z: 1.0 - registration.z }; + corners[2] = { x: -registration.x, y: 1.0 - registration.y, z: -registration.z }; + corners[3] = { x: -registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z }; + corners[4] = { x: 1.0 - registration.x, y: -registration.y, z: -registration.z }; + corners[5] = { x: 1.0 - registration.x, y: -registration.y, z: 1.0 - registration.z }; + corners[6] = { x: 1.0 - registration.x, y: 1.0 - registration.y, z: -registration.z }; + corners[7] = { x: 1.0 - registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z }; + + position = selection[i].position; + rotation = selection[i].rotation; + dimensions = selection[i].dimensions; + + for (j = 0; j < NUM_CORNERS; j += 1) { + // Corner position in world coordinates. + corners[j] = Vec3.sum(position, Vec3.multiplyQbyV(rotation, Vec3.multiplyVbyV(corners[j], dimensions))); + // Corner position in root entity coordinates. + corners[j] = Vec3.multiplyQbyV(inverseOrientation, Vec3.subtract(corners[j], selectionPosition)); + // Update min & max. + min = Vec3.min(corners[j], min); + max = Vec3.max(corners[j], max); + } + } + + // Calculate bounding box. + center = Vec3.sum(selectionPosition, + Vec3.multiplyQbyV(selectionOrientation, Vec3.multiply(0.5, Vec3.sum(min, max)))); + orientation = selectionOrientation; + dimensions = Vec3.subtract(max, min); + } + + return { + center: center, + orientation: orientation, + dimensions: dimensions + }; + } + function getPositionAndOrientation() { return { position: selectionPosition, @@ -280,6 +414,7 @@ select: select, selection: getSelection, rootEntityID: getRootEntityID, + boundingBox: getBoundingBox, getPositionAndOrientation: getPositionAndOrientation, setPositionAndOrientation: setPositionAndOrientation, startScaling: startScaling, @@ -459,7 +594,8 @@ laser, selection, - highlights; + highlights, + handles; hand = side; if (hand === LEFT_HAND) { @@ -478,6 +614,7 @@ laser = new Laser(hand); selection = new Selection(); highlights = new Highlights(hand); + handles = new Handles(); function setOtherHand(hand) { otherHand = hand; @@ -622,6 +759,7 @@ laser.clear(); selection.clear(); highlights.clear(); + handles.clear(); hoveredEntityID = null; } return; @@ -712,6 +850,9 @@ laserEditingDistance = laserLength; } startEditing(); + if (isScaleWithHandles) { + handles.display(selection.rootEntityID(), selection.boundingBox()); + } } } else { wasEditing = isEditing; @@ -733,6 +874,7 @@ if (hoveredEntityID) { selection.clear(); highlights.clear(); + handles.clear(); hoveredEntityID = null; } } @@ -767,6 +909,10 @@ highlights.destroy(); highlights = null; } + if (handles) { + handles.destroy(); + handles = null; + } } if (!this instanceof Hand) { From db37417ccda6d99f5085dd155c34303ea8ee8c16 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 12 Jul 2017 11:15:55 +1200 Subject: [PATCH 031/722] Tidying --- scripts/vr-edit/vr-edit.js | 46 ++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 2481d15aed..e1930c421d 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -96,7 +96,6 @@ Overlays.editOverlay(entityOverlays[index], { parentID: details.id, position: Vec3.sum(details.position, offset), - registrationPoint: details.registrationPoint, rotation: details.rotation, dimensions: details.dimensions, color: overlayColor, @@ -208,9 +207,9 @@ // Manages set of selected entities. Currently supports just one set of linked entities. var selection = [], selectedEntityID = null, - selectionPosition = null, - selectionOrientation, - rootEntityID, + rootEntityID = null, + rootPosition, + rootOrientation, scaleCenter, scaleRootOffset, scaleRootOrientation, @@ -255,8 +254,8 @@ } // Selection position and orientation is that of the root entity. - selectionPosition = entityProperties.position; - selectionOrientation = entityProperties.rotation; + rootPosition = entityProperties.position; + rootOrientation = entityProperties.rotation; // Find all children. selection = []; @@ -291,14 +290,14 @@ if (selection.length === 1) { if (Vec3.equal(selection[0].registrationPoint, Vec3.HALF)) { - center = selectionPosition; + center = rootPosition; } else { - center = Vec3.sum(selectionPosition, - Vec3.multiplyQbyV(selectionOrientation, + center = Vec3.sum(rootPosition, + Vec3.multiplyQbyV(rootOrientation, Vec3.multiplyVbyV(selection[0].dimensions, Vec3.subtract(Vec3.HALF, selection[0].registrationPoint)))); } - orientation = selectionOrientation; + orientation = rootOrientation; dimensions = selection[0].dimensions; } else if (selection.length > 1) { // Find min & max x, y, z values of entities' dimension box corners in root entity coordinate system. @@ -306,7 +305,7 @@ // bounding box be larger than necessary. min = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ZERO, selection[0].registrationPoint), selection[0].dimensions); max = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ONE, selection[0].registrationPoint), selection[0].dimensions); - inverseOrientation = Quat.inverse(selectionOrientation); + inverseOrientation = Quat.inverse(rootOrientation); for (i = 1, length = selection.length; i < length; i += 1) { registration = selection[i].registrationPoint; @@ -327,7 +326,7 @@ // Corner position in world coordinates. corners[j] = Vec3.sum(position, Vec3.multiplyQbyV(rotation, Vec3.multiplyVbyV(corners[j], dimensions))); // Corner position in root entity coordinates. - corners[j] = Vec3.multiplyQbyV(inverseOrientation, Vec3.subtract(corners[j], selectionPosition)); + corners[j] = Vec3.multiplyQbyV(inverseOrientation, Vec3.subtract(corners[j], rootPosition)); // Update min & max. min = Vec3.min(corners[j], min); max = Vec3.max(corners[j], max); @@ -335,9 +334,9 @@ } // Calculate bounding box. - center = Vec3.sum(selectionPosition, - Vec3.multiplyQbyV(selectionOrientation, Vec3.multiply(0.5, Vec3.sum(min, max)))); - orientation = selectionOrientation; + center = Vec3.sum(rootPosition, + Vec3.multiplyQbyV(rootOrientation, Vec3.multiply(0.5, Vec3.sum(min, max)))); + orientation = rootOrientation; dimensions = Vec3.subtract(max, min); } @@ -349,15 +348,17 @@ } function getPositionAndOrientation() { + // Position and orientation of root entity. return { - position: selectionPosition, - orientation: selectionOrientation + position: rootPosition, + orientation: rootOrientation }; } function setPositionAndOrientation(position, orientation) { - selectionPosition = position; - selectionOrientation = orientation; + // Position and orientation of root entity. + rootPosition = position; + rootOrientation = orientation; Entities.editEntity(rootEntityID, { position: position, rotation: orientation @@ -367,7 +368,7 @@ function startScaling(center) { scaleCenter = center; scaleRootOffset = Vec3.subtract(selection[0].position, center); - scaleRootOrientation = selectionOrientation; + scaleRootOrientation = rootOrientation; } function scale(factor, rotation, center) { @@ -379,8 +380,8 @@ // Position root. position = Vec3.sum(center, Vec3.multiply(factor, Vec3.multiplyQbyV(rotation, scaleRootOffset))); orientation = Quat.multiply(rotation, scaleRootOrientation); - selectionPosition = position; - selectionOrientation = orientation; + rootPosition = position; + rootOrientation = orientation; Entities.editEntity(selection[0].id, { dimensions: Vec3.multiply(factor, selection[0].dimensions), position: position, @@ -935,6 +936,7 @@ // Main update loop. updateTimer = null; + // Each hand's action depends on the state of the other hand, so update the states first then apply in actions. hands[LEFT_HAND].update(); hands[RIGHT_HAND].update(); hands[LEFT_HAND].apply(); From 196f5a43b106bebcecaf8164ff191d022a1dd5a4 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 12 Jul 2017 12:18:59 +1200 Subject: [PATCH 032/722] Don't scale with hands if scaling with handles --- scripts/vr-edit/vr-edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index e1930c421d..22674d517b 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -840,7 +840,7 @@ if (isEditing) { // Perform edit. doEdit = true; - } else if (intersection.intersects) { + } else if (intersection.intersects && (!isScaleWithHandles || !otherHand.isEditing(intersection.entityID))) { // Start editing. if (intersection.entityID !== hoveredEntityID) { hoveredEntityID = intersection.entityID; From 850b94220f79bd7d6a6ffb8157335c842c360be5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 12 Jul 2017 15:22:44 +1200 Subject: [PATCH 033/722] Display face scale handles --- scripts/vr-edit/vr-edit.js | 88 ++++++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 12 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 22674d517b..cdaa793616 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -159,12 +159,37 @@ Handles = function () { var boundingBoxOverlay, - //HIGHLIGHT_COLOR = { red: 0, green: 240, blue: 240 }, - HIGHLIGHT_COLOR = { red: 255, green: 0, blue: 255 }, - BOUNDING_BOX_ALPHA = 0.8; + faceHandleOverlays = [], + BOUNDING_BOX_COLOR = { red: 0, green: 240, blue: 240 }, + BOUNDING_BOX_ALPHA = 0.8, + HANDLE_NORMAL_COLOR = { red: 0, green: 240, blue: 240 }, + HANDLE_ALPHA = 0.7, + NUM_FACE_HANDLES = 6, + FACE_HANDLE_OVERLAY_AXES, + FACE_HANDLE_OVERLAY_ROTATIONS, + ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), + i; + + FACE_HANDLE_OVERLAY_AXES = [ + { x: -0.5, y: 0, z: 0 }, + { x: 0.5, y: 0, z: 0 }, + { x: 0, y: -0.5, z: 0 }, + { x: 0, y: 0.5, z: 0 }, + { x: 0, y: 0, z: -0.5 }, + { x: 0, y: 0, z: 0.5 } + ]; + + FACE_HANDLE_OVERLAY_ROTATIONS = [ + Quat.fromVec3Degrees({ x: 0, y: 0, z: 90 }), + Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }), + Quat.fromVec3Degrees({ x: 180, y: 0, z: 0 }), + Quat.fromVec3Degrees({ x: 0, y: 0, z: 0 }), + Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }), + Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) + ]; boundingBoxOverlay = Overlays.addOverlay("cube", { - color: HIGHLIGHT_COLOR, + color: BOUNDING_BOX_COLOR, alpha: BOUNDING_BOX_ALPHA, solid: false, drawInFront: true, @@ -172,19 +197,54 @@ visible: false }); - function display(rootEntityID, boundingBox) { - // Selection bounding box. - Overlays.editOverlay(boundingBoxOverlay, { - parentID: rootEntityID, - position: boundingBox.center, - rotation: boundingBox.orientation, - dimensions: boundingBox.dimensions, - visible: true + for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + faceHandleOverlays[i] = Overlays.addOverlay("shape", { + shape: "Cone", + color: HANDLE_NORMAL_COLOR, + alpha: HANDLE_ALPHA, + solid: true, + drawInFront: true, + ignoreRayIntersection: true, + dimensions: { x: 0.1, y: 0.12, z: 0.1 }, + visible: false }); } + + function display(rootEntityID, boundingBox) { + var boundingBoxDimensions = boundingBox.dimensions, + boundingBoxLocalCenter = boundingBox.localCenter, + i; + + // Selection bounding box. + Overlays.editOverlay(boundingBoxOverlay, { + parentID: rootEntityID, + localPosition: boundingBoxLocalCenter, + localRotation: ZERO_ROTATION, + dimensions: boundingBoxDimensions, + visible: true + }); + + // Face scale handles. + for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + Overlays.editOverlay(faceHandleOverlays[i], { + parentID: rootEntityID, + localPosition: Vec3.sum(boundingBoxLocalCenter, + Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], + Vec3.sum(boundingBoxDimensions, { x: 0.12, y: 0.12, z: 0.12 }))), + localRotation: FACE_HANDLE_OVERLAY_ROTATIONS[i], + visible: true + }); + } + } + function clear() { + var i; + Overlays.editOverlay(boundingBoxOverlay, { visible: false }); + for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + Overlays.editOverlay(faceHandleOverlays[i], { visible: false }); + } } function destroy() { @@ -274,6 +334,7 @@ function getBoundingBox() { var center, + localCenter, orientation, inverseOrientation, dimensions, @@ -297,6 +358,7 @@ Vec3.multiplyVbyV(selection[0].dimensions, Vec3.subtract(Vec3.HALF, selection[0].registrationPoint)))); } + localCenter = Vec3.multiplyQbyV(Quat.inverse(rootOrientation), Vec3.subtract(center, rootPosition)); orientation = rootOrientation; dimensions = selection[0].dimensions; } else if (selection.length > 1) { @@ -336,12 +398,14 @@ // Calculate bounding box. center = Vec3.sum(rootPosition, Vec3.multiplyQbyV(rootOrientation, Vec3.multiply(0.5, Vec3.sum(min, max)))); + localCenter = Vec3.multiply(0.5, Vec3.sum(min, max)); orientation = rootOrientation; dimensions = Vec3.subtract(max, min); } return { center: center, + localCenter: localCenter, orientation: orientation, dimensions: dimensions }; From 2c3cd53f8dcbf4eea68e96345827165d931bc148 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 12 Jul 2017 15:30:13 +1200 Subject: [PATCH 034/722] Display face scale handles only for single entities --- scripts/vr-edit/vr-edit.js | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index cdaa793616..47512cc4c3 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -211,7 +211,7 @@ } - function display(rootEntityID, boundingBox) { + function display(rootEntityID, boundingBox, isMultiple) { var boundingBoxDimensions = boundingBox.dimensions, boundingBoxLocalCenter = boundingBox.localCenter, i; @@ -226,15 +226,23 @@ }); // Face scale handles. - for (i = 0; i < NUM_FACE_HANDLES; i += 1) { - Overlays.editOverlay(faceHandleOverlays[i], { - parentID: rootEntityID, - localPosition: Vec3.sum(boundingBoxLocalCenter, - Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], - Vec3.sum(boundingBoxDimensions, { x: 0.12, y: 0.12, z: 0.12 }))), - localRotation: FACE_HANDLE_OVERLAY_ROTATIONS[i], - visible: true - }); + // Only valid for a single entity because for multiple entities, some may be at an angle relative to the root entity + // which would necessitate a (non-existent) shear transform be applied to them when scaling a face of the set. + if (!isMultiple) { + for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + Overlays.editOverlay(faceHandleOverlays[i], { + parentID: rootEntityID, + localPosition: Vec3.sum(boundingBoxLocalCenter, + Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], + Vec3.sum(boundingBoxDimensions, { x: 0.12, y: 0.12, z: 0.12 }))), + localRotation: FACE_HANDLE_OVERLAY_ROTATIONS[i], + visible: true + }); + } + } else { + for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + Overlays.editOverlay(faceHandleOverlays[i], { visible: false }); + } } } @@ -332,6 +340,10 @@ return selection; } + function count() { + return selection.length; + } + function getBoundingBox() { var center, localCenter, @@ -478,6 +490,7 @@ return { select: select, selection: getSelection, + count: count, rootEntityID: getRootEntityID, boundingBox: getBoundingBox, getPositionAndOrientation: getPositionAndOrientation, @@ -916,7 +929,7 @@ } startEditing(); if (isScaleWithHandles) { - handles.display(selection.rootEntityID(), selection.boundingBox()); + handles.display(selection.rootEntityID(), selection.boundingBox(), selection.count() > 1); } } } else { From 972cf1a1bedb68783f77431001e1186ceb1f1e8a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 12 Jul 2017 16:11:59 +1200 Subject: [PATCH 035/722] Size handles to compensate for physical distance --- scripts/vr-edit/vr-edit.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 47512cc4c3..650cd6d55b 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -165,9 +165,12 @@ HANDLE_NORMAL_COLOR = { red: 0, green: 240, blue: 240 }, HANDLE_ALPHA = 0.7, NUM_FACE_HANDLES = 6, + FACE_HANDLE_OVERLAY_DIMENSIONS = { x: 0.1, y: 0.12, z: 0.1 }, FACE_HANDLE_OVERLAY_AXES, + FACE_HANDLE_OVERLAY_OFFSETS, FACE_HANDLE_OVERLAY_ROTATIONS, ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), + DISTANCE_MULTIPLIER_MULTIPLIER = 0.5, i; FACE_HANDLE_OVERLAY_AXES = [ @@ -179,6 +182,12 @@ { x: 0, y: 0, z: 0.5 } ]; + FACE_HANDLE_OVERLAY_OFFSETS = { + x: FACE_HANDLE_OVERLAY_DIMENSIONS.y, + y: FACE_HANDLE_OVERLAY_DIMENSIONS.y, + z: FACE_HANDLE_OVERLAY_DIMENSIONS.y + }; + FACE_HANDLE_OVERLAY_ROTATIONS = [ Quat.fromVec3Degrees({ x: 0, y: 0, z: 90 }), Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }), @@ -205,7 +214,6 @@ solid: true, drawInFront: true, ignoreRayIntersection: true, - dimensions: { x: 0.1, y: 0.12, z: 0.1 }, visible: false }); } @@ -214,6 +222,10 @@ function display(rootEntityID, boundingBox, isMultiple) { var boundingBoxDimensions = boundingBox.dimensions, boundingBoxLocalCenter = boundingBox.localCenter, + faceHandleDimensions, + faceHandleOffsets, + boundingBoxVector, + distanceMultiplier, i; // Selection bounding box. @@ -225,17 +237,26 @@ visible: true }); + // Somewhat maintain general angular size of scale handles per bounding box center but make more distance ones + // display smaller in order to give comfortable depth cue. + boundingBoxVector = Vec3.subtract(boundingBox.center, Camera.position); + distanceMultiplier = DISTANCE_MULTIPLIER_MULTIPLIER + * Vec3.dot(Quat.getForward(Camera.orientation), boundingBoxVector) + / Math.sqrt(Vec3.length(boundingBoxVector)); + // Face scale handles. // Only valid for a single entity because for multiple entities, some may be at an angle relative to the root entity // which would necessitate a (non-existent) shear transform be applied to them when scaling a face of the set. if (!isMultiple) { + faceHandleDimensions = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_DIMENSIONS); + faceHandleOffsets = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_OFFSETS); for (i = 0; i < NUM_FACE_HANDLES; i += 1) { Overlays.editOverlay(faceHandleOverlays[i], { parentID: rootEntityID, localPosition: Vec3.sum(boundingBoxLocalCenter, - Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], - Vec3.sum(boundingBoxDimensions, { x: 0.12, y: 0.12, z: 0.12 }))), + Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], Vec3.sum(boundingBoxDimensions, faceHandleOffsets))), localRotation: FACE_HANDLE_OVERLAY_ROTATIONS[i], + dimensions: faceHandleDimensions, visible: true }); } From 1fc2d7ed1b004caea385ff965637940315e0a019 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 12 Jul 2017 18:44:08 +1200 Subject: [PATCH 036/722] Display corner scale handles --- scripts/vr-edit/vr-edit.js | 78 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 650cd6d55b..cd4470f075 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -159,11 +159,17 @@ Handles = function () { var boundingBoxOverlay, + cornerIndexes = [], + cornerHandleOverlays = [], faceHandleOverlays = [], BOUNDING_BOX_COLOR = { red: 0, green: 240, blue: 240 }, BOUNDING_BOX_ALPHA = 0.8, HANDLE_NORMAL_COLOR = { red: 0, green: 240, blue: 240 }, HANDLE_ALPHA = 0.7, + NUM_CORNERS = 8, + NUM_CORNER_HANDLES = 2, + CORNER_HANDLE_OVERLAY_DIMENSIONS = { x: 0.1, y: 0.1, z: 0.1 }, + CORNER_HANDLE_OVERLAY_AXES, NUM_FACE_HANDLES = 6, FACE_HANDLE_OVERLAY_DIMENSIONS = { x: 0.1, y: 0.12, z: 0.1 }, FACE_HANDLE_OVERLAY_AXES, @@ -173,6 +179,18 @@ DISTANCE_MULTIPLIER_MULTIPLIER = 0.5, i; + CORNER_HANDLE_OVERLAY_AXES = [ + // Ordered such that items 4 apart are opposite corners - used in display(). + { x: -0.5, y: -0.5, z: -0.5 }, + { x: -0.5, y: -0.5, z: 0.5 }, + { x: -0.5, y: 0.5, z: -0.5 }, + { x: -0.5, y: 0.5, z: 0.5 }, + { x: 0.5, y: 0.5, z: 0.5 }, + { x: 0.5, y: 0.5, z: -0.5 }, + { x: 0.5, y: -0.5, z: 0.5 }, + { x: 0.5, y: -0.5, z: -0.5 } + ]; + FACE_HANDLE_OVERLAY_AXES = [ { x: -0.5, y: 0, z: 0 }, { x: 0.5, y: 0, z: 0 }, @@ -206,6 +224,17 @@ visible: false }); + for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { + cornerHandleOverlays[i] = Overlays.addOverlay("sphere", { + color: HANDLE_NORMAL_COLOR, + alpha: HANDLE_ALPHA, + solid: true, + drawInFront: true, + ignoreRayIntersection: true, + visible: false + }); + } + for (i = 0; i < NUM_FACE_HANDLES; i += 1) { faceHandleOverlays[i] = Overlays.addOverlay("shape", { shape: "Cone", @@ -218,14 +247,24 @@ }); } - function display(rootEntityID, boundingBox, isMultiple) { var boundingBoxDimensions = boundingBox.dimensions, + boundingBoxCenter = boundingBox.center, boundingBoxLocalCenter = boundingBox.localCenter, - faceHandleDimensions, - faceHandleOffsets, + boundingBoxOrientation = boundingBox.orientation, + cameraPosition, boundingBoxVector, distanceMultiplier, + cameraUp, + cornerPosition, + cornerVector, + crossProductScale, + maxCrossProductScale, + rightCornerIndex, + leftCornerIndex, + cornerHandleDimensions, + faceHandleDimensions, + faceHandleOffsets, i; // Selection bounding box. @@ -239,11 +278,41 @@ // Somewhat maintain general angular size of scale handles per bounding box center but make more distance ones // display smaller in order to give comfortable depth cue. + cameraPosition = Camera.position; boundingBoxVector = Vec3.subtract(boundingBox.center, Camera.position); distanceMultiplier = DISTANCE_MULTIPLIER_MULTIPLIER * Vec3.dot(Quat.getForward(Camera.orientation), boundingBoxVector) / Math.sqrt(Vec3.length(boundingBoxVector)); + // Corner scale handles. + // At right-most and opposite corners of bounding box. + cameraUp = Quat.getUp(Camera.orientation); + maxCrossProductScale = 0; + for (i = 0; i < NUM_CORNERS; i += 1) { + cornerPosition = Vec3.sum(boundingBoxCenter, + Vec3.multiplyQbyV(boundingBoxOrientation, + Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[i], boundingBoxDimensions))); + cornerVector = Vec3.subtract(cornerPosition, cameraPosition); + crossProductScale = Vec3.dot(Vec3.cross(cornerVector, boundingBoxVector), cameraUp); + if (crossProductScale > maxCrossProductScale) { + maxCrossProductScale = crossProductScale; + rightCornerIndex = i; + } + } + leftCornerIndex = (rightCornerIndex + 4) % NUM_CORNERS; + cornerIndexes[0] = leftCornerIndex; + cornerIndexes[1] = rightCornerIndex; + cornerHandleDimensions = Vec3.multiply(distanceMultiplier, CORNER_HANDLE_OVERLAY_DIMENSIONS); + for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { + Overlays.editOverlay(cornerHandleOverlays[i], { + parentID: rootEntityID, + localPosition: Vec3.sum(boundingBoxLocalCenter, + Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[i]], boundingBoxDimensions)), + dimensions: cornerHandleDimensions, + visible: true + }); + } + // Face scale handles. // Only valid for a single entity because for multiple entities, some may be at an angle relative to the root entity // which would necessitate a (non-existent) shear transform be applied to them when scaling a face of the set. @@ -271,6 +340,9 @@ var i; Overlays.editOverlay(boundingBoxOverlay, { visible: false }); + for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { + Overlays.editOverlay(cornerHandleOverlays[i], { visible: false }); + } for (i = 0; i < NUM_FACE_HANDLES; i += 1) { Overlays.editOverlay(faceHandleOverlays[i], { visible: false }); } From 23ebb791e847d3696a4e2299dc908fb36c0daf04 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 12 Jul 2017 19:22:08 +1200 Subject: [PATCH 037/722] Fix don't scale with hands if scaling multiple entities with handles --- scripts/vr-edit/vr-edit.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index cd4470f075..c7dc3c8499 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -51,6 +51,21 @@ }; } + if (typeof Entities.rootOf !== "function") { + Entities.rootOf = function (entityID) { + var rootEntityID, + entityProperties, + PARENT_PROPERTIES = ["parentID"]; + rootEntityID = entityID; + entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); + while (entityProperties.parentID !== NULL_UUID) { + rootEntityID = entityProperties.parentID; + entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); + } + return rootEntityID; + }; + } + Highlights = function (hand) { // Draws highlights on selected entities. @@ -407,14 +422,10 @@ PARENT_PROPERTIES = ["parentID", "position", "rotation"]; // Find root parent. - rootEntityID = entityID; - entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); - while (entityProperties.parentID !== NULL_UUID) { - rootEntityID = entityProperties.parentID; - entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); - } + rootEntityID = Entities.rootOf(entityID); // Selection position and orientation is that of the root entity. + entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); rootPosition = entityProperties.position; rootOrientation = entityProperties.rotation; @@ -1010,7 +1021,8 @@ if (isEditing) { // Perform edit. doEdit = true; - } else if (intersection.intersects && (!isScaleWithHandles || !otherHand.isEditing(intersection.entityID))) { + } else if (intersection.intersects && (!isScaleWithHandles + || !otherHand.isEditing(Entities.rootOf(intersection.entityID)))) { // Start editing. if (intersection.entityID !== hoveredEntityID) { hoveredEntityID = intersection.entityID; From e644aabaf7a5cdccccba11c1649dcb144e567809 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Jul 2017 15:04:25 +1200 Subject: [PATCH 038/722] Intersect overlays with hands --- scripts/vr-edit/vr-edit.js | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index c7dc3c8499..d6199a162b 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -915,6 +915,10 @@ function update() { var gripValue, palmPosition, + overlayID, + overlayIDs, + overlayDistance, + distance, entityID, entityIDs, entitySize, @@ -963,11 +967,34 @@ } } + // Hand-overlay intersection, if any. + overlayID = null; + palmPosition = hand === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); + overlayIDs = Overlays.findOverlays(palmPosition, NEAR_HOVER_RADIUS); + if (overlayIDs.length > 0) { + // Typically, there will be only one overlay; optimize for that case. + overlayID = overlayIDs[0]; + if (overlayIDs.length > 1) { + // Find closest overlay. + overlayDistance = Vec3.length(Vec3.subtract(Overlays.getProperty(overlayID, "position"), palmPosition)); + for (i = 1, length = overlayIDs.length; i < length; i += 1) { + distance = + Vec3.length(Vec3.subtract(Overlays.getProperty(overlayIDs[i], "position"), palmPosition)); + if (distance > overlayDistance) { + overlayID = overlayIDs[i]; + overlayDistance = distance; + } + } + } + } + // Hand-entity intersection, if any. + // TODO: Only test intersection if overlay not intersected? entityID = null; palmPosition = hand === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); entityIDs = Entities.findEntities(palmPosition, NEAR_GRAB_RADIUS); if (entityIDs.length > 0) { + // TODO: If number of entities is often 1 in practice, optimize code for this case. // Find smallest, editable entity. entitySize = HALF_TREE_SCALE; for (i = 0, length = entityIDs.length; i < length; i += 1) { @@ -980,13 +1007,15 @@ } } } + intersection = { - intersects: entityID !== null, + intersects: overlayID !== null || entityID !== null, + overlayID: overlayID, entityID: entityID, handSelected: true }; - // Laser-entity intersection, if any. + // Laser-entity intersection, if any, if hand not intersected. if (!intersection.intersects && isTriggerPressed) { handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); From 9cbde6d99e5c7265eb54b0c5c61c31b56016904a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Jul 2017 15:05:04 +1200 Subject: [PATCH 039/722] Hover intersected handle --- scripts/vr-edit/vr-edit.js | 45 +++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index d6199a162b..eafdd94f43 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -180,6 +180,7 @@ BOUNDING_BOX_COLOR = { red: 0, green: 240, blue: 240 }, BOUNDING_BOX_ALPHA = 0.8, HANDLE_NORMAL_COLOR = { red: 0, green: 240, blue: 240 }, + HANDLE_HOVER_COLOR = { red: 0, green: 255, blue: 120 }, HANDLE_ALPHA = 0.7, NUM_CORNERS = 8, NUM_CORNER_HANDLES = 2, @@ -192,6 +193,7 @@ FACE_HANDLE_OVERLAY_ROTATIONS, ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), DISTANCE_MULTIPLIER_MULTIPLIER = 0.5, + hoveredOverlayID = null, i; CORNER_HANDLE_OVERLAY_AXES = [ @@ -245,7 +247,7 @@ alpha: HANDLE_ALPHA, solid: true, drawInFront: true, - ignoreRayIntersection: true, + ignoreRayIntersection: false, visible: false }); } @@ -257,7 +259,7 @@ alpha: HANDLE_ALPHA, solid: true, drawInFront: true, - ignoreRayIntersection: true, + ignoreRayIntersection: false, visible: false }); } @@ -351,6 +353,21 @@ } } + function hover(overlayID) { + if (overlayID !== hoveredOverlayID) { + if (hoveredOverlayID !== null) { + Overlays.editOverlay(hoveredOverlayID, { color: HANDLE_NORMAL_COLOR }); + hoveredOverlayID = null; + } + + if (overlayID !== null + && (faceHandleOverlays.indexOf(overlayID) !== -1 || cornerHandleOverlays.indexOf(overlayID) !== -1)) { + Overlays.editOverlay(overlayID, { color: HANDLE_HOVER_COLOR }); + hoveredOverlayID = overlayID; + } + } + } + function clear() { var i; @@ -374,6 +391,7 @@ return { display: display, + hover: hover, clear: clear, destroy: destroy }; @@ -737,6 +755,7 @@ GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. NEAR_GRAB_RADIUS = 0.1, // Per handControllerGrab.js. + NEAR_HOVER_RADIUS = 0.025, PICK_MAX_DISTANCE = 500, // Per handControllerGrab.js. PRECISION_PICKING = true, @@ -772,7 +791,6 @@ doHighlight, otherHand, - otherHandWasEditing, laser, selection, @@ -912,6 +930,10 @@ return properties.visible && !properties.locked && NONEDITABLE_ENTITY_TYPES.indexOf(properties.type) === -1; } + function hoverHandle(overlayID) { + handles.hover(overlayID); + } + function update() { var gripValue, palmPosition, @@ -932,7 +954,6 @@ isTriggerPressed, isTriggerClicked, wasEditing, - otherHandIsEditing, i, length; @@ -1072,13 +1093,14 @@ // Stop editing. stopEditing(); } + if (isScaleWithHandles) { + otherHand.hoverHandle(intersection.overlayID); + } if (intersection.intersects) { // Hover entities. - otherHandIsEditing = otherHand.isEditing(selection.rootEntityID()); - if (wasEditing || intersection.entityID !== hoveredEntityID || otherHandIsEditing !== otherHandWasEditing) { + if (wasEditing || intersection.entityID !== hoveredEntityID) { hoveredEntityID = intersection.entityID; selection.select(hoveredEntityID); - otherHandWasEditing = otherHandIsEditing; doHighlight = true; } } else { @@ -1096,15 +1118,17 @@ function apply() { if (doEdit) { if (otherHand.isEditing(selection.rootEntityID())) { - if (isScaling) { + if (isScaling && !isScaleWithHandles) { applyScale(); } } else { applyGrab(); } } else if (doHighlight) { - highlights.display(intersection.handSelected, selection.selection(), - otherHand.isEditing(selection.rootEntityID()) || isScaleWithHandles); + if (!isScaleWithHandles || !otherHand.isEditing(selection.rootEntityID())) { + highlights.display(intersection.handSelected, selection.selection(), + otherHand.isEditing(selection.rootEntityID()) || isScaleWithHandles); + } } } @@ -1135,6 +1159,7 @@ setOtherHand: setOtherHand, setScaleWithHandles: setScaleWithHandles, isEditing: getIsEditing, + hoverHandle: hoverHandle, getTargetPosition: getTargetPosition, updateGrabOffset: updateGrabOffset, update: update, From ab6e278a487f18c15e5bc4537d356d20ef3ebf65 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 13 Jul 2017 17:01:56 +1200 Subject: [PATCH 040/722] Intersect and hover overlay handles with laser --- scripts/vr-edit/vr-edit.js | 44 ++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index eafdd94f43..1aa86016ed 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -945,6 +945,8 @@ entityIDs, entitySize, size, + isHandSelected, + laserIntersection, wasLaserOn, handPosition, handOrientation, @@ -1012,7 +1014,7 @@ // Hand-entity intersection, if any. // TODO: Only test intersection if overlay not intersected? entityID = null; - palmPosition = hand === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); + // palmPosition is set above. entityIDs = Entities.findEntities(palmPosition, NEAR_GRAB_RADIUS); if (entityIDs.length > 0) { // TODO: If number of entities is often 1 in practice, optimize code for this case. @@ -1029,15 +1031,10 @@ } } - intersection = { - intersects: overlayID !== null || entityID !== null, - overlayID: overlayID, - entityID: entityID, - handSelected: true - }; + isHandSelected = overlayID !== null || entityID !== null; - // Laser-entity intersection, if any, if hand not intersected. - if (!intersection.intersects && isTriggerPressed) { + // Laser-overlay or -entity intersection if not already intersected. + if (!isHandSelected && isTriggerPressed) { handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); @@ -1046,17 +1043,36 @@ direction: Quat.getUp(handOrientation), length: PICK_MAX_DISTANCE }; - intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + + laserIntersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, VISIBLE_ONLY); + if (laserIntersection.intersects) { + overlayID = laserIntersection.overlayID; + } + if (!laserIntersection.intersects) { + laserIntersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + if (laserIntersection.intersects && isEditableEntity(laserIntersection.entityID)) { + entityID = laserIntersection.entityID; + } else { + laserIntersection.intersects = false; + } + } laserLength = isEditing ? laserEditingDistance - : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); - intersection.intersects = isEditableEntity(intersection.entityID); + : (laserIntersection.intersects ? laserIntersection.distance : PICK_MAX_DISTANCE); isLaserOn = true; } else { isLaserOn = false; } + intersection = { + intersects: overlayID !== null || entityID !== null, + overlayID: overlayID, + entityID: entityID, + handSelected: isHandSelected + }; + // Laser update. if (isLaserOn) { laser.update(pickRay.origin, pickRay.direction, laserLength, isTriggerClicked); @@ -1071,7 +1087,7 @@ if (isEditing) { // Perform edit. doEdit = true; - } else if (intersection.intersects && (!isScaleWithHandles + } else if (intersection.intersects && intersection.entityID && (!isScaleWithHandles || !otherHand.isEditing(Entities.rootOf(intersection.entityID)))) { // Start editing. if (intersection.entityID !== hoveredEntityID) { @@ -1096,7 +1112,7 @@ if (isScaleWithHandles) { otherHand.hoverHandle(intersection.overlayID); } - if (intersection.intersects) { + if (intersection.intersects && intersection.entityID) { // Hover entities. if (wasEditing || intersection.entityID !== hoveredEntityID) { hoveredEntityID = intersection.entityID; From 7933280d03eb270b7e9703cfa0501063226bcc64 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 14 Jul 2017 12:08:22 +1200 Subject: [PATCH 041/722] Clear highlights etc. when turn application off --- scripts/vr-edit/vr-edit.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 1aa86016ed..2e5db8024c 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -17,6 +17,8 @@ APP_ICON_ACTIVE = "icons/tablet-icons/edit-a.svg", tablet, button, + + // Application state isAppActive = false, isScaleWithHandles = false, @@ -1148,6 +1150,18 @@ } } + function clear() { + laser.clear(); + selection.clear(); + highlights.clear(); + handles.clear(); + isLaserOn = false; + isEditing = false; + isEditingWithHand = false; + isScaling = false; + hoveredEntityID = null; + } + function destroy() { if (laser) { laser.destroy(); @@ -1180,6 +1194,7 @@ updateGrabOffset: updateGrabOffset, update: update, apply: apply, + clear: clear, destroy: destroy }; }; @@ -1202,7 +1217,8 @@ Settings.setValue(VR_EDIT_SETTING, isAppActive); } - function onButtonClicked() { + function onAppButtonClicked() { + // Application tablet/toolbar button clicked. isAppActive = !isAppActive; updateHandControllerGrab(); button.editProperties({ isActive: isAppActive }); @@ -1212,6 +1228,8 @@ } else { Script.clearTimeout(updateTimer); updateTimer = null; + hands[LEFT_HAND].clear(); + hands[RIGHT_HAND].clear(); } } @@ -1237,7 +1255,7 @@ isActive: isAppActive }); if (button) { - button.clicked.connect(onButtonClicked); + button.clicked.connect(onAppButtonClicked); } // Hands, each with a laser, selection, etc. @@ -1264,7 +1282,7 @@ } if (button) { - button.clicked.disconnect(onButtonClicked); + button.clicked.disconnect(onAppButtonClicked); tablet.removeButton(button); button = null; } From ee21797fcd18e0de20a1eb66f5d17c82df4203ea Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 14 Jul 2017 12:22:21 +1200 Subject: [PATCH 042/722] Simplify scale-with-handles state handling --- scripts/vr-edit/vr-edit.js | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 2e5db8024c..e1ffd9772d 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -20,7 +20,7 @@ // Application state isAppActive = false, - isScaleWithHandles = false, + isAppScaleWithHandles = false, VR_EDIT_SETTING = "io.highfidelity.isVREditing", // Note: This constant is duplicated in utils.js. @@ -768,8 +768,6 @@ handPose, intersection = {}, - isScaleWithHandles = false, - isLaserOn = false, hoveredEntityID = null, @@ -822,10 +820,6 @@ otherHand = hand; } - function setScaleWithHandles(value) { - isScaleWithHandles = value; - } - function getIsEditing(rootEntityID) { return isEditing && rootEntityID === selection.rootEntityID(); } @@ -1089,7 +1083,7 @@ if (isEditing) { // Perform edit. doEdit = true; - } else if (intersection.intersects && intersection.entityID && (!isScaleWithHandles + } else if (intersection.intersects && intersection.entityID && (!isAppScaleWithHandles || !otherHand.isEditing(Entities.rootOf(intersection.entityID)))) { // Start editing. if (intersection.entityID !== hoveredEntityID) { @@ -1101,7 +1095,7 @@ laserEditingDistance = laserLength; } startEditing(); - if (isScaleWithHandles) { + if (isAppScaleWithHandles) { handles.display(selection.rootEntityID(), selection.boundingBox(), selection.count() > 1); } } @@ -1111,7 +1105,7 @@ // Stop editing. stopEditing(); } - if (isScaleWithHandles) { + if (isAppScaleWithHandles) { otherHand.hoverHandle(intersection.overlayID); } if (intersection.intersects && intersection.entityID) { @@ -1136,16 +1130,16 @@ function apply() { if (doEdit) { if (otherHand.isEditing(selection.rootEntityID())) { - if (isScaling && !isScaleWithHandles) { + if (isScaling && !isAppScaleWithHandles) { applyScale(); } } else { applyGrab(); } } else if (doHighlight) { - if (!isScaleWithHandles || !otherHand.isEditing(selection.rootEntityID())) { + if (!isAppScaleWithHandles || !otherHand.isEditing(selection.rootEntityID())) { highlights.display(intersection.handSelected, selection.selection(), - otherHand.isEditing(selection.rootEntityID()) || isScaleWithHandles); + otherHand.isEditing(selection.rootEntityID()) || isAppScaleWithHandles); } } } @@ -1187,7 +1181,6 @@ return { setOtherHand: setOtherHand, - setScaleWithHandles: setScaleWithHandles, isEditing: getIsEditing, hoverHandle: hoverHandle, getTargetPosition: getTargetPosition, @@ -1234,9 +1227,7 @@ } function onGripClicked() { - isScaleWithHandles = !isScaleWithHandles; - hands[LEFT_HAND].setScaleWithHandles(isScaleWithHandles); - hands[RIGHT_HAND].setScaleWithHandles(isScaleWithHandles); + isAppScaleWithHandles = !isAppScaleWithHandles; } function setUp() { From 85c5b8778a6045e669eeebe110becf9f312d8c0f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 14 Jul 2017 12:45:19 +1200 Subject: [PATCH 043/722] Simplify left/right side value handling --- scripts/vr-edit/vr-edit.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index e1ffd9772d..741ff98633 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -68,9 +68,9 @@ }; } - Highlights = function (hand) { - // Draws highlights on selected entities. + Highlights = function (side) { + // Draws highlights on selected entities. var handOverlay, entityOverlays = [], GRAB_HIGHLIGHT_COLOR = { red: 240, green: 240, blue: 0 }, @@ -83,7 +83,7 @@ handOverlay = Overlays.addOverlay("sphere", { dimensions: HAND_HIGHLIGHT_DIMENSIONS, parentID: AVATAR_SELF_ID, - parentJointIndex: MyAvatar.getJointIndex(hand === LEFT_HAND + parentJointIndex: MyAvatar.getJointIndex(side === LEFT_HAND ? "_CONTROLLER_LEFTHAND" : "_CONTROLLER_RIGHTHAND"), localPosition: HAND_HIGHLIGHT_OFFSET, @@ -630,8 +630,7 @@ // Draws hand lasers. // May intersect with entities or bounding box of other hand's selection. - var hand, - laserLine = null, + var laserLine = null, laserSphere = null, searchDistance = 0.0, @@ -655,7 +654,6 @@ COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_HALF_SQUEEZE, BRIGHT_POW); COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_FULL_SQUEEZE, BRIGHT_POW); - hand = side; laserLine = Overlays.addOverlay("line3d", { lineWidth: 5, alpha: 1.0, @@ -663,7 +661,7 @@ ignoreRayIntersection: true, drawInFront: true, parentID: AVATAR_SELF_ID, - parentJointIndex: MyAvatar.getJointIndex(hand === LEFT_HAND + parentJointIndex: MyAvatar.getJointIndex(side === LEFT_HAND ? "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND" : "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"), visible: false @@ -742,8 +740,7 @@ // Hand controller input. // Each hand has a laser, an entity selection, and entity highlighter. - var hand, - handController, + var handController, controllerTrigger, controllerTriggerClicked, controllerGrip, @@ -797,8 +794,7 @@ highlights, handles; - hand = side; - if (hand === LEFT_HAND) { + if (side === LEFT_HAND) { handController = Controller.Standard.LeftHand; controllerTrigger = Controller.Standard.LT; controllerTriggerClicked = Controller.Standard.LTClick; @@ -811,9 +807,9 @@ controllerGrip = Controller.Standard.RightGrip; } - laser = new Laser(hand); + laser = new Laser(side); selection = new Selection(); - highlights = new Highlights(hand); + highlights = new Highlights(side); handles = new Handles(); function setOtherHand(hand) { @@ -830,7 +826,7 @@ function getTargetPosition() { if (isEditingWithHand) { - return hand === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); + return side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); } return Vec3.sum(getHandPosition(), Vec3.multiply(laserEditingDistance, Quat.getUp(Quat.multiply(MyAvatar.orientation, handPose.rotation)))); @@ -988,7 +984,7 @@ // Hand-overlay intersection, if any. overlayID = null; - palmPosition = hand === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); + palmPosition = side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); overlayIDs = Overlays.findOverlays(palmPosition, NEAR_HOVER_RADIUS); if (overlayIDs.length > 0) { // Typically, there will be only one overlay; optimize for that case. From 0506f516d48b3d7185766208ff46d5e797d4b724 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 14 Jul 2017 12:54:08 +1200 Subject: [PATCH 044/722] Rename Hand to Editor --- scripts/vr-edit/vr-edit.js | 75 +++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 741ff98633..df37d79ccf 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -24,7 +24,7 @@ VR_EDIT_SETTING = "io.highfidelity.isVREditing", // Note: This constant is duplicated in utils.js. - hands = [], + editors = [], LEFT_HAND = 0, RIGHT_HAND = 1, @@ -35,12 +35,13 @@ Handles, Selection, Laser, - Hand, + Editor, AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", NULL_UUID = "{00000000-0000-0000-0000-000000000000}", HALF_TREE_SCALE = 16384; + if (typeof Vec3.min !== "function") { Vec3.min = function (a, b) { return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), z: Math.min(a.z, b.z) }; @@ -174,6 +175,7 @@ }; }; + Handles = function () { var boundingBoxOverlay, cornerIndexes = [], @@ -399,6 +401,7 @@ }; }; + Selection = function () { // Manages set of selected entities. Currently supports just one set of linked entities. var selection = [], @@ -626,6 +629,7 @@ }; }; + Laser = function (side) { // Draws hand lasers. // May intersect with entities or bounding box of other hand's selection. @@ -736,7 +740,8 @@ }; }; - Hand = function (side, gripPressedCallback) { + + Editor = function (side, gripPressedCallback) { // Hand controller input. // Each hand has a laser, an entity selection, and entity highlighter. @@ -787,7 +792,7 @@ doEdit, doHighlight, - otherHand, + otherEditor, laser, selection, @@ -812,8 +817,8 @@ highlights = new Highlights(side); handles = new Handles(); - function setOtherHand(hand) { - otherHand = hand; + function setOtherEditor(editor) { + otherEditor = editor; } function getIsEditing(rootEntityID) { @@ -845,10 +850,10 @@ isEditingWithHand = intersection.handSelected; - if (otherHand.isEditing(selection.rootEntityID())) { + if (otherEditor.isEditing(selection.rootEntityID())) { // Store initial values for use in scaling. initialTargetPosition = getTargetPosition(); - initialOtherTargetPosition = otherHand.getTargetPosition(); + initialOtherTargetPosition = otherEditor.getTargetPosition(); initialTargetsCenter = Vec3.multiply(0.5, Vec3.sum(initialTargetPosition, initialOtherTargetPosition)); initialTargetsSeparation = Vec3.distance(initialTargetPosition, initialOtherTargetPosition); initialtargetsDirection = Vec3.subtract(initialOtherTargetPosition, initialTargetPosition); @@ -895,7 +900,7 @@ // Scale selection. targetPosition = getTargetPosition(); - otherTargetPosition = otherHand.getTargetPosition(); + otherTargetPosition = otherEditor.getTargetPosition(); targetsSeparation = Vec3.distance(targetPosition, otherTargetPosition); scale = targetsSeparation / initialTargetsSeparation; rotation = Quat.rotationBetween(initialtargetsDirection, Vec3.subtract(otherTargetPosition, targetPosition)); @@ -905,7 +910,7 @@ // Update grab offsets. selectionPositionAndOrientation = selection.getPositionAndOrientation(); updateGrabOffset(selectionPositionAndOrientation); - otherHand.updateGrabOffset(selectionPositionAndOrientation); + otherEditor.updateGrabOffset(selectionPositionAndOrientation); } function stopEditing() { @@ -1080,7 +1085,7 @@ // Perform edit. doEdit = true; } else if (intersection.intersects && intersection.entityID && (!isAppScaleWithHandles - || !otherHand.isEditing(Entities.rootOf(intersection.entityID)))) { + || !otherEditor.isEditing(Entities.rootOf(intersection.entityID)))) { // Start editing. if (intersection.entityID !== hoveredEntityID) { hoveredEntityID = intersection.entityID; @@ -1102,7 +1107,7 @@ stopEditing(); } if (isAppScaleWithHandles) { - otherHand.hoverHandle(intersection.overlayID); + otherEditor.hoverHandle(intersection.overlayID); } if (intersection.intersects && intersection.entityID) { // Hover entities. @@ -1125,7 +1130,7 @@ function apply() { if (doEdit) { - if (otherHand.isEditing(selection.rootEntityID())) { + if (otherEditor.isEditing(selection.rootEntityID())) { if (isScaling && !isAppScaleWithHandles) { applyScale(); } @@ -1133,9 +1138,9 @@ applyGrab(); } } else if (doHighlight) { - if (!isAppScaleWithHandles || !otherHand.isEditing(selection.rootEntityID())) { + if (!isAppScaleWithHandles || !otherEditor.isEditing(selection.rootEntityID())) { highlights.display(intersection.handSelected, selection.selection(), - otherHand.isEditing(selection.rootEntityID()) || isAppScaleWithHandles); + otherEditor.isEditing(selection.rootEntityID()) || isAppScaleWithHandles); } } } @@ -1171,12 +1176,12 @@ } } - if (!this instanceof Hand) { - return new Hand(); + if (!this instanceof Editor) { + return new Editor(); } return { - setOtherHand: setOtherHand, + setOtherEditor: setOtherEditor, isEditing: getIsEditing, hoverHandle: hoverHandle, getTargetPosition: getTargetPosition, @@ -1188,15 +1193,16 @@ }; }; + function update() { // Main update loop. updateTimer = null; // Each hand's action depends on the state of the other hand, so update the states first then apply in actions. - hands[LEFT_HAND].update(); - hands[RIGHT_HAND].update(); - hands[LEFT_HAND].apply(); - hands[RIGHT_HAND].apply(); + editors[LEFT_HAND].update(); + editors[RIGHT_HAND].update(); + editors[LEFT_HAND].apply(); + editors[RIGHT_HAND].apply(); updateTimer = Script.setTimeout(update, UPDATE_LOOP_TIMEOUT); } @@ -1217,8 +1223,8 @@ } else { Script.clearTimeout(updateTimer); updateTimer = null; - hands[LEFT_HAND].clear(); - hands[RIGHT_HAND].clear(); + editors[LEFT_HAND].clear(); + editors[RIGHT_HAND].clear(); } } @@ -1226,6 +1232,7 @@ isAppScaleWithHandles = !isAppScaleWithHandles; } + function setUp() { updateHandControllerGrab(); @@ -1246,10 +1253,10 @@ } // Hands, each with a laser, selection, etc. - hands[LEFT_HAND] = new Hand(LEFT_HAND, onGripClicked); - hands[RIGHT_HAND] = new Hand(RIGHT_HAND, onGripClicked); - hands[LEFT_HAND].setOtherHand(hands[RIGHT_HAND]); - hands[RIGHT_HAND].setOtherHand(hands[LEFT_HAND]); + editors[LEFT_HAND] = new Editor(LEFT_HAND, onGripClicked); + editors[RIGHT_HAND] = new Editor(RIGHT_HAND, onGripClicked); + editors[LEFT_HAND].setOtherEditor(editors[RIGHT_HAND]); + editors[RIGHT_HAND].setOtherEditor(editors[LEFT_HAND]); if (isAppActive) { update(); @@ -1274,13 +1281,13 @@ button = null; } - if (hands[LEFT_HAND]) { - hands[LEFT_HAND].destroy(); - hands[LEFT_HAND] = null; + if (editors[LEFT_HAND]) { + editors[LEFT_HAND].destroy(); + editors[LEFT_HAND] = null; } - if (hands[RIGHT_HAND]) { - hands[RIGHT_HAND].destroy(); - hands[RIGHT_HAND] = null; + if (editors[RIGHT_HAND]) { + editors[RIGHT_HAND].destroy(); + editors[RIGHT_HAND] = null; } tablet = null; From 2acb7335f1534480d516400890430ebc89116b57 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 14 Jul 2017 14:37:54 +1200 Subject: [PATCH 045/722] Move hand functionality into new Hand object --- scripts/vr-edit/vr-edit.js | 174 +++++++++++++++++++++++++------------ 1 file changed, 119 insertions(+), 55 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index df37d79ccf..a4b9ef1b77 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -35,6 +35,7 @@ Handles, Selection, Laser, + Hand, Editor, AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", @@ -741,11 +742,9 @@ }; - Editor = function (side, gripPressedCallback) { + Hand = function (side, gripPressedCallback) { // Hand controller input. - // Each hand has a laser, an entity selection, and entity highlighter. - - var handController, + var handController, // ####### Rename to "controller". controllerTrigger, controllerTriggerClicked, controllerGrip, @@ -754,8 +753,102 @@ GRIP_ON_VALUE = 0.99, GRIP_OFF_VALUE = 0.95, + isTriggerPressed, + isTriggerClicked, TRIGGER_ON_VALUE = 0.15, // Per handControllerGrab.js. TRIGGER_OFF_VALUE = 0.1, // Per handControllerGrab.js. + + handPose, + handPosition, + handOrientation; + + if (side === LEFT_HAND) { + handController = Controller.Standard.LeftHand; + controllerTrigger = Controller.Standard.LT; + controllerTriggerClicked = Controller.Standard.LTClick; + controllerGrip = Controller.Standard.LeftGrip; + } else { + handController = Controller.Standard.RightHand; + controllerTrigger = Controller.Standard.RT; + controllerTriggerClicked = Controller.Standard.RTClick; + controllerGrip = Controller.Standard.RightGrip; + } + + function valid() { + return handPose.valid; + } + + function position() { + return handPosition; + } + + function orientation() { + return handOrientation; + } + + function triggerPressed() { + return isTriggerPressed; + } + + function triggerClicked() { + return isTriggerClicked; + } + + function update() { + var gripValue; + + // Hand pose. + handPose = Controller.getPoseValue(handController); + handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); + handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); + + // Controller trigger. + isTriggerPressed = Controller.getValue(controllerTrigger) > (isTriggerPressed + ? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE); + isTriggerClicked = Controller.getValue(controllerTriggerClicked); + + // Controller grip. + gripValue = Controller.getValue(controllerGrip); + if (isGripPressed) { + isGripPressed = gripValue > GRIP_OFF_VALUE; + } else { + isGripPressed = gripValue > GRIP_ON_VALUE; + if (isGripPressed) { + gripPressedCallback(); + } + } + } + + function clear() { + // Nothing to do. + } + + function destroy() { + // Nothing to do. + } + + if (!this instanceof Hand) { + return new Hand(); + } + + return { + valid: valid, + position: position, + orientation: orientation, + triggerPressed: triggerPressed, + triggerClicked: triggerClicked, + update: update, + clear: clear, + destroy: destroy + }; + }; + + + Editor = function (side, gripPressedCallback) { + // Each controller has a hand, laser, an entity selection, entity highlighter, and entity handles. + + var intersection = {}, + GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. NEAR_GRAB_RADIUS = 0.1, // Per handControllerGrab.js. @@ -767,9 +860,6 @@ NO_EXCLUDE_IDS = [], VISIBLE_ONLY = true, - handPose, - intersection = {}, - isLaserOn = false, hoveredEntityID = null, @@ -794,24 +884,17 @@ otherEditor, + hand, laser, selection, highlights, handles; if (side === LEFT_HAND) { - handController = Controller.Standard.LeftHand; - controllerTrigger = Controller.Standard.LT; - controllerTriggerClicked = Controller.Standard.LTClick; - controllerGrip = Controller.Standard.LeftGrip; GRAB_POINT_SPHERE_OFFSET.x = -GRAB_POINT_SPHERE_OFFSET.x; - } else { - handController = Controller.Standard.RightHand; - controllerTrigger = Controller.Standard.RT; - controllerTriggerClicked = Controller.Standard.RTClick; - controllerGrip = Controller.Standard.RightGrip; } + hand = new Hand(side, gripPressedCallback); laser = new Laser(side); selection = new Selection(); highlights = new Highlights(side); @@ -825,27 +908,22 @@ return isEditing && rootEntityID === selection.rootEntityID(); } - function getHandPosition() { - return Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); - } - function getTargetPosition() { if (isEditingWithHand) { return side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); } - return Vec3.sum(getHandPosition(), Vec3.multiply(laserEditingDistance, - Quat.getUp(Quat.multiply(MyAvatar.orientation, handPose.rotation)))); + return Vec3.sum(hand.position(), Vec3.multiply(laserEditingDistance, Quat.getUp(hand.orientation()))); } function startEditing() { var selectionPositionAndOrientation, initialOtherTargetPosition; - initialHandOrientationInverse = Quat.inverse(Quat.multiply(MyAvatar.orientation, handPose.rotation)); + initialHandOrientationInverse = Quat.inverse(hand.orientation()); selection.select(hoveredEntityID); // Entity may have been moved by other hand so refresh position and orientation. selectionPositionAndOrientation = selection.getPositionAndOrientation(); - initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, getHandPosition()); + initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position()); initialSelectionOrientation = selectionPositionAndOrientation.orientation; isEditingWithHand = intersection.handSelected; @@ -867,8 +945,8 @@ } function updateGrabOffset(selectionPositionAndOrientation) { - initialHandOrientationInverse = Quat.inverse(Quat.multiply(MyAvatar.orientation, handPose.rotation)); - initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, getHandPosition()); + initialHandOrientationInverse = Quat.inverse(hand.orientation()); + initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position()); initialSelectionOrientation = selectionPositionAndOrientation.orientation; } @@ -879,8 +957,8 @@ selectionPosition, selectionOrientation; - handPosition = getHandPosition(); - handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); + handPosition = hand.position(); + handOrientation = hand.orientation(); deltaOrientation = Quat.multiply(handOrientation, initialHandOrientationInverse); selectionPosition = Vec3.sum(handPosition, Vec3.multiplyQbyV(deltaOrientation, initialHandToSelectionVector)); @@ -932,8 +1010,7 @@ } function update() { - var gripValue, - palmPosition, + var palmPosition, overlayID, overlayIDs, overlayDistance, @@ -950,16 +1027,14 @@ deltaOrigin, pickRay, laserLength, - isTriggerPressed, - isTriggerClicked, wasEditing, i, length; // Hand position and orientation. - handPose = Controller.getPoseValue(handController); + hand.update(); wasLaserOn = isLaserOn; - if (!handPose.valid) { + if (!hand.valid()) { isLaserOn = false; if (wasLaserOn) { laser.clear(); @@ -971,22 +1046,6 @@ return; } - // Controller trigger. - isTriggerPressed = Controller.getValue(controllerTrigger) > (isTriggerPressed - ? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE); - isTriggerClicked = Controller.getValue(controllerTriggerClicked); - - // Controller grip. - gripValue = Controller.getValue(controllerGrip); - if (isGripPressed) { - isGripPressed = gripValue > GRIP_OFF_VALUE; - } else { - isGripPressed = gripValue > GRIP_ON_VALUE; - if (isGripPressed) { - gripPressedCallback(); - } - } - // Hand-overlay intersection, if any. overlayID = null; palmPosition = side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); @@ -1031,9 +1090,9 @@ isHandSelected = overlayID !== null || entityID !== null; // Laser-overlay or -entity intersection if not already intersected. - if (!isHandSelected && isTriggerPressed) { - handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); - handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); + if (!isHandSelected && hand.triggerPressed()) { + handPosition = hand.position(); + handOrientation = hand.orientation(); deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); pickRay = { origin: Vec3.sum(handPosition, deltaOrigin), // Add a bit to ... @@ -1072,7 +1131,7 @@ // Laser update. if (isLaserOn) { - laser.update(pickRay.origin, pickRay.direction, laserLength, isTriggerClicked); + laser.update(pickRay.origin, pickRay.direction, laserLength, hand.triggerClicked()); } else if (wasLaserOn) { laser.clear(); } @@ -1080,7 +1139,7 @@ // Highlight / edit. doEdit = false; doHighlight = false; - if (isTriggerClicked) { + if (hand.triggerClicked()) { if (isEditing) { // Perform edit. doEdit = true; @@ -1146,6 +1205,7 @@ } function clear() { + hand.clear(); laser.clear(); selection.clear(); highlights.clear(); @@ -1158,6 +1218,10 @@ } function destroy() { + if (hand) { + hand.destroy(); + hand = null; + } if (laser) { laser.destroy(); laser = null; From 35e8e7762fc42e4940249b082ca8ca209d81ba22 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 14 Jul 2017 15:35:03 +1200 Subject: [PATCH 046/722] Move hand intersection into Hand object --- scripts/vr-edit/vr-edit.js | 195 ++++++++++++++++++++----------------- 1 file changed, 104 insertions(+), 91 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index a4b9ef1b77..5c2fd7e55e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -70,6 +70,17 @@ }; } + function isEditableRoot(entityID) { + var EDITIBLE_ENTITY_QUERY_PROPERTYES = ["parentID", "visible", "locked", "type"], + NONEDITABLE_ENTITY_TYPES = ["Unknown", "Zone", "Light"], + properties; + properties = Entities.getEntityProperties(entityID, EDITIBLE_ENTITY_QUERY_PROPERTYES); + while (properties.parentID && properties.parentID !== NULL_UUID) { + properties = Entities.getEntityProperties(properties.parentID, EDITIBLE_ENTITY_QUERY_PROPERTYES); + } + return properties.visible && !properties.locked && NONEDITABLE_ENTITY_TYPES.indexOf(properties.type) === -1; + } + Highlights = function (side) { // Draws highlights on selected entities. @@ -758,9 +769,14 @@ TRIGGER_ON_VALUE = 0.15, // Per handControllerGrab.js. TRIGGER_OFF_VALUE = 0.1, // Per handControllerGrab.js. + NEAR_GRAB_RADIUS = 0.1, // Per handControllerGrab.js. + NEAR_HOVER_RADIUS = 0.025, + handPose, handPosition, - handOrientation; + handOrientation, + + intersection; if (side === LEFT_HAND) { handController = Controller.Standard.LeftHand; @@ -794,8 +810,24 @@ return isTriggerClicked; } + function getIntersection() { + return intersection; + } + function update() { - var gripValue; + var gripValue, + palmPosition, + overlayID, + overlayIDs, + overlayDistance, + distance, + entityID, + entityIDs, + entitySize, + size, + i, + length; + // Hand pose. handPose = Controller.getPoseValue(handController); @@ -817,6 +849,59 @@ gripPressedCallback(); } } + + // Hand-overlay intersection, if any. + overlayID = null; + palmPosition = side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); + overlayIDs = Overlays.findOverlays(palmPosition, NEAR_HOVER_RADIUS); + if (overlayIDs.length > 0) { + // Typically, there will be only one overlay; optimize for that case. + overlayID = overlayIDs[0]; + if (overlayIDs.length > 1) { + // Find closest overlay. + overlayDistance = Vec3.length(Vec3.subtract(Overlays.getProperty(overlayID, "position"), palmPosition)); + for (i = 1, length = overlayIDs.length; i < length; i += 1) { + distance = + Vec3.length(Vec3.subtract(Overlays.getProperty(overlayIDs[i], "position"), palmPosition)); + if (distance > overlayDistance) { + overlayID = overlayIDs[i]; + overlayDistance = distance; + } + } + } + } + + // Hand-entity intersection, if any, if overlay not intersected. + entityID = null; + if (overlayID === null) { + // palmPosition is set above. + entityIDs = Entities.findEntities(palmPosition, NEAR_GRAB_RADIUS); + if (entityIDs.length > 0) { + // Typically, there will be only one entity; optimize for that case. + if (isEditableRoot(entityIDs[0])) { + entityID = entityIDs[0]; + } + if (entityIDs.length > 1) { + // Find smallest, editable entity. + entitySize = HALF_TREE_SCALE; + for (i = 0, length = entityIDs.length; i < length; i += 1) { + if (isEditableRoot(entityIDs[i])) { + size = Vec3.length(Entities.getEntityProperties(entityIDs[i], "dimensions").dimensions); + if (size < entitySize) { + entityID = entityIDs[i]; + entitySize = size; + } + } + } + } + } + } + + intersection = { + intersects: overlayID !== null || entityID !== null, + overlayID: overlayID, + entityID: entityID + }; } function clear() { @@ -837,6 +922,7 @@ orientation: orientation, triggerPressed: triggerPressed, triggerClicked: triggerClicked, + intersection: getIntersection, update: update, clear: clear, destroy: destroy @@ -851,8 +937,8 @@ GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. - NEAR_GRAB_RADIUS = 0.1, // Per handControllerGrab.js. - NEAR_HOVER_RADIUS = 0.025, + //NEAR_GRAB_RADIUS = 0.1, // Per handControllerGrab.js. + //NEAR_HOVER_RADIUS = 0.025, PICK_MAX_DISTANCE = 500, // Per handControllerGrab.js. PRECISION_PICKING = true, @@ -863,9 +949,6 @@ isLaserOn = false, hoveredEntityID = null, - EDITIBLE_ENTITY_QUERY_PROPERTYES = ["parentID", "visible", "locked", "type"], - NONEDITABLE_ENTITY_TYPES = ["Unknown", "Zone", "Light"], - isEditing = false, isEditingWithHand = false, initialHandOrientationInverse, @@ -996,40 +1079,19 @@ isEditing = false; } - function isEditableEntity(entityID) { - // Entity trees are moved as a group so check the root entity. - var properties = Entities.getEntityProperties(entityID, EDITIBLE_ENTITY_QUERY_PROPERTYES); - while (properties.parentID && properties.parentID !== NULL_UUID) { - properties = Entities.getEntityProperties(properties.parentID, EDITIBLE_ENTITY_QUERY_PROPERTYES); - } - return properties.visible && !properties.locked && NONEDITABLE_ENTITY_TYPES.indexOf(properties.type) === -1; - } - function hoverHandle(overlayID) { handles.hover(overlayID); } function update() { - var palmPosition, - overlayID, - overlayIDs, - overlayDistance, - distance, - entityID, - entityIDs, - entitySize, - size, - isHandSelected, - laserIntersection, + var isHandSelected, wasLaserOn, handPosition, handOrientation, deltaOrigin, pickRay, laserLength, - wasEditing, - i, - length; + wasEditing; // Hand position and orientation. hand.update(); @@ -1046,88 +1108,39 @@ return; } - // Hand-overlay intersection, if any. - overlayID = null; - palmPosition = side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); - overlayIDs = Overlays.findOverlays(palmPosition, NEAR_HOVER_RADIUS); - if (overlayIDs.length > 0) { - // Typically, there will be only one overlay; optimize for that case. - overlayID = overlayIDs[0]; - if (overlayIDs.length > 1) { - // Find closest overlay. - overlayDistance = Vec3.length(Vec3.subtract(Overlays.getProperty(overlayID, "position"), palmPosition)); - for (i = 1, length = overlayIDs.length; i < length; i += 1) { - distance = - Vec3.length(Vec3.subtract(Overlays.getProperty(overlayIDs[i], "position"), palmPosition)); - if (distance > overlayDistance) { - overlayID = overlayIDs[i]; - overlayDistance = distance; - } - } - } - } + // Hand-overlay or -entity intersection. + intersection = hand.intersection(); + isHandSelected = intersection.intersects; - // Hand-entity intersection, if any. - // TODO: Only test intersection if overlay not intersected? - entityID = null; - // palmPosition is set above. - entityIDs = Entities.findEntities(palmPosition, NEAR_GRAB_RADIUS); - if (entityIDs.length > 0) { - // TODO: If number of entities is often 1 in practice, optimize code for this case. - // Find smallest, editable entity. - entitySize = HALF_TREE_SCALE; - for (i = 0, length = entityIDs.length; i < length; i += 1) { - if (isEditableEntity(entityIDs[i])) { - size = Vec3.length(Entities.getEntityProperties(entityIDs[i], "dimensions").dimensions); - if (size < entitySize) { - entityID = entityIDs[i]; - entitySize = size; - } - } - } - } - - isHandSelected = overlayID !== null || entityID !== null; - - // Laser-overlay or -entity intersection if not already intersected. + // Laser-overlay or -entity intersection if no hand intersection. if (!isHandSelected && hand.triggerPressed()) { handPosition = hand.position(); handOrientation = hand.orientation(); deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); pickRay = { - origin: Vec3.sum(handPosition, deltaOrigin), // Add a bit to ... + origin: Vec3.sum(handPosition, deltaOrigin), direction: Quat.getUp(handOrientation), length: PICK_MAX_DISTANCE }; - laserIntersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, VISIBLE_ONLY); - if (laserIntersection.intersects) { - overlayID = laserIntersection.overlayID; - } - if (!laserIntersection.intersects) { - laserIntersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + if (!intersection.intersects) { + intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, VISIBLE_ONLY); - if (laserIntersection.intersects && isEditableEntity(laserIntersection.entityID)) { - entityID = laserIntersection.entityID; - } else { - laserIntersection.intersects = false; + if (intersection.intersects && !isEditableRoot(intersection.entityID)) { + intersection.intersects = false; } } laserLength = isEditing ? laserEditingDistance - : (laserIntersection.intersects ? laserIntersection.distance : PICK_MAX_DISTANCE); + : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); isLaserOn = true; } else { isLaserOn = false; } - intersection = { - intersects: overlayID !== null || entityID !== null, - overlayID: overlayID, - entityID: entityID, - handSelected: isHandSelected - }; + intersection.handSelected = isHandSelected; // Laser update. if (isLaserOn) { From c422eaec11272f84edee7264b8ad64ef54121a42 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 14 Jul 2017 18:12:13 +1200 Subject: [PATCH 047/722] Move laser functionality into Laser object --- scripts/vr-edit/vr-edit.js | 179 ++++++++++++++++++++++--------------- 1 file changed, 108 insertions(+), 71 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 5c2fd7e55e..9f84d33052 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -646,7 +646,9 @@ // Draws hand lasers. // May intersect with entities or bounding box of other hand's selection. - var laserLine = null, + var isLaserOn = false, + + laserLine = null, laserSphere = null, searchDistance = 0.0, @@ -657,7 +659,20 @@ COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }, // Per handControllgerGrab.js. COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT, COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT, - BRIGHT_POW = 0.06; // Per handControllgerGrab.js. + BRIGHT_POW = 0.06, // Per handControllgerGrab.js. + + GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. + + PICK_MAX_DISTANCE = 500, // Per handControllerGrab.js. + PRECISION_PICKING = true, + NO_INCLUDE_IDS = [], + NO_EXCLUDE_IDS = [], + VISIBLE_ONLY = true, + + laserLength, + specifiedLaserLength = null, + + intersection; function colorPow(color, power) { // Per handControllerGrab.js. return { @@ -670,6 +685,10 @@ COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_HALF_SQUEEZE, BRIGHT_POW); COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_FULL_SQUEEZE, BRIGHT_POW); + if (side === LEFT_HAND) { + GRAB_POINT_SPHERE_OFFSET.x = -GRAB_POINT_SPHERE_OFFSET.x; + } + laserLine = Overlays.addOverlay("line3d", { lineWidth: 5, alpha: 1.0, @@ -715,7 +734,7 @@ }); } - function update(origin, direction, distance, isClicked) { + function display(origin, direction, distance, isClicked) { var searchTarget, sphereSize, color, @@ -731,11 +750,75 @@ updateSphere(searchTarget, sphereSize, color, brightColor); } - function clear() { + function hide() { Overlays.editOverlay(laserLine, { visible: false }); Overlays.editOverlay(laserSphere, { visible: false }); } + function update(hand) { + var handPosition, + handOrientation, + deltaOrigin, + pickRay; + + if (!hand.intersection().intersects && hand.triggerPressed()) { + handPosition = hand.position(); + handOrientation = hand.orientation(); + deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); + pickRay = { + origin: Vec3.sum(handPosition, deltaOrigin), + direction: Quat.getUp(handOrientation), + length: PICK_MAX_DISTANCE + }; + + intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + if (!intersection.intersects) { + intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + if (intersection.intersects && !isEditableRoot(intersection.entityID)) { + intersection.intersects = false; + } + } + laserLength = (specifiedLaserLength !== null) + ? specifiedLaserLength + : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); + + isLaserOn = true; + display(pickRay.origin, pickRay.direction, laserLength, hand.triggerClicked()); + } else { + intersection = { + intersects: false + }; + if (isLaserOn) { + isLaserOn = false; + hide(); + } + } + } + + function getIntersection() { + return intersection; + } + + function setLength(length) { + specifiedLaserLength = length; + laserLength = length; + } + + function clearLength() { + specifiedLaserLength = null; + } + + function getLength() { + return laserLength; + } + + function clear() { + isLaserOn = false; + hide(); + } + function destroy() { Overlays.deleteOverlay(laserLine); Overlays.deleteOverlay(laserSphere); @@ -747,6 +830,10 @@ return { update: update, + intersection: getIntersection, + setLength: setLength, + clearLength: clearLength, + length: getLength, clear: clear, destroy: destroy }; @@ -933,22 +1020,11 @@ Editor = function (side, gripPressedCallback) { // Each controller has a hand, laser, an entity selection, entity highlighter, and entity handles. - var intersection = {}, - - GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. - - //NEAR_GRAB_RADIUS = 0.1, // Per handControllerGrab.js. - //NEAR_HOVER_RADIUS = 0.025, - - PICK_MAX_DISTANCE = 500, // Per handControllerGrab.js. - PRECISION_PICKING = true, - NO_INCLUDE_IDS = [], - NO_EXCLUDE_IDS = [], - VISIBLE_ONLY = true, - - isLaserOn = false, + var isUpdating = false, hoveredEntityID = null, + intersection, + isEditing = false, isEditingWithHand = false, initialHandOrientationInverse, @@ -973,10 +1049,6 @@ highlights, handles; - if (side === LEFT_HAND) { - GRAB_POINT_SPHERE_OFFSET.x = -GRAB_POINT_SPHERE_OFFSET.x; - } - hand = new Hand(side, gripPressedCallback); laser = new Laser(side); selection = new Selection(); @@ -1085,69 +1157,33 @@ function update() { var isHandSelected, - wasLaserOn, - handPosition, - handOrientation, - deltaOrigin, - pickRay, - laserLength, wasEditing; - // Hand position and orientation. + // Hand update. + // Early return if it's not present. hand.update(); - wasLaserOn = isLaserOn; if (!hand.valid()) { - isLaserOn = false; - if (wasLaserOn) { + if (isUpdating) { laser.clear(); selection.clear(); highlights.clear(); handles.clear(); hoveredEntityID = null; + isUpdating = false; } return; } - - // Hand-overlay or -entity intersection. + isUpdating = true; intersection = hand.intersection(); isHandSelected = intersection.intersects; - // Laser-overlay or -entity intersection if no hand intersection. - if (!isHandSelected && hand.triggerPressed()) { - handPosition = hand.position(); - handOrientation = hand.orientation(); - deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); - pickRay = { - origin: Vec3.sum(handPosition, deltaOrigin), - direction: Quat.getUp(handOrientation), - length: PICK_MAX_DISTANCE - }; - - intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, - VISIBLE_ONLY); - if (!intersection.intersects) { - intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, - VISIBLE_ONLY); - if (intersection.intersects && !isEditableRoot(intersection.entityID)) { - intersection.intersects = false; - } - } - laserLength = isEditing - ? laserEditingDistance - : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); - isLaserOn = true; - } else { - isLaserOn = false; - } - - intersection.handSelected = isHandSelected; - // Laser update. - if (isLaserOn) { - laser.update(pickRay.origin, pickRay.direction, laserLength, hand.triggerClicked()); - } else if (wasLaserOn) { - laser.clear(); + // Displays laser if hand has no intersection and trigger is pressed. + laser.update(hand); + if (!isHandSelected) { + intersection = laser.intersection(); } + intersection.handSelected = isHandSelected; // Highlight / edit. doEdit = false; @@ -1164,8 +1200,9 @@ selection.select(hoveredEntityID); } highlights.clear(); - if (isLaserOn) { - laserEditingDistance = laserLength; + if (!intersection.handSelected) { + laserEditingDistance = laser.length(); + laser.setLength(laserEditingDistance); } startEditing(); if (isAppScaleWithHandles) { @@ -1177,6 +1214,7 @@ if (isEditing) { // Stop editing. stopEditing(); + laser.clearLength(); } if (isAppScaleWithHandles) { otherEditor.hoverHandle(intersection.overlayID); @@ -1223,7 +1261,6 @@ selection.clear(); highlights.clear(); handles.clear(); - isLaserOn = false; isEditing = false; isEditingWithHand = false; isScaling = false; From ae1b6e20f2c3e4ced38809f4eef6d8261ea8a439 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 17 Jul 2017 09:20:38 +1200 Subject: [PATCH 048/722] Clear out state code for rework --- scripts/vr-edit/vr-edit.js | 243 ++++--------------------------------- 1 file changed, 25 insertions(+), 218 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 9f84d33052..c4aee6c198 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -133,7 +133,7 @@ }); } - function display(handSelected, selection, isScale) { + function display(handIntersected, selection, isScale) { var overlayColor = isScale ? SCALE_HIGHLIGHT_COLOR : GRAB_HIGHLIGHT_COLOR, i, length; @@ -141,7 +141,7 @@ // Show/hide hand overlay. Overlays.editOverlay(handOverlay, { color: overlayColor, - visible: handSelected + visible: handIntersected }); // Add/edit entity overlay. @@ -778,8 +778,10 @@ VISIBLE_ONLY); if (intersection.intersects && !isEditableRoot(intersection.entityID)) { intersection.intersects = false; + intersection.entityID = null; } } + intersection.laserIntersected = true; laserLength = (specifiedLaserLength !== null) ? specifiedLaserLength : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); @@ -863,7 +865,7 @@ handPosition, handOrientation, - intersection; + intersection = {}; if (side === LEFT_HAND) { handController = Controller.Standard.LeftHand; @@ -918,6 +920,10 @@ // Hand pose. handPose = Controller.getPoseValue(handController); + if (!handPose.valid) { + intersection = {}; + return; + } handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); @@ -987,7 +993,8 @@ intersection = { intersects: overlayID !== null || entityID !== null, overlayID: overlayID, - entityID: entityID + entityID: entityID, + handIntersected: true }; } @@ -1020,34 +1027,16 @@ Editor = function (side, gripPressedCallback) { // Each controller has a hand, laser, an entity selection, entity highlighter, and entity handles. - var isUpdating = false, - hoveredEntityID = null, - - intersection, - - isEditing = false, - isEditingWithHand = false, - initialHandOrientationInverse, - initialHandToSelectionVector, - initialSelectionOrientation, - laserEditingDistance, - - isScaling = false, - initialTargetPosition, - initialTargetsCenter, - initialTargetsSeparation, - initialtargetsDirection, - - doEdit, - doHighlight, - - otherEditor, + var otherEditor, // Other hand's Editor object. + // Primary objects. hand, laser, selection, highlights, - handles; + handles, + + intersection; hand = new Hand(side, gripPressedCallback); laser = new Laser(side); @@ -1059,200 +1048,26 @@ otherEditor = editor; } - function getIsEditing(rootEntityID) { - return isEditing && rootEntityID === selection.rootEntityID(); - } - - function getTargetPosition() { - if (isEditingWithHand) { - return side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); - } - return Vec3.sum(hand.position(), Vec3.multiply(laserEditingDistance, Quat.getUp(hand.orientation()))); - } - - function startEditing() { - var selectionPositionAndOrientation, - initialOtherTargetPosition; - - initialHandOrientationInverse = Quat.inverse(hand.orientation()); - - selection.select(hoveredEntityID); // Entity may have been moved by other hand so refresh position and orientation. - selectionPositionAndOrientation = selection.getPositionAndOrientation(); - initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position()); - initialSelectionOrientation = selectionPositionAndOrientation.orientation; - - isEditingWithHand = intersection.handSelected; - - if (otherEditor.isEditing(selection.rootEntityID())) { - // Store initial values for use in scaling. - initialTargetPosition = getTargetPosition(); - initialOtherTargetPosition = otherEditor.getTargetPosition(); - initialTargetsCenter = Vec3.multiply(0.5, Vec3.sum(initialTargetPosition, initialOtherTargetPosition)); - initialTargetsSeparation = Vec3.distance(initialTargetPosition, initialOtherTargetPosition); - initialtargetsDirection = Vec3.subtract(initialOtherTargetPosition, initialTargetPosition); - selection.startScaling(initialTargetsCenter); - isScaling = true; - } else { - isScaling = false; - } - - isEditing = true; - } - - function updateGrabOffset(selectionPositionAndOrientation) { - initialHandOrientationInverse = Quat.inverse(hand.orientation()); - initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position()); - initialSelectionOrientation = selectionPositionAndOrientation.orientation; - } - - function applyGrab() { - var handPosition, - handOrientation, - deltaOrientation, - selectionPosition, - selectionOrientation; - - handPosition = hand.position(); - handOrientation = hand.orientation(); - - deltaOrientation = Quat.multiply(handOrientation, initialHandOrientationInverse); - selectionPosition = Vec3.sum(handPosition, Vec3.multiplyQbyV(deltaOrientation, initialHandToSelectionVector)); - selectionOrientation = Quat.multiply(deltaOrientation, initialSelectionOrientation); - - selection.setPositionAndOrientation(selectionPosition, selectionOrientation); - } - - function applyScale() { - var targetPosition, - otherTargetPosition, - targetsSeparation, - scale, - rotation, - center, - selectionPositionAndOrientation; - - // Scale selection. - targetPosition = getTargetPosition(); - otherTargetPosition = otherEditor.getTargetPosition(); - targetsSeparation = Vec3.distance(targetPosition, otherTargetPosition); - scale = targetsSeparation / initialTargetsSeparation; - rotation = Quat.rotationBetween(initialtargetsDirection, Vec3.subtract(otherTargetPosition, targetPosition)); - center = Vec3.multiply(0.5, Vec3.sum(targetPosition, otherTargetPosition)); - selection.scale(scale, rotation, center); - - // Update grab offsets. - selectionPositionAndOrientation = selection.getPositionAndOrientation(); - updateGrabOffset(selectionPositionAndOrientation); - otherEditor.updateGrabOffset(selectionPositionAndOrientation); - } - - function stopEditing() { - isScaling = false; - isEditing = false; - } - - function hoverHandle(overlayID) { - handles.hover(overlayID); - } - function update() { - var isHandSelected, - wasEditing; - // Hand update. - // Early return if it's not present. hand.update(); - if (!hand.valid()) { - if (isUpdating) { - laser.clear(); - selection.clear(); - highlights.clear(); - handles.clear(); - hoveredEntityID = null; - isUpdating = false; - } - return; - } - isUpdating = true; intersection = hand.intersection(); - isHandSelected = intersection.intersects; // Laser update. // Displays laser if hand has no intersection and trigger is pressed. - laser.update(hand); - if (!isHandSelected) { - intersection = laser.intersection(); + if (hand.valid()) { + laser.update(hand); + if (!intersection.intersects) { + intersection = laser.intersection(); + } } - intersection.handSelected = isHandSelected; - // Highlight / edit. - doEdit = false; - doHighlight = false; - if (hand.triggerClicked()) { - if (isEditing) { - // Perform edit. - doEdit = true; - } else if (intersection.intersects && intersection.entityID && (!isAppScaleWithHandles - || !otherEditor.isEditing(Entities.rootOf(intersection.entityID)))) { - // Start editing. - if (intersection.entityID !== hoveredEntityID) { - hoveredEntityID = intersection.entityID; - selection.select(hoveredEntityID); - } - highlights.clear(); - if (!intersection.handSelected) { - laserEditingDistance = laser.length(); - laser.setLength(laserEditingDistance); - } - startEditing(); - if (isAppScaleWithHandles) { - handles.display(selection.rootEntityID(), selection.boundingBox(), selection.count() > 1); - } - } - } else { - wasEditing = isEditing; - if (isEditing) { - // Stop editing. - stopEditing(); - laser.clearLength(); - } - if (isAppScaleWithHandles) { - otherEditor.hoverHandle(intersection.overlayID); - } - if (intersection.intersects && intersection.entityID) { - // Hover entities. - if (wasEditing || intersection.entityID !== hoveredEntityID) { - hoveredEntityID = intersection.entityID; - selection.select(hoveredEntityID); - doHighlight = true; - } - } else { - // Unhover entities. - if (hoveredEntityID) { - selection.clear(); - highlights.clear(); - handles.clear(); - hoveredEntityID = null; - } - } - } + // State update. + // TODO } function apply() { - if (doEdit) { - if (otherEditor.isEditing(selection.rootEntityID())) { - if (isScaling && !isAppScaleWithHandles) { - applyScale(); - } - } else { - applyGrab(); - } - } else if (doHighlight) { - if (!isAppScaleWithHandles || !otherEditor.isEditing(selection.rootEntityID())) { - highlights.display(intersection.handSelected, selection.selection(), - otherEditor.isEditing(selection.rootEntityID()) || isAppScaleWithHandles); - } - } + // TODO } function clear() { @@ -1261,10 +1076,6 @@ selection.clear(); highlights.clear(); handles.clear(); - isEditing = false; - isEditingWithHand = false; - isScaling = false; - hoveredEntityID = null; } function destroy() { @@ -1296,10 +1107,6 @@ return { setOtherEditor: setOtherEditor, - isEditing: getIsEditing, - hoverHandle: hoverHandle, - getTargetPosition: getTargetPosition, - updateGrabOffset: updateGrabOffset, update: update, apply: apply, clear: clear, @@ -1312,7 +1119,7 @@ // Main update loop. updateTimer = null; - // Each hand's action depends on the state of the other hand, so update the states first then apply in actions. + // Each hand's action depends on the state of the other hand, so update the states first then apply actions. editors[LEFT_HAND].update(); editors[RIGHT_HAND].update(); editors[LEFT_HAND].apply(); From cb827d9e850eb538de63057951ae445b9f2b2987 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 17 Jul 2017 11:10:50 +1200 Subject: [PATCH 049/722] Idle, searching, and highlighting states --- scripts/vr-edit/vr-edit.js | 138 ++++++++++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index c4aee6c198..a7ffc49897 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -40,7 +40,9 @@ AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", NULL_UUID = "{00000000-0000-0000-0000-000000000000}", - HALF_TREE_SCALE = 16384; + HALF_TREE_SCALE = 16384, + + DEBUG = true; // TODO: Set false. if (typeof Vec3.min !== "function") { @@ -60,6 +62,9 @@ var rootEntityID, entityProperties, PARENT_PROPERTIES = ["parentID"]; + if (entityID === undefined || entityID === null) { + return null; + } rootEntityID = entityID; entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); while (entityProperties.parentID !== NULL_UUID) { @@ -70,6 +75,11 @@ }; } + + function log(message) { + print(APP_NAME + ": " + message); + } + function isEditableRoot(entityID) { var EDITIBLE_ENTITY_QUERY_PROPERTYES = ["parentID", "visible", "locked", "type"], NONEDITABLE_ENTITY_TYPES = ["Unknown", "Zone", "Light"], @@ -1029,6 +1039,18 @@ var otherEditor, // Other hand's Editor object. + // Editor states. + EDITOR_IDLE = 0, + EDITOR_SEARCHING = 1, + EDITOR_HIGHLIGHTING = 2, // Highlighting an entity (not hovering a handle). + editorState = EDITOR_IDLE, + EDITOR_STATE_STRINGS = ["EDITOR_IDLE", "EDITOR_SEARCHING", "EDITOR_HIGHLIGHTING"], + + // State machine. + STATE_MACHINE, + highlightedEntityID = null, + wasAppScaleWithHandles = false, + // Primary objects. hand, laser, @@ -1048,7 +1070,71 @@ otherEditor = editor; } + + function enterEditorIdle() { + selection.clear(); + } + + function exitEditorIdle() { + // Nothing to do. + } + + function enterEditorSearching() { + selection.clear(); + } + + function exitEditorSearching() { + } + + function enterEditorHighlighting() { + selection.select(highlightedEntityID); + highlights.display(intersection.handIntersected, selection.selection(), isAppScaleWithHandles); + } + + function updateEditorHighlighting() { + selection.select(highlightedEntityID); + highlights.display(intersection.handIntersected, selection.selection(), isAppScaleWithHandles); + } + + function exitEditorHighlighting() { + highlights.clear(); + } + + STATE_MACHINE = { + EDITOR_IDLE: { + enter: enterEditorIdle, + update: null, + exit: exitEditorIdle + }, + EDITOR_SEARCHING: { + enter: enterEditorSearching, + update: null, + exit: exitEditorSearching + }, + EDITOR_HIGHLIGHTING: { + enter: enterEditorHighlighting, + update: updateEditorHighlighting, + exit: exitEditorHighlighting + } + }; + + function setState(state) { + if (state !== editorState) { + STATE_MACHINE[EDITOR_STATE_STRINGS[editorState]].exit(); + STATE_MACHINE[EDITOR_STATE_STRINGS[state]].enter(); + editorState = state; + } else if (DEBUG) { + log("ERROR: Null state transition: " + state + "!"); + } + } + + function updateState() { + STATE_MACHINE[EDITOR_STATE_STRINGS[editorState]].update(); + } + function update() { + var previousState = editorState; + // Hand update. hand.update(); intersection = hand.intersection(); @@ -1063,7 +1149,55 @@ } // State update. - // TODO + switch (editorState) { + case EDITOR_IDLE: + if (!hand.valid()) { + // No transition. + break; + } + setState(EDITOR_SEARCHING); + break; + case EDITOR_SEARCHING: + if (hand.valid() && !intersection.entityID) { + // No transition. + break; + } + if (!hand.valid()) { + setState(EDITOR_IDLE); + } else if (intersection.entityID) { + highlightedEntityID = intersection.entityID; + wasAppScaleWithHandles = isAppScaleWithHandles; + setState(EDITOR_HIGHLIGHTING); + } else { + DEBUG("ERROR: Unexpected condition in EDITOR_SEARCHING!"); + } + break; + case EDITOR_HIGHLIGHTING: + if (hand.valid() && intersection.entityID === highlightedEntityID + && isAppScaleWithHandles === wasAppScaleWithHandles) { + // No transition. + break; + } + if (!hand.valid()) { + setState(EDITOR_IDLE); + } else if (intersection.entityID && intersection.entityID !== highlightedEntityID) { + highlightedEntityID = intersection.entityID; + wasAppScaleWithHandles = isAppScaleWithHandles; + updateState(); + } else if (intersection.entityID && isAppScaleWithHandles !== wasAppScaleWithHandles) { + wasAppScaleWithHandles = isAppScaleWithHandles; + updateState(); + } else if (!intersection.entityID) { + setState(EDITOR_SEARCHING); + } else { + DEBUG("ERROR: Unexpected condition in EDITOR_HIGHLIGHTING!"); + } + break; + } + + if (DEBUG && editorState !== previousState) { + log((side = LEFT_HAND ? "L " : "R ") + EDITOR_STATE_STRINGS[editorState]); + } } function apply() { From 7b3956df28dfbb116535d58f3c3143fb5e018dfb Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 17 Jul 2017 12:05:36 +1200 Subject: [PATCH 050/722] Grabbing state --- scripts/vr-edit/vr-edit.js | 95 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index a7ffc49897..e916877196 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1043,8 +1043,9 @@ EDITOR_IDLE = 0, EDITOR_SEARCHING = 1, EDITOR_HIGHLIGHTING = 2, // Highlighting an entity (not hovering a handle). + EDITOR_GRABBING = 3, editorState = EDITOR_IDLE, - EDITOR_STATE_STRINGS = ["EDITOR_IDLE", "EDITOR_SEARCHING", "EDITOR_HIGHLIGHTING"], + EDITOR_STATE_STRINGS = ["EDITOR_IDLE", "EDITOR_SEARCHING", "EDITOR_HIGHLIGHTING", "EDITOR_GRABBING"], // State machine. STATE_MACHINE, @@ -1058,6 +1059,11 @@ highlights, handles, + // Position values. + initialHandOrientationInverse, + initialHandToSelectionVector, + initialSelectionOrientation, + intersection; hand = new Hand(side, gripPressedCallback); @@ -1070,6 +1076,36 @@ otherEditor = editor; } + function startEditing() { + var selectionPositionAndOrientation; + + initialHandOrientationInverse = Quat.inverse(hand.orientation()); + selectionPositionAndOrientation = selection.getPositionAndOrientation(); + initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position()); + initialSelectionOrientation = selectionPositionAndOrientation.orientation; + } + + function stopEditing() { + // Nothing to do. + } + + function applyGrab() { + var handPosition, + handOrientation, + deltaOrientation, + selectionPosition, + selectionOrientation; + + handPosition = hand.position(); + handOrientation = hand.orientation(); + + deltaOrientation = Quat.multiply(handOrientation, initialHandOrientationInverse); + selectionPosition = Vec3.sum(handPosition, Vec3.multiplyQbyV(deltaOrientation, initialHandToSelectionVector)); + selectionOrientation = Quat.multiply(deltaOrientation, initialSelectionOrientation); + + selection.setPositionAndOrientation(selectionPosition, selectionOrientation); + } + function enterEditorIdle() { selection.clear(); @@ -1100,6 +1136,19 @@ highlights.clear(); } + function enterEditorGrabbing() { + selection.select(highlightedEntityID); // For when transitioning from EDITOR_SEARCHING. + if (intersection.laserIntersected) { + laser.setLength(laser.length()); + } + startEditing(); + } + + function exitEditorGrabbing() { + stopEditing(); + laser.clearLength(); + } + STATE_MACHINE = { EDITOR_IDLE: { enter: enterEditorIdle, @@ -1115,6 +1164,11 @@ enter: enterEditorHighlighting, update: updateEditorHighlighting, exit: exitEditorHighlighting + }, + EDITOR_GRABBING: { + enter: enterEditorGrabbing, + update: null, + exit: exitEditorGrabbing } }; @@ -1132,6 +1186,7 @@ STATE_MACHINE[EDITOR_STATE_STRINGS[editorState]].update(); } + function update() { var previousState = editorState; @@ -1164,22 +1219,28 @@ } if (!hand.valid()) { setState(EDITOR_IDLE); - } else if (intersection.entityID) { + } else if (intersection.entityID && !hand.triggerClicked()) { highlightedEntityID = intersection.entityID; wasAppScaleWithHandles = isAppScaleWithHandles; setState(EDITOR_HIGHLIGHTING); + } else if (intersection.entityID && hand.triggerClicked()) { + highlightedEntityID = intersection.entityID; + wasAppScaleWithHandles = isAppScaleWithHandles; + setState(EDITOR_GRABBING); } else { DEBUG("ERROR: Unexpected condition in EDITOR_SEARCHING!"); } break; case EDITOR_HIGHLIGHTING: if (hand.valid() && intersection.entityID === highlightedEntityID - && isAppScaleWithHandles === wasAppScaleWithHandles) { + && !hand.triggerClicked() && isAppScaleWithHandles === wasAppScaleWithHandles) { // No transition. break; } if (!hand.valid()) { setState(EDITOR_IDLE); + } else if (intersection.entityID && hand.triggerClicked()) { + setState(EDITOR_GRABBING); } else if (intersection.entityID && intersection.entityID !== highlightedEntityID) { highlightedEntityID = intersection.entityID; wasAppScaleWithHandles = isAppScaleWithHandles; @@ -1193,15 +1254,39 @@ DEBUG("ERROR: Unexpected condition in EDITOR_HIGHLIGHTING!"); } break; + case EDITOR_GRABBING: + if (hand.valid() && hand.triggerClicked()) { + // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. + // No transition. + break; + } + if (!hand.valid()) { + setState(EDITOR_IDLE); + } else if (!hand.triggerClicked()) { + if (intersection.entityID) { + highlightedEntityID = intersection.entityID; + wasAppScaleWithHandles = isAppScaleWithHandles; + setState(EDITOR_HIGHLIGHTING); + } else { + setState(EDITOR_SEARCHING); + } + } else { + DEBUG("ERROR: Unexpected condition in EDITOR_GRABBING!"); + } + break; } if (DEBUG && editorState !== previousState) { - log((side = LEFT_HAND ? "L " : "R ") + EDITOR_STATE_STRINGS[editorState]); + log((side === LEFT_HAND ? "L " : "R ") + EDITOR_STATE_STRINGS[editorState]); } } function apply() { - // TODO + switch (editorState) { + case EDITOR_GRABBING: + applyGrab(); + break; + } } function clear() { From 1ad3041bae2c2b088b64b55673fdf9f8759b0ba3 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 17 Jul 2017 15:33:39 +1200 Subject: [PATCH 051/722] Direct scaling --- scripts/vr-edit/vr-edit.js | 168 ++++++++++++++++++++++++++++++++++--- 1 file changed, 155 insertions(+), 13 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index e916877196..871354c9f7 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -592,7 +592,7 @@ function startScaling(center) { scaleCenter = center; - scaleRootOffset = Vec3.subtract(selection[0].position, center); + scaleRootOffset = Vec3.subtract(rootPosition, center); scaleRootOrientation = rootOrientation; } @@ -1044,12 +1044,15 @@ EDITOR_SEARCHING = 1, EDITOR_HIGHLIGHTING = 2, // Highlighting an entity (not hovering a handle). EDITOR_GRABBING = 3, + EDITOR_DIRECT_SCALING = 4, // Scaling data are sent to other editor's EDITOR_GRABBING state. editorState = EDITOR_IDLE, - EDITOR_STATE_STRINGS = ["EDITOR_IDLE", "EDITOR_SEARCHING", "EDITOR_HIGHLIGHTING", "EDITOR_GRABBING"], + EDITOR_STATE_STRINGS = ["EDITOR_IDLE", "EDITOR_SEARCHING", "EDITOR_HIGHLIGHTING", "EDITOR_GRABBING", + "EDITOR_DIRECT_SCALING"], // State machine. STATE_MACHINE, highlightedEntityID = null, + isOtherEditorEditingEntityID = false, wasAppScaleWithHandles = false, // Primary objects. @@ -1064,6 +1067,13 @@ initialHandToSelectionVector, initialSelectionOrientation, + // Scaling values. + isScaling = false, // Modifies EDITOR_GRABBING state. + initialTargetsSeparation, + initialtargetsDirection, + otherTargetPosition, + isScalingWithHand = false, + intersection; hand = new Hand(side, gripPressedCallback); @@ -1076,6 +1086,12 @@ otherEditor = editor; } + function isEditing(rootEntityID) { + // rootEntityID is an optional parameter. + return editorState > EDITOR_HIGHLIGHTING + && (rootEntityID === undefined || rootEntityID === selection.rootEntityID()); + } + function startEditing() { var selectionPositionAndOrientation; @@ -1089,6 +1105,38 @@ // Nothing to do. } + function getScaleTargetPosition() { + if (isScalingWithHand) { + return side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); + } + return Vec3.sum(hand.position(), Vec3.multiply(laser.length(), Quat.getUp(hand.orientation()))); + } + + function startScaling(targetPosition) { + var initialTargetPosition, + initialTargetsCenter; + + isScalingWithHand = intersection.handIntersected; + + otherTargetPosition = targetPosition; + initialTargetPosition = getScaleTargetPosition(); + initialTargetsCenter = Vec3.multiply(0.5, Vec3.sum(initialTargetPosition, otherTargetPosition)); + initialTargetsSeparation = Vec3.distance(initialTargetPosition, otherTargetPosition); + initialtargetsDirection = Vec3.subtract(otherTargetPosition, initialTargetPosition); + + selection.startScaling(initialTargetsCenter); + isScaling = true; + } + + function updateScaling(targetPosition) { + otherTargetPosition = targetPosition; + } + + function stopScaling() { + isScaling = false; + } + + function applyGrab() { var handPosition, handOrientation, @@ -1106,6 +1154,29 @@ selection.setPositionAndOrientation(selectionPosition, selectionOrientation); } + function applyScale() { + var targetPosition, + targetsSeparation, + scale, + rotation, + center, + selectionPositionAndOrientation; + + // Scale selection. + targetPosition = getScaleTargetPosition(); + targetsSeparation = Vec3.distance(targetPosition, otherTargetPosition); + scale = targetsSeparation / initialTargetsSeparation; + rotation = Quat.rotationBetween(initialtargetsDirection, Vec3.subtract(otherTargetPosition, targetPosition)); + center = Vec3.multiply(0.5, Vec3.sum(targetPosition, otherTargetPosition)); + selection.scale(scale, rotation, center); + + // Update grab offset. + selectionPositionAndOrientation = selection.getPositionAndOrientation(); + initialHandOrientationInverse = Quat.inverse(hand.orientation()); + initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position()); + initialSelectionOrientation = selectionPositionAndOrientation.orientation; + } + function enterEditorIdle() { selection.clear(); @@ -1124,12 +1195,16 @@ function enterEditorHighlighting() { selection.select(highlightedEntityID); - highlights.display(intersection.handIntersected, selection.selection(), isAppScaleWithHandles); + highlights.display(intersection.handIntersected, selection.selection(), + isAppScaleWithHandles || otherEditor.isEditing(highlightedEntityID)); + isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID); } function updateEditorHighlighting() { selection.select(highlightedEntityID); - highlights.display(intersection.handIntersected, selection.selection(), isAppScaleWithHandles); + highlights.display(intersection.handIntersected, selection.selection(), + isAppScaleWithHandles || otherEditor.isEditing(highlightedEntityID)); + isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID); } function exitEditorHighlighting() { @@ -1149,6 +1224,24 @@ laser.clearLength(); } + function enterEditorDirectScaling() { + selection.select(highlightedEntityID); // For when transitioning from EDITOR_SEARCHING. + isScalingWithHand = intersection.handIntersected; + if (intersection.laserIntersected) { + laser.setLength(laser.length()); + } + otherEditor.startScaling(getScaleTargetPosition()); + } + + function updateEditorDirectScaling() { + otherEditor.updateScaling(getScaleTargetPosition()); + } + + function exitEditorDirectScaling() { + otherEditor.stopScaling(); + laser.clearLength(); + } + STATE_MACHINE = { EDITOR_IDLE: { enter: enterEditorIdle, @@ -1169,6 +1262,11 @@ enter: enterEditorGrabbing, update: null, exit: exitEditorGrabbing + }, + EDITOR_DIRECT_SCALING: { + enter: enterEditorDirectScaling, + update: updateEditorDirectScaling, + exit: exitEditorDirectScaling } }; @@ -1220,29 +1318,42 @@ if (!hand.valid()) { setState(EDITOR_IDLE); } else if (intersection.entityID && !hand.triggerClicked()) { - highlightedEntityID = intersection.entityID; + highlightedEntityID = Entities.rootOf(intersection.entityID); wasAppScaleWithHandles = isAppScaleWithHandles; setState(EDITOR_HIGHLIGHTING); } else if (intersection.entityID && hand.triggerClicked()) { - highlightedEntityID = intersection.entityID; + highlightedEntityID = Entities.rootOf(intersection.entityID); wasAppScaleWithHandles = isAppScaleWithHandles; - setState(EDITOR_GRABBING); + if (otherEditor.isEditing(highlightedEntityID)) { + setState(EDITOR_DIRECT_SCALING); + } else { + setState(EDITOR_GRABBING); + } } else { DEBUG("ERROR: Unexpected condition in EDITOR_SEARCHING!"); } break; case EDITOR_HIGHLIGHTING: - if (hand.valid() && intersection.entityID === highlightedEntityID + if (hand.valid() && Entities.rootOf(intersection.entityID) === highlightedEntityID && !hand.triggerClicked() && isAppScaleWithHandles === wasAppScaleWithHandles) { // No transition. + if (otherEditor.isEditing(highlightedEntityID) !== isOtherEditorEditingEntityID) { + updateState(); + } break; } if (!hand.valid()) { setState(EDITOR_IDLE); } else if (intersection.entityID && hand.triggerClicked()) { - setState(EDITOR_GRABBING); - } else if (intersection.entityID && intersection.entityID !== highlightedEntityID) { - highlightedEntityID = intersection.entityID; + highlightedEntityID = Entities.rootOf(intersection.entityID); // May be a different entityID. + wasAppScaleWithHandles = isAppScaleWithHandles; + if (otherEditor.isEditing(highlightedEntityID)) { + setState(EDITOR_DIRECT_SCALING); + } else { + setState(EDITOR_GRABBING); + } + } else if (intersection.entityID && Entities.rootOf(intersection.entityID) !== highlightedEntityID) { + highlightedEntityID = Entities.rootOf(intersection.entityID); wasAppScaleWithHandles = isAppScaleWithHandles; updateState(); } else if (intersection.entityID && isAppScaleWithHandles !== wasAppScaleWithHandles) { @@ -1264,7 +1375,7 @@ setState(EDITOR_IDLE); } else if (!hand.triggerClicked()) { if (intersection.entityID) { - highlightedEntityID = intersection.entityID; + highlightedEntityID = Entities.rootOf(intersection.entityID); wasAppScaleWithHandles = isAppScaleWithHandles; setState(EDITOR_HIGHLIGHTING); } else { @@ -1274,6 +1385,29 @@ DEBUG("ERROR: Unexpected condition in EDITOR_GRABBING!"); } break; + case EDITOR_DIRECT_SCALING: + if (hand.valid() && hand.triggerClicked() && otherEditor.isEditing(highlightedEntityID)) { + // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. + // No transition. + updateState(); + break; + } + if (!hand.valid()) { + setState(EDITOR_IDLE); + } else if (!hand.triggerClicked()) { + if (!intersection.entityID) { + setState(EDITOR_SEARCHING); + } else { + highlightedEntityID = Entities.rootOf(intersection.entityID); + wasAppScaleWithHandles = isAppScaleWithHandles; + setState(EDITOR_HIGHLIGHTING); + } + } else if (!otherEditor.isEditing(highlightedEntityID)) { + // Grab highlightEntityID that was scaling and has already been set. + wasAppScaleWithHandles = isAppScaleWithHandles; + setState(EDITOR_GRABBING); + } + break; } if (DEBUG && editorState !== previousState) { @@ -1284,7 +1418,11 @@ function apply() { switch (editorState) { case EDITOR_GRABBING: - applyGrab(); + if (isScaling) { + applyScale(); + } else { + applyGrab(); + } break; } } @@ -1326,6 +1464,10 @@ return { setOtherEditor: setOtherEditor, + isEditing: isEditing, + startScaling: startScaling, + updateScaling: updateScaling, + stopScaling: stopScaling, update: update, apply: apply, clear: clear, From cb894ccbcb269e2795ec103339a5082f65256f15 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 17 Jul 2017 18:02:35 +1200 Subject: [PATCH 052/722] Display and hover sizing handles --- scripts/vr-edit/vr-edit.js | 66 ++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 871354c9f7..799ffd3bf2 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -80,6 +80,12 @@ print(APP_NAME + ": " + message); } + function debug(message) { + if (DEBUG) { + log(message); + } + } + function isEditableRoot(entityID) { var EDITIBLE_ENTITY_QUERY_PROPERTYES = ["parentID", "visible", "locked", "type"], NONEDITABLE_ENTITY_TYPES = ["Unknown", "Zone", "Light"], @@ -1086,6 +1092,11 @@ otherEditor = editor; } + function hoverHandle(overlayID) { + // Highlights handle if overlayID is a handle, otherwise unhighlights currently highlighted handle if any. + handles.hover(overlayID); + } + function isEditing(rootEntityID) { // rootEntityID is an optional parameter. return editorState > EDITOR_HIGHLIGHTING @@ -1188,27 +1199,44 @@ function enterEditorSearching() { selection.clear(); + hoveredOverlayID = intersection.overlayID; + otherEditor.hoverHandle(hoveredOverlayID); + } + + function updateEditorSearching() { + if (isAppScaleWithHandles && intersection.overlayID !== hoveredOverlayID && otherEditor.isEditing()) { + hoveredOverlayID = intersection.overlayID; + otherEditor.hoverHandle(hoveredOverlayID); + } } function exitEditorSearching() { + otherEditor.hoverHandle(null); } function enterEditorHighlighting() { selection.select(highlightedEntityID); - highlights.display(intersection.handIntersected, selection.selection(), - isAppScaleWithHandles || otherEditor.isEditing(highlightedEntityID)); + if (!isAppScaleWithHandles || !otherEditor.isEditing(highlightedEntityID)) { + highlights.display(intersection.handIntersected, selection.selection(), + isAppScaleWithHandles || otherEditor.isEditing(highlightedEntityID)); + } isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID); } function updateEditorHighlighting() { selection.select(highlightedEntityID); - highlights.display(intersection.handIntersected, selection.selection(), - isAppScaleWithHandles || otherEditor.isEditing(highlightedEntityID)); - isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID); + if (!isAppScaleWithHandles || !otherEditor.isEditing(highlightedEntityID)) { + highlights.display(intersection.handIntersected, selection.selection(), + isAppScaleWithHandles || otherEditor.isEditing(highlightedEntityID)); + } else { + highlights.clear(); + } + isOtherEditorEditingEntityID = !isOtherEditorEditingEntityID; } function exitEditorHighlighting() { highlights.clear(); + isOtherEditorEditingEntityID = false; } function enterEditorGrabbing() { @@ -1216,11 +1244,23 @@ if (intersection.laserIntersected) { laser.setLength(laser.length()); } + if (isAppScaleWithHandles) { + handles.display(highlightedEntityID, selection.boundingBox(), selection.count() > 1); + } startEditing(); } + function updateEditorGrabbing() { + if (isAppScaleWithHandles) { + handles.display(highlightedEntityID, selection.boundingBox(), selection.count() > 1); + } else { + handles.clear(); + } + } + function exitEditorGrabbing() { stopEditing(); + handles.clear(); laser.clearLength(); } @@ -1250,7 +1290,7 @@ }, EDITOR_SEARCHING: { enter: enterEditorSearching, - update: null, + update: updateEditorSearching, exit: exitEditorSearching }, EDITOR_HIGHLIGHTING: { @@ -1260,7 +1300,7 @@ }, EDITOR_GRABBING: { enter: enterEditorGrabbing, - update: null, + update: updateEditorGrabbing, exit: exitEditorGrabbing }, EDITOR_DIRECT_SCALING: { @@ -1313,6 +1353,7 @@ case EDITOR_SEARCHING: if (hand.valid() && !intersection.entityID) { // No transition. + updateState(); break; } if (!hand.valid()) { @@ -1330,7 +1371,7 @@ setState(EDITOR_GRABBING); } } else { - DEBUG("ERROR: Unexpected condition in EDITOR_SEARCHING!"); + debug("ERROR: Unexpected condition in EDITOR_SEARCHING!"); } break; case EDITOR_HIGHLIGHTING: @@ -1362,13 +1403,17 @@ } else if (!intersection.entityID) { setState(EDITOR_SEARCHING); } else { - DEBUG("ERROR: Unexpected condition in EDITOR_HIGHLIGHTING!"); + debug("ERROR: Unexpected condition in EDITOR_HIGHLIGHTING!"); } break; case EDITOR_GRABBING: if (hand.valid() && hand.triggerClicked()) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // No transition. + if (isAppScaleWithHandles !== wasAppScaleWithHandles) { + wasAppScaleWithHandles = isAppScaleWithHandles; + updateState(); + } break; } if (!hand.valid()) { @@ -1382,7 +1427,7 @@ setState(EDITOR_SEARCHING); } } else { - DEBUG("ERROR: Unexpected condition in EDITOR_GRABBING!"); + debug("ERROR: Unexpected condition in EDITOR_GRABBING!"); } break; case EDITOR_DIRECT_SCALING: @@ -1464,6 +1509,7 @@ return { setOtherEditor: setOtherEditor, + hoverHandle: hoverHandle, isEditing: isEditing, startScaling: startScaling, updateScaling: updateScaling, From 22422f30597f216d8645bcbdf6228eef49254d1b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 17 Jul 2017 21:29:37 +1200 Subject: [PATCH 053/722] Scaling with handles state and transitions --- scripts/vr-edit/vr-edit.js | 90 +++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 799ffd3bf2..eb97ddf58a 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -296,6 +296,10 @@ }); } + function isHandle(overlayID) { + return cornerHandleOverlays.indexOf(overlayID) !== -1 || faceHandleOverlays.indexOf(overlayID) !== -1; + } + function display(rootEntityID, boundingBox, isMultiple) { var boundingBoxDimensions = boundingBox.dimensions, boundingBoxCenter = boundingBox.center, @@ -423,6 +427,7 @@ return { display: display, + isHandle: isHandle, hover: hover, clear: clear, destroy: destroy @@ -1051,15 +1056,17 @@ EDITOR_HIGHLIGHTING = 2, // Highlighting an entity (not hovering a handle). EDITOR_GRABBING = 3, EDITOR_DIRECT_SCALING = 4, // Scaling data are sent to other editor's EDITOR_GRABBING state. + EDITOR_HANDLE_SCALING = 5, // "" editorState = EDITOR_IDLE, EDITOR_STATE_STRINGS = ["EDITOR_IDLE", "EDITOR_SEARCHING", "EDITOR_HIGHLIGHTING", "EDITOR_GRABBING", - "EDITOR_DIRECT_SCALING"], + "EDITOR_DIRECT_SCALING", "EDITOR_HANDLE_SCALING"], // State machine. STATE_MACHINE, highlightedEntityID = null, - isOtherEditorEditingEntityID = false, wasAppScaleWithHandles = false, + isOtherEditorEditingEntityID = false, + hoveredOverlayID = null, // Primary objects. hand, @@ -1097,6 +1104,10 @@ handles.hover(overlayID); } + function isHandle(overlayID) { + return handles.isHandle(overlayID); + } + function isEditing(rootEntityID) { // rootEntityID is an optional parameter. return editorState > EDITOR_HIGHLIGHTING @@ -1116,6 +1127,7 @@ // Nothing to do. } + function getScaleTargetPosition() { if (isScalingWithHand) { return side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); @@ -1282,6 +1294,27 @@ laser.clearLength(); } + function enterEditorHandleScaling() { + selection.select(highlightedEntityID); // For when transitioning from EDITOR_SEARCHING. + isScalingWithHand = intersection.handIntersected; + if (intersection.laserIntersected) { + laser.setLength(laser.length()); + } + // TODO + //otherEditor.startHandleScaling(getScaleTargetPosition()); + } + + function updateEditorHandleScaling() { + // TODO + //otherEditor.updateScaling(getScaleTargetPosition()); + } + + function exitEditorHandleScaling() { + // TODO + //otherEditor.stopHandleScaling(); + laser.clearLength(); + } + STATE_MACHINE = { EDITOR_IDLE: { enter: enterEditorIdle, @@ -1307,6 +1340,11 @@ enter: enterEditorDirectScaling, update: updateEditorDirectScaling, exit: exitEditorDirectScaling + }, + EDITOR_HANDLE_SCALING: { + enter: enterEditorHandleScaling, + update: updateEditorHandleScaling, + exit: exitEditorHandleScaling } }; @@ -1351,13 +1389,17 @@ setState(EDITOR_SEARCHING); break; case EDITOR_SEARCHING: - if (hand.valid() && !intersection.entityID) { + if (hand.valid() && !intersection.entityID + && !(intersection.overlayID && hand.triggerClicked())) { // No transition. updateState(); break; } if (!hand.valid()) { setState(EDITOR_IDLE); + } else if (intersection.overlayID && hand.triggerClicked() + && otherEditor.isHandle(intersection.overlayID)) { + setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && !hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); wasAppScaleWithHandles = isAppScaleWithHandles; @@ -1366,7 +1408,9 @@ highlightedEntityID = Entities.rootOf(intersection.entityID); wasAppScaleWithHandles = isAppScaleWithHandles; if (otherEditor.isEditing(highlightedEntityID)) { - setState(EDITOR_DIRECT_SCALING); + if (!isAppScaleWithHandles) { + setState(EDITOR_DIRECT_SCALING); + } } else { setState(EDITOR_GRABBING); } @@ -1385,11 +1429,16 @@ } if (!hand.valid()) { setState(EDITOR_IDLE); + } else if (intersection.overlayID && hand.triggerClicked() + && otherEditor.isHandle(intersection.overlayID)) { + setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); // May be a different entityID. wasAppScaleWithHandles = isAppScaleWithHandles; if (otherEditor.isEditing(highlightedEntityID)) { - setState(EDITOR_DIRECT_SCALING); + if (!isAppScaleWithHandles) { + setState(EDITOR_DIRECT_SCALING); + } } else { setState(EDITOR_GRABBING); } @@ -1401,6 +1450,7 @@ wasAppScaleWithHandles = isAppScaleWithHandles; updateState(); } else if (!intersection.entityID) { + // Note that this transition includes the case of highlighting a scaling handle. setState(EDITOR_SEARCHING); } else { debug("ERROR: Unexpected condition in EDITOR_HIGHLIGHTING!"); @@ -1433,6 +1483,33 @@ case EDITOR_DIRECT_SCALING: if (hand.valid() && hand.triggerClicked() && otherEditor.isEditing(highlightedEntityID)) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. + // Don't test isAppScaleWithHandles because this will eventually be a UI element and so not able to be + // changed while scaling with two hands. + // No transition. + updateState(); + break; + } + if (!hand.valid()) { + setState(EDITOR_IDLE); + } else if (!hand.triggerClicked()) { + if (!intersection.entityID) { + setState(EDITOR_SEARCHING); + } else { + highlightedEntityID = Entities.rootOf(intersection.entityID); + wasAppScaleWithHandles = isAppScaleWithHandles; + setState(EDITOR_HIGHLIGHTING); + } + } else if (!otherEditor.isEditing(highlightedEntityID)) { + // Grab highlightEntityID that was scaling and has already been set. + wasAppScaleWithHandles = isAppScaleWithHandles; + setState(EDITOR_GRABBING); + } + break; + case EDITOR_HANDLE_SCALING: + if (hand.valid() && hand.triggerClicked() && otherEditor.isEditing(highlightedEntityID)) { + // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. + // Don't test isAppScaleWithHandles because this will eventually be a UI element and so not able to be + // changed while scaling with two hands. // No transition. updateState(); break; @@ -1456,7 +1533,7 @@ } if (DEBUG && editorState !== previousState) { - log((side === LEFT_HAND ? "L " : "R ") + EDITOR_STATE_STRINGS[editorState]); + debug((side === LEFT_HAND ? "L " : "R ") + EDITOR_STATE_STRINGS[editorState]); } } @@ -1510,6 +1587,7 @@ return { setOtherEditor: setOtherEditor, hoverHandle: hoverHandle, + isHandle: isHandle, isEditing: isEditing, startScaling: startScaling, updateScaling: updateScaling, From 74dccace6a2e8678d1cb64c8d713b463a308ea43 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 17 Jul 2017 21:57:32 +1200 Subject: [PATCH 054/722] Grab handles state while scaling with handles --- scripts/vr-edit/vr-edit.js | 75 ++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index eb97ddf58a..b04e828ec0 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -404,6 +404,30 @@ } } + function grab(overlayID) { + var overlay, + isShowAll = overlayID === null, + color = isShowAll ? HANDLE_NORMAL_COLOR : HANDLE_HOVER_COLOR, + i, + length; + + for (i = 0, length = cornerHandleOverlays.length; i < length; i += 1) { + overlay = cornerHandleOverlays[i]; + Overlays.editOverlay(overlay, { + visible: isShowAll || overlay === overlayID, + color: color + }); + } + + for (i = 0, length = faceHandleOverlays.length; i < length; i += 1) { + overlay = faceHandleOverlays[i]; + Overlays.editOverlay(overlay, { + visible: isShowAll || overlay === overlayID, + color: color + }); + } + } + function clear() { var i; @@ -429,6 +453,7 @@ display: display, isHandle: isHandle, hover: hover, + grab: grab, clear: clear, destroy: destroy }; @@ -1135,7 +1160,7 @@ return Vec3.sum(hand.position(), Vec3.multiply(laser.length(), Quat.getUp(hand.orientation()))); } - function startScaling(targetPosition) { + function startDirectScaling(targetPosition) { var initialTargetPosition, initialTargetsCenter; @@ -1151,14 +1176,32 @@ isScaling = true; } - function updateScaling(targetPosition) { + function updateDirectScaling(targetPosition) { otherTargetPosition = targetPosition; } - function stopScaling() { + function stopDirectScaling() { isScaling = false; } + function startHandleScaling(targetPosition, overlayID) { + // Keep grabbed handle highlighted and hide other handles. + handles.grab(overlayID); + + // TODO + } + + function updateHandleScaling(targetPosition) { + // TODO + } + + function stopHandleScaling() { + // Stop highlighting grabbed handle and resume displaying all handles. + handles.grab(null); + + // TODO + } + function applyGrab() { var handPosition, @@ -1282,15 +1325,15 @@ if (intersection.laserIntersected) { laser.setLength(laser.length()); } - otherEditor.startScaling(getScaleTargetPosition()); + otherEditor.startDirectScaling(getScaleTargetPosition()); } function updateEditorDirectScaling() { - otherEditor.updateScaling(getScaleTargetPosition()); + otherEditor.updateDirectScaling(getScaleTargetPosition()); } function exitEditorDirectScaling() { - otherEditor.stopScaling(); + otherEditor.stopDirectScaling(); laser.clearLength(); } @@ -1300,18 +1343,15 @@ if (intersection.laserIntersected) { laser.setLength(laser.length()); } - // TODO - //otherEditor.startHandleScaling(getScaleTargetPosition()); + otherEditor.startHandleScaling(getScaleTargetPosition(), intersection.overlayID); } function updateEditorHandleScaling() { - // TODO - //otherEditor.updateScaling(getScaleTargetPosition()); + otherEditor.updateHandleScaling(getScaleTargetPosition()); } function exitEditorHandleScaling() { - // TODO - //otherEditor.stopHandleScaling(); + otherEditor.stopHandleScaling(); laser.clearLength(); } @@ -1390,7 +1430,7 @@ break; case EDITOR_SEARCHING: if (hand.valid() && !intersection.entityID - && !(intersection.overlayID && hand.triggerClicked())) { + && !(intersection.overlayID && hand.triggerClicked())) { // No transition. updateState(); break; @@ -1589,9 +1629,12 @@ hoverHandle: hoverHandle, isHandle: isHandle, isEditing: isEditing, - startScaling: startScaling, - updateScaling: updateScaling, - stopScaling: stopScaling, + startDirectScaling: startDirectScaling, + updateDirectScaling: updateDirectScaling, + stopDirectScaling: stopDirectScaling, + startHandleScaling: startHandleScaling, + updateHandleScaling: updateHandleScaling, + stopHandleScaling: stopHandleScaling, update: update, apply: apply, clear: clear, From 718d7a112003c0e47eff7051ba08cd472e3ee6da Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 18 Jul 2017 16:59:54 +1200 Subject: [PATCH 055/722] Scale with handles first pass --- scripts/vr-edit/vr-edit.js | 192 ++++++++++++++++++++++++++++++------- 1 file changed, 160 insertions(+), 32 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index b04e828ec0..aaadf93e61 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -206,6 +206,7 @@ Handles = function () { var boundingBoxOverlay, + boundingBoxDimensions, cornerIndexes = [], cornerHandleOverlays = [], faceHandleOverlays = [], @@ -223,6 +224,7 @@ FACE_HANDLE_OVERLAY_AXES, FACE_HANDLE_OVERLAY_OFFSETS, FACE_HANDLE_OVERLAY_ROTATIONS, + FACE_HANDLE_OVERLAY_SCALE_AXES, ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), DISTANCE_MULTIPLIER_MULTIPLIER = 0.5, hoveredOverlayID = null, @@ -264,6 +266,15 @@ Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) ]; + FACE_HANDLE_OVERLAY_SCALE_AXES = [ + Vec3.UNIT_X, + Vec3.UNIT_X, + Vec3.UNIT_Y, + Vec3.UNIT_Y, + Vec3.UNIT_Z, + Vec3.UNIT_Z + ]; + boundingBoxOverlay = Overlays.addOverlay("cube", { color: BOUNDING_BOX_COLOR, alpha: BOUNDING_BOX_ALPHA, @@ -296,15 +307,37 @@ }); } + function isAxisHandle(overlayID) { + return faceHandleOverlays.indexOf(overlayID) !== -1; + } + + function isCornerHandle(overlayID) { + return cornerHandleOverlays.indexOf(overlayID) !== -1; + } + function isHandle(overlayID) { - return cornerHandleOverlays.indexOf(overlayID) !== -1 || faceHandleOverlays.indexOf(overlayID) !== -1; + return isAxisHandle(overlayID) || isCornerHandle(overlayID); + } + + function scalingAxis(overlayID) { + if (isCornerHandle(overlayID)) { + return Vec3.normalize(Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerHandleOverlays.indexOf(overlayID)], + boundingBoxDimensions)); + } + return FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)]; + } + + function scalingDirections(overlayID) { + if (isCornerHandle(overlayID)) { + return Vec3.ONE; + } + return FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)]; } function display(rootEntityID, boundingBox, isMultiple) { - var boundingBoxDimensions = boundingBox.dimensions, - boundingBoxCenter = boundingBox.center, - boundingBoxLocalCenter = boundingBox.localCenter, - boundingBoxOrientation = boundingBox.orientation, + var boundingBoxCenter, + boundingBoxLocalCenter, + boundingBoxOrientation, cameraPosition, boundingBoxVector, distanceMultiplier, @@ -320,6 +353,11 @@ faceHandleOffsets, i; + boundingBoxDimensions = boundingBox.dimensions; + boundingBoxCenter = boundingBox.center; + boundingBoxLocalCenter = boundingBox.localCenter; + boundingBoxOrientation = boundingBox.orientation; + // Selection bounding box. Overlays.editOverlay(boundingBoxOverlay, { parentID: rootEntityID, @@ -452,6 +490,8 @@ return { display: display, isHandle: isHandle, + scalingAxis: scalingAxis, + scalingDirections: scalingDirections, hover: hover, grab: grab, clear: clear, @@ -467,9 +507,13 @@ rootEntityID = null, rootPosition, rootOrientation, + scaleRootPosition, + scaleRootRegistrationPoint, + scaleRootRegistrationOffset, scaleCenter, scaleRootOffset, scaleRootOrientation, + isRootCenterRegistration, ENTITY_TYPE = "entity"; function traverseEntityTree(id, result) { @@ -626,27 +670,25 @@ }); } - function startScaling(center) { + function startDirectScaling(center) { + // Save initial position and orientation so that can scale relative to these without accumulating float errors. scaleCenter = center; scaleRootOffset = Vec3.subtract(rootPosition, center); scaleRootOrientation = rootOrientation; } - function scale(factor, rotation, center) { - var position, - orientation, - i, + function directScale(factor, rotation, center) { + // Scale, position, and rotate selection. + var i, length; - // Position root. - position = Vec3.sum(center, Vec3.multiply(factor, Vec3.multiplyQbyV(rotation, scaleRootOffset))); - orientation = Quat.multiply(rotation, scaleRootOrientation); - rootPosition = position; - rootOrientation = orientation; + // Scale, position, and orient root. + rootPosition = Vec3.sum(center, Vec3.multiply(factor, Vec3.multiplyQbyV(rotation, scaleRootOffset))); + rootOrientation = Quat.multiply(rotation, scaleRootOrientation); Entities.editEntity(selection[0].id, { dimensions: Vec3.multiply(factor, selection[0].dimensions), - position: position, - rotation: orientation + position: rootPosition, + rotation: rootOrientation }); // Scale and position children. @@ -658,6 +700,34 @@ } } + function startHandleScaling() { + // Save initial position data so that can scale relative to these without accumulating float errors. + scaleRootPosition = rootPosition; + scaleRootRegistrationPoint = selection[0].registrationPoint; + isRootCenterRegistration = Vec3.equal(scaleRootRegistrationPoint, Vec3.HALF); + scaleRootRegistrationOffset = Vec3.subtract(scaleRootRegistrationPoint, Vec3.HALF); + } + + function handleScale(factor) { + // Scale selection about bounding box center. + + // Scale and position root. + if (isRootCenterRegistration) { + Entities.editEntity(selection[0].id, { + dimensions: Vec3.multiplyVbyV(factor, selection[0].dimensions) + }); + } else { + rootPosition = Vec3.sum(scaleRootPosition, Vec3.multiplyVbyV(factor, scaleRootRegistrationOffset)); + Entities.editEntity(selection[0].id, { + dimensions: Vec3.multiplyVbyV(factor, selection[0].dimensions), + position: rootPosition + }); + } + + // Scale and position children. + // TODO + } + function clear() { selection = []; selectedEntityID = null; @@ -680,8 +750,10 @@ boundingBox: getBoundingBox, getPositionAndOrientation: getPositionAndOrientation, setPositionAndOrientation: setPositionAndOrientation, - startScaling: startScaling, - scale: scale, + startDirectScaling: startDirectScaling, + directScale: directScale, + startHandleScaling: startHandleScaling, + handleScale: handleScale, clear: clear, destroy: destroy }; @@ -1106,11 +1178,16 @@ initialSelectionOrientation, // Scaling values. - isScaling = false, // Modifies EDITOR_GRABBING state. + isScalingWithHand = false, + isDirectScaling = false, // Modifies EDITOR_GRABBING state. + isHandleScaling = false, // "" initialTargetsSeparation, initialtargetsDirection, otherTargetPosition, - isScalingWithHand = false, + handleUnitScaleAxis, + handleScaleDirections, + handleHandOffset, + initialHandleDistance, intersection; @@ -1172,8 +1249,8 @@ initialTargetsSeparation = Vec3.distance(initialTargetPosition, otherTargetPosition); initialtargetsDirection = Vec3.subtract(otherTargetPosition, initialTargetPosition); - selection.startScaling(initialTargetsCenter); - isScaling = true; + selection.startDirectScaling(initialTargetsCenter); + isDirectScaling = true; } function updateDirectScaling(targetPosition) { @@ -1181,29 +1258,49 @@ } function stopDirectScaling() { - isScaling = false; + isDirectScaling = false; } function startHandleScaling(targetPosition, overlayID) { + var boundingBox, + scaleAxis, + handDistance; + + isScalingWithHand = intersection.handIntersected; + // Keep grabbed handle highlighted and hide other handles. handles.grab(overlayID); - // TODO + handleUnitScaleAxis = handles.scalingAxis(overlayID); // Unit vector in direction of scaling. + handleScaleDirections = handles.scalingDirections(overlayID); // Which axes to scale the selection on. + + // Distance from handle to bounding box center. + boundingBox = selection.boundingBox(); + initialHandleDistance = Vec3.length(Vec3.multiplyVbyV(boundingBox.dimensions, handleScaleDirections)) / 2; + + // Distance from hand to handle in direction of handle. + otherTargetPosition = targetPosition; + scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis); + handDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, boundingBox.center), scaleAxis)); + handleHandOffset = handDistance - initialHandleDistance; + + selection.startHandleScaling(); + isHandleScaling = true; } function updateHandleScaling(targetPosition) { - // TODO + otherTargetPosition = targetPosition; } function stopHandleScaling() { // Stop highlighting grabbed handle and resume displaying all handles. handles.grab(null); - - // TODO + isHandleScaling = false; } function applyGrab() { + // Sets position and orientation of selection per grabbing hand. var handPosition, handOrientation, deltaOrientation, @@ -1220,7 +1317,8 @@ selection.setPositionAndOrientation(selectionPosition, selectionOrientation); } - function applyScale() { + function applyDirectScale() { + // Scales, rotates, and positions selection per changing length, orientation, and position of vector between hands. var targetPosition, targetsSeparation, scale, @@ -1234,7 +1332,7 @@ scale = targetsSeparation / initialTargetsSeparation; rotation = Quat.rotationBetween(initialtargetsDirection, Vec3.subtract(otherTargetPosition, targetPosition)); center = Vec3.multiply(0.5, Vec3.sum(targetPosition, otherTargetPosition)); - selection.scale(scale, rotation, center); + selection.directScale(scale, rotation, center); // Update grab offset. selectionPositionAndOrientation = selection.getPositionAndOrientation(); @@ -1243,6 +1341,34 @@ initialSelectionOrientation = selectionPositionAndOrientation.orientation; } + function applyHandleScale() { + // Scales selection per changing position of scaling hand; positions and orients per grabbing hand. + var boundingBoxCenter, + scaleAxis, + handleDistance, + scale, + scale3D; + + // Position and orient selection per grabbing hand before scaling it per scaling hand. + applyGrab(); + + // Desired distance of handle from center of bounding box. + boundingBoxCenter = selection.boundingBox().center; // TODO: Too expensive for update loop? + scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis); + handleDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, boundingBoxCenter), scaleAxis)); + handleDistance -= handleHandOffset; + + // Scale selection relative to initial dimensions. + scale = handleDistance / initialHandleDistance; + scale3D = Vec3.multiply(scale, handleScaleDirections); + scale3D = { + x: scale3D.x !== 0 ? scale3D.x : 1, + y: scale3D.y !== 0 ? scale3D.y : 1, + z: scale3D.z !== 0 ? scale3D.z : 1 + }; + selection.handleScale(scale3D); + } + function enterEditorIdle() { selection.clear(); @@ -1580,8 +1706,10 @@ function apply() { switch (editorState) { case EDITOR_GRABBING: - if (isScaling) { - applyScale(); + if (isDirectScaling) { + applyDirectScale(); + } else if (isHandleScaling) { + applyHandleScale(); } else { applyGrab(); } From 485190456da5f0d5fd73fd5fe4a7bf6fece28cb1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 18 Jul 2017 20:29:02 +1200 Subject: [PATCH 056/722] Update handles when scale --- scripts/vr-edit/vr-edit.js | 63 +++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index aaadf93e61..d0ae3dbe7c 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -207,9 +207,11 @@ Handles = function () { var boundingBoxOverlay, boundingBoxDimensions, + boundingBoxLocalCenter, cornerIndexes = [], cornerHandleOverlays = [], faceHandleOverlays = [], + faceHandleOffsets, BOUNDING_BOX_COLOR = { red: 0, green: 240, blue: 240 }, BOUNDING_BOX_ALPHA = 0.8, HANDLE_NORMAL_COLOR = { red: 0, green: 240, blue: 240 }, @@ -228,6 +230,11 @@ ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), DISTANCE_MULTIPLIER_MULTIPLIER = 0.5, hoveredOverlayID = null, + + // Scaling. + scalingBoundingBoxDimensions, + scalingBoundingBoxLocalCenter, + i; CORNER_HANDLE_OVERLAY_AXES = [ @@ -336,7 +343,6 @@ function display(rootEntityID, boundingBox, isMultiple) { var boundingBoxCenter, - boundingBoxLocalCenter, boundingBoxOrientation, cameraPosition, boundingBoxVector, @@ -350,7 +356,6 @@ leftCornerIndex, cornerHandleDimensions, faceHandleDimensions, - faceHandleOffsets, i; boundingBoxDimensions = boundingBox.dimensions; @@ -427,6 +432,45 @@ } } + function startScaling() { + // Nothing to do. + } + + function scale(scale3D) { + // Scale relative to dimensions and positions at start of scaling. + + // Selection bounding box. + scalingBoundingBoxDimensions = Vec3.multiplyVbyV(scale3D, boundingBoxLocalCenter); + scalingBoundingBoxLocalCenter = Vec3.multiplyVbyV(scale3D, boundingBoxDimensions); + Overlays.editOverlay(boundingBoxOverlay, { + localPosition: scalingBoundingBoxDimensions, + dimensions: scalingBoundingBoxLocalCenter + }); + + // Corner scale handles. + for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { + Overlays.editOverlay(cornerHandleOverlays[i], { + localPosition: Vec3.sum(scalingBoundingBoxDimensions, + Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[i]], scalingBoundingBoxLocalCenter)) + }); + } + + // Face scale handles. + for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + Overlays.editOverlay(faceHandleOverlays[i], { + localPosition: Vec3.sum(scalingBoundingBoxDimensions, + Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], + Vec3.sum(scalingBoundingBoxLocalCenter, faceHandleOffsets))) + }); + } + } + + function finishScaling() { + // Adopt final scale. + boundingBoxLocalCenter = scalingBoundingBoxDimensions; + boundingBoxDimensions = scalingBoundingBoxLocalCenter; + } + function hover(overlayID) { if (overlayID !== hoveredOverlayID) { if (hoveredOverlayID !== null) { @@ -492,6 +536,9 @@ isHandle: isHandle, scalingAxis: scalingAxis, scalingDirections: scalingDirections, + startScaling: startScaling, + scale: scale, + finishScaling: finishScaling, hover: hover, grab: grab, clear: clear, @@ -728,6 +775,10 @@ // TODO } + function finishHandleScaling() { + select(selectedEntityID); // Refresh. + } + function clear() { selection = []; selectedEntityID = null; @@ -754,6 +805,7 @@ directScale: directScale, startHandleScaling: startHandleScaling, handleScale: handleScale, + finishHandleScaling: finishHandleScaling, clear: clear, destroy: destroy }; @@ -1285,6 +1337,7 @@ handleHandOffset = handDistance - initialHandleDistance; selection.startHandleScaling(); + handles.startScaling(); isHandleScaling = true; } @@ -1293,8 +1346,9 @@ } function stopHandleScaling() { - // Stop highlighting grabbed handle and resume displaying all handles. - handles.grab(null); + handles.finishScaling(); + selection.finishHandleScaling(); + handles.grab(null); // Stop highlighting grabbed handle and resume displaying all handles. isHandleScaling = false; } @@ -1366,6 +1420,7 @@ y: scale3D.y !== 0 ? scale3D.y : 1, z: scale3D.z !== 0 ? scale3D.z : 1 }; + handles.scale(scale3D); selection.handleScale(scale3D); } From 3d69e240a96a3d2a0a4c550ace80a27a1fc91a8a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 18 Jul 2017 20:29:41 +1200 Subject: [PATCH 057/722] Fix entity losing previous scale when resume direct scaling --- scripts/vr-edit/vr-edit.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index d0ae3dbe7c..c339a69e40 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -747,6 +747,10 @@ } } + function finishDirectScaling() { + select(selectedEntityID); // Refresh. + } + function startHandleScaling() { // Save initial position data so that can scale relative to these without accumulating float errors. scaleRootPosition = rootPosition; @@ -803,6 +807,7 @@ setPositionAndOrientation: setPositionAndOrientation, startDirectScaling: startDirectScaling, directScale: directScale, + finishDirectScaling: finishDirectScaling, startHandleScaling: startHandleScaling, handleScale: handleScale, finishHandleScaling: finishHandleScaling, @@ -1310,6 +1315,7 @@ } function stopDirectScaling() { + selection.finishDirectScaling(); isDirectScaling = false; } From 4c98cd26a4f89df75e5244c1ec66fe6795a158bb Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 18 Jul 2017 20:48:16 +1200 Subject: [PATCH 058/722] Fix corner handle scaling --- scripts/vr-edit/vr-edit.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index c339a69e40..2b789ace9b 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -327,9 +327,10 @@ } function scalingAxis(overlayID) { + var axesIndex; if (isCornerHandle(overlayID)) { - return Vec3.normalize(Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerHandleOverlays.indexOf(overlayID)], - boundingBoxDimensions)); + axesIndex = CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[cornerHandleOverlays.indexOf(overlayID)]]; + return Vec3.normalize(Vec3.multiplyVbyV(axesIndex, boundingBoxDimensions)); } return FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)]; } From 0f64da23bdef5d9bc1330543a096b8f5a8663fb0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 19 Jul 2017 12:09:52 +1200 Subject: [PATCH 059/722] Improve debug --- scripts/vr-edit/vr-edit.js | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 2b789ace9b..0d76aaf14c 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -80,9 +80,17 @@ print(APP_NAME + ": " + message); } - function debug(message) { + function debug(side, message) { + // Optional parameter: side. + var hand = "", + HAND_LETTERS = ["L", "R"]; if (DEBUG) { - log(message); + if (side === 0 || side === 1) { + hand = HAND_LETTERS[side] + " "; + } else { + message = side; + } + log(hand + message); } } @@ -204,7 +212,7 @@ }; - Handles = function () { + Handles = function (side) { var boundingBoxOverlay, boundingBoxDimensions, boundingBoxLocalCenter, @@ -529,7 +537,7 @@ } if (!this instanceof Handles) { - return new Handles(); + return new Handles(side); } return { @@ -548,7 +556,7 @@ }; - Selection = function () { + Selection = function (side) { // Manages set of selected entities. Currently supports just one set of linked entities. var selection = [], selectedEntityID = null, @@ -795,7 +803,7 @@ } if (!this instanceof Selection) { - return new Selection(); + return new Selection(side); } return { @@ -1003,7 +1011,7 @@ } if (!this instanceof Laser) { - return new Laser(); + return new Laser(side); } return { @@ -1183,7 +1191,7 @@ } if (!this instanceof Hand) { - return new Hand(); + return new Hand(side); } return { @@ -1251,9 +1259,9 @@ hand = new Hand(side, gripPressedCallback); laser = new Laser(side); - selection = new Selection(); + selection = new Selection(side); highlights = new Highlights(side); - handles = new Handles(); + handles = new Handles(side); function setOtherEditor(editor) { otherEditor = editor; @@ -1643,7 +1651,7 @@ setState(EDITOR_GRABBING); } } else { - debug("ERROR: Unexpected condition in EDITOR_SEARCHING!"); + debug(side, "ERROR: Unexpected condition in EDITOR_SEARCHING!"); } break; case EDITOR_HIGHLIGHTING: @@ -1681,7 +1689,7 @@ // Note that this transition includes the case of highlighting a scaling handle. setState(EDITOR_SEARCHING); } else { - debug("ERROR: Unexpected condition in EDITOR_HIGHLIGHTING!"); + debug(side, "ERROR: Unexpected condition in EDITOR_HIGHLIGHTING!"); } break; case EDITOR_GRABBING: @@ -1705,7 +1713,7 @@ setState(EDITOR_SEARCHING); } } else { - debug("ERROR: Unexpected condition in EDITOR_GRABBING!"); + debug(side, "ERROR: Unexpected condition in EDITOR_GRABBING!"); } break; case EDITOR_DIRECT_SCALING: @@ -1761,7 +1769,7 @@ } if (DEBUG && editorState !== previousState) { - debug((side === LEFT_HAND ? "L " : "R ") + EDITOR_STATE_STRINGS[editorState]); + debug(side, EDITOR_STATE_STRINGS[editorState]); } } From ce6e711f2d7f71f75583d56c507d403d15bec32f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 19 Jul 2017 12:12:20 +1200 Subject: [PATCH 060/722] Fix old scale handles displaying when switch hands --- scripts/vr-edit/vr-edit.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 0d76aaf14c..326d251659 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -238,6 +238,7 @@ ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), DISTANCE_MULTIPLIER_MULTIPLIER = 0.5, hoveredOverlayID = null, + isVisible = false, // Scaling. scalingBoundingBoxDimensions, @@ -367,6 +368,8 @@ faceHandleDimensions, i; + isVisible = true; + boundingBoxDimensions = boundingBox.dimensions; boundingBoxCenter = boundingBox.center; boundingBoxLocalCenter = boundingBox.localCenter; @@ -505,7 +508,7 @@ for (i = 0, length = cornerHandleOverlays.length; i < length; i += 1) { overlay = cornerHandleOverlays[i]; Overlays.editOverlay(overlay, { - visible: isShowAll || overlay === overlayID, + visible: isVisible && (isShowAll || overlay === overlayID), color: color }); } @@ -513,7 +516,7 @@ for (i = 0, length = faceHandleOverlays.length; i < length; i += 1) { overlay = faceHandleOverlays[i]; Overlays.editOverlay(overlay, { - visible: isShowAll || overlay === overlayID, + visible: isVisible && (isShowAll || overlay === overlayID), color: color }); } @@ -522,6 +525,8 @@ function clear() { var i; + isVisible = false; + Overlays.editOverlay(boundingBoxOverlay, { visible: false }); for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { Overlays.editOverlay(cornerHandleOverlays[i], { visible: false }); From 5cb5c71966bf79909a00c94b121f3d1562499138 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 19 Jul 2017 16:33:05 +1200 Subject: [PATCH 061/722] Avoid bounding box center calcs --- scripts/vr-edit/vr-edit.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 326d251659..573446efcd 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1254,11 +1254,13 @@ isHandleScaling = false, // "" initialTargetsSeparation, initialtargetsDirection, + initialTargetToBoundingBoxCenter, otherTargetPosition, handleUnitScaleAxis, handleScaleDirections, handleHandOffset, initialHandleDistance, + initialHandleScaleOrientationInverse, intersection; @@ -1334,24 +1336,33 @@ } function startHandleScaling(targetPosition, overlayID) { - var boundingBox, + var initialTargetPosition, + boundingBox, scaleAxis, handDistance; isScalingWithHand = intersection.handIntersected; + otherTargetPosition = targetPosition; + // Keep grabbed handle highlighted and hide other handles. handles.grab(overlayID); handleUnitScaleAxis = handles.scalingAxis(overlayID); // Unit vector in direction of scaling. handleScaleDirections = handles.scalingDirections(overlayID); // Which axes to scale the selection on. - // Distance from handle to bounding box center. + // Vector from target to bounding box center. + initialTargetPosition = getScaleTargetPosition(); boundingBox = selection.boundingBox(); + initialTargetToBoundingBoxCenter = Vec3.subtract(boundingBox.center, initialTargetPosition); + + // Initial hand orientation. + initialHandleScaleOrientationInverse = Quat.inverse(hand.orientation()); + + // Distance from handle to bounding box center. initialHandleDistance = Vec3.length(Vec3.multiplyVbyV(boundingBox.dimensions, handleScaleDirections)) / 2; // Distance from hand to handle in direction of handle. - otherTargetPosition = targetPosition; scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis); handDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, boundingBox.center), scaleAxis)); handleHandOffset = handDistance - initialHandleDistance; @@ -1417,7 +1428,9 @@ function applyHandleScale() { // Scales selection per changing position of scaling hand; positions and orients per grabbing hand. - var boundingBoxCenter, + var targetPosition, + deltaOrientation, + boundingBoxCenter, scaleAxis, handleDistance, scale, @@ -1427,7 +1440,9 @@ applyGrab(); // Desired distance of handle from center of bounding box. - boundingBoxCenter = selection.boundingBox().center; // TODO: Too expensive for update loop? + targetPosition = getScaleTargetPosition(); + deltaOrientation = Quat.multiply(hand.orientation(), initialHandleScaleOrientationInverse); + boundingBoxCenter = Vec3.sum(targetPosition, Vec3.multiplyQbyV(deltaOrientation, initialTargetToBoundingBoxCenter)); scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis); handleDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, boundingBoxCenter), scaleAxis)); handleDistance -= handleHandOffset; From f1fd6264f3b453f422d024f05a93a5aca4b052c1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 19 Jul 2017 17:51:36 +1200 Subject: [PATCH 062/722] Fix handle scaling entity with non-center registration point --- scripts/vr-edit/vr-edit.js | 85 +++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 573446efcd..d7a6779c8e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -568,13 +568,9 @@ rootEntityID = null, rootPosition, rootOrientation, - scaleRootPosition, - scaleRootRegistrationPoint, - scaleRootRegistrationOffset, scaleCenter, scaleRootOffset, scaleRootOrientation, - isRootCenterRegistration, ENTITY_TYPE = "entity"; function traverseEntityTree(id, result) { @@ -766,28 +762,20 @@ } function startHandleScaling() { - // Save initial position data so that can scale relative to these without accumulating float errors. - scaleRootPosition = rootPosition; - scaleRootRegistrationPoint = selection[0].registrationPoint; - isRootCenterRegistration = Vec3.equal(scaleRootRegistrationPoint, Vec3.HALF); - scaleRootRegistrationOffset = Vec3.subtract(scaleRootRegistrationPoint, Vec3.HALF); + // Nothing to do. } - function handleScale(factor) { - // Scale selection about bounding box center. + function handleScale(factor, position, orientation) { + // Scale and reposition and orient selection. // Scale and position root. - if (isRootCenterRegistration) { - Entities.editEntity(selection[0].id, { - dimensions: Vec3.multiplyVbyV(factor, selection[0].dimensions) - }); - } else { - rootPosition = Vec3.sum(scaleRootPosition, Vec3.multiplyVbyV(factor, scaleRootRegistrationOffset)); - Entities.editEntity(selection[0].id, { - dimensions: Vec3.multiplyVbyV(factor, selection[0].dimensions), - position: rootPosition - }); - } + rootPosition = position; + rootOrientation = orientation; + Entities.editEntity(selection[0].id, { + dimensions: Vec3.multiplyVbyV(factor, selection[0].dimensions), + position: rootPosition, + rotation: rootOrientation + }); // Scale and position children. // TODO @@ -1260,7 +1248,9 @@ handleScaleDirections, handleHandOffset, initialHandleDistance, - initialHandleScaleOrientationInverse, + initialHandleOrientationInverse, + initialHandleRegistrationOffset, + initialSelectionOrientationInverse, intersection; @@ -1348,22 +1338,25 @@ // Keep grabbed handle highlighted and hide other handles. handles.grab(overlayID); - handleUnitScaleAxis = handles.scalingAxis(overlayID); // Unit vector in direction of scaling. - handleScaleDirections = handles.scalingDirections(overlayID); // Which axes to scale the selection on. - // Vector from target to bounding box center. initialTargetPosition = getScaleTargetPosition(); boundingBox = selection.boundingBox(); initialTargetToBoundingBoxCenter = Vec3.subtract(boundingBox.center, initialTargetPosition); - // Initial hand orientation. - initialHandleScaleOrientationInverse = Quat.inverse(hand.orientation()); + // Selection information. + selectionPositionAndOrientation = selection.getPositionAndOrientation(); + initialSelectionOrientationInverse = Quat.inverse(selectionPositionAndOrientation.orientation); - // Distance from handle to bounding box center. + // Handle information. + initialHandleOrientationInverse = Quat.inverse(hand.orientation()); + handleUnitScaleAxis = handles.scalingAxis(overlayID); // Unit vector in direction of scaling. + handleScaleDirections = handles.scalingDirections(overlayID); // Which axes to scale the selection on. initialHandleDistance = Vec3.length(Vec3.multiplyVbyV(boundingBox.dimensions, handleScaleDirections)) / 2; + initialHandleRegistrationOffset = Vec3.multiplyQbyV(initialSelectionOrientationInverse, + Vec3.subtract(selectionPositionAndOrientation.position, boundingBox.center)); // Distance from hand to handle in direction of handle. - scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis); + scaleAxis = Vec3.multiplyQbyV(selectionPositionAndOrientation.orientation, handleUnitScaleAxis); handDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, boundingBox.center), scaleAxis)); handleHandOffset = handDistance - initialHandleDistance; @@ -1429,20 +1422,26 @@ function applyHandleScale() { // Scales selection per changing position of scaling hand; positions and orients per grabbing hand. var targetPosition, - deltaOrientation, + deltaHandOrientation, + deltaHandleOrientation, + selectionPosition, + selectionOrientation, boundingBoxCenter, scaleAxis, handleDistance, scale, - scale3D; + scale3D, + selectionPositionAndOrientation; - // Position and orient selection per grabbing hand before scaling it per scaling hand. - applyGrab(); + // Orient selection per grabbing hand. + deltaHandOrientation = Quat.multiply(hand.orientation(), initialHandOrientationInverse); + selectionOrientation = Quat.multiply(deltaHandOrientation, initialSelectionOrientation); // Desired distance of handle from center of bounding box. targetPosition = getScaleTargetPosition(); - deltaOrientation = Quat.multiply(hand.orientation(), initialHandleScaleOrientationInverse); - boundingBoxCenter = Vec3.sum(targetPosition, Vec3.multiplyQbyV(deltaOrientation, initialTargetToBoundingBoxCenter)); + deltaHandleOrientation = Quat.multiply(hand.orientation(), initialHandleOrientationInverse); + boundingBoxCenter = Vec3.sum(targetPosition, + Vec3.multiplyQbyV(deltaHandleOrientation, initialTargetToBoundingBoxCenter)); scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis); handleDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, boundingBoxCenter), scaleAxis)); handleDistance -= handleHandOffset; @@ -1455,8 +1454,20 @@ y: scale3D.y !== 0 ? scale3D.y : 1, z: scale3D.z !== 0 ? scale3D.z : 1 }; + + // Reposition selection per scale. + selectionPosition = Vec3.sum(boundingBoxCenter, + Vec3.multiplyQbyV(selectionOrientation, Vec3.multiplyVbyV(scale3D, initialHandleRegistrationOffset))); + + // Scale. handles.scale(scale3D); - selection.handleScale(scale3D); + selection.handleScale(scale3D, selectionPosition, selectionOrientation); + + // Update grab offset. + selectionPositionAndOrientation = selection.getPositionAndOrientation(); + initialHandOrientationInverse = Quat.inverse(hand.orientation()); + initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position()); + initialSelectionOrientation = selectionPositionAndOrientation.orientation; } From e4123070c443dbbcaaf8a1a8b08eeb04c96246f5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 19 Jul 2017 18:10:39 +1200 Subject: [PATCH 063/722] Add handle scaling of multiple entities --- scripts/vr-edit/vr-edit.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index d7a6779c8e..89ed0756b1 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -767,6 +767,8 @@ function handleScale(factor, position, orientation) { // Scale and reposition and orient selection. + var i, + length; // Scale and position root. rootPosition = position; @@ -778,7 +780,14 @@ }); // Scale and position children. - // TODO + // Only corner handles are used for scaling multiple entities so scale factor is the same in all dimensions. + // Therefore don't need to take into account orientation relative to parent when scaling local position. + for (i = 1, length = selection.length; i < length; i += 1) { + Entities.editEntity(selection[i].id, { + dimensions: Vec3.multiplyVbyV(factor, selection[i].dimensions), + localPosition: Vec3.multiplyVbyV(factor, selection[i].localPosition) + }); + } } function finishHandleScaling() { From 2bd3f87d73d00842b210b4a32e3f43f735f62d70 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 19 Jul 2017 18:11:45 +1200 Subject: [PATCH 064/722] Tidying --- scripts/vr-edit/vr-edit.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 89ed0756b1..99f160c6a5 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1310,6 +1310,7 @@ } function startDirectScaling(targetPosition) { + // Called on grabbing hand by scaling hand. var initialTargetPosition, initialTargetsCenter; @@ -1326,17 +1327,21 @@ } function updateDirectScaling(targetPosition) { + // Called on grabbing hand by scaling hand. otherTargetPosition = targetPosition; } function stopDirectScaling() { + // Called on grabbing hand by scaling hand. selection.finishDirectScaling(); isDirectScaling = false; } function startHandleScaling(targetPosition, overlayID) { + // Called on grabbing hand by scaling hand. var initialTargetPosition, boundingBox, + selectionPositionAndOrientation, scaleAxis, handDistance; @@ -1375,10 +1380,12 @@ } function updateHandleScaling(targetPosition) { + // Called on grabbing hand by scaling hand. otherTargetPosition = targetPosition; } function stopHandleScaling() { + // Called on grabbing hand by scaling hand. handles.finishScaling(); selection.finishHandleScaling(); handles.grab(null); // Stop highlighting grabbed handle and resume displaying all handles. @@ -1388,17 +1395,12 @@ function applyGrab() { // Sets position and orientation of selection per grabbing hand. - var handPosition, - handOrientation, - deltaOrientation, + var deltaOrientation, selectionPosition, selectionOrientation; - handPosition = hand.position(); - handOrientation = hand.orientation(); - - deltaOrientation = Quat.multiply(handOrientation, initialHandOrientationInverse); - selectionPosition = Vec3.sum(handPosition, Vec3.multiplyQbyV(deltaOrientation, initialHandToSelectionVector)); + deltaOrientation = Quat.multiply(hand.orientation(), initialHandOrientationInverse); + selectionPosition = Vec3.sum(hand.position(), Vec3.multiplyQbyV(deltaOrientation, initialHandToSelectionVector)); selectionOrientation = Quat.multiply(deltaOrientation, initialSelectionOrientation); selection.setPositionAndOrientation(selectionPosition, selectionOrientation); From 5551bb70d42994fbb0ab3852153b335c4f609567 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 20 Jul 2017 18:33:23 +1200 Subject: [PATCH 065/722] Disable collisions and dynamic behavior while entities are being moved --- scripts/vr-edit/vr-edit.js | 56 +++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 99f160c6a5..832de4ea5e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -577,7 +577,8 @@ // Recursively traverses tree of entities and their children, gather IDs and properties. var children, properties, - SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions", "localPosition"], + SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions", "localPosition", + "dynamic", "collisionless"], i, length; @@ -588,7 +589,9 @@ localPosition: properties.localPosition, registrationPoint: properties.registrationPoint, rotation: properties.rotation, - dimensions: properties.dimensions + dimensions: properties.dimensions, + dynamic: properties.dynamic, + collisionless: properties.collisionless }); children = Entities.getChildrenIDs(id); @@ -601,7 +604,7 @@ function select(entityID) { var entityProperties, - PARENT_PROPERTIES = ["parentID", "position", "rotation"]; + PARENT_PROPERTIES = ["parentID", "position", "rotation", "dymamic", "collisionless"]; // Find root parent. rootEntityID = Entities.rootOf(entityID); @@ -709,6 +712,47 @@ }; } + function startEditing() { + var count, + i; + + // Disable entity set's physics. + for (i = 0, count = selection.length; i < count; i += 1) { + Entities.editEntity(selection[i].id, { + dynamic: false, // So that gravity doesn't fight with us trying to hold the entity in place. + collisionless: true // So that entity doesn't bump us about as we resize the entity. + }); + } + } + + function finishEditing() { + var firstDynamicEntityID = null, + properties, + VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD + VELOCITY_KICK = { x: 0, y: 0.02, z: 0 }, + count, + i; + + // Restore entity set's physics. + for (i = 0, count = selection.length; i < count; i += 1) { + if (firstDynamicEntityID === null && selection[i].dynamic) { + firstDynamicEntityID = selection[i].id; + } + Entities.editEntity(selection[i].id, { + dynamic: selection[i].dynamic, + collisionless: selection[i].collisionless + }); + } + + // If dynamic with gravity, and velocity is zero, give the entity set a little kick to set off physics. + if (firstDynamicEntityID) { + properties = Entities.getEntityProperties(firstDynamicEntityID, ["velocity", "gravity"]); + if (Vec3.length(properties.gravity) > 0 && Vec3.length(properties.velocity) < VELOCITY_THRESHOLD) { + Entities.editEntity(firstDynamicEntityID, { velocity: VELOCITY_KICK }); + } + } + } + function getPositionAndOrientation() { // Position and orientation of root entity. return { @@ -816,12 +860,14 @@ boundingBox: getBoundingBox, getPositionAndOrientation: getPositionAndOrientation, setPositionAndOrientation: setPositionAndOrientation, + startEditing: startEditing, startDirectScaling: startDirectScaling, directScale: directScale, finishDirectScaling: finishDirectScaling, startHandleScaling: startHandleScaling, handleScale: handleScale, finishHandleScaling: finishHandleScaling, + finishEditing: finishEditing, clear: clear, destroy: destroy }; @@ -1295,10 +1341,12 @@ selectionPositionAndOrientation = selection.getPositionAndOrientation(); initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position()); initialSelectionOrientation = selectionPositionAndOrientation.orientation; + + selection.startEditing(); } function stopEditing() { - // Nothing to do. + selection.finishEditing(); } From 880a711d2bf36fe43490149a6a50b3b7ecd88bd8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 20 Jul 2017 18:36:02 +1200 Subject: [PATCH 066/722] Prevent invalid direct / handle scaling transition while scaling --- scripts/vr-edit/vr-edit.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 832de4ea5e..e440335be1 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1334,6 +1334,10 @@ && (rootEntityID === undefined || rootEntityID === selection.rootEntityID()); } + function isScaling() { + return editorState === EDITOR_DIRECT_SCALING || editorState === EDITOR_HANDLE_SCALING; + } + function startEditing() { var selectionPositionAndOrientation; @@ -1917,6 +1921,7 @@ hoverHandle: hoverHandle, isHandle: isHandle, isEditing: isEditing, + isScaling: isScaling, startDirectScaling: startDirectScaling, updateDirectScaling: updateDirectScaling, stopDirectScaling: stopDirectScaling, @@ -1966,7 +1971,10 @@ } function onGripClicked() { - isAppScaleWithHandles = !isAppScaleWithHandles; + // Do not change scale mode if are currently scaling. + if (!editors[LEFT_HAND].isScaling() && !editors[RIGHT_HAND].isScaling()) { + isAppScaleWithHandles = !isAppScaleWithHandles; + } } From 571d10fa89f9d513bafde01c2c44fb42970f5113 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 20 Jul 2017 21:05:21 +1200 Subject: [PATCH 067/722] Improve editor state update code --- scripts/vr-edit/vr-edit.js | 45 ++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index e440335be1..c6807002c5 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1566,6 +1566,7 @@ isAppScaleWithHandles || otherEditor.isEditing(highlightedEntityID)); } isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID); + wasAppScaleWithHandles = isAppScaleWithHandles; } function updateEditorHighlighting() { @@ -1593,6 +1594,7 @@ handles.display(highlightedEntityID, selection.boundingBox(), selection.count() > 1); } startEditing(); + wasAppScaleWithHandles = isAppScaleWithHandles; } function updateEditorGrabbing() { @@ -1694,7 +1696,8 @@ function update() { - var previousState = editorState; + var previousState = editorState, + doUpdateState; // Hand update. hand.update(); @@ -1732,11 +1735,9 @@ setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && !hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); - wasAppScaleWithHandles = isAppScaleWithHandles; setState(EDITOR_HIGHLIGHTING); } else if (intersection.entityID && hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); - wasAppScaleWithHandles = isAppScaleWithHandles; if (otherEditor.isEditing(highlightedEntityID)) { if (!isAppScaleWithHandles) { setState(EDITOR_DIRECT_SCALING); @@ -1749,10 +1750,24 @@ } break; case EDITOR_HIGHLIGHTING: - if (hand.valid() && Entities.rootOf(intersection.entityID) === highlightedEntityID - && !hand.triggerClicked() && isAppScaleWithHandles === wasAppScaleWithHandles) { + if (hand.valid() + && intersection.entityID + && !(hand.triggerClicked() && (!otherEditor.isEditing(highlightedEntityID) || !isAppScaleWithHandles)) + && !(hand.triggerClicked() && intersection.overlayID && otherEditor.isHandle(intersection.overlayID))) { // No transition. + doUpdateState = false; if (otherEditor.isEditing(highlightedEntityID) !== isOtherEditorEditingEntityID) { + doUpdateState = true; + } + if (Entities.rootOf(intersection.entityID) !== highlightedEntityID) { + highlightedEntityID = Entities.rootOf(intersection.entityID); + doUpdateState = true; + } + if (isAppScaleWithHandles !== wasAppScaleWithHandles) { + wasAppScaleWithHandles = isAppScaleWithHandles; + doUpdateState = true; + } + if (doUpdateState) { updateState(); } break; @@ -1764,26 +1779,19 @@ setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); // May be a different entityID. - wasAppScaleWithHandles = isAppScaleWithHandles; if (otherEditor.isEditing(highlightedEntityID)) { if (!isAppScaleWithHandles) { setState(EDITOR_DIRECT_SCALING); + } else { + debug(side, "ERROR: Unexpected condition in EDITOR_HIGHLIGHTING! A"); } } else { setState(EDITOR_GRABBING); } - } else if (intersection.entityID && Entities.rootOf(intersection.entityID) !== highlightedEntityID) { - highlightedEntityID = Entities.rootOf(intersection.entityID); - wasAppScaleWithHandles = isAppScaleWithHandles; - updateState(); - } else if (intersection.entityID && isAppScaleWithHandles !== wasAppScaleWithHandles) { - wasAppScaleWithHandles = isAppScaleWithHandles; - updateState(); } else if (!intersection.entityID) { - // Note that this transition includes the case of highlighting a scaling handle. setState(EDITOR_SEARCHING); } else { - debug(side, "ERROR: Unexpected condition in EDITOR_HIGHLIGHTING!"); + debug(side, "ERROR: Unexpected condition in EDITOR_HIGHLIGHTING! B"); } break; case EDITOR_GRABBING: @@ -1791,8 +1799,8 @@ // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // No transition. if (isAppScaleWithHandles !== wasAppScaleWithHandles) { - wasAppScaleWithHandles = isAppScaleWithHandles; updateState(); + wasAppScaleWithHandles = isAppScaleWithHandles; } break; } @@ -1801,7 +1809,6 @@ } else if (!hand.triggerClicked()) { if (intersection.entityID) { highlightedEntityID = Entities.rootOf(intersection.entityID); - wasAppScaleWithHandles = isAppScaleWithHandles; setState(EDITOR_HIGHLIGHTING); } else { setState(EDITOR_SEARCHING); @@ -1826,12 +1833,10 @@ setState(EDITOR_SEARCHING); } else { highlightedEntityID = Entities.rootOf(intersection.entityID); - wasAppScaleWithHandles = isAppScaleWithHandles; setState(EDITOR_HIGHLIGHTING); } } else if (!otherEditor.isEditing(highlightedEntityID)) { // Grab highlightEntityID that was scaling and has already been set. - wasAppScaleWithHandles = isAppScaleWithHandles; setState(EDITOR_GRABBING); } break; @@ -1851,12 +1856,10 @@ setState(EDITOR_SEARCHING); } else { highlightedEntityID = Entities.rootOf(intersection.entityID); - wasAppScaleWithHandles = isAppScaleWithHandles; setState(EDITOR_HIGHLIGHTING); } } else if (!otherEditor.isEditing(highlightedEntityID)) { // Grab highlightEntityID that was scaling and has already been set. - wasAppScaleWithHandles = isAppScaleWithHandles; setState(EDITOR_GRABBING); } break; From 3b966072a34470f2a6d7956936dcb64df277ab5e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 20 Jul 2017 21:30:39 +1200 Subject: [PATCH 068/722] Fix grabbing a handle without hovering entity beforehand --- scripts/vr-edit/vr-edit.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index c6807002c5..4a1ee92976 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1338,6 +1338,11 @@ return editorState === EDITOR_DIRECT_SCALING || editorState === EDITOR_HANDLE_SCALING; } + function rootEntityID() { + return selection.rootEntityID(); + } + + function startEditing() { var selectionPositionAndOrientation; @@ -1612,7 +1617,7 @@ } function enterEditorDirectScaling() { - selection.select(highlightedEntityID); // For when transitioning from EDITOR_SEARCHING. + selection.select(highlightedEntityID); // In case need to transition to EDITOR_GRABBING. isScalingWithHand = intersection.handIntersected; if (intersection.laserIntersected) { laser.setLength(laser.length()); @@ -1630,7 +1635,7 @@ } function enterEditorHandleScaling() { - selection.select(highlightedEntityID); // For when transitioning from EDITOR_SEARCHING. + selection.select(highlightedEntityID); // In case need to transition to EDITOR_GRABBING. isScalingWithHand = intersection.handIntersected; if (intersection.laserIntersected) { laser.setLength(laser.length()); @@ -1732,6 +1737,7 @@ setState(EDITOR_IDLE); } else if (intersection.overlayID && hand.triggerClicked() && otherEditor.isHandle(intersection.overlayID)) { + highlightedEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && !hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); @@ -1776,6 +1782,7 @@ setState(EDITOR_IDLE); } else if (intersection.overlayID && hand.triggerClicked() && otherEditor.isHandle(intersection.overlayID)) { + highlightedEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); // May be a different entityID. @@ -1818,7 +1825,8 @@ } break; case EDITOR_DIRECT_SCALING: - if (hand.valid() && hand.triggerClicked() && otherEditor.isEditing(highlightedEntityID)) { + if (hand.valid() && hand.triggerClicked() + && (otherEditor.isEditing(highlightedEntityID) || otherEditor.isHandle(intersection.overlayID))) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // Don't test isAppScaleWithHandles because this will eventually be a UI element and so not able to be // changed while scaling with two hands. @@ -1925,6 +1933,7 @@ isHandle: isHandle, isEditing: isEditing, isScaling: isScaling, + rootEntityID: rootEntityID, startDirectScaling: startDirectScaling, updateDirectScaling: updateDirectScaling, stopDirectScaling: stopDirectScaling, From 1009b676001dca98ff7f1b173d0873dde62e6061 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 20 Jul 2017 21:38:03 +1200 Subject: [PATCH 069/722] Fix unhandled state condition --- scripts/vr-edit/vr-edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 4a1ee92976..b1ae749e8e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1728,7 +1728,7 @@ break; case EDITOR_SEARCHING: if (hand.valid() && !intersection.entityID - && !(intersection.overlayID && hand.triggerClicked())) { + && !(intersection.overlayID && hand.triggerClicked() && otherEditor.isHandle(intersection.overlayID))) { // No transition. updateState(); break; From da97662ee186e688dd3e597373c919ae254f4e48 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 20 Jul 2017 22:23:48 +1200 Subject: [PATCH 070/722] Make hovered handles brighter --- scripts/vr-edit/vr-edit.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index b1ae749e8e..8abc742485 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -224,7 +224,8 @@ BOUNDING_BOX_ALPHA = 0.8, HANDLE_NORMAL_COLOR = { red: 0, green: 240, blue: 240 }, HANDLE_HOVER_COLOR = { red: 0, green: 255, blue: 120 }, - HANDLE_ALPHA = 0.7, + HANDLE_NORMAL_ALPHA = 0.7, + HANDLE_HOVER_ALPHA = 0.9, NUM_CORNERS = 8, NUM_CORNER_HANDLES = 2, CORNER_HANDLE_OVERLAY_DIMENSIONS = { x: 0.1, y: 0.1, z: 0.1 }, @@ -303,7 +304,7 @@ for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { cornerHandleOverlays[i] = Overlays.addOverlay("sphere", { color: HANDLE_NORMAL_COLOR, - alpha: HANDLE_ALPHA, + alpha: HANDLE_NORMAL_ALPHA, solid: true, drawInFront: true, ignoreRayIntersection: false, @@ -315,7 +316,7 @@ faceHandleOverlays[i] = Overlays.addOverlay("shape", { shape: "Cone", color: HANDLE_NORMAL_COLOR, - alpha: HANDLE_ALPHA, + alpha: HANDLE_NORMAL_ALPHA, solid: true, drawInFront: true, ignoreRayIntersection: false, @@ -492,7 +493,10 @@ if (overlayID !== null && (faceHandleOverlays.indexOf(overlayID) !== -1 || cornerHandleOverlays.indexOf(overlayID) !== -1)) { - Overlays.editOverlay(overlayID, { color: HANDLE_HOVER_COLOR }); + Overlays.editOverlay(overlayID, { + color: HANDLE_HOVER_COLOR, + alpha: HANDLE_HOVER_ALPHA + }); hoveredOverlayID = overlayID; } } @@ -502,6 +506,7 @@ var overlay, isShowAll = overlayID === null, color = isShowAll ? HANDLE_NORMAL_COLOR : HANDLE_HOVER_COLOR, + alpha = isShowAll ? HANDLE_NORMAL_ALPHA : HANDLE_HOVER_ALPHA, i, length; @@ -509,7 +514,8 @@ overlay = cornerHandleOverlays[i]; Overlays.editOverlay(overlay, { visible: isVisible && (isShowAll || overlay === overlayID), - color: color + color: color, + alpha: alpha }); } @@ -517,7 +523,8 @@ overlay = faceHandleOverlays[i]; Overlays.editOverlay(overlay, { visible: isVisible && (isShowAll || overlay === overlayID), - color: color + color: color, + alpha: alpha }); } } From e2cace2372fecba1c6dcfe8329bde219d0e12757 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 20 Jul 2017 22:29:13 +1200 Subject: [PATCH 071/722] Fix laser not turning off when lose controller tracking --- scripts/vr-edit/vr-edit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 8abc742485..5954e36812 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1547,6 +1547,7 @@ function enterEditorIdle() { + laser.clear(); selection.clear(); } From e09113fef5386516ace7f6a7fb3861ec2adda5f8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 21 Jul 2017 13:18:10 +1200 Subject: [PATCH 072/722] Fix handle scaling down to very small dimensions --- scripts/vr-edit/vr-edit.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 5954e36812..5a57323253 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1313,6 +1313,7 @@ initialHandleOrientationInverse, initialHandleRegistrationOffset, initialSelectionOrientationInverse, + MIN_SCALE = 0.001, intersection; @@ -1520,14 +1521,15 @@ scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis); handleDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, boundingBoxCenter), scaleAxis)); handleDistance -= handleHandOffset; + handleDistance = Math.max(handleDistance, MIN_SCALE); // Scale selection relative to initial dimensions. scale = handleDistance / initialHandleDistance; scale3D = Vec3.multiply(scale, handleScaleDirections); scale3D = { - x: scale3D.x !== 0 ? scale3D.x : 1, - y: scale3D.y !== 0 ? scale3D.y : 1, - z: scale3D.z !== 0 ? scale3D.z : 1 + x: handleScaleDirections.x !== 0 ? scale3D.x : 1, + y: handleScaleDirections.y !== 0 ? scale3D.y : 1, + z: handleScaleDirections.z !== 0 ? scale3D.z : 1 }; // Reposition selection per scale. From cf51c546d3905c75a13539fbe63557189dcb99b7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 21 Jul 2017 13:43:20 +1200 Subject: [PATCH 073/722] Fix direct scaling scale and position --- scripts/vr-edit/vr-edit.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 5a57323253..268619b6e8 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1055,6 +1055,10 @@ return laserLength; } + function handOffset() { + return GRAB_POINT_SPHERE_OFFSET; + } + function clear() { isLaserOn = false; hide(); @@ -1075,6 +1079,7 @@ setLength: setLength, clearLength: clearLength, length: getLength, + handOffset: handOffset, clear: clear, destroy: destroy }; @@ -1313,6 +1318,7 @@ initialHandleOrientationInverse, initialHandleRegistrationOffset, initialSelectionOrientationInverse, + laserOffset, MIN_SCALE = 0.001, intersection; @@ -1323,6 +1329,8 @@ highlights = new Highlights(side); handles = new Handles(side); + laserOffset = laser.handOffset(); + function setOtherEditor(editor) { otherEditor = editor; } @@ -1371,7 +1379,8 @@ if (isScalingWithHand) { return side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); } - return Vec3.sum(hand.position(), Vec3.multiply(laser.length(), Quat.getUp(hand.orientation()))); + return Vec3.sum(Vec3.sum(hand.position(), Vec3.multiplyQbyV(hand.orientation(), laserOffset)), + Vec3.multiply(laser.length(), Quat.getUp(hand.orientation()))); } function startDirectScaling(targetPosition) { @@ -1484,6 +1493,8 @@ targetPosition = getScaleTargetPosition(); targetsSeparation = Vec3.distance(targetPosition, otherTargetPosition); scale = targetsSeparation / initialTargetsSeparation; + scale = Math.max(scale, MIN_SCALE); + rotation = Quat.rotationBetween(initialtargetsDirection, Vec3.subtract(otherTargetPosition, targetPosition)); center = Vec3.multiply(0.5, Vec3.sum(targetPosition, otherTargetPosition)); selection.directScale(scale, rotation, center); From 1434f98dab1066595c67c17633a21ca11894e214 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 21 Jul 2017 13:49:26 +1200 Subject: [PATCH 074/722] Fix multi-selection scale handles box after toggling while grabbed --- scripts/vr-edit/vr-edit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 268619b6e8..d50c3b3b33 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1624,6 +1624,7 @@ } function updateEditorGrabbing() { + selection.select(highlightedEntityID); if (isAppScaleWithHandles) { handles.display(highlightedEntityID, selection.boundingBox(), selection.count() > 1); } else { From 218b13b0e9bb4cbd79daf89541593cee7920207f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 21 Jul 2017 14:17:50 +1200 Subject: [PATCH 075/722] Turn off laser when grabbing or scaling with hand --- scripts/vr-edit/vr-edit.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index d50c3b3b33..cf07bbf0f3 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -885,7 +885,8 @@ // Draws hand lasers. // May intersect with entities or bounding box of other hand's selection. - var isLaserOn = false, + var isLaserEnabled = true, + isLaserOn = false, laserLine = null, laserSphere = null, @@ -1000,6 +1001,10 @@ deltaOrigin, pickRay; + if (!isLaserEnabled) { + return; + } + if (!hand.intersection().intersects && hand.triggerPressed()) { handPosition = hand.position(); handOrientation = hand.orientation(); @@ -1064,6 +1069,18 @@ hide(); } + function enable() { + isLaserEnabled = true; + } + + function disable() { + isLaserEnabled = false; + if (isLaserOn) { + hide(); + } + isLaserOn = false; + } + function destroy() { Overlays.deleteOverlay(laserLine); Overlays.deleteOverlay(laserSphere); @@ -1079,6 +1096,8 @@ setLength: setLength, clearLength: clearLength, length: getLength, + enable: enable, + disable: disable, handOffset: handOffset, clear: clear, destroy: destroy @@ -1615,6 +1634,8 @@ selection.select(highlightedEntityID); // For when transitioning from EDITOR_SEARCHING. if (intersection.laserIntersected) { laser.setLength(laser.length()); + } else { + laser.disable(); } if (isAppScaleWithHandles) { handles.display(highlightedEntityID, selection.boundingBox(), selection.count() > 1); @@ -1636,6 +1657,7 @@ stopEditing(); handles.clear(); laser.clearLength(); + laser.enable(); } function enterEditorDirectScaling() { @@ -1643,6 +1665,8 @@ isScalingWithHand = intersection.handIntersected; if (intersection.laserIntersected) { laser.setLength(laser.length()); + } else { + laser.disable(); } otherEditor.startDirectScaling(getScaleTargetPosition()); } @@ -1654,6 +1678,7 @@ function exitEditorDirectScaling() { otherEditor.stopDirectScaling(); laser.clearLength(); + laser.enable(); } function enterEditorHandleScaling() { @@ -1661,6 +1686,8 @@ isScalingWithHand = intersection.handIntersected; if (intersection.laserIntersected) { laser.setLength(laser.length()); + } else { + laser.disable(); } otherEditor.startHandleScaling(getScaleTargetPosition(), intersection.overlayID); } @@ -1672,6 +1699,7 @@ function exitEditorHandleScaling() { otherEditor.stopHandleScaling(); laser.clearLength(); + laser.enable(); } STATE_MACHINE = { From f858abcf7cbf96c651422f7e83dcbc8fd25aa1d2 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 21 Jul 2017 14:31:15 +1200 Subject: [PATCH 076/722] Resume app from idle state --- scripts/vr-edit/vr-edit.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index cf07bbf0f3..29d59bd029 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1943,6 +1943,8 @@ } function clear() { + setState(EDITOR_IDLE); + hand.clear(); laser.clear(); selection.clear(); From 7e4e68ccf97fce37a9463eedc19bbc530c52b549 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 21 Jul 2017 14:52:24 +1200 Subject: [PATCH 077/722] Fix highlight overlay sometimes being out of position --- scripts/vr-edit/vr-edit.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 29d59bd029..f095b21331 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -115,7 +115,8 @@ HAND_HIGHLIGHT_ALPHA = 0.35, ENTITY_HIGHLIGHT_ALPHA = 0.8, HAND_HIGHLIGHT_DIMENSIONS = { x: 0.2, y: 0.2, z: 0.2 }, - HAND_HIGHLIGHT_OFFSET = { x: 0.0, y: 0.11, z: 0.02 }; + HAND_HIGHLIGHT_OFFSET = { x: 0.0, y: 0.11, z: 0.02 }, + ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO); handOverlay = Overlays.addOverlay("sphere", { dimensions: HAND_HIGHLIGHT_DIMENSIONS, @@ -144,13 +145,12 @@ } function editEntityOverlay(index, details, overlayColor) { - var offset = Vec3.multiplyQbyV(details.rotation, - Vec3.multiplyVbyV(Vec3.subtract(Vec3.HALF, details.registrationPoint), details.dimensions)); + var offset = Vec3.multiplyVbyV(Vec3.subtract(Vec3.HALF, details.registrationPoint), details.dimensions); Overlays.editOverlay(entityOverlays[index], { parentID: details.id, - position: Vec3.sum(details.position, offset), - rotation: details.rotation, + localPosition: offset, + localRotation: ZERO_ROTATION, dimensions: details.dimensions, color: overlayColor, visible: true From 233655b76bd4056a29b8b1e8b8a2894eed18d03c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 22 Jul 2017 11:20:17 +1200 Subject: [PATCH 078/722] Split script into multiple files --- scripts/vr-edit/modules/hand.js | 200 ++++ scripts/vr-edit/modules/handles.js | 372 +++++++ scripts/vr-edit/modules/highlights.js | 123 +++ scripts/vr-edit/modules/laser.js | 241 +++++ scripts/vr-edit/modules/selection.js | 328 +++++++ scripts/vr-edit/utilities/utilities.js | 51 + scripts/vr-edit/vr-edit.js | 1246 +----------------------- 7 files changed, 1330 insertions(+), 1231 deletions(-) create mode 100644 scripts/vr-edit/modules/hand.js create mode 100644 scripts/vr-edit/modules/handles.js create mode 100644 scripts/vr-edit/modules/highlights.js create mode 100644 scripts/vr-edit/modules/laser.js create mode 100644 scripts/vr-edit/modules/selection.js create mode 100644 scripts/vr-edit/utilities/utilities.js diff --git a/scripts/vr-edit/modules/hand.js b/scripts/vr-edit/modules/hand.js new file mode 100644 index 0000000000..a81c54e2cb --- /dev/null +++ b/scripts/vr-edit/modules/hand.js @@ -0,0 +1,200 @@ +// +// hand.js +// +// Created by David Rowe on 21 Jul 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global Hand */ + +Hand = function (side, gripPressedCallback) { + + "use strict"; + + // Hand controller input. + var handController, + controllerTrigger, + controllerTriggerClicked, + controllerGrip, + + isGripPressed = false, + GRIP_ON_VALUE = 0.99, + GRIP_OFF_VALUE = 0.95, + + isTriggerPressed, + isTriggerClicked, + TRIGGER_ON_VALUE = 0.15, // Per handControllerGrab.js. + TRIGGER_OFF_VALUE = 0.1, // Per handControllerGrab.js. + + NEAR_GRAB_RADIUS = 0.1, // Per handControllerGrab.js. + NEAR_HOVER_RADIUS = 0.025, + + LEFT_HAND = 0, + HALF_TREE_SCALE = 16384, + + handPose, + handPosition, + handOrientation, + + intersection = {}; + + if (side === LEFT_HAND) { + handController = Controller.Standard.LeftHand; + controllerTrigger = Controller.Standard.LT; + controllerTriggerClicked = Controller.Standard.LTClick; + controllerGrip = Controller.Standard.LeftGrip; + } else { + handController = Controller.Standard.RightHand; + controllerTrigger = Controller.Standard.RT; + controllerTriggerClicked = Controller.Standard.RTClick; + controllerGrip = Controller.Standard.RightGrip; + } + + function valid() { + return handPose.valid; + } + + function position() { + return handPosition; + } + + function orientation() { + return handOrientation; + } + + function triggerPressed() { + return isTriggerPressed; + } + + function triggerClicked() { + return isTriggerClicked; + } + + function getIntersection() { + return intersection; + } + + function update() { + var gripValue, + palmPosition, + overlayID, + overlayIDs, + overlayDistance, + distance, + entityID, + entityIDs, + entitySize, + size, + i, + length; + + + // Hand pose. + handPose = Controller.getPoseValue(handController); + if (!handPose.valid) { + intersection = {}; + return; + } + handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); + handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); + + // Controller trigger. + isTriggerPressed = Controller.getValue(controllerTrigger) > (isTriggerPressed + ? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE); + isTriggerClicked = Controller.getValue(controllerTriggerClicked); + + // Controller grip. + gripValue = Controller.getValue(controllerGrip); + if (isGripPressed) { + isGripPressed = gripValue > GRIP_OFF_VALUE; + } else { + isGripPressed = gripValue > GRIP_ON_VALUE; + if (isGripPressed) { + gripPressedCallback(); + } + } + + // Hand-overlay intersection, if any. + overlayID = null; + palmPosition = side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); + overlayIDs = Overlays.findOverlays(palmPosition, NEAR_HOVER_RADIUS); + if (overlayIDs.length > 0) { + // Typically, there will be only one overlay; optimize for that case. + overlayID = overlayIDs[0]; + if (overlayIDs.length > 1) { + // Find closest overlay. + overlayDistance = Vec3.length(Vec3.subtract(Overlays.getProperty(overlayID, "position"), palmPosition)); + for (i = 1, length = overlayIDs.length; i < length; i += 1) { + distance = + Vec3.length(Vec3.subtract(Overlays.getProperty(overlayIDs[i], "position"), palmPosition)); + if (distance > overlayDistance) { + overlayID = overlayIDs[i]; + overlayDistance = distance; + } + } + } + } + + // Hand-entity intersection, if any, if overlay not intersected. + entityID = null; + if (overlayID === null) { + // palmPosition is set above. + entityIDs = Entities.findEntities(palmPosition, NEAR_GRAB_RADIUS); + if (entityIDs.length > 0) { + // Typically, there will be only one entity; optimize for that case. + if (Entities.hasEditableRoot(entityIDs[0])) { + entityID = entityIDs[0]; + } + if (entityIDs.length > 1) { + // Find smallest, editable entity. + entitySize = HALF_TREE_SCALE; + for (i = 0, length = entityIDs.length; i < length; i += 1) { + if (Entities.hasEditableRoot(entityIDs[i])) { + size = Vec3.length(Entities.getEntityProperties(entityIDs[i], "dimensions").dimensions); + if (size < entitySize) { + entityID = entityIDs[i]; + entitySize = size; + } + } + } + } + } + } + + intersection = { + intersects: overlayID !== null || entityID !== null, + overlayID: overlayID, + entityID: entityID, + handIntersected: true + }; + } + + function clear() { + // Nothing to do. + } + + function destroy() { + // Nothing to do. + } + + if (!this instanceof Hand) { + return new Hand(side); + } + + return { + valid: valid, + position: position, + orientation: orientation, + triggerPressed: triggerPressed, + triggerClicked: triggerClicked, + intersection: getIntersection, + update: update, + clear: clear, + destroy: destroy + }; +}; + +Hand.prototype = {}; diff --git a/scripts/vr-edit/modules/handles.js b/scripts/vr-edit/modules/handles.js new file mode 100644 index 0000000000..6f238ccb79 --- /dev/null +++ b/scripts/vr-edit/modules/handles.js @@ -0,0 +1,372 @@ +// +// handles.js +// +// Created by David Rowe on 21 Jul 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global Handles */ + +Handles = function (side) { + // Draws scaling handles. + + "use strict"; + + var boundingBoxOverlay, + boundingBoxDimensions, + boundingBoxLocalCenter, + cornerIndexes = [], + cornerHandleOverlays = [], + faceHandleOverlays = [], + faceHandleOffsets, + BOUNDING_BOX_COLOR = { red: 0, green: 240, blue: 240 }, + BOUNDING_BOX_ALPHA = 0.8, + HANDLE_NORMAL_COLOR = { red: 0, green: 240, blue: 240 }, + HANDLE_HOVER_COLOR = { red: 0, green: 255, blue: 120 }, + HANDLE_NORMAL_ALPHA = 0.7, + HANDLE_HOVER_ALPHA = 0.9, + NUM_CORNERS = 8, + NUM_CORNER_HANDLES = 2, + CORNER_HANDLE_OVERLAY_DIMENSIONS = { x: 0.1, y: 0.1, z: 0.1 }, + CORNER_HANDLE_OVERLAY_AXES, + NUM_FACE_HANDLES = 6, + FACE_HANDLE_OVERLAY_DIMENSIONS = { x: 0.1, y: 0.12, z: 0.1 }, + FACE_HANDLE_OVERLAY_AXES, + FACE_HANDLE_OVERLAY_OFFSETS, + FACE_HANDLE_OVERLAY_ROTATIONS, + FACE_HANDLE_OVERLAY_SCALE_AXES, + ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), + DISTANCE_MULTIPLIER_MULTIPLIER = 0.5, + hoveredOverlayID = null, + isVisible = false, + + // Scaling. + scalingBoundingBoxDimensions, + scalingBoundingBoxLocalCenter, + + i; + + CORNER_HANDLE_OVERLAY_AXES = [ + // Ordered such that items 4 apart are opposite corners - used in display(). + { x: -0.5, y: -0.5, z: -0.5 }, + { x: -0.5, y: -0.5, z: 0.5 }, + { x: -0.5, y: 0.5, z: -0.5 }, + { x: -0.5, y: 0.5, z: 0.5 }, + { x: 0.5, y: 0.5, z: 0.5 }, + { x: 0.5, y: 0.5, z: -0.5 }, + { x: 0.5, y: -0.5, z: 0.5 }, + { x: 0.5, y: -0.5, z: -0.5 } + ]; + + FACE_HANDLE_OVERLAY_AXES = [ + { x: -0.5, y: 0, z: 0 }, + { x: 0.5, y: 0, z: 0 }, + { x: 0, y: -0.5, z: 0 }, + { x: 0, y: 0.5, z: 0 }, + { x: 0, y: 0, z: -0.5 }, + { x: 0, y: 0, z: 0.5 } + ]; + + FACE_HANDLE_OVERLAY_OFFSETS = { + x: FACE_HANDLE_OVERLAY_DIMENSIONS.y, + y: FACE_HANDLE_OVERLAY_DIMENSIONS.y, + z: FACE_HANDLE_OVERLAY_DIMENSIONS.y + }; + + FACE_HANDLE_OVERLAY_ROTATIONS = [ + Quat.fromVec3Degrees({ x: 0, y: 0, z: 90 }), + Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }), + Quat.fromVec3Degrees({ x: 180, y: 0, z: 0 }), + Quat.fromVec3Degrees({ x: 0, y: 0, z: 0 }), + Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }), + Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) + ]; + + FACE_HANDLE_OVERLAY_SCALE_AXES = [ + Vec3.UNIT_X, + Vec3.UNIT_X, + Vec3.UNIT_Y, + Vec3.UNIT_Y, + Vec3.UNIT_Z, + Vec3.UNIT_Z + ]; + + boundingBoxOverlay = Overlays.addOverlay("cube", { + color: BOUNDING_BOX_COLOR, + alpha: BOUNDING_BOX_ALPHA, + solid: false, + drawInFront: true, + ignoreRayIntersection: true, + visible: false + }); + + for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { + cornerHandleOverlays[i] = Overlays.addOverlay("sphere", { + color: HANDLE_NORMAL_COLOR, + alpha: HANDLE_NORMAL_ALPHA, + solid: true, + drawInFront: true, + ignoreRayIntersection: false, + visible: false + }); + } + + for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + faceHandleOverlays[i] = Overlays.addOverlay("shape", { + shape: "Cone", + color: HANDLE_NORMAL_COLOR, + alpha: HANDLE_NORMAL_ALPHA, + solid: true, + drawInFront: true, + ignoreRayIntersection: false, + visible: false + }); + } + + function isAxisHandle(overlayID) { + return faceHandleOverlays.indexOf(overlayID) !== -1; + } + + function isCornerHandle(overlayID) { + return cornerHandleOverlays.indexOf(overlayID) !== -1; + } + + function isHandle(overlayID) { + return isAxisHandle(overlayID) || isCornerHandle(overlayID); + } + + function scalingAxis(overlayID) { + var axesIndex; + if (isCornerHandle(overlayID)) { + axesIndex = CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[cornerHandleOverlays.indexOf(overlayID)]]; + return Vec3.normalize(Vec3.multiplyVbyV(axesIndex, boundingBoxDimensions)); + } + return FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)]; + } + + function scalingDirections(overlayID) { + if (isCornerHandle(overlayID)) { + return Vec3.ONE; + } + return FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)]; + } + + function display(rootEntityID, boundingBox, isMultiple) { + var boundingBoxCenter, + boundingBoxOrientation, + cameraPosition, + boundingBoxVector, + distanceMultiplier, + cameraUp, + cornerPosition, + cornerVector, + crossProductScale, + maxCrossProductScale, + rightCornerIndex, + leftCornerIndex, + cornerHandleDimensions, + faceHandleDimensions, + i; + + isVisible = true; + + boundingBoxDimensions = boundingBox.dimensions; + boundingBoxCenter = boundingBox.center; + boundingBoxLocalCenter = boundingBox.localCenter; + boundingBoxOrientation = boundingBox.orientation; + + // Selection bounding box. + Overlays.editOverlay(boundingBoxOverlay, { + parentID: rootEntityID, + localPosition: boundingBoxLocalCenter, + localRotation: ZERO_ROTATION, + dimensions: boundingBoxDimensions, + visible: true + }); + + // Somewhat maintain general angular size of scale handles per bounding box center but make more distance ones + // display smaller in order to give comfortable depth cue. + cameraPosition = Camera.position; + boundingBoxVector = Vec3.subtract(boundingBox.center, Camera.position); + distanceMultiplier = DISTANCE_MULTIPLIER_MULTIPLIER + * Vec3.dot(Quat.getForward(Camera.orientation), boundingBoxVector) + / Math.sqrt(Vec3.length(boundingBoxVector)); + + // Corner scale handles. + // At right-most and opposite corners of bounding box. + cameraUp = Quat.getUp(Camera.orientation); + maxCrossProductScale = 0; + for (i = 0; i < NUM_CORNERS; i += 1) { + cornerPosition = Vec3.sum(boundingBoxCenter, + Vec3.multiplyQbyV(boundingBoxOrientation, + Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[i], boundingBoxDimensions))); + cornerVector = Vec3.subtract(cornerPosition, cameraPosition); + crossProductScale = Vec3.dot(Vec3.cross(cornerVector, boundingBoxVector), cameraUp); + if (crossProductScale > maxCrossProductScale) { + maxCrossProductScale = crossProductScale; + rightCornerIndex = i; + } + } + leftCornerIndex = (rightCornerIndex + 4) % NUM_CORNERS; + cornerIndexes[0] = leftCornerIndex; + cornerIndexes[1] = rightCornerIndex; + cornerHandleDimensions = Vec3.multiply(distanceMultiplier, CORNER_HANDLE_OVERLAY_DIMENSIONS); + for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { + Overlays.editOverlay(cornerHandleOverlays[i], { + parentID: rootEntityID, + localPosition: Vec3.sum(boundingBoxLocalCenter, + Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[i]], boundingBoxDimensions)), + dimensions: cornerHandleDimensions, + visible: true + }); + } + + // Face scale handles. + // Only valid for a single entity because for multiple entities, some may be at an angle relative to the root entity + // which would necessitate a (non-existent) shear transform be applied to them when scaling a face of the set. + if (!isMultiple) { + faceHandleDimensions = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_DIMENSIONS); + faceHandleOffsets = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_OFFSETS); + for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + Overlays.editOverlay(faceHandleOverlays[i], { + parentID: rootEntityID, + localPosition: Vec3.sum(boundingBoxLocalCenter, + Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], Vec3.sum(boundingBoxDimensions, faceHandleOffsets))), + localRotation: FACE_HANDLE_OVERLAY_ROTATIONS[i], + dimensions: faceHandleDimensions, + visible: true + }); + } + } else { + for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + Overlays.editOverlay(faceHandleOverlays[i], { visible: false }); + } + } + } + + function startScaling() { + // Nothing to do. + } + + function scale(scale3D) { + // Scale relative to dimensions and positions at start of scaling. + + // Selection bounding box. + scalingBoundingBoxDimensions = Vec3.multiplyVbyV(scale3D, boundingBoxLocalCenter); + scalingBoundingBoxLocalCenter = Vec3.multiplyVbyV(scale3D, boundingBoxDimensions); + Overlays.editOverlay(boundingBoxOverlay, { + localPosition: scalingBoundingBoxDimensions, + dimensions: scalingBoundingBoxLocalCenter + }); + + // Corner scale handles. + for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { + Overlays.editOverlay(cornerHandleOverlays[i], { + localPosition: Vec3.sum(scalingBoundingBoxDimensions, + Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[i]], scalingBoundingBoxLocalCenter)) + }); + } + + // Face scale handles. + for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + Overlays.editOverlay(faceHandleOverlays[i], { + localPosition: Vec3.sum(scalingBoundingBoxDimensions, + Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], + Vec3.sum(scalingBoundingBoxLocalCenter, faceHandleOffsets))) + }); + } + } + + function finishScaling() { + // Adopt final scale. + boundingBoxLocalCenter = scalingBoundingBoxDimensions; + boundingBoxDimensions = scalingBoundingBoxLocalCenter; + } + + function hover(overlayID) { + if (overlayID !== hoveredOverlayID) { + if (hoveredOverlayID !== null) { + Overlays.editOverlay(hoveredOverlayID, { color: HANDLE_NORMAL_COLOR }); + hoveredOverlayID = null; + } + + if (overlayID !== null + && (faceHandleOverlays.indexOf(overlayID) !== -1 || cornerHandleOverlays.indexOf(overlayID) !== -1)) { + Overlays.editOverlay(overlayID, { + color: HANDLE_HOVER_COLOR, + alpha: HANDLE_HOVER_ALPHA + }); + hoveredOverlayID = overlayID; + } + } + } + + function grab(overlayID) { + var overlay, + isShowAll = overlayID === null, + color = isShowAll ? HANDLE_NORMAL_COLOR : HANDLE_HOVER_COLOR, + alpha = isShowAll ? HANDLE_NORMAL_ALPHA : HANDLE_HOVER_ALPHA, + i, + length; + + for (i = 0, length = cornerHandleOverlays.length; i < length; i += 1) { + overlay = cornerHandleOverlays[i]; + Overlays.editOverlay(overlay, { + visible: isVisible && (isShowAll || overlay === overlayID), + color: color, + alpha: alpha + }); + } + + for (i = 0, length = faceHandleOverlays.length; i < length; i += 1) { + overlay = faceHandleOverlays[i]; + Overlays.editOverlay(overlay, { + visible: isVisible && (isShowAll || overlay === overlayID), + color: color, + alpha: alpha + }); + } + } + + function clear() { + var i; + + isVisible = false; + + Overlays.editOverlay(boundingBoxOverlay, { visible: false }); + for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { + Overlays.editOverlay(cornerHandleOverlays[i], { visible: false }); + } + for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + Overlays.editOverlay(faceHandleOverlays[i], { visible: false }); + } + } + + function destroy() { + clear(); + Overlays.deleteOverlay(boundingBoxOverlay); + } + + if (!this instanceof Handles) { + return new Handles(side); + } + + return { + display: display, + isHandle: isHandle, + scalingAxis: scalingAxis, + scalingDirections: scalingDirections, + startScaling: startScaling, + scale: scale, + finishScaling: finishScaling, + hover: hover, + grab: grab, + clear: clear, + destroy: destroy + }; +}; + +Handles.prototype = {}; diff --git a/scripts/vr-edit/modules/highlights.js b/scripts/vr-edit/modules/highlights.js new file mode 100644 index 0000000000..26170e9374 --- /dev/null +++ b/scripts/vr-edit/modules/highlights.js @@ -0,0 +1,123 @@ +// +// highlights.js +// +// Created by David Rowe on 21 Jul 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global Highlights */ + +Highlights = function (side) { + // Draws highlights on selected entities. + + "use strict"; + + var handOverlay, + entityOverlays = [], + GRAB_HIGHLIGHT_COLOR = { red: 240, green: 240, blue: 0 }, + SCALE_HIGHLIGHT_COLOR = { red: 0, green: 240, blue: 240 }, + HAND_HIGHLIGHT_ALPHA = 0.35, + ENTITY_HIGHLIGHT_ALPHA = 0.8, + HAND_HIGHLIGHT_DIMENSIONS = { x: 0.2, y: 0.2, z: 0.2 }, + HAND_HIGHLIGHT_OFFSET = { x: 0.0, y: 0.11, z: 0.02 }, + LEFT_HAND = 0, + AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", + ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO); + + handOverlay = Overlays.addOverlay("sphere", { + dimensions: HAND_HIGHLIGHT_DIMENSIONS, + parentID: AVATAR_SELF_ID, + parentJointIndex: MyAvatar.getJointIndex(side === LEFT_HAND + ? "_CONTROLLER_LEFTHAND" + : "_CONTROLLER_RIGHTHAND"), + localPosition: HAND_HIGHLIGHT_OFFSET, + alpha: HAND_HIGHLIGHT_ALPHA, + solid: true, + drawInFront: true, + ignoreRayIntersection: true, + visible: false + }); + + function maybeAddEntityOverlay(index) { + if (index >= entityOverlays.length) { + entityOverlays.push(Overlays.addOverlay("cube", { + alpha: ENTITY_HIGHLIGHT_ALPHA, + solid: false, + drawInFront: true, + ignoreRayIntersection: true, + visible: false + })); + } + } + + function editEntityOverlay(index, details, overlayColor) { + var offset = Vec3.multiplyVbyV(Vec3.subtract(Vec3.HALF, details.registrationPoint), details.dimensions); + + Overlays.editOverlay(entityOverlays[index], { + parentID: details.id, + localPosition: offset, + localRotation: ZERO_ROTATION, + dimensions: details.dimensions, + color: overlayColor, + visible: true + }); + } + + function display(handIntersected, selection, isScale) { + var overlayColor = isScale ? SCALE_HIGHLIGHT_COLOR : GRAB_HIGHLIGHT_COLOR, + i, + length; + + // Show/hide hand overlay. + Overlays.editOverlay(handOverlay, { + color: overlayColor, + visible: handIntersected + }); + + // Add/edit entity overlay. + for (i = 0, length = selection.length; i < length; i += 1) { + maybeAddEntityOverlay(i); + editEntityOverlay(i, selection[i], overlayColor); + } + + // Delete extra entity overlays. + for (i = entityOverlays.length - 1, length = selection.length; i >= length; i -= 1) { + Overlays.deleteOverlay(entityOverlays[i]); + entityOverlays.splice(i, 1); + } + } + + function clear() { + var i, + length; + + // Hide hand overlay. + Overlays.editOverlay(handOverlay, { visible: false }); + + // Delete entity overlays. + for (i = 0, length = entityOverlays.length; i < length; i += 1) { + Overlays.deleteOverlay(entityOverlays[i]); + } + entityOverlays = []; + } + + function destroy() { + clear(); + Overlays.deleteOverlay(handOverlay); + } + + if (!this instanceof Highlights) { + return new Highlights(); + } + + return { + display: display, + clear: clear, + destroy: destroy + }; +}; + +Highlights.prototype = {}; diff --git a/scripts/vr-edit/modules/laser.js b/scripts/vr-edit/modules/laser.js new file mode 100644 index 0000000000..0439baef72 --- /dev/null +++ b/scripts/vr-edit/modules/laser.js @@ -0,0 +1,241 @@ +// +// laser.js +// +// Created by David Rowe on 21 Jul 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global Laser */ + +Laser = function (side) { + // Draws hand lasers. + // May intersect with entities or bounding box of other hand's selection. + + "use strict"; + + var isLaserEnabled = true, + isLaserOn = false, + + laserLine = null, + laserSphere = null, + + searchDistance = 0.0, + + SEARCH_SPHERE_SIZE = 0.013, // Per handControllerGrab.js multiplied by 1.2 per handControllerGrab.js. + SEARCH_SPHERE_FOLLOW_RATE = 0.5, // Per handControllerGrab.js. + COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 }, // Per handControllgerGrab.js. + COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }, // Per handControllgerGrab.js. + COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT, + COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT, + BRIGHT_POW = 0.06, // Per handControllgerGrab.js. + + GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. + + PICK_MAX_DISTANCE = 500, // Per handControllerGrab.js. + PRECISION_PICKING = true, + NO_INCLUDE_IDS = [], + NO_EXCLUDE_IDS = [], + VISIBLE_ONLY = true, + + laserLength, + specifiedLaserLength = null, + + LEFT_HAND = 0, + AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", + + intersection; + + function colorPow(color, power) { // Per handControllerGrab.js. + return { + red: Math.pow(color.red / 255, power) * 255, + green: Math.pow(color.green / 255, power) * 255, + blue: Math.pow(color.blue / 255, power) * 255 + }; + } + + COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_HALF_SQUEEZE, BRIGHT_POW); + COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_FULL_SQUEEZE, BRIGHT_POW); + + if (side === LEFT_HAND) { + GRAB_POINT_SPHERE_OFFSET.x = -GRAB_POINT_SPHERE_OFFSET.x; + } + + laserLine = Overlays.addOverlay("line3d", { + lineWidth: 5, + alpha: 1.0, + glow: 1.0, + ignoreRayIntersection: true, + drawInFront: true, + parentID: AVATAR_SELF_ID, + parentJointIndex: MyAvatar.getJointIndex(side === LEFT_HAND + ? "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND" + : "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"), + visible: false + }); + laserSphere = Overlays.addOverlay("circle3d", { + innerAlpha: 1.0, + outerAlpha: 0.0, + solid: true, + ignoreRayIntersection: true, + drawInFront: true, + visible: false + }); + + function updateLine(start, end, color) { + Overlays.editOverlay(laserLine, { + start: start, + end: end, + color: color, + visible: true + }); + } + + function updateSphere(location, size, color, brightColor) { + var rotation; + + rotation = Quat.lookAt(location, Camera.getPosition(), Vec3.UP); + + Overlays.editOverlay(laserSphere, { + position: location, + rotation: rotation, + innerColor: brightColor, + outerColor: color, + outerRadius: size, + visible: true + }); + } + + function display(origin, direction, distance, isClicked) { + var searchTarget, + sphereSize, + color, + brightColor; + + searchDistance = SEARCH_SPHERE_FOLLOW_RATE * searchDistance + (1.0 - SEARCH_SPHERE_FOLLOW_RATE) * distance; + searchTarget = Vec3.sum(origin, Vec3.multiply(searchDistance, direction)); + sphereSize = SEARCH_SPHERE_SIZE * searchDistance; + color = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE : COLORS_GRAB_SEARCHING_HALF_SQUEEZE; + brightColor = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT : COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT; + + updateLine(origin, searchTarget, color); + updateSphere(searchTarget, sphereSize, color, brightColor); + } + + function hide() { + Overlays.editOverlay(laserLine, { visible: false }); + Overlays.editOverlay(laserSphere, { visible: false }); + } + + function update(hand) { + var handPosition, + handOrientation, + deltaOrigin, + pickRay; + + if (!isLaserEnabled) { + return; + } + + if (!hand.intersection().intersects && hand.triggerPressed()) { + handPosition = hand.position(); + handOrientation = hand.orientation(); + deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); + pickRay = { + origin: Vec3.sum(handPosition, deltaOrigin), + direction: Quat.getUp(handOrientation), + length: PICK_MAX_DISTANCE + }; + + intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + if (!intersection.intersects) { + intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + if (intersection.intersects && !Entities.hasEditableRoot(intersection.entityID)) { + intersection.intersects = false; + intersection.entityID = null; + } + } + intersection.laserIntersected = true; + laserLength = (specifiedLaserLength !== null) + ? specifiedLaserLength + : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); + + isLaserOn = true; + display(pickRay.origin, pickRay.direction, laserLength, hand.triggerClicked()); + } else { + intersection = { + intersects: false + }; + if (isLaserOn) { + isLaserOn = false; + hide(); + } + } + } + + function getIntersection() { + return intersection; + } + + function setLength(length) { + specifiedLaserLength = length; + laserLength = length; + } + + function clearLength() { + specifiedLaserLength = null; + } + + function getLength() { + return laserLength; + } + + function handOffset() { + return GRAB_POINT_SPHERE_OFFSET; + } + + function clear() { + isLaserOn = false; + hide(); + } + + function enable() { + isLaserEnabled = true; + } + + function disable() { + isLaserEnabled = false; + if (isLaserOn) { + hide(); + } + isLaserOn = false; + } + + function destroy() { + Overlays.deleteOverlay(laserLine); + Overlays.deleteOverlay(laserSphere); + } + + if (!this instanceof Laser) { + return new Laser(side); + } + + return { + update: update, + intersection: getIntersection, + setLength: setLength, + clearLength: clearLength, + length: getLength, + enable: enable, + disable: disable, + handOffset: handOffset, + clear: clear, + destroy: destroy + }; +}; + +Laser.prototype = {}; diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js new file mode 100644 index 0000000000..92a4962bce --- /dev/null +++ b/scripts/vr-edit/modules/selection.js @@ -0,0 +1,328 @@ +// +// selection.js +// +// Created by David Rowe on 21 Jul 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global Selection */ + +Selection = function (side) { + // Manages set of selected entities. Currently supports just one set of linked entities. + + "use strict"; + + var selection = [], + selectedEntityID = null, + rootEntityID = null, + rootPosition, + rootOrientation, + scaleCenter, + scaleRootOffset, + scaleRootOrientation, + ENTITY_TYPE = "entity"; + + function traverseEntityTree(id, result) { + // Recursively traverses tree of entities and their children, gather IDs and properties. + var children, + properties, + SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions", "localPosition", + "dynamic", "collisionless"], + i, + length; + + properties = Entities.getEntityProperties(id, SELECTION_PROPERTIES); + result.push({ + id: id, + position: properties.position, + localPosition: properties.localPosition, + registrationPoint: properties.registrationPoint, + rotation: properties.rotation, + dimensions: properties.dimensions, + dynamic: properties.dynamic, + collisionless: properties.collisionless + }); + + children = Entities.getChildrenIDs(id); + for (i = 0, length = children.length; i < length; i += 1) { + if (Entities.getNestableType(children[i]) === ENTITY_TYPE) { + traverseEntityTree(children[i], result); + } + } + } + + function select(entityID) { + var entityProperties, + PARENT_PROPERTIES = ["parentID", "position", "rotation", "dymamic", "collisionless"]; + + // Find root parent. + rootEntityID = Entities.rootOf(entityID); + + // Selection position and orientation is that of the root entity. + entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); + rootPosition = entityProperties.position; + rootOrientation = entityProperties.rotation; + + // Find all children. + selection = []; + traverseEntityTree(rootEntityID, selection); + + selectedEntityID = entityID; + } + + function getRootEntityID() { + return rootEntityID; + } + + function getSelection() { + return selection; + } + + function count() { + return selection.length; + } + + function getBoundingBox() { + var center, + localCenter, + orientation, + inverseOrientation, + dimensions, + min, + max, + i, + j, + length, + registration, + position, + rotation, + corners = [], + NUM_CORNERS = 8; + + if (selection.length === 1) { + if (Vec3.equal(selection[0].registrationPoint, Vec3.HALF)) { + center = rootPosition; + } else { + center = Vec3.sum(rootPosition, + Vec3.multiplyQbyV(rootOrientation, + Vec3.multiplyVbyV(selection[0].dimensions, + Vec3.subtract(Vec3.HALF, selection[0].registrationPoint)))); + } + localCenter = Vec3.multiplyQbyV(Quat.inverse(rootOrientation), Vec3.subtract(center, rootPosition)); + orientation = rootOrientation; + dimensions = selection[0].dimensions; + } else if (selection.length > 1) { + // Find min & max x, y, z values of entities' dimension box corners in root entity coordinate system. + // Note: Don't use entities' bounding boxes because they're in world coordinates and may make the calculated + // bounding box be larger than necessary. + min = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ZERO, selection[0].registrationPoint), selection[0].dimensions); + max = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ONE, selection[0].registrationPoint), selection[0].dimensions); + inverseOrientation = Quat.inverse(rootOrientation); + for (i = 1, length = selection.length; i < length; i += 1) { + + registration = selection[i].registrationPoint; + corners[0] = { x: -registration.x, y: -registration.y, z: -registration.z }; + corners[1] = { x: -registration.x, y: -registration.y, z: 1.0 - registration.z }; + corners[2] = { x: -registration.x, y: 1.0 - registration.y, z: -registration.z }; + corners[3] = { x: -registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z }; + corners[4] = { x: 1.0 - registration.x, y: -registration.y, z: -registration.z }; + corners[5] = { x: 1.0 - registration.x, y: -registration.y, z: 1.0 - registration.z }; + corners[6] = { x: 1.0 - registration.x, y: 1.0 - registration.y, z: -registration.z }; + corners[7] = { x: 1.0 - registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z }; + + position = selection[i].position; + rotation = selection[i].rotation; + dimensions = selection[i].dimensions; + + for (j = 0; j < NUM_CORNERS; j += 1) { + // Corner position in world coordinates. + corners[j] = Vec3.sum(position, Vec3.multiplyQbyV(rotation, Vec3.multiplyVbyV(corners[j], dimensions))); + // Corner position in root entity coordinates. + corners[j] = Vec3.multiplyQbyV(inverseOrientation, Vec3.subtract(corners[j], rootPosition)); + // Update min & max. + min = Vec3.min(corners[j], min); + max = Vec3.max(corners[j], max); + } + } + + // Calculate bounding box. + center = Vec3.sum(rootPosition, + Vec3.multiplyQbyV(rootOrientation, Vec3.multiply(0.5, Vec3.sum(min, max)))); + localCenter = Vec3.multiply(0.5, Vec3.sum(min, max)); + orientation = rootOrientation; + dimensions = Vec3.subtract(max, min); + } + + return { + center: center, + localCenter: localCenter, + orientation: orientation, + dimensions: dimensions + }; + } + + function startEditing() { + var count, + i; + + // Disable entity set's physics. + for (i = 0, count = selection.length; i < count; i += 1) { + Entities.editEntity(selection[i].id, { + dynamic: false, // So that gravity doesn't fight with us trying to hold the entity in place. + collisionless: true // So that entity doesn't bump us about as we resize the entity. + }); + } + } + + function finishEditing() { + var firstDynamicEntityID = null, + properties, + VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD + VELOCITY_KICK = { x: 0, y: 0.02, z: 0 }, + count, + i; + + // Restore entity set's physics. + for (i = 0, count = selection.length; i < count; i += 1) { + if (firstDynamicEntityID === null && selection[i].dynamic) { + firstDynamicEntityID = selection[i].id; + } + Entities.editEntity(selection[i].id, { + dynamic: selection[i].dynamic, + collisionless: selection[i].collisionless + }); + } + + // If dynamic with gravity, and velocity is zero, give the entity set a little kick to set off physics. + if (firstDynamicEntityID) { + properties = Entities.getEntityProperties(firstDynamicEntityID, ["velocity", "gravity"]); + if (Vec3.length(properties.gravity) > 0 && Vec3.length(properties.velocity) < VELOCITY_THRESHOLD) { + Entities.editEntity(firstDynamicEntityID, { velocity: VELOCITY_KICK }); + } + } + } + + function getPositionAndOrientation() { + // Position and orientation of root entity. + return { + position: rootPosition, + orientation: rootOrientation + }; + } + + function setPositionAndOrientation(position, orientation) { + // Position and orientation of root entity. + rootPosition = position; + rootOrientation = orientation; + Entities.editEntity(rootEntityID, { + position: position, + rotation: orientation + }); + } + + function startDirectScaling(center) { + // Save initial position and orientation so that can scale relative to these without accumulating float errors. + scaleCenter = center; + scaleRootOffset = Vec3.subtract(rootPosition, center); + scaleRootOrientation = rootOrientation; + } + + function directScale(factor, rotation, center) { + // Scale, position, and rotate selection. + var i, + length; + + // Scale, position, and orient root. + rootPosition = Vec3.sum(center, Vec3.multiply(factor, Vec3.multiplyQbyV(rotation, scaleRootOffset))); + rootOrientation = Quat.multiply(rotation, scaleRootOrientation); + Entities.editEntity(selection[0].id, { + dimensions: Vec3.multiply(factor, selection[0].dimensions), + position: rootPosition, + rotation: rootOrientation + }); + + // Scale and position children. + for (i = 1, length = selection.length; i < length; i += 1) { + Entities.editEntity(selection[i].id, { + dimensions: Vec3.multiply(factor, selection[i].dimensions), + localPosition: Vec3.multiply(factor, selection[i].localPosition) + }); + } + } + + function finishDirectScaling() { + select(selectedEntityID); // Refresh. + } + + function startHandleScaling() { + // Nothing to do. + } + + function handleScale(factor, position, orientation) { + // Scale and reposition and orient selection. + var i, + length; + + // Scale and position root. + rootPosition = position; + rootOrientation = orientation; + Entities.editEntity(selection[0].id, { + dimensions: Vec3.multiplyVbyV(factor, selection[0].dimensions), + position: rootPosition, + rotation: rootOrientation + }); + + // Scale and position children. + // Only corner handles are used for scaling multiple entities so scale factor is the same in all dimensions. + // Therefore don't need to take into account orientation relative to parent when scaling local position. + for (i = 1, length = selection.length; i < length; i += 1) { + Entities.editEntity(selection[i].id, { + dimensions: Vec3.multiplyVbyV(factor, selection[i].dimensions), + localPosition: Vec3.multiplyVbyV(factor, selection[i].localPosition) + }); + } + } + + function finishHandleScaling() { + select(selectedEntityID); // Refresh. + } + + function clear() { + selection = []; + selectedEntityID = null; + rootEntityID = null; + } + + function destroy() { + clear(); + } + + if (!this instanceof Selection) { + return new Selection(side); + } + + return { + select: select, + selection: getSelection, + count: count, + rootEntityID: getRootEntityID, + boundingBox: getBoundingBox, + getPositionAndOrientation: getPositionAndOrientation, + setPositionAndOrientation: setPositionAndOrientation, + startEditing: startEditing, + startDirectScaling: startDirectScaling, + directScale: directScale, + finishDirectScaling: finishDirectScaling, + startHandleScaling: startHandleScaling, + handleScale: handleScale, + finishHandleScaling: finishHandleScaling, + finishEditing: finishEditing, + clear: clear, + destroy: destroy + }; +}; + +Selection.prototype = {}; diff --git a/scripts/vr-edit/utilities/utilities.js b/scripts/vr-edit/utilities/utilities.js new file mode 100644 index 0000000000..a6914b332c --- /dev/null +++ b/scripts/vr-edit/utilities/utilities.js @@ -0,0 +1,51 @@ +// +// utilities.js +// +// Created by David Rowe on 21 Jul 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +if (typeof Vec3.min !== "function") { + Vec3.min = function (a, b) { + return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), z: Math.min(a.z, b.z) }; + }; +} + +if (typeof Vec3.max !== "function") { + Vec3.max = function (a, b) { + return { x: Math.max(a.x, b.x), y: Math.max(a.y, b.y), z: Math.max(a.z, b.z) }; + }; +} + +if (typeof Entities.rootOf !== "function") { + Entities.rootOf = function (entityID) { + var rootEntityID, + entityProperties, + PARENT_PROPERTIES = ["parentID"], + NULL_UUID = "{00000000-0000-0000-0000-000000000000}"; + rootEntityID = entityID; + entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); + while (entityProperties.parentID && entityProperties.parentID !== NULL_UUID) { + rootEntityID = entityProperties.parentID; + entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); + } + return rootEntityID; + }; +} + +if (typeof Entities.hasEditableRoot !== "function") { + Entities.hasEditableRoot = function (entityID) { + var EDITIBLE_ENTITY_QUERY_PROPERTYES = ["parentID", "visible", "locked", "type"], + NONEDITABLE_ENTITY_TYPES = ["Unknown", "Zone", "Light"], + NULL_UUID = "{00000000-0000-0000-0000-000000000000}", + properties; + properties = Entities.getEntityProperties(entityID, EDITIBLE_ENTITY_QUERY_PROPERTYES); + while (properties.parentID && properties.parentID !== NULL_UUID) { + properties = Entities.getEntityProperties(properties.parentID, EDITIBLE_ENTITY_QUERY_PROPERTYES); + } + return properties.visible && !properties.locked && NONEDITABLE_ENTITY_TYPES.indexOf(properties.type) === -1; + }; +} diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index f095b21331..3df0f93a53 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1,5 +1,3 @@ -"use strict"; - // // vr-edit.js // @@ -12,6 +10,8 @@ (function () { + "use strict"; + var APP_NAME = "VR EDIT", // TODO: App name. APP_ICON_INACTIVE = "icons/tablet-icons/edit-i.svg", // TODO: App icons. APP_ICON_ACTIVE = "icons/tablet-icons/edit-a.svg", @@ -31,49 +31,25 @@ UPDATE_LOOP_TIMEOUT = 16, updateTimer = null, - Highlights, - Handles, - Selection, - Laser, + // Modules Hand, + Handles, + Highlights, + Laser, + Selection, Editor, - AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", - NULL_UUID = "{00000000-0000-0000-0000-000000000000}", - HALF_TREE_SCALE = 16384, - DEBUG = true; // TODO: Set false. + // Utilities + Script.include("./utilities/utilities.js"); - if (typeof Vec3.min !== "function") { - Vec3.min = function (a, b) { - return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), z: Math.min(a.z, b.z) }; - }; - } - - if (typeof Vec3.max !== "function") { - Vec3.max = function (a, b) { - return { x: Math.max(a.x, b.x), y: Math.max(a.y, b.y), z: Math.max(a.z, b.z) }; - }; - } - - if (typeof Entities.rootOf !== "function") { - Entities.rootOf = function (entityID) { - var rootEntityID, - entityProperties, - PARENT_PROPERTIES = ["parentID"]; - if (entityID === undefined || entityID === null) { - return null; - } - rootEntityID = entityID; - entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); - while (entityProperties.parentID !== NULL_UUID) { - rootEntityID = entityProperties.parentID; - entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); - } - return rootEntityID; - }; - } + // Modules + Script.include("./modules/hand.js"); + Script.include("./modules/handles.js"); + Script.include("./modules/highlights.js"); + Script.include("./modules/laser.js"); + Script.include("./modules/selection.js"); function log(message) { @@ -94,1198 +70,6 @@ } } - function isEditableRoot(entityID) { - var EDITIBLE_ENTITY_QUERY_PROPERTYES = ["parentID", "visible", "locked", "type"], - NONEDITABLE_ENTITY_TYPES = ["Unknown", "Zone", "Light"], - properties; - properties = Entities.getEntityProperties(entityID, EDITIBLE_ENTITY_QUERY_PROPERTYES); - while (properties.parentID && properties.parentID !== NULL_UUID) { - properties = Entities.getEntityProperties(properties.parentID, EDITIBLE_ENTITY_QUERY_PROPERTYES); - } - return properties.visible && !properties.locked && NONEDITABLE_ENTITY_TYPES.indexOf(properties.type) === -1; - } - - - Highlights = function (side) { - // Draws highlights on selected entities. - var handOverlay, - entityOverlays = [], - GRAB_HIGHLIGHT_COLOR = { red: 240, green: 240, blue: 0 }, - SCALE_HIGHLIGHT_COLOR = { red: 0, green: 240, blue: 240 }, - HAND_HIGHLIGHT_ALPHA = 0.35, - ENTITY_HIGHLIGHT_ALPHA = 0.8, - HAND_HIGHLIGHT_DIMENSIONS = { x: 0.2, y: 0.2, z: 0.2 }, - HAND_HIGHLIGHT_OFFSET = { x: 0.0, y: 0.11, z: 0.02 }, - ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO); - - handOverlay = Overlays.addOverlay("sphere", { - dimensions: HAND_HIGHLIGHT_DIMENSIONS, - parentID: AVATAR_SELF_ID, - parentJointIndex: MyAvatar.getJointIndex(side === LEFT_HAND - ? "_CONTROLLER_LEFTHAND" - : "_CONTROLLER_RIGHTHAND"), - localPosition: HAND_HIGHLIGHT_OFFSET, - alpha: HAND_HIGHLIGHT_ALPHA, - solid: true, - drawInFront: true, - ignoreRayIntersection: true, - visible: false - }); - - function maybeAddEntityOverlay(index) { - if (index >= entityOverlays.length) { - entityOverlays.push(Overlays.addOverlay("cube", { - alpha: ENTITY_HIGHLIGHT_ALPHA, - solid: false, - drawInFront: true, - ignoreRayIntersection: true, - visible: false - })); - } - } - - function editEntityOverlay(index, details, overlayColor) { - var offset = Vec3.multiplyVbyV(Vec3.subtract(Vec3.HALF, details.registrationPoint), details.dimensions); - - Overlays.editOverlay(entityOverlays[index], { - parentID: details.id, - localPosition: offset, - localRotation: ZERO_ROTATION, - dimensions: details.dimensions, - color: overlayColor, - visible: true - }); - } - - function display(handIntersected, selection, isScale) { - var overlayColor = isScale ? SCALE_HIGHLIGHT_COLOR : GRAB_HIGHLIGHT_COLOR, - i, - length; - - // Show/hide hand overlay. - Overlays.editOverlay(handOverlay, { - color: overlayColor, - visible: handIntersected - }); - - // Add/edit entity overlay. - for (i = 0, length = selection.length; i < length; i += 1) { - maybeAddEntityOverlay(i); - editEntityOverlay(i, selection[i], overlayColor); - } - - // Delete extra entity overlays. - for (i = entityOverlays.length - 1, length = selection.length; i >= length; i -= 1) { - Overlays.deleteOverlay(entityOverlays[i]); - entityOverlays.splice(i, 1); - } - } - - function clear() { - var i, - length; - - // Hide hand overlay. - Overlays.editOverlay(handOverlay, { visible: false }); - - // Delete entity overlays. - for (i = 0, length = entityOverlays.length; i < length; i += 1) { - Overlays.deleteOverlay(entityOverlays[i]); - } - entityOverlays = []; - } - - function destroy() { - clear(); - Overlays.deleteOverlay(handOverlay); - } - - if (!this instanceof Highlights) { - return new Highlights(); - } - - return { - display: display, - clear: clear, - destroy: destroy - }; - }; - - - Handles = function (side) { - var boundingBoxOverlay, - boundingBoxDimensions, - boundingBoxLocalCenter, - cornerIndexes = [], - cornerHandleOverlays = [], - faceHandleOverlays = [], - faceHandleOffsets, - BOUNDING_BOX_COLOR = { red: 0, green: 240, blue: 240 }, - BOUNDING_BOX_ALPHA = 0.8, - HANDLE_NORMAL_COLOR = { red: 0, green: 240, blue: 240 }, - HANDLE_HOVER_COLOR = { red: 0, green: 255, blue: 120 }, - HANDLE_NORMAL_ALPHA = 0.7, - HANDLE_HOVER_ALPHA = 0.9, - NUM_CORNERS = 8, - NUM_CORNER_HANDLES = 2, - CORNER_HANDLE_OVERLAY_DIMENSIONS = { x: 0.1, y: 0.1, z: 0.1 }, - CORNER_HANDLE_OVERLAY_AXES, - NUM_FACE_HANDLES = 6, - FACE_HANDLE_OVERLAY_DIMENSIONS = { x: 0.1, y: 0.12, z: 0.1 }, - FACE_HANDLE_OVERLAY_AXES, - FACE_HANDLE_OVERLAY_OFFSETS, - FACE_HANDLE_OVERLAY_ROTATIONS, - FACE_HANDLE_OVERLAY_SCALE_AXES, - ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), - DISTANCE_MULTIPLIER_MULTIPLIER = 0.5, - hoveredOverlayID = null, - isVisible = false, - - // Scaling. - scalingBoundingBoxDimensions, - scalingBoundingBoxLocalCenter, - - i; - - CORNER_HANDLE_OVERLAY_AXES = [ - // Ordered such that items 4 apart are opposite corners - used in display(). - { x: -0.5, y: -0.5, z: -0.5 }, - { x: -0.5, y: -0.5, z: 0.5 }, - { x: -0.5, y: 0.5, z: -0.5 }, - { x: -0.5, y: 0.5, z: 0.5 }, - { x: 0.5, y: 0.5, z: 0.5 }, - { x: 0.5, y: 0.5, z: -0.5 }, - { x: 0.5, y: -0.5, z: 0.5 }, - { x: 0.5, y: -0.5, z: -0.5 } - ]; - - FACE_HANDLE_OVERLAY_AXES = [ - { x: -0.5, y: 0, z: 0 }, - { x: 0.5, y: 0, z: 0 }, - { x: 0, y: -0.5, z: 0 }, - { x: 0, y: 0.5, z: 0 }, - { x: 0, y: 0, z: -0.5 }, - { x: 0, y: 0, z: 0.5 } - ]; - - FACE_HANDLE_OVERLAY_OFFSETS = { - x: FACE_HANDLE_OVERLAY_DIMENSIONS.y, - y: FACE_HANDLE_OVERLAY_DIMENSIONS.y, - z: FACE_HANDLE_OVERLAY_DIMENSIONS.y - }; - - FACE_HANDLE_OVERLAY_ROTATIONS = [ - Quat.fromVec3Degrees({ x: 0, y: 0, z: 90 }), - Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }), - Quat.fromVec3Degrees({ x: 180, y: 0, z: 0 }), - Quat.fromVec3Degrees({ x: 0, y: 0, z: 0 }), - Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }), - Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) - ]; - - FACE_HANDLE_OVERLAY_SCALE_AXES = [ - Vec3.UNIT_X, - Vec3.UNIT_X, - Vec3.UNIT_Y, - Vec3.UNIT_Y, - Vec3.UNIT_Z, - Vec3.UNIT_Z - ]; - - boundingBoxOverlay = Overlays.addOverlay("cube", { - color: BOUNDING_BOX_COLOR, - alpha: BOUNDING_BOX_ALPHA, - solid: false, - drawInFront: true, - ignoreRayIntersection: true, - visible: false - }); - - for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { - cornerHandleOverlays[i] = Overlays.addOverlay("sphere", { - color: HANDLE_NORMAL_COLOR, - alpha: HANDLE_NORMAL_ALPHA, - solid: true, - drawInFront: true, - ignoreRayIntersection: false, - visible: false - }); - } - - for (i = 0; i < NUM_FACE_HANDLES; i += 1) { - faceHandleOverlays[i] = Overlays.addOverlay("shape", { - shape: "Cone", - color: HANDLE_NORMAL_COLOR, - alpha: HANDLE_NORMAL_ALPHA, - solid: true, - drawInFront: true, - ignoreRayIntersection: false, - visible: false - }); - } - - function isAxisHandle(overlayID) { - return faceHandleOverlays.indexOf(overlayID) !== -1; - } - - function isCornerHandle(overlayID) { - return cornerHandleOverlays.indexOf(overlayID) !== -1; - } - - function isHandle(overlayID) { - return isAxisHandle(overlayID) || isCornerHandle(overlayID); - } - - function scalingAxis(overlayID) { - var axesIndex; - if (isCornerHandle(overlayID)) { - axesIndex = CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[cornerHandleOverlays.indexOf(overlayID)]]; - return Vec3.normalize(Vec3.multiplyVbyV(axesIndex, boundingBoxDimensions)); - } - return FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)]; - } - - function scalingDirections(overlayID) { - if (isCornerHandle(overlayID)) { - return Vec3.ONE; - } - return FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)]; - } - - function display(rootEntityID, boundingBox, isMultiple) { - var boundingBoxCenter, - boundingBoxOrientation, - cameraPosition, - boundingBoxVector, - distanceMultiplier, - cameraUp, - cornerPosition, - cornerVector, - crossProductScale, - maxCrossProductScale, - rightCornerIndex, - leftCornerIndex, - cornerHandleDimensions, - faceHandleDimensions, - i; - - isVisible = true; - - boundingBoxDimensions = boundingBox.dimensions; - boundingBoxCenter = boundingBox.center; - boundingBoxLocalCenter = boundingBox.localCenter; - boundingBoxOrientation = boundingBox.orientation; - - // Selection bounding box. - Overlays.editOverlay(boundingBoxOverlay, { - parentID: rootEntityID, - localPosition: boundingBoxLocalCenter, - localRotation: ZERO_ROTATION, - dimensions: boundingBoxDimensions, - visible: true - }); - - // Somewhat maintain general angular size of scale handles per bounding box center but make more distance ones - // display smaller in order to give comfortable depth cue. - cameraPosition = Camera.position; - boundingBoxVector = Vec3.subtract(boundingBox.center, Camera.position); - distanceMultiplier = DISTANCE_MULTIPLIER_MULTIPLIER - * Vec3.dot(Quat.getForward(Camera.orientation), boundingBoxVector) - / Math.sqrt(Vec3.length(boundingBoxVector)); - - // Corner scale handles. - // At right-most and opposite corners of bounding box. - cameraUp = Quat.getUp(Camera.orientation); - maxCrossProductScale = 0; - for (i = 0; i < NUM_CORNERS; i += 1) { - cornerPosition = Vec3.sum(boundingBoxCenter, - Vec3.multiplyQbyV(boundingBoxOrientation, - Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[i], boundingBoxDimensions))); - cornerVector = Vec3.subtract(cornerPosition, cameraPosition); - crossProductScale = Vec3.dot(Vec3.cross(cornerVector, boundingBoxVector), cameraUp); - if (crossProductScale > maxCrossProductScale) { - maxCrossProductScale = crossProductScale; - rightCornerIndex = i; - } - } - leftCornerIndex = (rightCornerIndex + 4) % NUM_CORNERS; - cornerIndexes[0] = leftCornerIndex; - cornerIndexes[1] = rightCornerIndex; - cornerHandleDimensions = Vec3.multiply(distanceMultiplier, CORNER_HANDLE_OVERLAY_DIMENSIONS); - for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { - Overlays.editOverlay(cornerHandleOverlays[i], { - parentID: rootEntityID, - localPosition: Vec3.sum(boundingBoxLocalCenter, - Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[i]], boundingBoxDimensions)), - dimensions: cornerHandleDimensions, - visible: true - }); - } - - // Face scale handles. - // Only valid for a single entity because for multiple entities, some may be at an angle relative to the root entity - // which would necessitate a (non-existent) shear transform be applied to them when scaling a face of the set. - if (!isMultiple) { - faceHandleDimensions = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_DIMENSIONS); - faceHandleOffsets = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_OFFSETS); - for (i = 0; i < NUM_FACE_HANDLES; i += 1) { - Overlays.editOverlay(faceHandleOverlays[i], { - parentID: rootEntityID, - localPosition: Vec3.sum(boundingBoxLocalCenter, - Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], Vec3.sum(boundingBoxDimensions, faceHandleOffsets))), - localRotation: FACE_HANDLE_OVERLAY_ROTATIONS[i], - dimensions: faceHandleDimensions, - visible: true - }); - } - } else { - for (i = 0; i < NUM_FACE_HANDLES; i += 1) { - Overlays.editOverlay(faceHandleOverlays[i], { visible: false }); - } - } - } - - function startScaling() { - // Nothing to do. - } - - function scale(scale3D) { - // Scale relative to dimensions and positions at start of scaling. - - // Selection bounding box. - scalingBoundingBoxDimensions = Vec3.multiplyVbyV(scale3D, boundingBoxLocalCenter); - scalingBoundingBoxLocalCenter = Vec3.multiplyVbyV(scale3D, boundingBoxDimensions); - Overlays.editOverlay(boundingBoxOverlay, { - localPosition: scalingBoundingBoxDimensions, - dimensions: scalingBoundingBoxLocalCenter - }); - - // Corner scale handles. - for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { - Overlays.editOverlay(cornerHandleOverlays[i], { - localPosition: Vec3.sum(scalingBoundingBoxDimensions, - Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[i]], scalingBoundingBoxLocalCenter)) - }); - } - - // Face scale handles. - for (i = 0; i < NUM_FACE_HANDLES; i += 1) { - Overlays.editOverlay(faceHandleOverlays[i], { - localPosition: Vec3.sum(scalingBoundingBoxDimensions, - Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], - Vec3.sum(scalingBoundingBoxLocalCenter, faceHandleOffsets))) - }); - } - } - - function finishScaling() { - // Adopt final scale. - boundingBoxLocalCenter = scalingBoundingBoxDimensions; - boundingBoxDimensions = scalingBoundingBoxLocalCenter; - } - - function hover(overlayID) { - if (overlayID !== hoveredOverlayID) { - if (hoveredOverlayID !== null) { - Overlays.editOverlay(hoveredOverlayID, { color: HANDLE_NORMAL_COLOR }); - hoveredOverlayID = null; - } - - if (overlayID !== null - && (faceHandleOverlays.indexOf(overlayID) !== -1 || cornerHandleOverlays.indexOf(overlayID) !== -1)) { - Overlays.editOverlay(overlayID, { - color: HANDLE_HOVER_COLOR, - alpha: HANDLE_HOVER_ALPHA - }); - hoveredOverlayID = overlayID; - } - } - } - - function grab(overlayID) { - var overlay, - isShowAll = overlayID === null, - color = isShowAll ? HANDLE_NORMAL_COLOR : HANDLE_HOVER_COLOR, - alpha = isShowAll ? HANDLE_NORMAL_ALPHA : HANDLE_HOVER_ALPHA, - i, - length; - - for (i = 0, length = cornerHandleOverlays.length; i < length; i += 1) { - overlay = cornerHandleOverlays[i]; - Overlays.editOverlay(overlay, { - visible: isVisible && (isShowAll || overlay === overlayID), - color: color, - alpha: alpha - }); - } - - for (i = 0, length = faceHandleOverlays.length; i < length; i += 1) { - overlay = faceHandleOverlays[i]; - Overlays.editOverlay(overlay, { - visible: isVisible && (isShowAll || overlay === overlayID), - color: color, - alpha: alpha - }); - } - } - - function clear() { - var i; - - isVisible = false; - - Overlays.editOverlay(boundingBoxOverlay, { visible: false }); - for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { - Overlays.editOverlay(cornerHandleOverlays[i], { visible: false }); - } - for (i = 0; i < NUM_FACE_HANDLES; i += 1) { - Overlays.editOverlay(faceHandleOverlays[i], { visible: false }); - } - } - - function destroy() { - clear(); - Overlays.deleteOverlay(boundingBoxOverlay); - } - - if (!this instanceof Handles) { - return new Handles(side); - } - - return { - display: display, - isHandle: isHandle, - scalingAxis: scalingAxis, - scalingDirections: scalingDirections, - startScaling: startScaling, - scale: scale, - finishScaling: finishScaling, - hover: hover, - grab: grab, - clear: clear, - destroy: destroy - }; - }; - - - Selection = function (side) { - // Manages set of selected entities. Currently supports just one set of linked entities. - var selection = [], - selectedEntityID = null, - rootEntityID = null, - rootPosition, - rootOrientation, - scaleCenter, - scaleRootOffset, - scaleRootOrientation, - ENTITY_TYPE = "entity"; - - function traverseEntityTree(id, result) { - // Recursively traverses tree of entities and their children, gather IDs and properties. - var children, - properties, - SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions", "localPosition", - "dynamic", "collisionless"], - i, - length; - - properties = Entities.getEntityProperties(id, SELECTION_PROPERTIES); - result.push({ - id: id, - position: properties.position, - localPosition: properties.localPosition, - registrationPoint: properties.registrationPoint, - rotation: properties.rotation, - dimensions: properties.dimensions, - dynamic: properties.dynamic, - collisionless: properties.collisionless - }); - - children = Entities.getChildrenIDs(id); - for (i = 0, length = children.length; i < length; i += 1) { - if (Entities.getNestableType(children[i]) === ENTITY_TYPE) { - traverseEntityTree(children[i], result); - } - } - } - - function select(entityID) { - var entityProperties, - PARENT_PROPERTIES = ["parentID", "position", "rotation", "dymamic", "collisionless"]; - - // Find root parent. - rootEntityID = Entities.rootOf(entityID); - - // Selection position and orientation is that of the root entity. - entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); - rootPosition = entityProperties.position; - rootOrientation = entityProperties.rotation; - - // Find all children. - selection = []; - traverseEntityTree(rootEntityID, selection); - - selectedEntityID = entityID; - } - - function getRootEntityID() { - return rootEntityID; - } - - function getSelection() { - return selection; - } - - function count() { - return selection.length; - } - - function getBoundingBox() { - var center, - localCenter, - orientation, - inverseOrientation, - dimensions, - min, - max, - i, - j, - length, - registration, - position, - rotation, - corners = [], - NUM_CORNERS = 8; - - if (selection.length === 1) { - if (Vec3.equal(selection[0].registrationPoint, Vec3.HALF)) { - center = rootPosition; - } else { - center = Vec3.sum(rootPosition, - Vec3.multiplyQbyV(rootOrientation, - Vec3.multiplyVbyV(selection[0].dimensions, - Vec3.subtract(Vec3.HALF, selection[0].registrationPoint)))); - } - localCenter = Vec3.multiplyQbyV(Quat.inverse(rootOrientation), Vec3.subtract(center, rootPosition)); - orientation = rootOrientation; - dimensions = selection[0].dimensions; - } else if (selection.length > 1) { - // Find min & max x, y, z values of entities' dimension box corners in root entity coordinate system. - // Note: Don't use entities' bounding boxes because they're in world coordinates and may make the calculated - // bounding box be larger than necessary. - min = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ZERO, selection[0].registrationPoint), selection[0].dimensions); - max = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ONE, selection[0].registrationPoint), selection[0].dimensions); - inverseOrientation = Quat.inverse(rootOrientation); - for (i = 1, length = selection.length; i < length; i += 1) { - - registration = selection[i].registrationPoint; - corners[0] = { x: -registration.x, y: -registration.y, z: -registration.z }; - corners[1] = { x: -registration.x, y: -registration.y, z: 1.0 - registration.z }; - corners[2] = { x: -registration.x, y: 1.0 - registration.y, z: -registration.z }; - corners[3] = { x: -registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z }; - corners[4] = { x: 1.0 - registration.x, y: -registration.y, z: -registration.z }; - corners[5] = { x: 1.0 - registration.x, y: -registration.y, z: 1.0 - registration.z }; - corners[6] = { x: 1.0 - registration.x, y: 1.0 - registration.y, z: -registration.z }; - corners[7] = { x: 1.0 - registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z }; - - position = selection[i].position; - rotation = selection[i].rotation; - dimensions = selection[i].dimensions; - - for (j = 0; j < NUM_CORNERS; j += 1) { - // Corner position in world coordinates. - corners[j] = Vec3.sum(position, Vec3.multiplyQbyV(rotation, Vec3.multiplyVbyV(corners[j], dimensions))); - // Corner position in root entity coordinates. - corners[j] = Vec3.multiplyQbyV(inverseOrientation, Vec3.subtract(corners[j], rootPosition)); - // Update min & max. - min = Vec3.min(corners[j], min); - max = Vec3.max(corners[j], max); - } - } - - // Calculate bounding box. - center = Vec3.sum(rootPosition, - Vec3.multiplyQbyV(rootOrientation, Vec3.multiply(0.5, Vec3.sum(min, max)))); - localCenter = Vec3.multiply(0.5, Vec3.sum(min, max)); - orientation = rootOrientation; - dimensions = Vec3.subtract(max, min); - } - - return { - center: center, - localCenter: localCenter, - orientation: orientation, - dimensions: dimensions - }; - } - - function startEditing() { - var count, - i; - - // Disable entity set's physics. - for (i = 0, count = selection.length; i < count; i += 1) { - Entities.editEntity(selection[i].id, { - dynamic: false, // So that gravity doesn't fight with us trying to hold the entity in place. - collisionless: true // So that entity doesn't bump us about as we resize the entity. - }); - } - } - - function finishEditing() { - var firstDynamicEntityID = null, - properties, - VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD - VELOCITY_KICK = { x: 0, y: 0.02, z: 0 }, - count, - i; - - // Restore entity set's physics. - for (i = 0, count = selection.length; i < count; i += 1) { - if (firstDynamicEntityID === null && selection[i].dynamic) { - firstDynamicEntityID = selection[i].id; - } - Entities.editEntity(selection[i].id, { - dynamic: selection[i].dynamic, - collisionless: selection[i].collisionless - }); - } - - // If dynamic with gravity, and velocity is zero, give the entity set a little kick to set off physics. - if (firstDynamicEntityID) { - properties = Entities.getEntityProperties(firstDynamicEntityID, ["velocity", "gravity"]); - if (Vec3.length(properties.gravity) > 0 && Vec3.length(properties.velocity) < VELOCITY_THRESHOLD) { - Entities.editEntity(firstDynamicEntityID, { velocity: VELOCITY_KICK }); - } - } - } - - function getPositionAndOrientation() { - // Position and orientation of root entity. - return { - position: rootPosition, - orientation: rootOrientation - }; - } - - function setPositionAndOrientation(position, orientation) { - // Position and orientation of root entity. - rootPosition = position; - rootOrientation = orientation; - Entities.editEntity(rootEntityID, { - position: position, - rotation: orientation - }); - } - - function startDirectScaling(center) { - // Save initial position and orientation so that can scale relative to these without accumulating float errors. - scaleCenter = center; - scaleRootOffset = Vec3.subtract(rootPosition, center); - scaleRootOrientation = rootOrientation; - } - - function directScale(factor, rotation, center) { - // Scale, position, and rotate selection. - var i, - length; - - // Scale, position, and orient root. - rootPosition = Vec3.sum(center, Vec3.multiply(factor, Vec3.multiplyQbyV(rotation, scaleRootOffset))); - rootOrientation = Quat.multiply(rotation, scaleRootOrientation); - Entities.editEntity(selection[0].id, { - dimensions: Vec3.multiply(factor, selection[0].dimensions), - position: rootPosition, - rotation: rootOrientation - }); - - // Scale and position children. - for (i = 1, length = selection.length; i < length; i += 1) { - Entities.editEntity(selection[i].id, { - dimensions: Vec3.multiply(factor, selection[i].dimensions), - localPosition: Vec3.multiply(factor, selection[i].localPosition) - }); - } - } - - function finishDirectScaling() { - select(selectedEntityID); // Refresh. - } - - function startHandleScaling() { - // Nothing to do. - } - - function handleScale(factor, position, orientation) { - // Scale and reposition and orient selection. - var i, - length; - - // Scale and position root. - rootPosition = position; - rootOrientation = orientation; - Entities.editEntity(selection[0].id, { - dimensions: Vec3.multiplyVbyV(factor, selection[0].dimensions), - position: rootPosition, - rotation: rootOrientation - }); - - // Scale and position children. - // Only corner handles are used for scaling multiple entities so scale factor is the same in all dimensions. - // Therefore don't need to take into account orientation relative to parent when scaling local position. - for (i = 1, length = selection.length; i < length; i += 1) { - Entities.editEntity(selection[i].id, { - dimensions: Vec3.multiplyVbyV(factor, selection[i].dimensions), - localPosition: Vec3.multiplyVbyV(factor, selection[i].localPosition) - }); - } - } - - function finishHandleScaling() { - select(selectedEntityID); // Refresh. - } - - function clear() { - selection = []; - selectedEntityID = null; - rootEntityID = null; - } - - function destroy() { - clear(); - } - - if (!this instanceof Selection) { - return new Selection(side); - } - - return { - select: select, - selection: getSelection, - count: count, - rootEntityID: getRootEntityID, - boundingBox: getBoundingBox, - getPositionAndOrientation: getPositionAndOrientation, - setPositionAndOrientation: setPositionAndOrientation, - startEditing: startEditing, - startDirectScaling: startDirectScaling, - directScale: directScale, - finishDirectScaling: finishDirectScaling, - startHandleScaling: startHandleScaling, - handleScale: handleScale, - finishHandleScaling: finishHandleScaling, - finishEditing: finishEditing, - clear: clear, - destroy: destroy - }; - }; - - - Laser = function (side) { - // Draws hand lasers. - // May intersect with entities or bounding box of other hand's selection. - - var isLaserEnabled = true, - isLaserOn = false, - - laserLine = null, - laserSphere = null, - - searchDistance = 0.0, - - SEARCH_SPHERE_SIZE = 0.013, // Per handControllerGrab.js multiplied by 1.2 per handControllerGrab.js. - SEARCH_SPHERE_FOLLOW_RATE = 0.5, // Per handControllerGrab.js. - COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 }, // Per handControllgerGrab.js. - COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }, // Per handControllgerGrab.js. - COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT, - COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT, - BRIGHT_POW = 0.06, // Per handControllgerGrab.js. - - GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. - - PICK_MAX_DISTANCE = 500, // Per handControllerGrab.js. - PRECISION_PICKING = true, - NO_INCLUDE_IDS = [], - NO_EXCLUDE_IDS = [], - VISIBLE_ONLY = true, - - laserLength, - specifiedLaserLength = null, - - intersection; - - function colorPow(color, power) { // Per handControllerGrab.js. - return { - red: Math.pow(color.red / 255, power) * 255, - green: Math.pow(color.green / 255, power) * 255, - blue: Math.pow(color.blue / 255, power) * 255 - }; - } - - COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_HALF_SQUEEZE, BRIGHT_POW); - COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_FULL_SQUEEZE, BRIGHT_POW); - - if (side === LEFT_HAND) { - GRAB_POINT_SPHERE_OFFSET.x = -GRAB_POINT_SPHERE_OFFSET.x; - } - - laserLine = Overlays.addOverlay("line3d", { - lineWidth: 5, - alpha: 1.0, - glow: 1.0, - ignoreRayIntersection: true, - drawInFront: true, - parentID: AVATAR_SELF_ID, - parentJointIndex: MyAvatar.getJointIndex(side === LEFT_HAND - ? "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND" - : "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"), - visible: false - }); - laserSphere = Overlays.addOverlay("circle3d", { - innerAlpha: 1.0, - outerAlpha: 0.0, - solid: true, - ignoreRayIntersection: true, - drawInFront: true, - visible: false - }); - - function updateLine(start, end, color) { - Overlays.editOverlay(laserLine, { - start: start, - end: end, - color: color, - visible: true - }); - } - - function updateSphere(location, size, color, brightColor) { - var rotation; - - rotation = Quat.lookAt(location, Camera.getPosition(), Vec3.UP); - - Overlays.editOverlay(laserSphere, { - position: location, - rotation: rotation, - innerColor: brightColor, - outerColor: color, - outerRadius: size, - visible: true - }); - } - - function display(origin, direction, distance, isClicked) { - var searchTarget, - sphereSize, - color, - brightColor; - - searchDistance = SEARCH_SPHERE_FOLLOW_RATE * searchDistance + (1.0 - SEARCH_SPHERE_FOLLOW_RATE) * distance; - searchTarget = Vec3.sum(origin, Vec3.multiply(searchDistance, direction)); - sphereSize = SEARCH_SPHERE_SIZE * searchDistance; - color = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE : COLORS_GRAB_SEARCHING_HALF_SQUEEZE; - brightColor = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT : COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT; - - updateLine(origin, searchTarget, color); - updateSphere(searchTarget, sphereSize, color, brightColor); - } - - function hide() { - Overlays.editOverlay(laserLine, { visible: false }); - Overlays.editOverlay(laserSphere, { visible: false }); - } - - function update(hand) { - var handPosition, - handOrientation, - deltaOrigin, - pickRay; - - if (!isLaserEnabled) { - return; - } - - if (!hand.intersection().intersects && hand.triggerPressed()) { - handPosition = hand.position(); - handOrientation = hand.orientation(); - deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); - pickRay = { - origin: Vec3.sum(handPosition, deltaOrigin), - direction: Quat.getUp(handOrientation), - length: PICK_MAX_DISTANCE - }; - - intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, - VISIBLE_ONLY); - if (!intersection.intersects) { - intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, - VISIBLE_ONLY); - if (intersection.intersects && !isEditableRoot(intersection.entityID)) { - intersection.intersects = false; - intersection.entityID = null; - } - } - intersection.laserIntersected = true; - laserLength = (specifiedLaserLength !== null) - ? specifiedLaserLength - : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); - - isLaserOn = true; - display(pickRay.origin, pickRay.direction, laserLength, hand.triggerClicked()); - } else { - intersection = { - intersects: false - }; - if (isLaserOn) { - isLaserOn = false; - hide(); - } - } - } - - function getIntersection() { - return intersection; - } - - function setLength(length) { - specifiedLaserLength = length; - laserLength = length; - } - - function clearLength() { - specifiedLaserLength = null; - } - - function getLength() { - return laserLength; - } - - function handOffset() { - return GRAB_POINT_SPHERE_OFFSET; - } - - function clear() { - isLaserOn = false; - hide(); - } - - function enable() { - isLaserEnabled = true; - } - - function disable() { - isLaserEnabled = false; - if (isLaserOn) { - hide(); - } - isLaserOn = false; - } - - function destroy() { - Overlays.deleteOverlay(laserLine); - Overlays.deleteOverlay(laserSphere); - } - - if (!this instanceof Laser) { - return new Laser(side); - } - - return { - update: update, - intersection: getIntersection, - setLength: setLength, - clearLength: clearLength, - length: getLength, - enable: enable, - disable: disable, - handOffset: handOffset, - clear: clear, - destroy: destroy - }; - }; - - - Hand = function (side, gripPressedCallback) { - // Hand controller input. - var handController, // ####### Rename to "controller". - controllerTrigger, - controllerTriggerClicked, - controllerGrip, - - isGripPressed = false, - GRIP_ON_VALUE = 0.99, - GRIP_OFF_VALUE = 0.95, - - isTriggerPressed, - isTriggerClicked, - TRIGGER_ON_VALUE = 0.15, // Per handControllerGrab.js. - TRIGGER_OFF_VALUE = 0.1, // Per handControllerGrab.js. - - NEAR_GRAB_RADIUS = 0.1, // Per handControllerGrab.js. - NEAR_HOVER_RADIUS = 0.025, - - handPose, - handPosition, - handOrientation, - - intersection = {}; - - if (side === LEFT_HAND) { - handController = Controller.Standard.LeftHand; - controllerTrigger = Controller.Standard.LT; - controllerTriggerClicked = Controller.Standard.LTClick; - controllerGrip = Controller.Standard.LeftGrip; - } else { - handController = Controller.Standard.RightHand; - controllerTrigger = Controller.Standard.RT; - controllerTriggerClicked = Controller.Standard.RTClick; - controllerGrip = Controller.Standard.RightGrip; - } - - function valid() { - return handPose.valid; - } - - function position() { - return handPosition; - } - - function orientation() { - return handOrientation; - } - - function triggerPressed() { - return isTriggerPressed; - } - - function triggerClicked() { - return isTriggerClicked; - } - - function getIntersection() { - return intersection; - } - - function update() { - var gripValue, - palmPosition, - overlayID, - overlayIDs, - overlayDistance, - distance, - entityID, - entityIDs, - entitySize, - size, - i, - length; - - - // Hand pose. - handPose = Controller.getPoseValue(handController); - if (!handPose.valid) { - intersection = {}; - return; - } - handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); - handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); - - // Controller trigger. - isTriggerPressed = Controller.getValue(controllerTrigger) > (isTriggerPressed - ? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE); - isTriggerClicked = Controller.getValue(controllerTriggerClicked); - - // Controller grip. - gripValue = Controller.getValue(controllerGrip); - if (isGripPressed) { - isGripPressed = gripValue > GRIP_OFF_VALUE; - } else { - isGripPressed = gripValue > GRIP_ON_VALUE; - if (isGripPressed) { - gripPressedCallback(); - } - } - - // Hand-overlay intersection, if any. - overlayID = null; - palmPosition = side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); - overlayIDs = Overlays.findOverlays(palmPosition, NEAR_HOVER_RADIUS); - if (overlayIDs.length > 0) { - // Typically, there will be only one overlay; optimize for that case. - overlayID = overlayIDs[0]; - if (overlayIDs.length > 1) { - // Find closest overlay. - overlayDistance = Vec3.length(Vec3.subtract(Overlays.getProperty(overlayID, "position"), palmPosition)); - for (i = 1, length = overlayIDs.length; i < length; i += 1) { - distance = - Vec3.length(Vec3.subtract(Overlays.getProperty(overlayIDs[i], "position"), palmPosition)); - if (distance > overlayDistance) { - overlayID = overlayIDs[i]; - overlayDistance = distance; - } - } - } - } - - // Hand-entity intersection, if any, if overlay not intersected. - entityID = null; - if (overlayID === null) { - // palmPosition is set above. - entityIDs = Entities.findEntities(palmPosition, NEAR_GRAB_RADIUS); - if (entityIDs.length > 0) { - // Typically, there will be only one entity; optimize for that case. - if (isEditableRoot(entityIDs[0])) { - entityID = entityIDs[0]; - } - if (entityIDs.length > 1) { - // Find smallest, editable entity. - entitySize = HALF_TREE_SCALE; - for (i = 0, length = entityIDs.length; i < length; i += 1) { - if (isEditableRoot(entityIDs[i])) { - size = Vec3.length(Entities.getEntityProperties(entityIDs[i], "dimensions").dimensions); - if (size < entitySize) { - entityID = entityIDs[i]; - entitySize = size; - } - } - } - } - } - } - - intersection = { - intersects: overlayID !== null || entityID !== null, - overlayID: overlayID, - entityID: entityID, - handIntersected: true - }; - } - - function clear() { - // Nothing to do. - } - - function destroy() { - // Nothing to do. - } - - if (!this instanceof Hand) { - return new Hand(side); - } - - return { - valid: valid, - position: position, - orientation: orientation, - triggerPressed: triggerPressed, - triggerClicked: triggerClicked, - intersection: getIntersection, - update: update, - clear: clear, - destroy: destroy - }; - }; - Editor = function (side, gripPressedCallback) { // Each controller has a hand, laser, an entity selection, entity highlighter, and entity handles. From 44fe60ddda3081533d8c84ad822fea99c90d8c49 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 22 Jul 2017 11:47:14 +1200 Subject: [PATCH 079/722] Stub dominant hand setting support --- scripts/vr-edit/vr-edit.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 3df0f93a53..c47deb9961 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -18,11 +18,12 @@ tablet, button, + VR_EDIT_SETTING = "io.highfidelity.isVREditing", // Note: This constant is duplicated in utils.js. + // Application state isAppActive = false, isAppScaleWithHandles = false, - - VR_EDIT_SETTING = "io.highfidelity.isVREditing", // Note: This constant is duplicated in utils.js. + dominantHand, editors = [], LEFT_HAND = 0, @@ -825,6 +826,13 @@ } } + function onDominantHandChanged() { + /* + // TODO: API coming. + dominantHand = TODO; + */ + } + function setUp() { updateHandControllerGrab(); @@ -851,6 +859,14 @@ editors[LEFT_HAND].setOtherEditor(editors[RIGHT_HAND]); editors[RIGHT_HAND].setOtherEditor(editors[LEFT_HAND]); + // Dominant hand from settings. + // TODO: API coming. + dominantHand = RIGHT_HAND; + /* + dominantHand = TODO; + TODO.change.connect(onDominantHandChanged); + */ + if (isAppActive) { update(); } From 9cb8aea4fde9f3469ac2dc2e1196e319cf8246ce Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 22 Jul 2017 13:06:15 +1200 Subject: [PATCH 080/722] Make grip click delete selection --- scripts/vr-edit/modules/selection.js | 8 +++++++ scripts/vr-edit/vr-edit.js | 34 ++++++++++++++++------------ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 92a4962bce..bf1d519941 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -296,6 +296,13 @@ Selection = function (side) { rootEntityID = null; } + function deleteEntities() { + if (rootEntityID) { + Entities.deleteEntity(rootEntityID); // Children are automatically deleted. + clear(); + } + } + function destroy() { clear(); } @@ -320,6 +327,7 @@ Selection = function (side) { handleScale: handleScale, finishHandleScaling: finishHandleScaling, finishEditing: finishEditing, + deleteEntities: deleteEntities, clear: clear, destroy: destroy }; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index c47deb9961..48a066ea56 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -72,7 +72,7 @@ } - Editor = function (side, gripPressedCallback) { + Editor = function (side) { // Each controller has a hand, laser, an entity selection, entity highlighter, and entity handles. var otherEditor, // Other hand's Editor object. @@ -84,16 +84,17 @@ EDITOR_GRABBING = 3, EDITOR_DIRECT_SCALING = 4, // Scaling data are sent to other editor's EDITOR_GRABBING state. EDITOR_HANDLE_SCALING = 5, // "" - editorState = EDITOR_IDLE, EDITOR_STATE_STRINGS = ["EDITOR_IDLE", "EDITOR_SEARCHING", "EDITOR_HIGHLIGHTING", "EDITOR_GRABBING", "EDITOR_DIRECT_SCALING", "EDITOR_HANDLE_SCALING"], + editorState = EDITOR_IDLE, // State machine. STATE_MACHINE, - highlightedEntityID = null, + highlightedEntityID = null, // Root entity of highlighted entity set. wasAppScaleWithHandles = false, isOtherEditorEditingEntityID = false, hoveredOverlayID = null, + doDeleteEntities = false, // Primary objects. hand, @@ -127,7 +128,14 @@ intersection; - hand = new Hand(side, gripPressedCallback); + function onGripClicked() { + // Delete entity set grabbed by this hand. + if (editorState === EDITOR_GRABBING) { + doDeleteEntities = true; + } + } + + hand = new Hand(side, onGripClicked); laser = new Laser(side); selection = new Selection(side); highlights = new Highlights(side); @@ -135,6 +143,7 @@ laserOffset = laser.handOffset(); + function setOtherEditor(editor) { otherEditor = editor; } @@ -427,6 +436,7 @@ } startEditing(); wasAppScaleWithHandles = isAppScaleWithHandles; + doDeleteEntities = false; } function updateEditorGrabbing() { @@ -637,7 +647,7 @@ } break; case EDITOR_GRABBING: - if (hand.valid() && hand.triggerClicked()) { + if (hand.valid() && hand.triggerClicked() && !doDeleteEntities) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // No transition. if (isAppScaleWithHandles !== wasAppScaleWithHandles) { @@ -655,6 +665,9 @@ } else { setState(EDITOR_SEARCHING); } + } else if (doDeleteEntities) { + selection.deleteEntities(); + setState(EDITOR_SEARCHING); } else { debug(side, "ERROR: Unexpected condition in EDITOR_GRABBING!"); } @@ -819,13 +832,6 @@ } } - function onGripClicked() { - // Do not change scale mode if are currently scaling. - if (!editors[LEFT_HAND].isScaling() && !editors[RIGHT_HAND].isScaling()) { - isAppScaleWithHandles = !isAppScaleWithHandles; - } - } - function onDominantHandChanged() { /* // TODO: API coming. @@ -854,8 +860,8 @@ } // Hands, each with a laser, selection, etc. - editors[LEFT_HAND] = new Editor(LEFT_HAND, onGripClicked); - editors[RIGHT_HAND] = new Editor(RIGHT_HAND, onGripClicked); + editors[LEFT_HAND] = new Editor(LEFT_HAND); + editors[RIGHT_HAND] = new Editor(RIGHT_HAND); editors[LEFT_HAND].setOtherEditor(editors[RIGHT_HAND]); editors[RIGHT_HAND].setOtherEditor(editors[LEFT_HAND]); From a7f7a2c401c1fefa8086cf47f2a3634b24475b92 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 22 Jul 2017 17:47:49 +1200 Subject: [PATCH 081/722] Add representative Tool menu UI elements --- scripts/vr-edit/modules/toolMenu.js | 136 ++++++++++++++++++++++++++++ scripts/vr-edit/vr-edit.js | 29 ++++-- 2 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 scripts/vr-edit/modules/toolMenu.js diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js new file mode 100644 index 0000000000..d67a9349e4 --- /dev/null +++ b/scripts/vr-edit/modules/toolMenu.js @@ -0,0 +1,136 @@ +// +// toolMenu.js +// +// Created by David Rowe on 22 Jul 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global ToolMenu */ + +ToolMenu = function (side, scaleModeChangedCallback) { + // Tool menu displayed on top of forearm. + + "use strict"; + + var panelEntity, + buttonEntity, + + LEFT_HAND = 0, + AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", + + FOREARM_JOINT_NAME = side === LEFT_HAND ? "LeftForeArm" : "RightForeArm", + HAND_JOINT_NAME = side === LEFT_HAND ? "LeftHand" : "RightHand", + + ROOT_X_OFFSET = side === LEFT_HAND ? -0.05 : 0.05, + ROOT_Z_OFFSET = side === LEFT_HAND ? -0.05 : 0.05, + ROOT_ROTATION = side === LEFT_HAND + ? Quat.fromVec3Degrees({ x: 180, y: -90, z: 0 }) + : Quat.fromVec3Degrees({ x: 180, y: 90, z: 0 }), + + PANEL_ENTITY_PROPERTIES = { + type: "Box", + dimensions: { x: 0.1, y: 0.2, z: 0.01 }, + color: { red: 192, green: 192, blue: 192 }, + parentID: AVATAR_SELF_ID, + registrationPoint: { x: 0, y: 0.0, z: 0.0 }, + localRotation: ROOT_ROTATION, + ignoreRayIntersection: false, + lifetime: 3600, + visible: true + }, + + BUTTON_ENTITY_PROPERTIES = { + type: "Box", + dimensions: { x: 0.03, y: 0.03, z: 0.01 }, + registrationPoint: { x: 0, y: 0.0, z: 0.0 }, + localPosition: { x: 0.005, y: 0.005, z: -0.005 }, // Relative to the root panel entity. + color: { red: 240, green: 0, blue: 0 }, + ignoreRayIntersection: false, + lifetime: 3600, + visible: true + }, + + isDisplaying = false, + + SCALE_MODE_DIRECT = 0, + SCALE_MODE_HANDLES = 1; + + function setHand(hand) { + side = hand; + + if (isDisplaying) { + // TODO: Move UI to other hand. + } + } + + function update() { + // TODO + } + + function display() { + // Creates and shows menu entities. + var forearmJointIndex, + handJointIndex, + forearmLength, + rootOffset; + + if (isDisplaying) { + return; + } + + // Joint indexes. + forearmJointIndex = MyAvatar.getJointIndex(FOREARM_JOINT_NAME); + handJointIndex = MyAvatar.getJointIndex(HAND_JOINT_NAME); + if (forearmJointIndex === -1 || handJointIndex === -1) { + // Don't display if joint isn't available (yet) to attach to. + // User can clear this condition by toggling the app off and back on once avatar finishes loading. + // TODO: Log error. + return; + } + + // Calculate position to put menu. + forearmLength = Vec3.distance(MyAvatar.getJointPosition(forearmJointIndex), MyAvatar.getJointPosition(handJointIndex)); + rootOffset = { x: ROOT_X_OFFSET, y: forearmLength, z: ROOT_Z_OFFSET }; + PANEL_ENTITY_PROPERTIES.parentJointIndex = forearmJointIndex; + PANEL_ENTITY_PROPERTIES.localPosition = rootOffset; + + // Create menu items. + panelEntity = Entities.addEntity(PANEL_ENTITY_PROPERTIES, true); + BUTTON_ENTITY_PROPERTIES.parentID = panelEntity; + buttonEntity = Entities.addEntity(BUTTON_ENTITY_PROPERTIES, true); + + isDisplaying = true; + } + + function clear() { + // Deletes menu entities. + if (!isDisplaying) { + return; + } + + Entities.deleteEntity(buttonEntity); + Entities.deleteEntity(panelEntity); + isDisplaying = false; + } + + function destroy() { + clear(); + } + + if (!this instanceof ToolMenu) { + return new ToolMenu(); + } + + return { + setHand: setHand, + update: update, + display: display, + clear: clear, + destroy: destroy + }; +}; + +ToolMenu.prototype = {}; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 48a066ea56..39c6dd9c04 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -15,9 +15,6 @@ var APP_NAME = "VR EDIT", // TODO: App name. APP_ICON_INACTIVE = "icons/tablet-icons/edit-i.svg", // TODO: App icons. APP_ICON_ACTIVE = "icons/tablet-icons/edit-a.svg", - tablet, - button, - VR_EDIT_SETTING = "io.highfidelity.isVREditing", // Note: This constant is duplicated in utils.js. // Application state @@ -25,12 +22,11 @@ isAppScaleWithHandles = false, dominantHand, + // Primary objects editors = [], LEFT_HAND = 0, RIGHT_HAND = 1, - - UPDATE_LOOP_TIMEOUT = 16, - updateTimer = null, + toolMenu, // Modules Hand, @@ -38,8 +34,15 @@ Highlights, Laser, Selection, + ToolMenu, Editor, + // Miscellaneous + UPDATE_LOOP_TIMEOUT = 16, + updateTimer = null, + tablet, + button, + DEBUG = true; // TODO: Set false. // Utilities @@ -51,6 +54,7 @@ Script.include("./modules/highlights.js"); Script.include("./modules/laser.js"); Script.include("./modules/selection.js"); + Script.include("./modules/toolMenu.js"); function log(message) { @@ -823,20 +827,27 @@ button.editProperties({ isActive: isAppActive }); if (isAppActive) { + toolMenu.display(); update(); } else { Script.clearTimeout(updateTimer); updateTimer = null; editors[LEFT_HAND].clear(); editors[RIGHT_HAND].clear(); + toolMenu.clear(); } } + function otherHand(hand) { + return (hand + 1) % 2; + } + function onDominantHandChanged() { /* // TODO: API coming. dominantHand = TODO; */ + toolMenu.setHand(otherHand(dominantHand)); } @@ -872,6 +883,7 @@ dominantHand = TODO; TODO.change.connect(onDominantHandChanged); */ + toolMenu = new ToolMenu(otherHand(dominantHand)); if (isAppActive) { update(); @@ -896,6 +908,11 @@ button = null; } + if (toolMenu) { + toolMenu.destroy(); + toolMenu = null; + } + if (editors[LEFT_HAND]) { editors[LEFT_HAND].destroy(); editors[LEFT_HAND] = null; From 4353400d23482371586c4594c6fc97f9b4030c31 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 22 Jul 2017 17:55:00 +1200 Subject: [PATCH 082/722] Fix null state transition --- scripts/vr-edit/vr-edit.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 39c6dd9c04..7ca637497f 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -745,7 +745,9 @@ } function clear() { - setState(EDITOR_IDLE); + if (editorState !== EDITOR_IDLE) { + setState(EDITOR_IDLE); + } hand.clear(); laser.clear(); From 1f244fb487080e26a4c2b607bf1f52bda9bc0395 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 24 Jul 2017 15:25:00 +1200 Subject: [PATCH 083/722] Better way of handling grip click --- scripts/vr-edit/modules/hand.js | 10 ++++++---- scripts/vr-edit/vr-edit.js | 15 +++------------ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/scripts/vr-edit/modules/hand.js b/scripts/vr-edit/modules/hand.js index a81c54e2cb..c8a473f623 100644 --- a/scripts/vr-edit/modules/hand.js +++ b/scripts/vr-edit/modules/hand.js @@ -10,7 +10,7 @@ /* global Hand */ -Hand = function (side, gripPressedCallback) { +Hand = function (side) { "use strict"; @@ -73,6 +73,10 @@ Hand = function (side, gripPressedCallback) { return isTriggerClicked; } + function gripPressed() { + return isGripPressed; + } + function getIntersection() { return intersection; } @@ -112,9 +116,6 @@ Hand = function (side, gripPressedCallback) { isGripPressed = gripValue > GRIP_OFF_VALUE; } else { isGripPressed = gripValue > GRIP_ON_VALUE; - if (isGripPressed) { - gripPressedCallback(); - } } // Hand-overlay intersection, if any. @@ -190,6 +191,7 @@ Hand = function (side, gripPressedCallback) { orientation: orientation, triggerPressed: triggerPressed, triggerClicked: triggerClicked, + gripPressed: gripPressed, intersection: getIntersection, update: update, clear: clear, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 7ca637497f..a9dcde9242 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -98,7 +98,6 @@ wasAppScaleWithHandles = false, isOtherEditorEditingEntityID = false, hoveredOverlayID = null, - doDeleteEntities = false, // Primary objects. hand, @@ -132,14 +131,7 @@ intersection; - function onGripClicked() { - // Delete entity set grabbed by this hand. - if (editorState === EDITOR_GRABBING) { - doDeleteEntities = true; - } - } - - hand = new Hand(side, onGripClicked); + hand = new Hand(side); laser = new Laser(side); selection = new Selection(side); highlights = new Highlights(side); @@ -440,7 +432,6 @@ } startEditing(); wasAppScaleWithHandles = isAppScaleWithHandles; - doDeleteEntities = false; } function updateEditorGrabbing() { @@ -651,7 +642,7 @@ } break; case EDITOR_GRABBING: - if (hand.valid() && hand.triggerClicked() && !doDeleteEntities) { + if (hand.valid() && hand.triggerClicked() && !hand.gripPressed()) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // No transition. if (isAppScaleWithHandles !== wasAppScaleWithHandles) { @@ -669,7 +660,7 @@ } else { setState(EDITOR_SEARCHING); } - } else if (doDeleteEntities) { + } else if (hand.gripPressed()) { selection.deleteEntities(); setState(EDITOR_SEARCHING); } else { From 51522edd9445211b874ac3cb89bd604f1c2b6ce3 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 24 Jul 2017 16:32:10 +1200 Subject: [PATCH 084/722] Separate out input objects from editor object --- scripts/vr-edit/vr-edit.js | 144 +++++++++++++++++++++++++++---------- 1 file changed, 107 insertions(+), 37 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index a9dcde9242..3842fbcc23 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -23,6 +23,9 @@ dominantHand, // Primary objects + Inputs, + inputs = [], + Editor, editors = [], LEFT_HAND = 0, RIGHT_HAND = 1, @@ -35,7 +38,6 @@ Laser, Selection, ToolMenu, - Editor, // Miscellaneous UPDATE_LOOP_TIMEOUT = 16, @@ -76,8 +78,75 @@ } + Inputs = function (side) { + // A hand plus a laser. + + var + // Primary objects. + hand, + laser, + + intersection = { x: "hello" }; + + hand = new Hand(side); + laser = new Laser(side); + + function getHand() { + return hand; + } + + function getLaser() { + return laser; + } + + function getIntersection() { + return intersection; + } + + function update() { + // Hand update. + hand.update(); + intersection = hand.intersection(); + + // Laser update. + // Displays laser if hand has no intersection and trigger is pressed. + if (hand.valid()) { + laser.update(hand); + if (!intersection.intersects) { + intersection = laser.intersection(); + } + } + } + + function clear() { + hand.clear(); + laser.clear(); + } + + function destroy() { + if (hand) { + hand.destroy(); + hand = null; + } + if (laser) { + laser.destroy(); + laser = null; + } + } + + return { + hand: getHand, + laser: getLaser, + getIntersection: getIntersection, + update: update, + clear: clear, + destroy: destroy + }; + }; + + Editor = function (side) { - // Each controller has a hand, laser, an entity selection, entity highlighter, and entity handles. + // An entity selection, entity highlights, and entity handles. var otherEditor, // Other hand's Editor object. @@ -100,12 +169,14 @@ hoveredOverlayID = null, // Primary objects. - hand, - laser, selection, highlights, handles, + // Input objects. + hand, + laser, + // Position values. initialHandOrientationInverse, initialHandToSelectionVector, @@ -129,19 +200,20 @@ laserOffset, MIN_SCALE = 0.001, + getIntersection, // Function. intersection; - hand = new Hand(side); - laser = new Laser(side); selection = new Selection(side); highlights = new Highlights(side); handles = new Handles(side); - laserOffset = laser.handOffset(); + function setReferences(inputs, editor) { + hand = inputs.hand(); // Object. + laser = inputs.laser(); // Object. + getIntersection = inputs.getIntersection; // Function. + otherEditor = editor; // Object. - - function setOtherEditor(editor) { - otherEditor = editor; + laserOffset = laser.handOffset(); // Value. } function hoverHandle(overlayID) { @@ -544,18 +616,7 @@ var previousState = editorState, doUpdateState; - // Hand update. - hand.update(); - intersection = hand.intersection(); - - // Laser update. - // Displays laser if hand has no intersection and trigger is pressed. - if (hand.valid()) { - laser.update(hand); - if (!intersection.intersects) { - intersection = laser.intersection(); - } - } + intersection = getIntersection(); // State update. switch (editorState) { @@ -740,22 +801,12 @@ setState(EDITOR_IDLE); } - hand.clear(); - laser.clear(); selection.clear(); highlights.clear(); handles.clear(); } function destroy() { - if (hand) { - hand.destroy(); - hand = null; - } - if (laser) { - laser.destroy(); - laser = null; - } if (selection) { selection.destroy(); selection = null; @@ -775,7 +826,7 @@ } return { - setOtherEditor: setOtherEditor, + setReferences: setReferences, hoverHandle: hoverHandle, isHandle: isHandle, isEditing: isEditing, @@ -799,7 +850,11 @@ // Main update loop. updateTimer = null; - // Each hand's action depends on the state of the other hand, so update the states first then apply actions. + // Update inputs - hands and lasers. + inputs[LEFT_HAND].update(); + inputs[RIGHT_HAND].update(); + + // Each hand's edit action depends on the state of the other hand, so update the states first then apply actions. editors[LEFT_HAND].update(); editors[RIGHT_HAND].update(); editors[LEFT_HAND].apply(); @@ -825,6 +880,8 @@ } else { Script.clearTimeout(updateTimer); updateTimer = null; + inputs[LEFT_HAND].clear(); + inputs[RIGHT_HAND].clear(); editors[LEFT_HAND].clear(); editors[RIGHT_HAND].clear(); toolMenu.clear(); @@ -863,11 +920,15 @@ button.clicked.connect(onAppButtonClicked); } - // Hands, each with a laser, selection, etc. + // Input objects. + inputs[LEFT_HAND] = new Inputs(LEFT_HAND); + inputs[RIGHT_HAND] = new Inputs(RIGHT_HAND); + + // Editor objects. editors[LEFT_HAND] = new Editor(LEFT_HAND); editors[RIGHT_HAND] = new Editor(RIGHT_HAND); - editors[LEFT_HAND].setOtherEditor(editors[RIGHT_HAND]); - editors[RIGHT_HAND].setOtherEditor(editors[LEFT_HAND]); + editors[LEFT_HAND].setReferences(inputs[LEFT_HAND], editors[RIGHT_HAND]); + editors[RIGHT_HAND].setReferences(inputs[RIGHT_HAND], editors[LEFT_HAND]); // Dominant hand from settings. // TODO: API coming. @@ -915,6 +976,15 @@ editors[RIGHT_HAND] = null; } + if (inputs[LEFT_HAND]) { + inputs[LEFT_HAND].destroy(); + inputs[LEFT_HAND] = null; + } + if (inputs[RIGHT_HAND]) { + inputs[RIGHT_HAND].destroy(); + inputs[RIGHT_HAND] = null; + } + tablet = null; } From 72e3c9a881ff75b5af82a952a8083cb508ee2716 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 24 Jul 2017 16:59:34 +1200 Subject: [PATCH 085/722] Insert UI handling between input updates and editor actions --- scripts/vr-edit/vr-edit.js | 76 +++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 3842fbcc23..d9c17bcfb5 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -25,11 +25,12 @@ // Primary objects Inputs, inputs = [], + UI, + ui, Editor, editors = [], LEFT_HAND = 0, RIGHT_HAND = 1, - toolMenu, // Modules Hand, @@ -134,6 +135,10 @@ } } + if (!this instanceof Inputs) { + return new Inputs(); + } + return { hand: getHand, laser: getLaser, @@ -145,6 +150,52 @@ }; + UI = function (side) { + // Tool menu and Create palette. + + var // Primary objects. + toolMenu; + + toolMenu = new ToolMenu(side); + + + function setHand(side) { + toolMenu.setHand(side); + } + + function display() { + toolMenu.display(); + } + + function update() { + // TODO + } + + function clear() { + toolMenu.clear(); + } + + function destroy() { + if (toolMenu) { + toolMenu.destroy(); + toolMenu = null; + } + } + + if (!this instanceof UI) { + return new UI(); + } + + return { + setHand: setHand, + display: display, + update: update, + clear: clear, + destroy: destroy + }; + }; + + Editor = function (side) { // An entity selection, entity highlights, and entity handles. @@ -854,6 +905,9 @@ inputs[LEFT_HAND].update(); inputs[RIGHT_HAND].update(); + // UI has first dibs on handling inputs. + ui.update(); + // Each hand's edit action depends on the state of the other hand, so update the states first then apply actions. editors[LEFT_HAND].update(); editors[RIGHT_HAND].update(); @@ -875,16 +929,16 @@ button.editProperties({ isActive: isAppActive }); if (isAppActive) { - toolMenu.display(); + ui.display(); update(); } else { Script.clearTimeout(updateTimer); updateTimer = null; inputs[LEFT_HAND].clear(); inputs[RIGHT_HAND].clear(); + ui.clear(); editors[LEFT_HAND].clear(); editors[RIGHT_HAND].clear(); - toolMenu.clear(); } } @@ -897,7 +951,7 @@ // TODO: API coming. dominantHand = TODO; */ - toolMenu.setHand(otherHand(dominantHand)); + ui.setHand(otherHand(dominantHand)); } @@ -924,6 +978,9 @@ inputs[LEFT_HAND] = new Inputs(LEFT_HAND); inputs[RIGHT_HAND] = new Inputs(RIGHT_HAND); + // UI object. + ui = new UI(otherHand(dominantHand)); + // Editor objects. editors[LEFT_HAND] = new Editor(LEFT_HAND); editors[RIGHT_HAND] = new Editor(RIGHT_HAND); @@ -937,7 +994,6 @@ dominantHand = TODO; TODO.change.connect(onDominantHandChanged); */ - toolMenu = new ToolMenu(otherHand(dominantHand)); if (isAppActive) { update(); @@ -962,11 +1018,6 @@ button = null; } - if (toolMenu) { - toolMenu.destroy(); - toolMenu = null; - } - if (editors[LEFT_HAND]) { editors[LEFT_HAND].destroy(); editors[LEFT_HAND] = null; @@ -976,6 +1027,11 @@ editors[RIGHT_HAND] = null; } + if (ui) { + ui.destroy(); + ui = null; + } + if (inputs[LEFT_HAND]) { inputs[LEFT_HAND].destroy(); inputs[LEFT_HAND] = null; From bf69402219b99b0cb93539b1f0ddc2983e0d28be Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 26 Jul 2017 15:31:54 +1200 Subject: [PATCH 086/722] Laser intersect but don't highlight non-editable entities --- scripts/vr-edit/modules/hand.js | 5 +++-- scripts/vr-edit/modules/laser.js | 5 +---- scripts/vr-edit/vr-edit.js | 18 +++++++++--------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/scripts/vr-edit/modules/hand.js b/scripts/vr-edit/modules/hand.js index c8a473f623..c4f5a6efc9 100644 --- a/scripts/vr-edit/modules/hand.js +++ b/scripts/vr-edit/modules/hand.js @@ -139,7 +139,7 @@ Hand = function (side) { } } - // Hand-entity intersection, if any, if overlay not intersected. + // Hand-entity intersection, if any editable, if overlay not intersected. entityID = null; if (overlayID === null) { // palmPosition is set above. @@ -169,7 +169,8 @@ Hand = function (side) { intersects: overlayID !== null || entityID !== null, overlayID: overlayID, entityID: entityID, - handIntersected: true + handIntersected: true, + editableEntity: entityID !== null }; } diff --git a/scripts/vr-edit/modules/laser.js b/scripts/vr-edit/modules/laser.js index 0439baef72..e7016af315 100644 --- a/scripts/vr-edit/modules/laser.js +++ b/scripts/vr-edit/modules/laser.js @@ -154,10 +154,7 @@ Laser = function (side) { if (!intersection.intersects) { intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, VISIBLE_ONLY); - if (intersection.intersects && !Entities.hasEditableRoot(intersection.entityID)) { - intersection.intersects = false; - intersection.entityID = null; - } + intersection.editableEntity = intersection.intersects && Entities.hasEditableRoot(intersection.entityID); } intersection.laserIntersected = true; laserLength = (specifiedLaserLength !== null) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index d9c17bcfb5..99b813c4fc 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -679,7 +679,7 @@ setState(EDITOR_SEARCHING); break; case EDITOR_SEARCHING: - if (hand.valid() && !intersection.entityID + if (hand.valid() && (!intersection.entityID || !intersection.editableEntity) && !(intersection.overlayID && hand.triggerClicked() && otherEditor.isHandle(intersection.overlayID))) { // No transition. updateState(); @@ -691,10 +691,10 @@ && otherEditor.isHandle(intersection.overlayID)) { highlightedEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); - } else if (intersection.entityID && !hand.triggerClicked()) { + } else if (intersection.entityID && intersection.editableEntity && !hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); setState(EDITOR_HIGHLIGHTING); - } else if (intersection.entityID && hand.triggerClicked()) { + } else if (intersection.entityID && intersection.editableEntity && hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); if (otherEditor.isEditing(highlightedEntityID)) { if (!isAppScaleWithHandles) { @@ -709,7 +709,7 @@ break; case EDITOR_HIGHLIGHTING: if (hand.valid() - && intersection.entityID + && intersection.entityID && intersection.editableEntity && !(hand.triggerClicked() && (!otherEditor.isEditing(highlightedEntityID) || !isAppScaleWithHandles)) && !(hand.triggerClicked() && intersection.overlayID && otherEditor.isHandle(intersection.overlayID))) { // No transition. @@ -736,7 +736,7 @@ && otherEditor.isHandle(intersection.overlayID)) { highlightedEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); - } else if (intersection.entityID && hand.triggerClicked()) { + } else if (intersection.entityID && intersection.editableEntity && hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); // May be a different entityID. if (otherEditor.isEditing(highlightedEntityID)) { if (!isAppScaleWithHandles) { @@ -747,7 +747,7 @@ } else { setState(EDITOR_GRABBING); } - } else if (!intersection.entityID) { + } else if (!intersection.entityID || !intersection.editableEntity) { setState(EDITOR_SEARCHING); } else { debug(side, "ERROR: Unexpected condition in EDITOR_HIGHLIGHTING! B"); @@ -766,7 +766,7 @@ if (!hand.valid()) { setState(EDITOR_IDLE); } else if (!hand.triggerClicked()) { - if (intersection.entityID) { + if (intersection.entityID && intersection.editableEntity) { highlightedEntityID = Entities.rootOf(intersection.entityID); setState(EDITOR_HIGHLIGHTING); } else { @@ -792,7 +792,7 @@ if (!hand.valid()) { setState(EDITOR_IDLE); } else if (!hand.triggerClicked()) { - if (!intersection.entityID) { + if (!intersection.entityID || !intersection.editableEntity) { setState(EDITOR_SEARCHING); } else { highlightedEntityID = Entities.rootOf(intersection.entityID); @@ -815,7 +815,7 @@ if (!hand.valid()) { setState(EDITOR_IDLE); } else if (!hand.triggerClicked()) { - if (!intersection.entityID) { + if (!intersection.entityID || !intersection.editableEntity) { setState(EDITOR_SEARCHING); } else { highlightedEntityID = Entities.rootOf(intersection.entityID); From 0f2176127e6ec5bd7899cb09204fc6e01dc903c0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 26 Jul 2017 15:41:16 +1200 Subject: [PATCH 087/722] Fix locations of dominant hand settings and update setup --- scripts/vr-edit/vr-edit.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 99b813c4fc..dcd8145472 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -963,6 +963,13 @@ return; } + // Settings values. + // TODO: API coming. + dominantHand = RIGHT_HAND; + /* + dominantHand = TODO; + */ + // Tablet/toolbar button. button = tablet.addButton({ icon: APP_ICON_INACTIVE, @@ -987,11 +994,9 @@ editors[LEFT_HAND].setReferences(inputs[LEFT_HAND], editors[RIGHT_HAND]); editors[RIGHT_HAND].setReferences(inputs[RIGHT_HAND], editors[LEFT_HAND]); - // Dominant hand from settings. - // TODO: API coming. - dominantHand = RIGHT_HAND; + // Settings changes. /* - dominantHand = TODO; + // TODO: API coming. TODO.change.connect(onDominantHandChanged); */ From 194a82974bc3da7c3277745ed9cd51e57fae3dc8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 26 Jul 2017 15:53:34 +1200 Subject: [PATCH 088/722] Make laser dot a minimum size commensurate with near laser beam width --- scripts/vr-edit/modules/laser.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/laser.js b/scripts/vr-edit/modules/laser.js index e7016af315..2a765cfb4d 100644 --- a/scripts/vr-edit/modules/laser.js +++ b/scripts/vr-edit/modules/laser.js @@ -25,6 +25,7 @@ Laser = function (side) { searchDistance = 0.0, SEARCH_SPHERE_SIZE = 0.013, // Per handControllerGrab.js multiplied by 1.2 per handControllerGrab.js. + MINUMUM_SEARCH_SPHERE_SIZE = 0.006, SEARCH_SPHERE_FOLLOW_RATE = 0.5, // Per handControllerGrab.js. COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 }, // Per handControllgerGrab.js. COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }, // Per handControllgerGrab.js. @@ -116,7 +117,7 @@ Laser = function (side) { searchDistance = SEARCH_SPHERE_FOLLOW_RATE * searchDistance + (1.0 - SEARCH_SPHERE_FOLLOW_RATE) * distance; searchTarget = Vec3.sum(origin, Vec3.multiply(searchDistance, direction)); - sphereSize = SEARCH_SPHERE_SIZE * searchDistance; + sphereSize = Math.max(SEARCH_SPHERE_SIZE * searchDistance, MINUMUM_SEARCH_SPHERE_SIZE); color = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE : COLORS_GRAB_SEARCHING_HALF_SQUEEZE; brightColor = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT : COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT; From 8dceb3cc6c0c2d6aee62fec5f5aa667748a48b44 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 26 Jul 2017 17:26:43 +1200 Subject: [PATCH 089/722] Show laser dot on UI even if trigger isn't squeezed --- scripts/vr-edit/modules/laser.js | 73 ++++++++++++++++++++++------- scripts/vr-edit/modules/toolMenu.js | 5 ++ scripts/vr-edit/vr-edit.js | 26 +++++++++- 3 files changed, 86 insertions(+), 18 deletions(-) diff --git a/scripts/vr-edit/modules/laser.js b/scripts/vr-edit/modules/laser.js index 2a765cfb4d..22d7b5794f 100644 --- a/scripts/vr-edit/modules/laser.js +++ b/scripts/vr-edit/modules/laser.js @@ -12,7 +12,8 @@ Laser = function (side) { // Draws hand lasers. - // May intersect with entities or bounding box of other hand's selection. + // May intersect with overlays or entities, or bounding box of other hand's selection. + // Laser dot is always drawn on UI entities. "use strict"; @@ -47,6 +48,8 @@ Laser = function (side) { LEFT_HAND = 0, AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", + uiEntityIDs = [], + intersection; function colorPow(color, power) { // Per handControllerGrab.js. @@ -109,7 +112,7 @@ Laser = function (side) { }); } - function display(origin, direction, distance, isClicked) { + function display(origin, direction, distance, isPressed, isClicked) { var searchTarget, sphereSize, color, @@ -121,7 +124,11 @@ Laser = function (side) { color = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE : COLORS_GRAB_SEARCHING_HALF_SQUEEZE; brightColor = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT : COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT; - updateLine(origin, searchTarget, color); + if (isPressed) { + updateLine(origin, searchTarget, color); + } else { + Overlays.editOverlay(laserLine, { visible: false }); + } updateSphere(searchTarget, sphereSize, color, brightColor); } @@ -130,6 +137,10 @@ Laser = function (side) { Overlays.editOverlay(laserSphere, { visible: false }); } + function setUIEntities(entityIDs) { + uiEntityIDs = entityIDs; + } + function update(hand) { var handPosition, handOrientation, @@ -140,7 +151,7 @@ Laser = function (side) { return; } - if (!hand.intersection().intersects && hand.triggerPressed()) { + if (!hand.intersection().intersects) { handPosition = hand.position(); handOrientation = hand.orientation(); deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); @@ -150,20 +161,47 @@ Laser = function (side) { length: PICK_MAX_DISTANCE }; - intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, - VISIBLE_ONLY); - if (!intersection.intersects) { - intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, - VISIBLE_ONLY); - intersection.editableEntity = intersection.intersects && Entities.hasEditableRoot(intersection.entityID); - } - intersection.laserIntersected = true; - laserLength = (specifiedLaserLength !== null) - ? specifiedLaserLength - : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); + if (hand.triggerPressed()) { - isLaserOn = true; - display(pickRay.origin, pickRay.direction, laserLength, hand.triggerClicked()); + // Normal laser operation with trigger. + intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + if (!intersection.intersects) { + intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + intersection.editableEntity = intersection.intersects && Entities.hasEditableRoot(intersection.entityID); + } + intersection.laserIntersected = true; + laserLength = (specifiedLaserLength !== null) + ? specifiedLaserLength + : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); + isLaserOn = true; + display(pickRay.origin, pickRay.direction, laserLength, true, hand.triggerClicked()); + + } else { + + // Special hovering of UI. + intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + VISIBLE_ONLY); // Check for overlay intersections in case they occlude the UI entities. + if (!intersection.intersects) { + intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, uiEntityIDs, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + } + if (intersection.intersects && intersection.entityID) { + intersection.laserIntersected = true; + laserLength = (specifiedLaserLength !== null) + ? specifiedLaserLength + : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); + isLaserOn = true; + display(pickRay.origin, pickRay.direction, laserLength, false, false); + } else { + if (isLaserOn) { + isLaserOn = false; + hide(); + } + } + + } } else { intersection = { intersects: false @@ -223,6 +261,7 @@ Laser = function (side) { } return { + setUIEntities: setUIEntities, update: update, intersection: getIntersection, setLength: setLength, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index d67a9349e4..6e81ef7634 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -66,6 +66,10 @@ ToolMenu = function (side, scaleModeChangedCallback) { } } + function getEntityIDs() { + return [panelEntity, buttonEntity]; + } + function update() { // TODO } @@ -126,6 +130,7 @@ ToolMenu = function (side, scaleModeChangedCallback) { return { setHand: setHand, + getEntityIDs: getEntityIDs, update: update, display: display, clear: clear, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index dcd8145472..c03e40284e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -92,6 +92,10 @@ hand = new Hand(side); laser = new Laser(side); + function setUIEntities(entityIDs) { + laser.setUIEntities(entityIDs); + } + function getHand() { return hand; } @@ -140,6 +144,7 @@ } return { + setUIEntities: setUIEntities, hand: getHand, laser: getLaser, getIntersection: getIntersection, @@ -154,17 +159,32 @@ // Tool menu and Create palette. var // Primary objects. - toolMenu; + toolMenu, + + // References. + leftInputs, + rightInputs; toolMenu = new ToolMenu(side); + function setReferences(left, right) { + leftInputs = left; + rightInputs = right; + } + function setHand(side) { toolMenu.setHand(side); } function display() { + var uiEntityIDs; + toolMenu.display(); + + uiEntityIDs = toolMenu.getEntityIDs(); + leftInputs.setUIEntities(uiEntityIDs); + rightInputs.setUIEntities(uiEntityIDs); } function update() { @@ -172,6 +192,8 @@ } function clear() { + leftInputs.setUIEntities([]); + rightInputs.setUIEntities([]); toolMenu.clear(); } @@ -187,6 +209,7 @@ } return { + setReferences: setReferences, setHand: setHand, display: display, update: update, @@ -987,6 +1010,7 @@ // UI object. ui = new UI(otherHand(dominantHand)); + ui.setReferences(inputs[LEFT_HAND], inputs[RIGHT_HAND]); // Editor objects. editors[LEFT_HAND] = new Editor(LEFT_HAND); From 52759be61836ad93e1c14c5e30874a0afc34d287 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 26 Jul 2017 20:40:12 +1200 Subject: [PATCH 090/722] Don't transition laser length when start intersecting UI --- scripts/vr-edit/modules/laser.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/vr-edit/modules/laser.js b/scripts/vr-edit/modules/laser.js index 22d7b5794f..3f875ccf06 100644 --- a/scripts/vr-edit/modules/laser.js +++ b/scripts/vr-edit/modules/laser.js @@ -192,6 +192,10 @@ Laser = function (side) { laserLength = (specifiedLaserLength !== null) ? specifiedLaserLength : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); + if (!isLaserOn) { + // Start laser dot at UI distance. + searchDistance = laserLength; + } isLaserOn = true; display(pickRay.origin, pickRay.direction, laserLength, false, false); } else { From 3566609173c53d356b977c0335981a631b1e51a0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 26 Jul 2017 21:34:13 +1200 Subject: [PATCH 091/722] Highlight button as it is hovered --- scripts/vr-edit/modules/toolMenu.js | 28 ++++++++++++++++++++++++++-- scripts/vr-edit/vr-edit.js | 20 +++++++++++++++++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 6e81ef7634..2ffb044e3a 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -17,6 +17,7 @@ ToolMenu = function (side, scaleModeChangedCallback) { var panelEntity, buttonEntity, + buttonHighlightOverlay, LEFT_HAND = 0, AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", @@ -30,6 +31,8 @@ ToolMenu = function (side, scaleModeChangedCallback) { ? Quat.fromVec3Degrees({ x: 180, y: -90, z: 0 }) : Quat.fromVec3Degrees({ x: 180, y: 90, z: 0 }), + ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), + PANEL_ENTITY_PROPERTIES = { type: "Box", dimensions: { x: 0.1, y: 0.2, z: 0.01 }, @@ -53,7 +56,20 @@ ToolMenu = function (side, scaleModeChangedCallback) { visible: true }, + BUTTON_HIGHLIGHT_PROPERTIES = { + dimensions: { x: 0.034, y: 0.034, z: 0.001 }, + color: { red: 240, green: 240, blue: 0 }, + alpha: 0.8, + localPosition: { x: 0.0155, y: 0.0155, z: 0.003 }, + localRotation: ZERO_ROTATION, + solid: false, + drawInFront: true, + ignoreRayIntersection: true, + visible: false + }, + isDisplaying = false, + isHighlightingButton = false, SCALE_MODE_DIRECT = 0, SCALE_MODE_HANDLES = 1; @@ -70,8 +86,12 @@ ToolMenu = function (side, scaleModeChangedCallback) { return [panelEntity, buttonEntity]; } - function update() { - // TODO + function update(intersectionEntityID) { + // Highlight button. + if (intersectionEntityID === buttonEntity !== isHighlightingButton) { + isHighlightingButton = !isHighlightingButton; + Overlays.editOverlay(buttonHighlightOverlay, { visible: isHighlightingButton }); + } } function display() { @@ -106,6 +126,10 @@ ToolMenu = function (side, scaleModeChangedCallback) { BUTTON_ENTITY_PROPERTIES.parentID = panelEntity; buttonEntity = Entities.addEntity(BUTTON_ENTITY_PROPERTIES, true); + // Prepare highlight overlay. + BUTTON_HIGHLIGHT_PROPERTIES.parentID = buttonEntity; + buttonHighlightOverlay = Overlays.addOverlay("cube", BUTTON_HIGHLIGHT_PROPERTIES); + isDisplaying = true; } diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index c03e40284e..7cff8fc9f6 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -87,7 +87,7 @@ hand, laser, - intersection = { x: "hello" }; + intersection = {}; hand = new Hand(side); laser = new Laser(side); @@ -163,7 +163,12 @@ // References. leftInputs, - rightInputs; + rightInputs, + + isDisplaying = false, + + getIntersection, // Function. + intersection; toolMenu = new ToolMenu(side); @@ -171,10 +176,12 @@ function setReferences(left, right) { leftInputs = left; rightInputs = right; + getIntersection = side === LEFT_HAND ? rightInputs.getIntersection : leftInputs.getIntersection; } function setHand(side) { toolMenu.setHand(side); + getIntersection = side === LEFT_HAND ? rightInputs.getIntersection : leftInputs.getIntersection; } function display() { @@ -185,16 +192,23 @@ uiEntityIDs = toolMenu.getEntityIDs(); leftInputs.setUIEntities(uiEntityIDs); rightInputs.setUIEntities(uiEntityIDs); + + isDisplaying = true; } function update() { - // TODO + if (isDisplaying) { + intersection = getIntersection(); + toolMenu.update(intersection.entityID); + } } function clear() { leftInputs.setUIEntities([]); rightInputs.setUIEntities([]); toolMenu.clear(); + + isDisplaying = false; } function destroy() { From d4b872d9e1cacca55dcaebafa85e02301f2129d5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 26 Jul 2017 22:30:52 +1200 Subject: [PATCH 092/722] Detect button press --- scripts/vr-edit/modules/hand.js | 3 ++- scripts/vr-edit/modules/toolMenu.js | 25 ++++++++++++++++++++++--- scripts/vr-edit/vr-edit.js | 12 +++++++++--- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/scripts/vr-edit/modules/hand.js b/scripts/vr-edit/modules/hand.js index c4f5a6efc9..48e44069ed 100644 --- a/scripts/vr-edit/modules/hand.js +++ b/scripts/vr-edit/modules/hand.js @@ -28,6 +28,7 @@ Hand = function (side) { isTriggerClicked, TRIGGER_ON_VALUE = 0.15, // Per handControllerGrab.js. TRIGGER_OFF_VALUE = 0.1, // Per handControllerGrab.js. + TRIGGER_CLICKED_VALUE = 1.0, NEAR_GRAB_RADIUS = 0.1, // Per handControllerGrab.js. NEAR_HOVER_RADIUS = 0.025, @@ -108,7 +109,7 @@ Hand = function (side) { // Controller trigger. isTriggerPressed = Controller.getValue(controllerTrigger) > (isTriggerPressed ? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE); - isTriggerClicked = Controller.getValue(controllerTriggerClicked); + isTriggerClicked = Controller.getValue(controllerTriggerClicked) === TRIGGER_CLICKED_VALUE; // Controller grip. gripValue = Controller.getValue(controllerGrip); diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 2ffb044e3a..089fb3f3e3 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -70,12 +70,25 @@ ToolMenu = function (side, scaleModeChangedCallback) { isDisplaying = false, isHighlightingButton = false, + isButtonPressed = false, SCALE_MODE_DIRECT = 0, - SCALE_MODE_HANDLES = 1; + SCALE_MODE_HANDLES = 1, - function setHand(hand) { - side = hand; + // References. + leftInputs, + rightInputs, + controlHand; + + function setReferences(left, right) { + leftInputs = left; + rightInputs = right; + controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); + } + + function setHand(uiSide) { + side = uiSide; + controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); if (isDisplaying) { // TODO: Move UI to other hand. @@ -92,6 +105,11 @@ ToolMenu = function (side, scaleModeChangedCallback) { isHighlightingButton = !isHighlightingButton; Overlays.editOverlay(buttonHighlightOverlay, { visible: isHighlightingButton }); } + + // Button click. + if (isHighlightingButton && controlHand.triggerClicked() !== isButtonPressed) { + isButtonPressed = controlHand.triggerClicked(); + } } function display() { @@ -153,6 +171,7 @@ ToolMenu = function (side, scaleModeChangedCallback) { } return { + setReferences: setReferences, setHand: setHand, getEntityIDs: getEntityIDs, update: update, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 7cff8fc9f6..c8fa86fcad 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -155,7 +155,7 @@ }; - UI = function (side) { + UI = function (side, setAppScaleWithHandlesCallback) { // Tool menu and Create palette. var // Primary objects. @@ -170,13 +170,15 @@ getIntersection, // Function. intersection; - toolMenu = new ToolMenu(side); + toolMenu = new ToolMenu(side, setAppScaleWithHandlesCallback); function setReferences(left, right) { leftInputs = left; rightInputs = right; getIntersection = side === LEFT_HAND ? rightInputs.getIntersection : leftInputs.getIntersection; + + toolMenu.setReferences(left, right); } function setHand(side) { @@ -959,6 +961,10 @@ Settings.setValue(VR_EDIT_SETTING, isAppActive); } + function setAppScaleWithHandles(appScaleWithHandles) { + isAppScaleWithHandles = appScaleWithHandles; + } + function onAppButtonClicked() { // Application tablet/toolbar button clicked. isAppActive = !isAppActive; @@ -1023,7 +1029,7 @@ inputs[RIGHT_HAND] = new Inputs(RIGHT_HAND); // UI object. - ui = new UI(otherHand(dominantHand)); + ui = new UI(otherHand(dominantHand), setAppScaleWithHandles); ui.setReferences(inputs[LEFT_HAND], inputs[RIGHT_HAND]); // Editor objects. From 6203a1ad68a54f2bb7178b5c9188b9eaace8ef1f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 26 Jul 2017 22:41:38 +1200 Subject: [PATCH 093/722] Wire up button to toggle scale-with-hands / scale-with-handles --- scripts/vr-edit/modules/toolMenu.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 089fb3f3e3..d64926ce5e 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -10,12 +10,18 @@ /* global ToolMenu */ -ToolMenu = function (side, scaleModeChangedCallback) { +ToolMenu = function (side, setAppScaleWithHandlesCallback) { // Tool menu displayed on top of forearm. "use strict"; - var panelEntity, + var SCALE_MODE_DIRECT = 0, + SCALE_MODE_HANDLES = 1, + scaleMode = SCALE_MODE_DIRECT, + SCALE_MODE_DIRECT_COLOR = { red: 240, green: 240, blue: 0 }, + SCALE_MODE_HANDLES_COLOR = { red: 0, green: 240, blue: 240 }, + + panelEntity, buttonEntity, buttonHighlightOverlay, @@ -50,7 +56,7 @@ ToolMenu = function (side, scaleModeChangedCallback) { dimensions: { x: 0.03, y: 0.03, z: 0.01 }, registrationPoint: { x: 0, y: 0.0, z: 0.0 }, localPosition: { x: 0.005, y: 0.005, z: -0.005 }, // Relative to the root panel entity. - color: { red: 240, green: 0, blue: 0 }, + color: scaleMode === SCALE_MODE_DIRECT ? SCALE_MODE_DIRECT_COLOR : SCALE_MODE_HANDLES_COLOR, ignoreRayIntersection: false, lifetime: 3600, visible: true @@ -72,9 +78,6 @@ ToolMenu = function (side, scaleModeChangedCallback) { isHighlightingButton = false, isButtonPressed = false, - SCALE_MODE_DIRECT = 0, - SCALE_MODE_HANDLES = 1, - // References. leftInputs, rightInputs, @@ -109,6 +112,14 @@ ToolMenu = function (side, scaleModeChangedCallback) { // Button click. if (isHighlightingButton && controlHand.triggerClicked() !== isButtonPressed) { isButtonPressed = controlHand.triggerClicked(); + + if (isButtonPressed) { + scaleMode = scaleMode === SCALE_MODE_DIRECT ? SCALE_MODE_HANDLES : SCALE_MODE_DIRECT; + Entities.editEntity(buttonEntity, { + color: scaleMode === SCALE_MODE_DIRECT ? SCALE_MODE_DIRECT_COLOR : SCALE_MODE_HANDLES_COLOR + }); + setAppScaleWithHandlesCallback(scaleMode === SCALE_MODE_HANDLES); + } } } From 136ea78873f27da6fe1dd1d6e8477b61124dd576 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 27 Jul 2017 13:47:35 +1200 Subject: [PATCH 094/722] Use overlays instead of entities --- scripts/vr-edit/modules/laser.js | 12 ++---- scripts/vr-edit/modules/toolMenu.js | 67 +++++++++++++++++------------ scripts/vr-edit/vr-edit.js | 2 +- 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/scripts/vr-edit/modules/laser.js b/scripts/vr-edit/modules/laser.js index 3f875ccf06..3130fbbb25 100644 --- a/scripts/vr-edit/modules/laser.js +++ b/scripts/vr-edit/modules/laser.js @@ -180,14 +180,10 @@ Laser = function (side) { } else { - // Special hovering of UI. - intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, - VISIBLE_ONLY); // Check for overlay intersections in case they occlude the UI entities. - if (!intersection.intersects) { - intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, uiEntityIDs, NO_EXCLUDE_IDS, - VISIBLE_ONLY); - } - if (intersection.intersects && intersection.entityID) { + // Special UI cursor. + intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, uiEntityIDs, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + if (intersection.intersects) { intersection.laserIntersected = true; laserLength = (specifiedLaserLength !== null) ? specifiedLaserLength diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index d64926ce5e..f87f3a5301 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -21,8 +21,9 @@ ToolMenu = function (side, setAppScaleWithHandlesCallback) { SCALE_MODE_DIRECT_COLOR = { red: 240, green: 240, blue: 0 }, SCALE_MODE_HANDLES_COLOR = { red: 0, green: 240, blue: 240 }, - panelEntity, - buttonEntity, + originOverlay, + panelOverlay, + buttonOverlay, buttonHighlightOverlay, LEFT_HAND = 0, @@ -39,35 +40,44 @@ ToolMenu = function (side, setAppScaleWithHandlesCallback) { ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), - PANEL_ENTITY_PROPERTIES = { - type: "Box", - dimensions: { x: 0.1, y: 0.2, z: 0.01 }, - color: { red: 192, green: 192, blue: 192 }, + ORIGIN_PROPERTIES = { + dimensions: { x: 0.005, y: 0.005, z: 0.005 }, + color: { red: 255, blue: 0, green: 0 }, + alpha: 1.0, parentID: AVATAR_SELF_ID, - registrationPoint: { x: 0, y: 0.0, z: 0.0 }, localRotation: ROOT_ROTATION, + ignoreRayIntersection: true, + visible: false + }, + + PANEL_PROPERTIES = { + dimensions: { x: 0.1, y: 0.2, z: 0.01 }, + localPosition: { x: 0.05, y: 0.1, z: 0.005 }, + localRotation: ZERO_ROTATION, + color: { red: 192, green: 192, blue: 192 }, + alpha: 1.0, + solid: true, ignoreRayIntersection: false, - lifetime: 3600, visible: true }, - BUTTON_ENTITY_PROPERTIES = { - type: "Box", + BUTTON_PROPERTIES = { dimensions: { x: 0.03, y: 0.03, z: 0.01 }, - registrationPoint: { x: 0, y: 0.0, z: 0.0 }, - localPosition: { x: 0.005, y: 0.005, z: -0.005 }, // Relative to the root panel entity. + localPosition: { x: 0.02, y: 0.02, z: 0.0 }, + localRotation: ZERO_ROTATION, color: scaleMode === SCALE_MODE_DIRECT ? SCALE_MODE_DIRECT_COLOR : SCALE_MODE_HANDLES_COLOR, + alpha: 1.0, + solid: true, ignoreRayIntersection: false, - lifetime: 3600, visible: true }, BUTTON_HIGHLIGHT_PROPERTIES = { dimensions: { x: 0.034, y: 0.034, z: 0.001 }, + localPosition: { x: 0, y: 0, z: -0.002 }, + localRotation: ZERO_ROTATION, color: { red: 240, green: 240, blue: 0 }, alpha: 0.8, - localPosition: { x: 0.0155, y: 0.0155, z: 0.003 }, - localRotation: ZERO_ROTATION, solid: false, drawInFront: true, ignoreRayIntersection: true, @@ -99,12 +109,12 @@ ToolMenu = function (side, setAppScaleWithHandlesCallback) { } function getEntityIDs() { - return [panelEntity, buttonEntity]; + return [panelOverlay, buttonOverlay]; } - function update(intersectionEntityID) { + function update(intersectionOverlayID) { // Highlight button. - if (intersectionEntityID === buttonEntity !== isHighlightingButton) { + if (intersectionOverlayID === buttonOverlay !== isHighlightingButton) { isHighlightingButton = !isHighlightingButton; Overlays.editOverlay(buttonHighlightOverlay, { visible: isHighlightingButton }); } @@ -115,7 +125,7 @@ ToolMenu = function (side, setAppScaleWithHandlesCallback) { if (isButtonPressed) { scaleMode = scaleMode === SCALE_MODE_DIRECT ? SCALE_MODE_HANDLES : SCALE_MODE_DIRECT; - Entities.editEntity(buttonEntity, { + Overlays.editOverlay(buttonOverlay, { color: scaleMode === SCALE_MODE_DIRECT ? SCALE_MODE_DIRECT_COLOR : SCALE_MODE_HANDLES_COLOR }); setAppScaleWithHandlesCallback(scaleMode === SCALE_MODE_HANDLES); @@ -147,16 +157,18 @@ ToolMenu = function (side, setAppScaleWithHandlesCallback) { // Calculate position to put menu. forearmLength = Vec3.distance(MyAvatar.getJointPosition(forearmJointIndex), MyAvatar.getJointPosition(handJointIndex)); rootOffset = { x: ROOT_X_OFFSET, y: forearmLength, z: ROOT_Z_OFFSET }; - PANEL_ENTITY_PROPERTIES.parentJointIndex = forearmJointIndex; - PANEL_ENTITY_PROPERTIES.localPosition = rootOffset; + ORIGIN_PROPERTIES.parentJointIndex = forearmJointIndex; + ORIGIN_PROPERTIES.localPosition = rootOffset; + originOverlay = Overlays.addOverlay("sphere", ORIGIN_PROPERTIES); // Create menu items. - panelEntity = Entities.addEntity(PANEL_ENTITY_PROPERTIES, true); - BUTTON_ENTITY_PROPERTIES.parentID = panelEntity; - buttonEntity = Entities.addEntity(BUTTON_ENTITY_PROPERTIES, true); + PANEL_PROPERTIES.parentID = originOverlay; + panelOverlay = Overlays.addOverlay("cube", PANEL_PROPERTIES); + BUTTON_PROPERTIES.parentID = originOverlay; + buttonOverlay = Overlays.addOverlay("cube", BUTTON_PROPERTIES); // Prepare highlight overlay. - BUTTON_HIGHLIGHT_PROPERTIES.parentID = buttonEntity; + BUTTON_HIGHLIGHT_PROPERTIES.parentID = buttonOverlay; buttonHighlightOverlay = Overlays.addOverlay("cube", BUTTON_HIGHLIGHT_PROPERTIES); isDisplaying = true; @@ -168,8 +180,9 @@ ToolMenu = function (side, setAppScaleWithHandlesCallback) { return; } - Entities.deleteEntity(buttonEntity); - Entities.deleteEntity(panelEntity); + Overlays.deleteOverlay(buttonHighlightOverlay); + Overlays.deleteOverlay(buttonOverlay); + Overlays.deleteOverlay(panelOverlay); isDisplaying = false; } diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index c8fa86fcad..e38640c593 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -201,7 +201,7 @@ function update() { if (isDisplaying) { intersection = getIntersection(); - toolMenu.update(intersection.entityID); + toolMenu.update(intersection.overlayID); } } From 573a49853ad866e79a85c6e47114d9829078d2dc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 27 Jul 2017 15:09:22 +1200 Subject: [PATCH 095/722] Tidying --- scripts/vr-edit/modules/hand.js | 8 +-- scripts/vr-edit/modules/handles.js | 8 +-- scripts/vr-edit/modules/highlights.js | 8 +-- scripts/vr-edit/modules/laser.js | 10 +-- scripts/vr-edit/modules/selection.js | 8 +-- scripts/vr-edit/modules/toolMenu.js | 20 +++--- scripts/vr-edit/vr-edit.js | 87 ++++++++++++--------------- 7 files changed, 67 insertions(+), 82 deletions(-) diff --git a/scripts/vr-edit/modules/hand.js b/scripts/vr-edit/modules/hand.js index 48e44069ed..0430067eb9 100644 --- a/scripts/vr-edit/modules/hand.js +++ b/scripts/vr-edit/modules/hand.js @@ -42,6 +42,10 @@ Hand = function (side) { intersection = {}; + if (!this instanceof Hand) { + return new Hand(side); + } + if (side === LEFT_HAND) { handController = Controller.Standard.LeftHand; controllerTrigger = Controller.Standard.LT; @@ -183,10 +187,6 @@ Hand = function (side) { // Nothing to do. } - if (!this instanceof Hand) { - return new Hand(side); - } - return { valid: valid, position: position, diff --git a/scripts/vr-edit/modules/handles.js b/scripts/vr-edit/modules/handles.js index 6f238ccb79..e124e5f945 100644 --- a/scripts/vr-edit/modules/handles.js +++ b/scripts/vr-edit/modules/handles.js @@ -49,6 +49,10 @@ Handles = function (side) { i; + if (!this instanceof Handles) { + return new Handles(side); + } + CORNER_HANDLE_OVERLAY_AXES = [ // Ordered such that items 4 apart are opposite corners - used in display(). { x: -0.5, y: -0.5, z: -0.5 }, @@ -350,10 +354,6 @@ Handles = function (side) { Overlays.deleteOverlay(boundingBoxOverlay); } - if (!this instanceof Handles) { - return new Handles(side); - } - return { display: display, isHandle: isHandle, diff --git a/scripts/vr-edit/modules/highlights.js b/scripts/vr-edit/modules/highlights.js index 26170e9374..ecde8ffa3e 100644 --- a/scripts/vr-edit/modules/highlights.js +++ b/scripts/vr-edit/modules/highlights.js @@ -27,6 +27,10 @@ Highlights = function (side) { AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO); + if (!this instanceof Highlights) { + return new Highlights(); + } + handOverlay = Overlays.addOverlay("sphere", { dimensions: HAND_HIGHLIGHT_DIMENSIONS, parentID: AVATAR_SELF_ID, @@ -109,10 +113,6 @@ Highlights = function (side) { Overlays.deleteOverlay(handOverlay); } - if (!this instanceof Highlights) { - return new Highlights(); - } - return { display: display, clear: clear, diff --git a/scripts/vr-edit/modules/laser.js b/scripts/vr-edit/modules/laser.js index 3130fbbb25..5ef0a223cf 100644 --- a/scripts/vr-edit/modules/laser.js +++ b/scripts/vr-edit/modules/laser.js @@ -32,7 +32,7 @@ Laser = function (side) { COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }, // Per handControllgerGrab.js. COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT, COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT, - BRIGHT_POW = 0.06, // Per handControllgerGrab.js. + BRIGHT_POW = 0.06, // Per handControllerGrab.js. GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. @@ -52,6 +52,10 @@ Laser = function (side) { intersection; + if (!this instanceof Laser) { + return new Laser(side); + } + function colorPow(color, power) { // Per handControllerGrab.js. return { red: Math.pow(color.red / 255, power) * 255, @@ -256,10 +260,6 @@ Laser = function (side) { Overlays.deleteOverlay(laserSphere); } - if (!this instanceof Laser) { - return new Laser(side); - } - return { setUIEntities: setUIEntities, update: update, diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index bf1d519941..5437e49bf3 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -25,6 +25,10 @@ Selection = function (side) { scaleRootOrientation, ENTITY_TYPE = "entity"; + if (!this instanceof Selection) { + return new Selection(side); + } + function traverseEntityTree(id, result) { // Recursively traverses tree of entities and their children, gather IDs and properties. var children, @@ -307,10 +311,6 @@ Selection = function (side) { clear(); } - if (!this instanceof Selection) { - return new Selection(side); - } - return { select: select, selection: getSelection, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index f87f3a5301..26adbf5a77 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -10,7 +10,7 @@ /* global ToolMenu */ -ToolMenu = function (side, setAppScaleWithHandlesCallback) { +ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallback) { // Tool menu displayed on top of forearm. "use strict"; @@ -89,16 +89,15 @@ ToolMenu = function (side, setAppScaleWithHandlesCallback) { isButtonPressed = false, // References. - leftInputs, - rightInputs, controlHand; - function setReferences(left, right) { - leftInputs = left; - rightInputs = right; - controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); + + if (!this instanceof ToolMenu) { + return new ToolMenu(); } + controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); + function setHand(uiSide) { side = uiSide; controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); @@ -190,14 +189,9 @@ ToolMenu = function (side, setAppScaleWithHandlesCallback) { clear(); } - if (!this instanceof ToolMenu) { - return new ToolMenu(); - } - return { - setReferences: setReferences, setHand: setHand, - getEntityIDs: getEntityIDs, + entityIDs: getEntityIDs, update: update, display: display, clear: clear, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index e38640c593..bd8af9dabd 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -89,9 +89,15 @@ intersection = {}; + + if (!this instanceof Inputs) { + return new Inputs(); + } + hand = new Hand(side); laser = new Laser(side); + function setUIEntities(entityIDs) { laser.setUIEntities(entityIDs); } @@ -139,15 +145,11 @@ } } - if (!this instanceof Inputs) { - return new Inputs(); - } - return { setUIEntities: setUIEntities, hand: getHand, laser: getLaser, - getIntersection: getIntersection, + intersection: getIntersection, update: update, clear: clear, destroy: destroy @@ -155,35 +157,29 @@ }; - UI = function (side, setAppScaleWithHandlesCallback) { + UI = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallback) { // Tool menu and Create palette. var // Primary objects. toolMenu, - // References. - leftInputs, - rightInputs, - isDisplaying = false, - getIntersection, // Function. - intersection; - - toolMenu = new ToolMenu(side, setAppScaleWithHandlesCallback); + getIntersection; // Function. - function setReferences(left, right) { - leftInputs = left; - rightInputs = right; - getIntersection = side === LEFT_HAND ? rightInputs.getIntersection : leftInputs.getIntersection; - - toolMenu.setReferences(left, right); + if (!this instanceof UI) { + return new UI(); } + toolMenu = new ToolMenu(side, leftInputs, rightInputs, setAppScaleWithHandlesCallback); + + getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; + + function setHand(side) { toolMenu.setHand(side); - getIntersection = side === LEFT_HAND ? rightInputs.getIntersection : leftInputs.getIntersection; + getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; } function display() { @@ -191,17 +187,16 @@ toolMenu.display(); - uiEntityIDs = toolMenu.getEntityIDs(); - leftInputs.setUIEntities(uiEntityIDs); - rightInputs.setUIEntities(uiEntityIDs); + uiEntityIDs = toolMenu.entityIDs(); + leftInputs.setUIEntities(side === RIGHT_HAND ? uiEntityIDs : []); + rightInputs.setUIEntities(side === LEFT_HAND ? uiEntityIDs : []); isDisplaying = true; } function update() { if (isDisplaying) { - intersection = getIntersection(); - toolMenu.update(intersection.overlayID); + toolMenu.update(getIntersection().overlayID); } } @@ -220,12 +215,7 @@ } } - if (!this instanceof UI) { - return new UI(); - } - return { - setReferences: setReferences, setHand: setHand, display: display, update: update, @@ -238,7 +228,16 @@ Editor = function (side) { // An entity selection, entity highlights, and entity handles. - var otherEditor, // Other hand's Editor object. + var + // Primary objects. + selection, + highlights, + handles, + + // References. + otherEditor, // Other hand's Editor object. + hand, + laser, // Editor states. EDITOR_IDLE = 0, @@ -258,15 +257,6 @@ isOtherEditorEditingEntityID = false, hoveredOverlayID = null, - // Primary objects. - selection, - highlights, - handles, - - // Input objects. - hand, - laser, - // Position values. initialHandOrientationInverse, initialHandToSelectionVector, @@ -293,6 +283,11 @@ getIntersection, // Function. intersection; + + if (!this instanceof Editor) { + return new Editor(); + } + selection = new Selection(side); highlights = new Highlights(side); handles = new Handles(side); @@ -300,12 +295,13 @@ function setReferences(inputs, editor) { hand = inputs.hand(); // Object. laser = inputs.laser(); // Object. - getIntersection = inputs.getIntersection; // Function. + getIntersection = inputs.intersection; // Function. otherEditor = editor; // Object. laserOffset = laser.handOffset(); // Value. } + function hoverHandle(overlayID) { // Highlights handle if overlayID is a handle, otherwise unhighlights currently highlighted handle if any. handles.hover(overlayID); @@ -911,10 +907,6 @@ } } - if (!this instanceof Editor) { - return new Editor(); - } - return { setReferences: setReferences, hoverHandle: hoverHandle, @@ -1029,8 +1021,7 @@ inputs[RIGHT_HAND] = new Inputs(RIGHT_HAND); // UI object. - ui = new UI(otherHand(dominantHand), setAppScaleWithHandles); - ui.setReferences(inputs[LEFT_HAND], inputs[RIGHT_HAND]); + ui = new UI(otherHand(dominantHand), inputs[LEFT_HAND], inputs[RIGHT_HAND], setAppScaleWithHandles); // Editor objects. editors[LEFT_HAND] = new Editor(LEFT_HAND); From 3b111b85ef3a58022d9d572a168467a398f41167 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 27 Jul 2017 15:56:42 +1200 Subject: [PATCH 096/722] Move UI to hand --- scripts/vr-edit/modules/toolMenu.js | 33 ++++++++++++----------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 26adbf5a77..c0fb6a17a8 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -29,30 +29,30 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba LEFT_HAND = 0, AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", - FOREARM_JOINT_NAME = side === LEFT_HAND ? "LeftForeArm" : "RightForeArm", HAND_JOINT_NAME = side === LEFT_HAND ? "LeftHand" : "RightHand", - ROOT_X_OFFSET = side === LEFT_HAND ? -0.05 : 0.05, - ROOT_Z_OFFSET = side === LEFT_HAND ? -0.05 : 0.05, - ROOT_ROTATION = side === LEFT_HAND - ? Quat.fromVec3Degrees({ x: 180, y: -90, z: 0 }) - : Quat.fromVec3Degrees({ x: 180, y: 90, z: 0 }), + CANVAS_SIZE = { x: 0.21, y: 0.13 }, + + LATERAL_OFFSET = side === LEFT_HAND ? -0.01 : 0.01, + ROOT_POSITION = { x: CANVAS_SIZE.x / 2 + LATERAL_OFFSET, y: 0.15, z: -0.03 }, + ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }), ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), ORIGIN_PROPERTIES = { dimensions: { x: 0.005, y: 0.005, z: 0.005 }, + localPosition: ROOT_POSITION, + localRotation: ROOT_ROTATION, color: { red: 255, blue: 0, green: 0 }, alpha: 1.0, parentID: AVATAR_SELF_ID, - localRotation: ROOT_ROTATION, ignoreRayIntersection: true, visible: false }, PANEL_PROPERTIES = { - dimensions: { x: 0.1, y: 0.2, z: 0.01 }, - localPosition: { x: 0.05, y: 0.1, z: 0.005 }, + dimensions: { x: CANVAS_SIZE.x, y: CANVAS_SIZE.y, z: 0.01 }, + localPosition: { x: CANVAS_SIZE.x / 2, y: CANVAS_SIZE.y / 2, z: 0.005 }, localRotation: ZERO_ROTATION, color: { red: 192, green: 192, blue: 192 }, alpha: 1.0, @@ -134,19 +134,15 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba function display() { // Creates and shows menu entities. - var forearmJointIndex, - handJointIndex, - forearmLength, - rootOffset; + var handJointIndex; if (isDisplaying) { return; } - // Joint indexes. - forearmJointIndex = MyAvatar.getJointIndex(FOREARM_JOINT_NAME); + // Joint index. handJointIndex = MyAvatar.getJointIndex(HAND_JOINT_NAME); - if (forearmJointIndex === -1 || handJointIndex === -1) { + if (handJointIndex === -1) { // Don't display if joint isn't available (yet) to attach to. // User can clear this condition by toggling the app off and back on once avatar finishes loading. // TODO: Log error. @@ -154,10 +150,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba } // Calculate position to put menu. - forearmLength = Vec3.distance(MyAvatar.getJointPosition(forearmJointIndex), MyAvatar.getJointPosition(handJointIndex)); - rootOffset = { x: ROOT_X_OFFSET, y: forearmLength, z: ROOT_Z_OFFSET }; - ORIGIN_PROPERTIES.parentJointIndex = forearmJointIndex; - ORIGIN_PROPERTIES.localPosition = rootOffset; + ORIGIN_PROPERTIES.parentJointIndex = handJointIndex; originOverlay = Overlays.addOverlay("sphere", ORIGIN_PROPERTIES); // Create menu items. From e8c5e1c2d5c517c11cd004a695917007076b730c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 27 Jul 2017 16:05:32 +1200 Subject: [PATCH 097/722] Make the button move down when pressed --- scripts/vr-edit/modules/toolMenu.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index c0fb6a17a8..69eccf3fe1 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -74,7 +74,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba BUTTON_HIGHLIGHT_PROPERTIES = { dimensions: { x: 0.034, y: 0.034, z: 0.001 }, - localPosition: { x: 0, y: 0, z: -0.002 }, + localPosition: { x: 0.02, y: 0.02, z: -0.002 }, localRotation: ZERO_ROTATION, color: { red: 240, green: 240, blue: 0 }, alpha: 0.8, @@ -125,9 +125,14 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba if (isButtonPressed) { scaleMode = scaleMode === SCALE_MODE_DIRECT ? SCALE_MODE_HANDLES : SCALE_MODE_DIRECT; Overlays.editOverlay(buttonOverlay, { - color: scaleMode === SCALE_MODE_DIRECT ? SCALE_MODE_DIRECT_COLOR : SCALE_MODE_HANDLES_COLOR + color: scaleMode === SCALE_MODE_DIRECT ? SCALE_MODE_DIRECT_COLOR : SCALE_MODE_HANDLES_COLOR, + localPosition: Vec3.sum(BUTTON_PROPERTIES.localPosition, { x: 0, y: 0, z: 0.004 }) }); setAppScaleWithHandlesCallback(scaleMode === SCALE_MODE_HANDLES); + } else { + Overlays.editOverlay(buttonOverlay, { + localPosition: BUTTON_PROPERTIES.localPosition + }); } } } @@ -160,7 +165,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba buttonOverlay = Overlays.addOverlay("cube", BUTTON_PROPERTIES); // Prepare highlight overlay. - BUTTON_HIGHLIGHT_PROPERTIES.parentID = buttonOverlay; + BUTTON_HIGHLIGHT_PROPERTIES.parentID = originOverlay; buttonHighlightOverlay = Overlays.addOverlay("cube", BUTTON_HIGHLIGHT_PROPERTIES); isDisplaying = true; From 86f33727ebfe0dc989f6d393e46b6d8662178c5c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 27 Jul 2017 16:53:15 +1200 Subject: [PATCH 098/722] Add prototype Create palette --- scripts/vr-edit/modules/toolMenu.js | 143 ++++++++++++++++++++++++---- 1 file changed, 125 insertions(+), 18 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 69eccf3fe1..6c13ccbbac 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -21,28 +21,36 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba SCALE_MODE_DIRECT_COLOR = { red: 240, green: 240, blue: 0 }, SCALE_MODE_HANDLES_COLOR = { red: 0, green: 240, blue: 240 }, - originOverlay, - panelOverlay, + menuOriginOverlay, + menuPanelOverlay, buttonOverlay, buttonHighlightOverlay, + paletteOriginOverlay, + palettePanelOverlay, + cubeOverlay, + cubeHighlightOverlay, + LEFT_HAND = 0, AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", HAND_JOINT_NAME = side === LEFT_HAND ? "LeftHand" : "RightHand", CANVAS_SIZE = { x: 0.21, y: 0.13 }, - LATERAL_OFFSET = side === LEFT_HAND ? -0.01 : 0.01, - ROOT_POSITION = { x: CANVAS_SIZE.x / 2 + LATERAL_OFFSET, y: 0.15, z: -0.03 }, - ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }), + + PANEL_ROOT_POSITION = { x: CANVAS_SIZE.x / 2 + LATERAL_OFFSET, y: 0.15, z: -0.03 }, + PANEL_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }), + + PALETTE_ROOT_POSITION = { x: -CANVAS_SIZE.x / 2 + LATERAL_OFFSET, y: 0.15, z: 0.09 }, + PALETTE_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 180 }), ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), - ORIGIN_PROPERTIES = { + PANEL_ORIGIN_PROPERTIES = { dimensions: { x: 0.005, y: 0.005, z: 0.005 }, - localPosition: ROOT_POSITION, - localRotation: ROOT_ROTATION, + localPosition: PANEL_ROOT_POSITION, + localRotation: PANEL_ROOT_ROTATION, color: { red: 255, blue: 0, green: 0 }, alpha: 1.0, parentID: AVATAR_SELF_ID, @@ -50,7 +58,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba visible: false }, - PANEL_PROPERTIES = { + MENU_PANEL_PROPERTIES = { dimensions: { x: CANVAS_SIZE.x, y: CANVAS_SIZE.y, z: 0.01 }, localPosition: { x: CANVAS_SIZE.x / 2, y: CANVAS_SIZE.y / 2, z: 0.005 }, localRotation: ZERO_ROTATION, @@ -84,9 +92,63 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba visible: false }, + PALETTE_ORIGIN_PROPERTIES = { + dimensions: { x: 0.005, y: 0.005, z: 0.005 }, + localPosition: PALETTE_ROOT_POSITION, + localRotation: PALETTE_ROOT_ROTATION, + color: { red: 255, blue: 0, green: 0 }, + alpha: 1.0, + parentID: AVATAR_SELF_ID, + ignoreRayIntersection: true, + visible: false + }, + + PALETTE_PANEL_PROPERTIES = { + dimensions: { x: CANVAS_SIZE.x, y: CANVAS_SIZE.y, z: 0.001 }, + localPosition: { x: CANVAS_SIZE.x / 2, y: CANVAS_SIZE.y / 2, z: 0 }, + localRotation: ZERO_ROTATION, + color: { red: 192, green: 192, blue: 192 }, + alpha: 0.3, + solid: true, + ignoreRayIntersection: false, + visible: true + }, + + CUBE_PROPERTIES = { + dimensions: { x: 0.03, y: 0.03, z: 0.03 }, + localPosition: { x: 0.02, y: 0.02, z: 0.0 }, + localRotation: ZERO_ROTATION, + color: { red: 240, green: 0, blue: 0 }, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + }, + + CUBE_HIGHLIGHT_PROPERTIES = { + dimensions: { x: 0.034, y: 0.034, z: 0.034 }, + localPosition: { x: 0.02, y: 0.02, z: 0.0 }, + localRotation: ZERO_ROTATION, + color: { red: 240, green: 240, blue: 0 }, + alpha: 0.8, + solid: false, + drawInFront: true, + ignoreRayIntersection: true, + visible: false + }, + + CUBE_ENTITY_PROPERTIES = { + type: "Box", + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + color: { red: 192, green: 192, blue: 192 } + }, + isDisplaying = false, + isHighlightingButton = false, isButtonPressed = false, + isHighlightingCube = false, + isCubePressed = false, // References. controlHand; @@ -108,7 +170,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba } function getEntityIDs() { - return [panelOverlay, buttonOverlay]; + return [menuPanelOverlay, buttonOverlay, palettePanelOverlay, cubeOverlay]; } function update(intersectionOverlayID) { @@ -135,6 +197,31 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba }); } } + + // Highlight cube. + if (intersectionOverlayID === cubeOverlay !== isHighlightingCube) { + isHighlightingCube = !isHighlightingCube; + Overlays.editOverlay(cubeHighlightOverlay, { visible: isHighlightingCube }); + } + + // Cube click. + if (isHighlightingCube && controlHand.triggerClicked() !== isCubePressed) { + isCubePressed = controlHand.triggerClicked(); + + if (isCubePressed) { + Overlays.editOverlay(cubeOverlay, { + localPosition: Vec3.sum(BUTTON_PROPERTIES.localPosition, { x: 0, y: 0, z: 0.01 }) + }); + CUBE_ENTITY_PROPERTIES.position = + Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0.2, z: -1.0 })); + CUBE_ENTITY_PROPERTIES.rotation = MyAvatar.orientation; + Entities.addEntity(CUBE_ENTITY_PROPERTIES); + } else { + Overlays.editOverlay(cubeOverlay, { + localPosition: BUTTON_PROPERTIES.localPosition + }); + } + } } function display() { @@ -155,19 +242,33 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba } // Calculate position to put menu. - ORIGIN_PROPERTIES.parentJointIndex = handJointIndex; - originOverlay = Overlays.addOverlay("sphere", ORIGIN_PROPERTIES); + PANEL_ORIGIN_PROPERTIES.parentJointIndex = handJointIndex; + menuOriginOverlay = Overlays.addOverlay("sphere", PANEL_ORIGIN_PROPERTIES); // Create menu items. - PANEL_PROPERTIES.parentID = originOverlay; - panelOverlay = Overlays.addOverlay("cube", PANEL_PROPERTIES); - BUTTON_PROPERTIES.parentID = originOverlay; + MENU_PANEL_PROPERTIES.parentID = menuOriginOverlay; + menuPanelOverlay = Overlays.addOverlay("cube", MENU_PANEL_PROPERTIES); + BUTTON_PROPERTIES.parentID = menuOriginOverlay; buttonOverlay = Overlays.addOverlay("cube", BUTTON_PROPERTIES); - // Prepare highlight overlay. - BUTTON_HIGHLIGHT_PROPERTIES.parentID = originOverlay; + // Prepare button highlight overlay. + BUTTON_HIGHLIGHT_PROPERTIES.parentID = menuOriginOverlay; buttonHighlightOverlay = Overlays.addOverlay("cube", BUTTON_HIGHLIGHT_PROPERTIES); + // Calculate position to put palette. + PALETTE_ORIGIN_PROPERTIES.parentJointIndex = handJointIndex; + paletteOriginOverlay = Overlays.addOverlay("sphere", PALETTE_ORIGIN_PROPERTIES); + + // Create palette items. + PALETTE_PANEL_PROPERTIES.parentID = paletteOriginOverlay; + palettePanelOverlay = Overlays.addOverlay("cube", PALETTE_PANEL_PROPERTIES); + CUBE_PROPERTIES.parentID = paletteOriginOverlay; + cubeOverlay = Overlays.addOverlay("cube", CUBE_PROPERTIES); + + // Prepare cube highlight overlay. + CUBE_HIGHLIGHT_PROPERTIES.parentID = paletteOriginOverlay; + cubeHighlightOverlay = Overlays.addOverlay("cube", CUBE_HIGHLIGHT_PROPERTIES); + isDisplaying = true; } @@ -177,9 +278,15 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba return; } + Overlays.deleteOverlay(cubeHighlightOverlay); + Overlays.deleteOverlay(cubeOverlay); + Overlays.deleteOverlay(palettePanelOverlay); + Overlays.deleteOverlay(paletteOriginOverlay); + Overlays.deleteOverlay(buttonHighlightOverlay); Overlays.deleteOverlay(buttonOverlay); - Overlays.deleteOverlay(panelOverlay); + Overlays.deleteOverlay(menuPanelOverlay); + Overlays.deleteOverlay(menuOriginOverlay); isDisplaying = false; } From 01f37b53e955472a0e94b3a5ce0d090ed59679fb Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 28 Jul 2017 09:20:21 +1200 Subject: [PATCH 099/722] Fix non-dominant hand ciursor dot showing when it shouldn't --- scripts/vr-edit/modules/laser.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/vr-edit/modules/laser.js b/scripts/vr-edit/modules/laser.js index 5ef0a223cf..fefa13b8ff 100644 --- a/scripts/vr-edit/modules/laser.js +++ b/scripts/vr-edit/modules/laser.js @@ -182,7 +182,7 @@ Laser = function (side) { isLaserOn = true; display(pickRay.origin, pickRay.direction, laserLength, true, hand.triggerClicked()); - } else { + } else if (uiEntityIDs.length > 0) { // Special UI cursor. intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, uiEntityIDs, NO_EXCLUDE_IDS, @@ -198,18 +198,20 @@ Laser = function (side) { } isLaserOn = true; display(pickRay.origin, pickRay.direction, laserLength, false, false); - } else { - if (isLaserOn) { - isLaserOn = false; - hide(); - } + } else if (isLaserOn) { + isLaserOn = false; + hide(); } + } else { + intersection = { intersects: false }; + if (isLaserOn) { + isLaserOn = false; + hide(); + } } } else { - intersection = { - intersects: false - }; + intersection = { intersects: false }; if (isLaserOn) { isLaserOn = false; hide(); From 21c15120ba340870a25b5745693fa5bec67b8e4c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 28 Jul 2017 09:24:15 +1200 Subject: [PATCH 100/722] Raise Tool menu up a little to accommodate wooden mannequin's hand --- scripts/vr-edit/modules/toolMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 6c13ccbbac..5f02eba58a 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -39,7 +39,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba CANVAS_SIZE = { x: 0.21, y: 0.13 }, LATERAL_OFFSET = side === LEFT_HAND ? -0.01 : 0.01, - PANEL_ROOT_POSITION = { x: CANVAS_SIZE.x / 2 + LATERAL_OFFSET, y: 0.15, z: -0.03 }, + PANEL_ROOT_POSITION = { x: CANVAS_SIZE.x / 2 + LATERAL_OFFSET, y: 0.15, z: -0.04 }, PANEL_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }), PALETTE_ROOT_POSITION = { x: -CANVAS_SIZE.x / 2 + LATERAL_OFFSET, y: 0.15, z: 0.09 }, From 9fe3a823ee3b20774d2ce3190cb5870ce93f2fd0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 28 Jul 2017 09:35:29 +1200 Subject: [PATCH 101/722] Fix unexpectedly deleting multiple entities --- scripts/vr-edit/vr-edit.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index bd8af9dabd..a10f99ab77 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -255,6 +255,7 @@ highlightedEntityID = null, // Root entity of highlighted entity set. wasAppScaleWithHandles = false, isOtherEditorEditingEntityID = false, + wasGripPressed = false, hoveredOverlayID = null, // Position values. @@ -590,6 +591,7 @@ } startEditing(); wasAppScaleWithHandles = isAppScaleWithHandles; + wasGripPressed = hand.gripPressed(); } function updateEditorGrabbing() { @@ -796,6 +798,7 @@ updateState(); wasAppScaleWithHandles = isAppScaleWithHandles; } + wasGripPressed = false; break; } if (!hand.valid()) { @@ -808,8 +811,10 @@ setState(EDITOR_SEARCHING); } } else if (hand.gripPressed()) { - selection.deleteEntities(); - setState(EDITOR_SEARCHING); + if (!wasGripPressed) { + selection.deleteEntities(); + setState(EDITOR_SEARCHING); + } } else { debug(side, "ERROR: Unexpected condition in EDITOR_GRABBING!"); } From 64f5fe4009a9c04bfd0c0f4584539e8944ab5f22 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 28 Jul 2017 10:36:54 +1200 Subject: [PATCH 102/722] Make Create palette a separate object --- scripts/vr-edit/modules/createPalette.js | 204 +++++++++++++++++++++++ scripts/vr-edit/modules/toolMenu.js | 107 +----------- scripts/vr-edit/vr-edit.js | 14 +- 3 files changed, 218 insertions(+), 107 deletions(-) create mode 100644 scripts/vr-edit/modules/createPalette.js diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js new file mode 100644 index 0000000000..d9a19853c9 --- /dev/null +++ b/scripts/vr-edit/modules/createPalette.js @@ -0,0 +1,204 @@ +// +// createPalette.js +// +// Created by David Rowe on 28 Jul 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global CreatePalette */ + +CreatePalette = function (side, leftInputs, rightInputs) { + // Tool menu displayed on top of forearm. + + "use strict"; + + var paletteOriginOverlay, + palettePanelOverlay, + cubeOverlay, + cubeHighlightOverlay, + + LEFT_HAND = 0, + AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", + + HAND_JOINT_NAME = side === LEFT_HAND ? "LeftHand" : "RightHand", + + CANVAS_SIZE = { x: 0.21, y: 0.13 }, + LATERAL_OFFSET = side === LEFT_HAND ? -0.01 : 0.01, + + PALETTE_ROOT_POSITION = { x: -CANVAS_SIZE.x / 2 + LATERAL_OFFSET, y: 0.15, z: 0.09 }, + PALETTE_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 180 }), + + ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), + + PALETTE_ORIGIN_PROPERTIES = { + dimensions: { x: 0.005, y: 0.005, z: 0.005 }, + localPosition: PALETTE_ROOT_POSITION, + localRotation: PALETTE_ROOT_ROTATION, + color: { red: 255, blue: 0, green: 0 }, + alpha: 1.0, + parentID: AVATAR_SELF_ID, + ignoreRayIntersection: true, + visible: false + }, + + PALETTE_PANEL_PROPERTIES = { + dimensions: { x: CANVAS_SIZE.x, y: CANVAS_SIZE.y, z: 0.001 }, + localPosition: { x: CANVAS_SIZE.x / 2, y: CANVAS_SIZE.y / 2, z: 0 }, + localRotation: ZERO_ROTATION, + color: { red: 192, green: 192, blue: 192 }, + alpha: 0.3, + solid: true, + ignoreRayIntersection: false, + visible: true + }, + + CUBE_PROPERTIES = { + dimensions: { x: 0.03, y: 0.03, z: 0.03 }, + localPosition: { x: 0.02, y: 0.02, z: 0.0 }, + localRotation: ZERO_ROTATION, + color: { red: 240, green: 0, blue: 0 }, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + }, + + CUBE_HIGHLIGHT_PROPERTIES = { + dimensions: { x: 0.034, y: 0.034, z: 0.034 }, + localPosition: { x: 0.02, y: 0.02, z: 0.0 }, + localRotation: ZERO_ROTATION, + color: { red: 240, green: 240, blue: 0 }, + alpha: 0.8, + solid: false, + drawInFront: true, + ignoreRayIntersection: true, + visible: false + }, + + CUBE_ENTITY_PROPERTIES = { + type: "Box", + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + color: { red: 192, green: 192, blue: 192 } + }, + + isDisplaying = false, + + isHighlightingCube = false, + isCubePressed = false, + + // References. + controlHand; + + + if (!this instanceof CreatePalette) { + return new CreatePalette(); + } + + controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); + + function setHand(uiSide) { + side = uiSide; + controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); + + if (isDisplaying) { + // TODO: Move UI to other hand. + } + } + + function getEntityIDs() { + return [palettePanelOverlay, cubeOverlay]; + } + + function update(intersectionOverlayID) { + // Highlight cube. + if (intersectionOverlayID === cubeOverlay !== isHighlightingCube) { + isHighlightingCube = !isHighlightingCube; + Overlays.editOverlay(cubeHighlightOverlay, { visible: isHighlightingCube }); + } + + // Cube click. + if (isHighlightingCube && controlHand.triggerClicked() !== isCubePressed) { + isCubePressed = controlHand.triggerClicked(); + + if (isCubePressed) { + Overlays.editOverlay(cubeOverlay, { + localPosition: Vec3.sum(CUBE_PROPERTIES.localPosition, { x: 0, y: 0, z: 0.01 }) + }); + CUBE_ENTITY_PROPERTIES.position = + Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0.2, z: -1.0 })); + CUBE_ENTITY_PROPERTIES.rotation = MyAvatar.orientation; + Entities.addEntity(CUBE_ENTITY_PROPERTIES); + } else { + Overlays.editOverlay(cubeOverlay, { + localPosition: CUBE_PROPERTIES.localPosition + }); + } + } + } + + function display() { + // Creates and shows menu entities. + var handJointIndex; + + if (isDisplaying) { + return; + } + + // Joint index. + handJointIndex = MyAvatar.getJointIndex(HAND_JOINT_NAME); + if (handJointIndex === -1) { + // Don't display if joint isn't available (yet) to attach to. + // User can clear this condition by toggling the app off and back on once avatar finishes loading. + // TODO: Log error. + return; + } + + // Calculate position to put palette. + PALETTE_ORIGIN_PROPERTIES.parentJointIndex = handJointIndex; + paletteOriginOverlay = Overlays.addOverlay("sphere", PALETTE_ORIGIN_PROPERTIES); + + // Create palette items. + PALETTE_PANEL_PROPERTIES.parentID = paletteOriginOverlay; + palettePanelOverlay = Overlays.addOverlay("cube", PALETTE_PANEL_PROPERTIES); + CUBE_PROPERTIES.parentID = paletteOriginOverlay; + cubeOverlay = Overlays.addOverlay("cube", CUBE_PROPERTIES); + + // Prepare cube highlight overlay. + CUBE_HIGHLIGHT_PROPERTIES.parentID = paletteOriginOverlay; + cubeHighlightOverlay = Overlays.addOverlay("cube", CUBE_HIGHLIGHT_PROPERTIES); + + isDisplaying = true; + } + + function clear() { + // Deletes menu entities. + if (!isDisplaying) { + return; + } + + Overlays.deleteOverlay(cubeHighlightOverlay); + Overlays.deleteOverlay(cubeOverlay); + Overlays.deleteOverlay(palettePanelOverlay); + Overlays.deleteOverlay(paletteOriginOverlay); + + isDisplaying = false; + } + + function destroy() { + clear(); + } + + return { + setHand: setHand, + entityIDs: getEntityIDs, + update: update, + display: display, + clear: clear, + destroy: destroy + }; +}; + +CreatePalette.prototype = {}; diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 5f02eba58a..0034155d1a 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -26,11 +26,6 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba buttonOverlay, buttonHighlightOverlay, - paletteOriginOverlay, - palettePanelOverlay, - cubeOverlay, - cubeHighlightOverlay, - LEFT_HAND = 0, AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", @@ -42,9 +37,6 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba PANEL_ROOT_POSITION = { x: CANVAS_SIZE.x / 2 + LATERAL_OFFSET, y: 0.15, z: -0.04 }, PANEL_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }), - PALETTE_ROOT_POSITION = { x: -CANVAS_SIZE.x / 2 + LATERAL_OFFSET, y: 0.15, z: 0.09 }, - PALETTE_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 180 }), - ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), PANEL_ORIGIN_PROPERTIES = { @@ -92,63 +84,10 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba visible: false }, - PALETTE_ORIGIN_PROPERTIES = { - dimensions: { x: 0.005, y: 0.005, z: 0.005 }, - localPosition: PALETTE_ROOT_POSITION, - localRotation: PALETTE_ROOT_ROTATION, - color: { red: 255, blue: 0, green: 0 }, - alpha: 1.0, - parentID: AVATAR_SELF_ID, - ignoreRayIntersection: true, - visible: false - }, - - PALETTE_PANEL_PROPERTIES = { - dimensions: { x: CANVAS_SIZE.x, y: CANVAS_SIZE.y, z: 0.001 }, - localPosition: { x: CANVAS_SIZE.x / 2, y: CANVAS_SIZE.y / 2, z: 0 }, - localRotation: ZERO_ROTATION, - color: { red: 192, green: 192, blue: 192 }, - alpha: 0.3, - solid: true, - ignoreRayIntersection: false, - visible: true - }, - - CUBE_PROPERTIES = { - dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: 0.02, y: 0.02, z: 0.0 }, - localRotation: ZERO_ROTATION, - color: { red: 240, green: 0, blue: 0 }, - alpha: 1.0, - solid: true, - ignoreRayIntersection: false, - visible: true - }, - - CUBE_HIGHLIGHT_PROPERTIES = { - dimensions: { x: 0.034, y: 0.034, z: 0.034 }, - localPosition: { x: 0.02, y: 0.02, z: 0.0 }, - localRotation: ZERO_ROTATION, - color: { red: 240, green: 240, blue: 0 }, - alpha: 0.8, - solid: false, - drawInFront: true, - ignoreRayIntersection: true, - visible: false - }, - - CUBE_ENTITY_PROPERTIES = { - type: "Box", - dimensions: { x: 0.2, y: 0.2, z: 0.2 }, - color: { red: 192, green: 192, blue: 192 } - }, - isDisplaying = false, isHighlightingButton = false, isButtonPressed = false, - isHighlightingCube = false, - isCubePressed = false, // References. controlHand; @@ -170,7 +109,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba } function getEntityIDs() { - return [menuPanelOverlay, buttonOverlay, palettePanelOverlay, cubeOverlay]; + return [menuPanelOverlay, buttonOverlay]; } function update(intersectionOverlayID) { @@ -197,31 +136,6 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba }); } } - - // Highlight cube. - if (intersectionOverlayID === cubeOverlay !== isHighlightingCube) { - isHighlightingCube = !isHighlightingCube; - Overlays.editOverlay(cubeHighlightOverlay, { visible: isHighlightingCube }); - } - - // Cube click. - if (isHighlightingCube && controlHand.triggerClicked() !== isCubePressed) { - isCubePressed = controlHand.triggerClicked(); - - if (isCubePressed) { - Overlays.editOverlay(cubeOverlay, { - localPosition: Vec3.sum(BUTTON_PROPERTIES.localPosition, { x: 0, y: 0, z: 0.01 }) - }); - CUBE_ENTITY_PROPERTIES.position = - Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0.2, z: -1.0 })); - CUBE_ENTITY_PROPERTIES.rotation = MyAvatar.orientation; - Entities.addEntity(CUBE_ENTITY_PROPERTIES); - } else { - Overlays.editOverlay(cubeOverlay, { - localPosition: BUTTON_PROPERTIES.localPosition - }); - } - } } function display() { @@ -255,20 +169,6 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba BUTTON_HIGHLIGHT_PROPERTIES.parentID = menuOriginOverlay; buttonHighlightOverlay = Overlays.addOverlay("cube", BUTTON_HIGHLIGHT_PROPERTIES); - // Calculate position to put palette. - PALETTE_ORIGIN_PROPERTIES.parentJointIndex = handJointIndex; - paletteOriginOverlay = Overlays.addOverlay("sphere", PALETTE_ORIGIN_PROPERTIES); - - // Create palette items. - PALETTE_PANEL_PROPERTIES.parentID = paletteOriginOverlay; - palettePanelOverlay = Overlays.addOverlay("cube", PALETTE_PANEL_PROPERTIES); - CUBE_PROPERTIES.parentID = paletteOriginOverlay; - cubeOverlay = Overlays.addOverlay("cube", CUBE_PROPERTIES); - - // Prepare cube highlight overlay. - CUBE_HIGHLIGHT_PROPERTIES.parentID = paletteOriginOverlay; - cubeHighlightOverlay = Overlays.addOverlay("cube", CUBE_HIGHLIGHT_PROPERTIES); - isDisplaying = true; } @@ -278,11 +178,6 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba return; } - Overlays.deleteOverlay(cubeHighlightOverlay); - Overlays.deleteOverlay(cubeOverlay); - Overlays.deleteOverlay(palettePanelOverlay); - Overlays.deleteOverlay(paletteOriginOverlay); - Overlays.deleteOverlay(buttonHighlightOverlay); Overlays.deleteOverlay(buttonOverlay); Overlays.deleteOverlay(menuPanelOverlay); diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index a10f99ab77..e73f385f2e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -39,6 +39,7 @@ Laser, Selection, ToolMenu, + CreatePalette, // Miscellaneous UPDATE_LOOP_TIMEOUT = 16, @@ -52,6 +53,7 @@ Script.include("./utilities/utilities.js"); // Modules + Script.include("./modules/createPalette.js"); Script.include("./modules/hand.js"); Script.include("./modules/handles.js"); Script.include("./modules/highlights.js"); @@ -162,6 +164,7 @@ var // Primary objects. toolMenu, + createPalette, isDisplaying = false, @@ -173,12 +176,14 @@ } toolMenu = new ToolMenu(side, leftInputs, rightInputs, setAppScaleWithHandlesCallback); + createPalette = new CreatePalette(side, leftInputs, rightInputs); getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; function setHand(side) { toolMenu.setHand(side); + createPalette.setHand(side); getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; } @@ -186,8 +191,9 @@ var uiEntityIDs; toolMenu.display(); + createPalette.display(); - uiEntityIDs = toolMenu.entityIDs(); + uiEntityIDs = [].concat(toolMenu.entityIDs(), createPalette.entityIDs()); leftInputs.setUIEntities(side === RIGHT_HAND ? uiEntityIDs : []); rightInputs.setUIEntities(side === LEFT_HAND ? uiEntityIDs : []); @@ -197,6 +203,7 @@ function update() { if (isDisplaying) { toolMenu.update(getIntersection().overlayID); + createPalette.update(getIntersection().overlayID); } } @@ -204,11 +211,16 @@ leftInputs.setUIEntities([]); rightInputs.setUIEntities([]); toolMenu.clear(); + createPalette.clear(); isDisplaying = false; } function destroy() { + if (createPalette) { + createPalette.destroy(); + createPalette = null; + } if (toolMenu) { toolMenu.destroy(); toolMenu = null; From c201e7b65af3ab1252360f40a124fdee16bb83ec Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 29 Jul 2017 10:59:15 +1200 Subject: [PATCH 103/722] Display placeholder for tool icon on dominant hand --- scripts/vr-edit/modules/toolIcon.js | 100 +++++++++++++++++++++++++ scripts/vr-edit/utilities/utilities.js | 6 ++ scripts/vr-edit/vr-edit.js | 35 +++++++-- 3 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 scripts/vr-edit/modules/toolIcon.js diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js new file mode 100644 index 0000000000..813178c5f8 --- /dev/null +++ b/scripts/vr-edit/modules/toolIcon.js @@ -0,0 +1,100 @@ +// +// toolIcon.js +// +// Created by David Rowe on 28 Jul 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global ToolIcon */ + +ToolIcon = function (side) { + // Tool icon displayed on non-dominant hand. + + "use strict"; + + var NONE = 0, + SCALE_HANDLES = 1, + + ICON_COLORS = [ + { red: 0, green: 0, blue: 0 }, // Unused entry for NONE. + { red: 0, green: 240, blue: 240 } + ], + + LEFT_HAND = 0, + AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", + + ICON_DIMENSIONS = { x: 0.1, y: 0.01, z: 0.1 }, + ICON_POSITION = { x: 0, y: 0.01, z: 0 }, + ICON_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: 0 }), + + ICON_TYPE = "sphere", + ICON_PROPERTIES = { + dimensions: ICON_DIMENSIONS, + localPosition: ICON_POSITION, + localRotation: ICON_ROTATION, + solid: true, + alpha: 1.0, + parentID: AVATAR_SELF_ID, + ignoreRayIntersection: false, + visible: true + }, + + HAND_JOINT_NAME = side === LEFT_HAND ? "LeftHand" : "RightHand", + + iconOverlay = null; + + if (!this instanceof ToolIcon) { + return new ToolIcon(); + } + + function update() { + // TODO: Display icon animation. + // TODO: Clear icon animation. + } + + function display(icon) { + // Displays icon on hand. + var handJointIndex, + iconProperties; + + // Joint index. + handJointIndex = MyAvatar.getJointIndex(HAND_JOINT_NAME); + if (handJointIndex === -1) { + // Don't display if joint isn't available (yet) to attach to. + // User can clear this condition by toggling the app off and back on once avatar finishes loading. + // TODO: Log error. + return; + } + + iconProperties = Object.clone(ICON_PROPERTIES); + iconProperties.parentJointIndex = handJointIndex; + iconProperties.color = ICON_COLORS[icon]; + iconOverlay = Overlays.addOverlay(ICON_TYPE, iconProperties); + } + + function clear() { + // Deletes current icon. + if (iconOverlay) { + Overlays.deleteOverlay(iconOverlay); + iconOverlay = null; + } + } + + function destroy() { + clear(); + } + + return { + NONE: NONE, + SCALE_HANDLES: SCALE_HANDLES, + update: update, + display: display, + clear: clear, + destroy: destroy + }; +}; + +ToolIcon.prototype = {}; diff --git a/scripts/vr-edit/utilities/utilities.js b/scripts/vr-edit/utilities/utilities.js index a6914b332c..d737d820ea 100644 --- a/scripts/vr-edit/utilities/utilities.js +++ b/scripts/vr-edit/utilities/utilities.js @@ -49,3 +49,9 @@ if (typeof Entities.hasEditableRoot !== "function") { return properties.visible && !properties.locked && NONEDITABLE_ENTITY_TYPES.indexOf(properties.type) === -1; }; } + +if (typeof Object.clone !== "function") { + Object.clone = function (object) { + return JSON.parse(JSON.stringify(object)); + }; +} diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index e73f385f2e..12bbe91301 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -33,13 +33,14 @@ RIGHT_HAND = 1, // Modules + CreatePalette, Hand, Handles, Highlights, Laser, Selection, + ToolIcon, ToolMenu, - CreatePalette, // Miscellaneous UPDATE_LOOP_TIMEOUT = 16, @@ -59,6 +60,7 @@ Script.include("./modules/highlights.js"); Script.include("./modules/laser.js"); Script.include("./modules/selection.js"); + Script.include("./modules/toolIcon.js"); Script.include("./modules/toolMenu.js"); @@ -80,6 +82,10 @@ } } + function otherHand(hand) { + return (hand + 1) % 2; + } + Inputs = function (side) { // A hand plus a laser. @@ -164,6 +170,7 @@ var // Primary objects. toolMenu, + toolIcon, createPalette, isDisplaying = false, @@ -175,6 +182,7 @@ return new UI(); } + toolIcon = new ToolIcon(otherHand(side)); toolMenu = new ToolMenu(side, leftInputs, rightInputs, setAppScaleWithHandlesCallback); createPalette = new CreatePalette(side, leftInputs, rightInputs); @@ -187,6 +195,14 @@ getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; } + function setToolIcon(icon) { + toolIcon.display(icon); + } + + function clearToolIcon() { + toolIcon.clear(); + } + function display() { var uiEntityIDs; @@ -204,6 +220,7 @@ if (isDisplaying) { toolMenu.update(getIntersection().overlayID); createPalette.update(getIntersection().overlayID); + toolIcon.update(); } } @@ -225,10 +242,17 @@ toolMenu.destroy(); toolMenu = null; } + if (toolIcon) { + toolIcon.destroy(); + toolIcon = null; + } } return { setHand: setHand, + setToolIcon: setToolIcon, + clearToolIcon: clearToolIcon, + SCALE_HANDLES: toolIcon.SCALE_HANDLES, display: display, update: update, clear: clear, @@ -972,6 +996,11 @@ function setAppScaleWithHandles(appScaleWithHandles) { isAppScaleWithHandles = appScaleWithHandles; + if (isAppScaleWithHandles) { + ui.setToolIcon(ui.SCALE_HANDLES); + } else { + ui.clearToolIcon(); + } } function onAppButtonClicked() { @@ -994,10 +1023,6 @@ } } - function otherHand(hand) { - return (hand + 1) % 2; - } - function onDominantHandChanged() { /* // TODO: API coming. From f4b2e399efdbdd83f36ef7e808c932a7848c09bc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 29 Jul 2017 12:54:21 +1200 Subject: [PATCH 104/722] Fix multiple and malingering tool icons --- scripts/vr-edit/modules/toolIcon.js | 13 ++++++++----- scripts/vr-edit/vr-edit.js | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 813178c5f8..781530207c 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -60,7 +60,6 @@ ToolIcon = function (side) { var handJointIndex, iconProperties; - // Joint index. handJointIndex = MyAvatar.getJointIndex(HAND_JOINT_NAME); if (handJointIndex === -1) { // Don't display if joint isn't available (yet) to attach to. @@ -69,10 +68,14 @@ ToolIcon = function (side) { return; } - iconProperties = Object.clone(ICON_PROPERTIES); - iconProperties.parentJointIndex = handJointIndex; - iconProperties.color = ICON_COLORS[icon]; - iconOverlay = Overlays.addOverlay(ICON_TYPE, iconProperties); + if (iconOverlay === null) { + iconProperties = Object.clone(ICON_PROPERTIES); + iconProperties.parentJointIndex = handJointIndex; + iconProperties.color = ICON_COLORS[icon]; + iconOverlay = Overlays.addOverlay(ICON_TYPE, iconProperties); + } else { + Overlays.editOverlay(iconOverlay, { color: ICON_COLORS[icon] }); + } } function clear() { diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 12bbe91301..a2c76a5ad2 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -227,6 +227,7 @@ function clear() { leftInputs.setUIEntities([]); rightInputs.setUIEntities([]); + toolIcon.clear(); toolMenu.clear(); createPalette.clear(); From 9f90960a125aa415163054f215201d6b14d51c4f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 29 Jul 2017 13:31:08 +1200 Subject: [PATCH 105/722] Make grip press discard tool --- scripts/vr-edit/modules/toolMenu.js | 14 +++----------- scripts/vr-edit/vr-edit.js | 24 +++++++++++++++++------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 0034155d1a..f4cb8fe818 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -15,13 +15,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba "use strict"; - var SCALE_MODE_DIRECT = 0, - SCALE_MODE_HANDLES = 1, - scaleMode = SCALE_MODE_DIRECT, - SCALE_MODE_DIRECT_COLOR = { red: 240, green: 240, blue: 0 }, - SCALE_MODE_HANDLES_COLOR = { red: 0, green: 240, blue: 240 }, - - menuOriginOverlay, + var menuOriginOverlay, menuPanelOverlay, buttonOverlay, buttonHighlightOverlay, @@ -65,7 +59,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba dimensions: { x: 0.03, y: 0.03, z: 0.01 }, localPosition: { x: 0.02, y: 0.02, z: 0.0 }, localRotation: ZERO_ROTATION, - color: scaleMode === SCALE_MODE_DIRECT ? SCALE_MODE_DIRECT_COLOR : SCALE_MODE_HANDLES_COLOR, + color: { red: 0, green: 240, blue: 240 }, alpha: 1.0, solid: true, ignoreRayIntersection: false, @@ -124,12 +118,10 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba isButtonPressed = controlHand.triggerClicked(); if (isButtonPressed) { - scaleMode = scaleMode === SCALE_MODE_DIRECT ? SCALE_MODE_HANDLES : SCALE_MODE_DIRECT; Overlays.editOverlay(buttonOverlay, { - color: scaleMode === SCALE_MODE_DIRECT ? SCALE_MODE_DIRECT_COLOR : SCALE_MODE_HANDLES_COLOR, localPosition: Vec3.sum(BUTTON_PROPERTIES.localPosition, { x: 0, y: 0, z: 0.004 }) }); - setAppScaleWithHandlesCallback(scaleMode === SCALE_MODE_HANDLES); + setAppScaleWithHandlesCallback(); } else { Overlays.editOverlay(buttonOverlay, { localPosition: BUTTON_PROPERTIES.localPosition diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index a2c76a5ad2..888cab02af 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -577,6 +577,7 @@ selection.clear(); hoveredOverlayID = intersection.overlayID; otherEditor.hoverHandle(hoveredOverlayID); + wasGripPressed = hand.gripPressed(); } function updateEditorSearching() { @@ -598,6 +599,7 @@ } isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID); wasAppScaleWithHandles = isAppScaleWithHandles; + wasGripPressed = hand.gripPressed(); } function updateEditorHighlighting() { @@ -737,6 +739,16 @@ } + function updateTool() { + var isGripPressed = hand.gripPressed(); + if (!wasGripPressed && isGripPressed && isAppScaleWithHandles) { + isAppScaleWithHandles = false; + ui.clearToolIcon(); + } + wasGripPressed = isGripPressed; + } + + function update() { var previousState = editorState, doUpdateState; @@ -757,6 +769,7 @@ && !(intersection.overlayID && hand.triggerClicked() && otherEditor.isHandle(intersection.overlayID))) { // No transition. updateState(); + updateTool(); break; } if (!hand.valid()) { @@ -802,6 +815,7 @@ if (doUpdateState) { updateState(); } + updateTool(); break; } if (!hand.valid()) { @@ -995,13 +1009,9 @@ Settings.setValue(VR_EDIT_SETTING, isAppActive); } - function setAppScaleWithHandles(appScaleWithHandles) { - isAppScaleWithHandles = appScaleWithHandles; - if (isAppScaleWithHandles) { - ui.setToolIcon(ui.SCALE_HANDLES); - } else { - ui.clearToolIcon(); - } + function setAppScaleWithHandles() { + isAppScaleWithHandles = true; + ui.setToolIcon(ui.SCALE_HANDLES); } function onAppButtonClicked() { From 52a2538d73d5fa0bb8119708c7e4d393fd059150 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 29 Jul 2017 14:41:58 +1200 Subject: [PATCH 106/722] Fix scale handles not displaying after delete entity --- scripts/vr-edit/modules/handles.js | 97 +++++++++++++----------------- 1 file changed, 41 insertions(+), 56 deletions(-) diff --git a/scripts/vr-edit/modules/handles.js b/scripts/vr-edit/modules/handles.js index e124e5f945..b9390b02d3 100644 --- a/scripts/vr-edit/modules/handles.js +++ b/scripts/vr-edit/modules/handles.js @@ -98,38 +98,6 @@ Handles = function (side) { Vec3.UNIT_Z ]; - boundingBoxOverlay = Overlays.addOverlay("cube", { - color: BOUNDING_BOX_COLOR, - alpha: BOUNDING_BOX_ALPHA, - solid: false, - drawInFront: true, - ignoreRayIntersection: true, - visible: false - }); - - for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { - cornerHandleOverlays[i] = Overlays.addOverlay("sphere", { - color: HANDLE_NORMAL_COLOR, - alpha: HANDLE_NORMAL_ALPHA, - solid: true, - drawInFront: true, - ignoreRayIntersection: false, - visible: false - }); - } - - for (i = 0; i < NUM_FACE_HANDLES; i += 1) { - faceHandleOverlays[i] = Overlays.addOverlay("shape", { - shape: "Cone", - color: HANDLE_NORMAL_COLOR, - alpha: HANDLE_NORMAL_ALPHA, - solid: true, - drawInFront: true, - ignoreRayIntersection: false, - visible: false - }); - } - function isAxisHandle(overlayID) { return faceHandleOverlays.indexOf(overlayID) !== -1; } @@ -158,7 +126,7 @@ Handles = function (side) { return FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)]; } - function display(rootEntityID, boundingBox, isMultiple) { + function display(rootEntityID, boundingBox, isMultipleEntities) { var boundingBoxCenter, boundingBoxOrientation, cameraPosition, @@ -183,11 +151,16 @@ Handles = function (side) { boundingBoxOrientation = boundingBox.orientation; // Selection bounding box. - Overlays.editOverlay(boundingBoxOverlay, { + boundingBoxOverlay = Overlays.addOverlay("cube", { parentID: rootEntityID, localPosition: boundingBoxLocalCenter, localRotation: ZERO_ROTATION, dimensions: boundingBoxDimensions, + color: BOUNDING_BOX_COLOR, + alpha: BOUNDING_BOX_ALPHA, + solid: false, + drawInFront: true, + ignoreRayIntersection: true, visible: true }); @@ -219,11 +192,17 @@ Handles = function (side) { cornerIndexes[1] = rightCornerIndex; cornerHandleDimensions = Vec3.multiply(distanceMultiplier, CORNER_HANDLE_OVERLAY_DIMENSIONS); for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { - Overlays.editOverlay(cornerHandleOverlays[i], { + cornerHandleOverlays[i] = Overlays.addOverlay("sphere", { parentID: rootEntityID, localPosition: Vec3.sum(boundingBoxLocalCenter, Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[i]], boundingBoxDimensions)), + localRotation: ZERO_ROTATION, dimensions: cornerHandleDimensions, + color: HANDLE_NORMAL_COLOR, + alpha: HANDLE_NORMAL_ALPHA, + solid: true, + drawInFront: true, + ignoreRayIntersection: false, visible: true }); } @@ -231,23 +210,27 @@ Handles = function (side) { // Face scale handles. // Only valid for a single entity because for multiple entities, some may be at an angle relative to the root entity // which would necessitate a (non-existent) shear transform be applied to them when scaling a face of the set. - if (!isMultiple) { + if (!isMultipleEntities) { faceHandleDimensions = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_DIMENSIONS); faceHandleOffsets = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_OFFSETS); for (i = 0; i < NUM_FACE_HANDLES; i += 1) { - Overlays.editOverlay(faceHandleOverlays[i], { + faceHandleOverlays[i] = Overlays.addOverlay("shape", { parentID: rootEntityID, localPosition: Vec3.sum(boundingBoxLocalCenter, Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], Vec3.sum(boundingBoxDimensions, faceHandleOffsets))), localRotation: FACE_HANDLE_OVERLAY_ROTATIONS[i], dimensions: faceHandleDimensions, + shape: "Cone", + color: HANDLE_NORMAL_COLOR, + alpha: HANDLE_NORMAL_ALPHA, + solid: true, + drawInFront: true, + ignoreRayIntersection: false, visible: true }); } } else { - for (i = 0; i < NUM_FACE_HANDLES; i += 1) { - Overlays.editOverlay(faceHandleOverlays[i], { visible: false }); - } + faceHandleOverlays = []; } } @@ -275,12 +258,14 @@ Handles = function (side) { } // Face scale handles. - for (i = 0; i < NUM_FACE_HANDLES; i += 1) { - Overlays.editOverlay(faceHandleOverlays[i], { - localPosition: Vec3.sum(scalingBoundingBoxDimensions, - Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], - Vec3.sum(scalingBoundingBoxLocalCenter, faceHandleOffsets))) - }); + if (faceHandleOverlays.length > 0) { + for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + Overlays.editOverlay(faceHandleOverlays[i], { + localPosition: Vec3.sum(scalingBoundingBoxDimensions, + Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], + Vec3.sum(scalingBoundingBoxLocalCenter, faceHandleOffsets))) + }); + } } } @@ -336,22 +321,22 @@ Handles = function (side) { } function clear() { - var i; + var i, + length; + + Overlays.deleteOverlay(boundingBoxOverlay); + for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { + Overlays.deleteOverlay(cornerHandleOverlays[i]); + } + for (i = 0, length = faceHandleOverlays.length; i < length; i += 1) { + Overlays.deleteOverlay(faceHandleOverlays[i]); + } isVisible = false; - - Overlays.editOverlay(boundingBoxOverlay, { visible: false }); - for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { - Overlays.editOverlay(cornerHandleOverlays[i], { visible: false }); - } - for (i = 0; i < NUM_FACE_HANDLES; i += 1) { - Overlays.editOverlay(faceHandleOverlays[i], { visible: false }); - } } function destroy() { clear(); - Overlays.deleteOverlay(boundingBoxOverlay); } return { From 30595c78a0ee3e02dc5e0dec8a614acd9ad1933d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 29 Jul 2017 14:51:58 +1200 Subject: [PATCH 107/722] Fix button not unpressing when cursor moves off it --- scripts/vr-edit/modules/toolMenu.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index f4cb8fe818..d0c00cb5a5 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -114,9 +114,8 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba } // Button click. - if (isHighlightingButton && controlHand.triggerClicked() !== isButtonPressed) { - isButtonPressed = controlHand.triggerClicked(); - + if (!isHighlightingButton || controlHand.triggerClicked() !== isButtonPressed) { + isButtonPressed = isHighlightingButton && controlHand.triggerClicked(); if (isButtonPressed) { Overlays.editOverlay(buttonOverlay, { localPosition: Vec3.sum(BUTTON_PROPERTIES.localPosition, { x: 0, y: 0, z: 0.004 }) From 1d500dd3aaebb1476d6b77333cd3bd10a8efed46 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 29 Jul 2017 16:10:48 +1200 Subject: [PATCH 108/722] Use dominant hand setting and handle setting changes --- scripts/vr-edit/modules/createPalette.js | 51 +++++++++++++----------- scripts/vr-edit/modules/toolIcon.js | 12 +++++- scripts/vr-edit/modules/toolMenu.js | 49 +++++++++++++---------- scripts/vr-edit/vr-edit.js | 42 ++++++++++++------- 4 files changed, 92 insertions(+), 62 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index d9a19853c9..aea2d365b3 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -22,16 +22,14 @@ CreatePalette = function (side, leftInputs, rightInputs) { LEFT_HAND = 0, AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", + ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), - HAND_JOINT_NAME = side === LEFT_HAND ? "LeftHand" : "RightHand", + controlJointName, CANVAS_SIZE = { x: 0.21, y: 0.13 }, - LATERAL_OFFSET = side === LEFT_HAND ? -0.01 : 0.01, - - PALETTE_ROOT_POSITION = { x: -CANVAS_SIZE.x / 2 + LATERAL_OFFSET, y: 0.15, z: 0.09 }, + PALETTE_ROOT_POSITION = { x: -CANVAS_SIZE.x / 2, y: 0.15, z: 0.09 }, PALETTE_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 180 }), - - ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), + lateralOffset, PALETTE_ORIGIN_PROPERTIES = { dimensions: { x: 0.005, y: 0.005, z: 0.005 }, @@ -97,17 +95,16 @@ CreatePalette = function (side, leftInputs, rightInputs) { return new CreatePalette(); } - controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); - - function setHand(uiSide) { - side = uiSide; + function setHand(hand) { + // Assumes UI is not displaying. + side = hand; controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); - - if (isDisplaying) { - // TODO: Move UI to other hand. - } + controlJointName = side === LEFT_HAND ? "LeftHand" : "RightHand"; + lateralOffset = side === LEFT_HAND ? -0.01 : 0.01; } + setHand(side); + function getEntityIDs() { return [palettePanelOverlay, cubeOverlay]; } @@ -141,14 +138,15 @@ CreatePalette = function (side, leftInputs, rightInputs) { function display() { // Creates and shows menu entities. - var handJointIndex; + var handJointIndex, + properties; if (isDisplaying) { return; } // Joint index. - handJointIndex = MyAvatar.getJointIndex(HAND_JOINT_NAME); + handJointIndex = MyAvatar.getJointIndex(controlJointName); if (handJointIndex === -1) { // Don't display if joint isn't available (yet) to attach to. // User can clear this condition by toggling the app off and back on once avatar finishes loading. @@ -157,18 +155,23 @@ CreatePalette = function (side, leftInputs, rightInputs) { } // Calculate position to put palette. - PALETTE_ORIGIN_PROPERTIES.parentJointIndex = handJointIndex; - paletteOriginOverlay = Overlays.addOverlay("sphere", PALETTE_ORIGIN_PROPERTIES); + properties = Object.clone(PALETTE_ORIGIN_PROPERTIES); + properties.parentJointIndex = handJointIndex; + properties.localPosition = Vec3.sum(PALETTE_ROOT_POSITION, { x: lateralOffset, y: 0, z: 0 }); + paletteOriginOverlay = Overlays.addOverlay("sphere", properties); // Create palette items. - PALETTE_PANEL_PROPERTIES.parentID = paletteOriginOverlay; - palettePanelOverlay = Overlays.addOverlay("cube", PALETTE_PANEL_PROPERTIES); - CUBE_PROPERTIES.parentID = paletteOriginOverlay; - cubeOverlay = Overlays.addOverlay("cube", CUBE_PROPERTIES); + properties = Object.clone(PALETTE_PANEL_PROPERTIES); + properties.parentID = paletteOriginOverlay; + palettePanelOverlay = Overlays.addOverlay("cube", properties); + properties = Object.clone(CUBE_PROPERTIES); + properties.parentID = paletteOriginOverlay; + cubeOverlay = Overlays.addOverlay("cube", properties); // Prepare cube highlight overlay. - CUBE_HIGHLIGHT_PROPERTIES.parentID = paletteOriginOverlay; - cubeHighlightOverlay = Overlays.addOverlay("cube", CUBE_HIGHLIGHT_PROPERTIES); + properties = Object.clone(CUBE_HIGHLIGHT_PROPERTIES); + properties.parentID = paletteOriginOverlay; + cubeHighlightOverlay = Overlays.addOverlay("cube", properties); isDisplaying = true; } diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 781530207c..8af0e3a884 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -42,7 +42,7 @@ ToolIcon = function (side) { visible: true }, - HAND_JOINT_NAME = side === LEFT_HAND ? "LeftHand" : "RightHand", + handJointName, iconOverlay = null; @@ -50,6 +50,13 @@ ToolIcon = function (side) { return new ToolIcon(); } + function setHand(side) { + // Assumes UI is not displaying. + handJointName = side === LEFT_HAND ? "LeftHand" : "RightHand"; + } + + setHand(side); + function update() { // TODO: Display icon animation. // TODO: Clear icon animation. @@ -60,7 +67,7 @@ ToolIcon = function (side) { var handJointIndex, iconProperties; - handJointIndex = MyAvatar.getJointIndex(HAND_JOINT_NAME); + handJointIndex = MyAvatar.getJointIndex(handJointName); if (handJointIndex === -1) { // Don't display if joint isn't available (yet) to attach to. // User can clear this condition by toggling the app off and back on once avatar finishes loading. @@ -93,6 +100,7 @@ ToolIcon = function (side) { return { NONE: NONE, SCALE_HANDLES: SCALE_HANDLES, + setHand: setHand, update: update, display: display, clear: clear, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index d0c00cb5a5..09eef7c38a 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -22,16 +22,14 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba LEFT_HAND = 0, AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", + ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), - HAND_JOINT_NAME = side === LEFT_HAND ? "LeftHand" : "RightHand", + controlJointName, CANVAS_SIZE = { x: 0.21, y: 0.13 }, - LATERAL_OFFSET = side === LEFT_HAND ? -0.01 : 0.01, - - PANEL_ROOT_POSITION = { x: CANVAS_SIZE.x / 2 + LATERAL_OFFSET, y: 0.15, z: -0.04 }, + PANEL_ROOT_POSITION = { x: CANVAS_SIZE.x / 2, y: 0.15, z: -0.04 }, PANEL_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }), - - ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), + lateralOffset, PANEL_ORIGIN_PROPERTIES = { dimensions: { x: 0.005, y: 0.005, z: 0.005 }, @@ -93,15 +91,16 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); - function setHand(uiSide) { - side = uiSide; + function setHand(hand) { + // Assumes UI is not displaying. + side = hand; controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); - - if (isDisplaying) { - // TODO: Move UI to other hand. - } + controlJointName = side === LEFT_HAND ? "LeftHand" : "RightHand"; + lateralOffset = side === LEFT_HAND ? -0.01 : 0.01; } + setHand(side); + function getEntityIDs() { return [menuPanelOverlay, buttonOverlay]; } @@ -131,14 +130,15 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba function display() { // Creates and shows menu entities. - var handJointIndex; + var handJointIndex, + properties; if (isDisplaying) { return; } // Joint index. - handJointIndex = MyAvatar.getJointIndex(HAND_JOINT_NAME); + handJointIndex = MyAvatar.getJointIndex(controlJointName); if (handJointIndex === -1) { // Don't display if joint isn't available (yet) to attach to. // User can clear this condition by toggling the app off and back on once avatar finishes loading. @@ -147,18 +147,23 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba } // Calculate position to put menu. - PANEL_ORIGIN_PROPERTIES.parentJointIndex = handJointIndex; - menuOriginOverlay = Overlays.addOverlay("sphere", PANEL_ORIGIN_PROPERTIES); + properties = Object.clone(PANEL_ORIGIN_PROPERTIES); + properties.parentJointIndex = handJointIndex; + properties.localPosition = Vec3.sum(PANEL_ROOT_POSITION, { x: lateralOffset, y: 0, z: 0 }); + menuOriginOverlay = Overlays.addOverlay("sphere", properties); // Create menu items. - MENU_PANEL_PROPERTIES.parentID = menuOriginOverlay; - menuPanelOverlay = Overlays.addOverlay("cube", MENU_PANEL_PROPERTIES); - BUTTON_PROPERTIES.parentID = menuOriginOverlay; - buttonOverlay = Overlays.addOverlay("cube", BUTTON_PROPERTIES); + properties = Object.clone(MENU_PANEL_PROPERTIES); + properties.parentID = menuOriginOverlay; + menuPanelOverlay = Overlays.addOverlay("cube", properties); + properties = Object.clone(BUTTON_PROPERTIES); + properties.parentID = menuOriginOverlay; + buttonOverlay = Overlays.addOverlay("cube", properties); // Prepare button highlight overlay. - BUTTON_HIGHLIGHT_PROPERTIES.parentID = menuOriginOverlay; - buttonHighlightOverlay = Overlays.addOverlay("cube", BUTTON_HIGHLIGHT_PROPERTIES); + properties = Object.clone(BUTTON_HIGHLIGHT_PROPERTIES); + properties.parentID = menuOriginOverlay; + buttonHighlightOverlay = Overlays.addOverlay("cube", properties); isDisplaying = true; } diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 888cab02af..4c4ca78dd5 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -190,6 +190,7 @@ function setHand(side) { + toolIcon.setHand(otherHand(side)); toolMenu.setHand(side); createPalette.setHand(side); getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; @@ -1034,12 +1035,31 @@ } } - function onDominantHandChanged() { - /* - // TODO: API coming. - dominantHand = TODO; - */ + function onDominantHandChanged(hand) { + dominantHand = hand === "left" ? LEFT_HAND : RIGHT_HAND; + + if (isAppActive) { + // Stop operations. + Script.clearTimeout(updateTimer); + updateTimer = null; + inputs[LEFT_HAND].clear(); + inputs[RIGHT_HAND].clear(); + ui.clear(); + editors[LEFT_HAND].clear(); + editors[RIGHT_HAND].clear(); + } + + // Swap UI hands. ui.setHand(otherHand(dominantHand)); + if (isAppScaleWithHandles) { + ui.setToolIcon(ui.SCALE_HANDLES); + } + + if (isAppActive) { + // Resume operations. + ui.display(); + update(); + } } @@ -1052,11 +1072,7 @@ } // Settings values. - // TODO: API coming. - dominantHand = RIGHT_HAND; - /* - dominantHand = TODO; - */ + dominantHand = MyAvatar.getDominantHand() === "left" ? LEFT_HAND : RIGHT_HAND; // Tablet/toolbar button. button = tablet.addButton({ @@ -1083,11 +1099,9 @@ editors[RIGHT_HAND].setReferences(inputs[RIGHT_HAND], editors[LEFT_HAND]); // Settings changes. - /* - // TODO: API coming. - TODO.change.connect(onDominantHandChanged); - */ + MyAvatar.dominantHandChanged.connect(onDominantHandChanged); + // Start main update loop. if (isAppActive) { update(); } From 676deb8a5caadc458ff00523a2bf2e1f696b2d7d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 29 Jul 2017 16:16:48 +1200 Subject: [PATCH 109/722] Make grip naming consistent with trigger naming --- scripts/vr-edit/modules/hand.js | 14 +++++++------- scripts/vr-edit/vr-edit.js | 22 +++++++++++----------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/scripts/vr-edit/modules/hand.js b/scripts/vr-edit/modules/hand.js index 0430067eb9..0e2bf1c0b3 100644 --- a/scripts/vr-edit/modules/hand.js +++ b/scripts/vr-edit/modules/hand.js @@ -20,7 +20,7 @@ Hand = function (side) { controllerTriggerClicked, controllerGrip, - isGripPressed = false, + isGripClicked = false, GRIP_ON_VALUE = 0.99, GRIP_OFF_VALUE = 0.95, @@ -78,8 +78,8 @@ Hand = function (side) { return isTriggerClicked; } - function gripPressed() { - return isGripPressed; + function gripClicked() { + return isGripClicked; } function getIntersection() { @@ -117,10 +117,10 @@ Hand = function (side) { // Controller grip. gripValue = Controller.getValue(controllerGrip); - if (isGripPressed) { - isGripPressed = gripValue > GRIP_OFF_VALUE; + if (isGripClicked) { + isGripClicked = gripValue > GRIP_OFF_VALUE; } else { - isGripPressed = gripValue > GRIP_ON_VALUE; + isGripClicked = gripValue > GRIP_ON_VALUE; } // Hand-overlay intersection, if any. @@ -193,7 +193,7 @@ Hand = function (side) { orientation: orientation, triggerPressed: triggerPressed, triggerClicked: triggerClicked, - gripPressed: gripPressed, + gripClicked: gripClicked, intersection: getIntersection, update: update, clear: clear, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 4c4ca78dd5..190110e261 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -293,7 +293,7 @@ highlightedEntityID = null, // Root entity of highlighted entity set. wasAppScaleWithHandles = false, isOtherEditorEditingEntityID = false, - wasGripPressed = false, + wasGripClicked = false, hoveredOverlayID = null, // Position values. @@ -578,7 +578,7 @@ selection.clear(); hoveredOverlayID = intersection.overlayID; otherEditor.hoverHandle(hoveredOverlayID); - wasGripPressed = hand.gripPressed(); + wasGripClicked = hand.gripClicked(); } function updateEditorSearching() { @@ -600,7 +600,7 @@ } isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID); wasAppScaleWithHandles = isAppScaleWithHandles; - wasGripPressed = hand.gripPressed(); + wasGripClicked = hand.gripClicked(); } function updateEditorHighlighting() { @@ -631,7 +631,7 @@ } startEditing(); wasAppScaleWithHandles = isAppScaleWithHandles; - wasGripPressed = hand.gripPressed(); + wasGripClicked = hand.gripClicked(); } function updateEditorGrabbing() { @@ -741,12 +741,12 @@ function updateTool() { - var isGripPressed = hand.gripPressed(); - if (!wasGripPressed && isGripPressed && isAppScaleWithHandles) { + var isGripClicked = hand.gripClicked(); + if (!wasGripClicked && isGripClicked && isAppScaleWithHandles) { isAppScaleWithHandles = false; ui.clearToolIcon(); } - wasGripPressed = isGripPressed; + wasGripClicked = isGripClicked; } @@ -843,14 +843,14 @@ } break; case EDITOR_GRABBING: - if (hand.valid() && hand.triggerClicked() && !hand.gripPressed()) { + if (hand.valid() && hand.triggerClicked() && !hand.gripClicked()) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // No transition. if (isAppScaleWithHandles !== wasAppScaleWithHandles) { updateState(); wasAppScaleWithHandles = isAppScaleWithHandles; } - wasGripPressed = false; + wasGripClicked = false; break; } if (!hand.valid()) { @@ -862,8 +862,8 @@ } else { setState(EDITOR_SEARCHING); } - } else if (hand.gripPressed()) { - if (!wasGripPressed) { + } else if (hand.gripClicked()) { + if (!wasGripClicked) { selection.deleteEntities(); setState(EDITOR_SEARCHING); } From 18a9dad9185c9355909b5b2fa0eac5ed12ab6798 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 29 Jul 2017 17:15:33 +1200 Subject: [PATCH 110/722] Add button for clone tool --- scripts/vr-edit/modules/toolMenu.js | 63 ++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 09eef7c38a..ac8c04af18 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -17,7 +17,8 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba var menuOriginOverlay, menuPanelOverlay, - buttonOverlay, + scaleButtonOverlay, + cloneButtonOverlay, buttonHighlightOverlay, LEFT_HAND = 0, @@ -39,7 +40,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba alpha: 1.0, parentID: AVATAR_SELF_ID, ignoreRayIntersection: true, - visible: false + visible: true }, MENU_PANEL_PROPERTIES = { @@ -55,9 +56,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba BUTTON_PROPERTIES = { dimensions: { x: 0.03, y: 0.03, z: 0.01 }, - localPosition: { x: 0.02, y: 0.02, z: 0.0 }, localRotation: ZERO_ROTATION, - color: { red: 0, green: 240, blue: 240 }, alpha: 1.0, solid: true, ignoreRayIntersection: false, @@ -76,6 +75,18 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba visible: false }, + HIGHLIGHT_Z_OFFSET = -0.002, + + BUTTON_POSITIONS = [ + { x: 0.02, y: 0.02, z: 0.0 }, + { x: 0.06, y: 0.02, z: 0.0 } + ], + + BUTTON_COLORS = [ + { red: 0, green: 240, blue: 240 }, + { red: 240, green: 0, blue: 240 } + ], + isDisplaying = false, isHighlightingButton = false, @@ -102,29 +113,45 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba setHand(side); function getEntityIDs() { - return [menuPanelOverlay, buttonOverlay]; + return [menuPanelOverlay, scaleButtonOverlay, cloneButtonOverlay]; } function update(intersectionOverlayID) { // Highlight button. - if (intersectionOverlayID === buttonOverlay !== isHighlightingButton) { + if ((intersectionOverlayID === scaleButtonOverlay || intersectionOverlayID === cloneButtonOverlay) + !== isHighlightingButton) { isHighlightingButton = !isHighlightingButton; - Overlays.editOverlay(buttonHighlightOverlay, { visible: isHighlightingButton }); + Overlays.editOverlay(buttonHighlightOverlay, { + localPosition: Vec3.sum({ x: 0, y: 0, z: HIGHLIGHT_Z_OFFSET }, + intersectionOverlayID === scaleButtonOverlay ? BUTTON_POSITIONS[0] : BUTTON_POSITIONS[1]), + visible: isHighlightingButton + }); } // Button click. if (!isHighlightingButton || controlHand.triggerClicked() !== isButtonPressed) { isButtonPressed = isHighlightingButton && controlHand.triggerClicked(); - if (isButtonPressed) { - Overlays.editOverlay(buttonOverlay, { - localPosition: Vec3.sum(BUTTON_PROPERTIES.localPosition, { x: 0, y: 0, z: 0.004 }) + if (isButtonPressed && intersectionOverlayID === scaleButtonOverlay) { + Overlays.editOverlay(scaleButtonOverlay, { + localPosition: Vec3.sum(BUTTON_POSITIONS[0], { x: 0, y: 0, z: 0.004 }) }); - setAppScaleWithHandlesCallback(); } else { - Overlays.editOverlay(buttonOverlay, { - localPosition: BUTTON_PROPERTIES.localPosition + Overlays.editOverlay(scaleButtonOverlay, { + localPosition: BUTTON_POSITIONS[0] }); } + if (isButtonPressed && intersectionOverlayID === cloneButtonOverlay) { + Overlays.editOverlay(cloneButtonOverlay, { + localPosition: Vec3.sum(BUTTON_POSITIONS[1], { x: 0, y: 0, z: 0.004 }) + }); + } else { + Overlays.editOverlay(cloneButtonOverlay, { + localPosition: BUTTON_POSITIONS[1] + }); + } + if (isButtonPressed) { + setAppScaleWithHandlesCallback(); + } } } @@ -158,7 +185,12 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba menuPanelOverlay = Overlays.addOverlay("cube", properties); properties = Object.clone(BUTTON_PROPERTIES); properties.parentID = menuOriginOverlay; - buttonOverlay = Overlays.addOverlay("cube", properties); + properties.localPosition = BUTTON_POSITIONS[0]; + properties.color = BUTTON_COLORS[0]; + scaleButtonOverlay = Overlays.addOverlay("cube", properties); + properties.localPosition = BUTTON_POSITIONS[1]; + properties.color = BUTTON_COLORS[1]; + cloneButtonOverlay = Overlays.addOverlay("cube", properties); // Prepare button highlight overlay. properties = Object.clone(BUTTON_HIGHLIGHT_PROPERTIES); @@ -175,7 +207,8 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba } Overlays.deleteOverlay(buttonHighlightOverlay); - Overlays.deleteOverlay(buttonOverlay); + Overlays.deleteOverlay(cloneButtonOverlay); + Overlays.deleteOverlay(scaleButtonOverlay); Overlays.deleteOverlay(menuPanelOverlay); Overlays.deleteOverlay(menuOriginOverlay); isDisplaying = false; From 48ee7a3b1a52c112d83d0b05225474758c7151cb Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 29 Jul 2017 17:39:23 +1200 Subject: [PATCH 111/722] Add icon for clone tool --- scripts/vr-edit/modules/toolIcon.js | 12 +++++----- scripts/vr-edit/modules/toolMenu.js | 4 ++-- scripts/vr-edit/vr-edit.js | 36 +++++++++++++++++++++-------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 8af0e3a884..c9f955ecf5 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -15,12 +15,12 @@ ToolIcon = function (side) { "use strict"; - var NONE = 0, - SCALE_HANDLES = 1, + var SCALE_TOOL = 0, + CLONE_TOOL = 1, ICON_COLORS = [ - { red: 0, green: 0, blue: 0 }, // Unused entry for NONE. - { red: 0, green: 240, blue: 240 } + { red: 0, green: 240, blue: 240 }, + { red: 240, green: 0, blue: 240 } ], LEFT_HAND = 0, @@ -98,8 +98,8 @@ ToolIcon = function (side) { } return { - NONE: NONE, - SCALE_HANDLES: SCALE_HANDLES, + SCALE_TOOL: SCALE_TOOL, + CLONE_TOOL: CLONE_TOOL, setHand: setHand, update: update, display: display, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index ac8c04af18..7684fa66d3 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -10,7 +10,7 @@ /* global ToolMenu */ -ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallback) { +ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { // Tool menu displayed on top of forearm. "use strict"; @@ -150,7 +150,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallba }); } if (isButtonPressed) { - setAppScaleWithHandlesCallback(); + setToolCallback(intersectionOverlayID === scaleButtonOverlay ? "scale" : "clone"); } } } diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 190110e261..06d438a19e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -20,6 +20,7 @@ // Application state isAppActive = false, isAppScaleWithHandles = false, + isAppCloneEntities = false, dominantHand, // Primary objects @@ -165,7 +166,7 @@ }; - UI = function (side, leftInputs, rightInputs, setAppScaleWithHandlesCallback) { + UI = function (side, leftInputs, rightInputs, setToolCallback) { // Tool menu and Create palette. var // Primary objects. @@ -183,7 +184,7 @@ } toolIcon = new ToolIcon(otherHand(side)); - toolMenu = new ToolMenu(side, leftInputs, rightInputs, setAppScaleWithHandlesCallback); + toolMenu = new ToolMenu(side, leftInputs, rightInputs, setToolCallback); createPalette = new CreatePalette(side, leftInputs, rightInputs); getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; @@ -254,7 +255,8 @@ setHand: setHand, setToolIcon: setToolIcon, clearToolIcon: clearToolIcon, - SCALE_HANDLES: toolIcon.SCALE_HANDLES, + SCALE_TOOL: toolIcon.SCALE_TOOL, + CLONE_TOOL: toolIcon.CLONE_TOOL, display: display, update: update, clear: clear, @@ -742,8 +744,9 @@ function updateTool() { var isGripClicked = hand.gripClicked(); - if (!wasGripClicked && isGripClicked && isAppScaleWithHandles) { + if (!wasGripClicked && isGripClicked && (isAppScaleWithHandles || isAppCloneEntities)) { isAppScaleWithHandles = false; + isAppCloneEntities = false; ui.clearToolIcon(); } wasGripClicked = isGripClicked; @@ -1010,9 +1013,21 @@ Settings.setValue(VR_EDIT_SETTING, isAppActive); } - function setAppScaleWithHandles() { - isAppScaleWithHandles = true; - ui.setToolIcon(ui.SCALE_HANDLES); + function setTool(tool) { + switch (tool) { + case "scale": + isAppScaleWithHandles = true; + isAppCloneEntities = false; + ui.setToolIcon(ui.SCALE_TOOL); + break; + case "clone": + isAppScaleWithHandles = false; + isAppCloneEntities = true; + ui.setToolIcon(ui.CLONE_TOOL); + break; + default: + debug("ERROR: Unexpected condition in setTool()!"); + } } function onAppButtonClicked() { @@ -1052,7 +1067,10 @@ // Swap UI hands. ui.setHand(otherHand(dominantHand)); if (isAppScaleWithHandles) { - ui.setToolIcon(ui.SCALE_HANDLES); + ui.setToolIcon(ui.SCALE_TOOL); + } + if (isAppCloneEntities) { + ui.setToolIcon(ui.CLONE_TOOL); } if (isAppActive) { @@ -1090,7 +1108,7 @@ inputs[RIGHT_HAND] = new Inputs(RIGHT_HAND); // UI object. - ui = new UI(otherHand(dominantHand), inputs[LEFT_HAND], inputs[RIGHT_HAND], setAppScaleWithHandles); + ui = new UI(otherHand(dominantHand), inputs[LEFT_HAND], inputs[RIGHT_HAND], setTool); // Editor objects. editors[LEFT_HAND] = new Editor(LEFT_HAND); From ab8bccf16b10dce10e5c8028a37fc5126f3a0ca0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 30 Jul 2017 11:03:19 +1200 Subject: [PATCH 112/722] Implement cloning action --- scripts/vr-edit/modules/selection.js | 35 +++++++++++++++++++++++++- scripts/vr-edit/vr-edit.js | 37 +++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 5437e49bf3..dc4d05a374 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -33,7 +33,7 @@ Selection = function (side) { // Recursively traverses tree of entities and their children, gather IDs and properties. var children, properties, - SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions", "localPosition", + SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions", "parentID", "localPosition", "dynamic", "collisionless"], i, length; @@ -42,6 +42,7 @@ Selection = function (side) { result.push({ id: id, position: properties.position, + parentID: properties.parentID, localPosition: properties.localPosition, registrationPoint: properties.registrationPoint, rotation: properties.rotation, @@ -294,6 +295,37 @@ Selection = function (side) { select(selectedEntityID); // Refresh. } + function cloneEntities() { + var parentIDIndexes = [], + parentID, + properties, + i, + j, + length; + + // Map parent IDs. + for (i = 1, length = selection.length; i < length; i += 1) { + parentID = selection[i].parentID; + for (j = 0; j < i; j += 1) { + if (parentID === selection[j].id) { + parentIDIndexes[i] = j; + break; + } + } + } + + // Clone entities. + for (i = 0, length = selection.length; i < length; i += 1) { + properties = Entities.getEntityProperties(selection[i].id); + if (i > 0) { + properties.parentID = selection[parentIDIndexes[i]].id; + } + selection[i].id = Entities.addEntity(properties); + } + + rootEntityID = selection[0].id; + } + function clear() { selection = []; selectedEntityID = null; @@ -327,6 +359,7 @@ Selection = function (side) { handleScale: handleScale, finishHandleScaling: finishHandleScaling, finishEditing: finishEditing, + cloneEntities: cloneEntities, deleteEntities: deleteEntities, clear: clear, destroy: destroy diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 06d438a19e..09ad63280f 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -286,8 +286,9 @@ EDITOR_GRABBING = 3, EDITOR_DIRECT_SCALING = 4, // Scaling data are sent to other editor's EDITOR_GRABBING state. EDITOR_HANDLE_SCALING = 5, // "" + EDITOR_CLONING = 6, EDITOR_STATE_STRINGS = ["EDITOR_IDLE", "EDITOR_SEARCHING", "EDITOR_HIGHLIGHTING", "EDITOR_GRABBING", - "EDITOR_DIRECT_SCALING", "EDITOR_HANDLE_SCALING"], + "EDITOR_DIRECT_SCALING", "EDITOR_HANDLE_SCALING", "EDITOR_CLONING"], editorState = EDITOR_IDLE, // State machine. @@ -694,6 +695,16 @@ laser.enable(); } + function enterEditorCloning() { + selection.select(highlightedEntityID); // For when transitioning from EDITOR_SEARCHING. + selection.cloneEntities(); + highlightedEntityID = selection.rootEntityID(); + } + + function exitEditorCloning() { + // Nothing to do. + } + STATE_MACHINE = { EDITOR_IDLE: { enter: enterEditorIdle, @@ -724,6 +735,11 @@ enter: enterEditorHandleScaling, update: updateEditorHandleScaling, exit: exitEditorHandleScaling + }, + EDITOR_CLONING: { + enter: enterEditorCloning, + update: null, + exit: exitEditorCloning } }; @@ -791,6 +807,8 @@ if (!isAppScaleWithHandles) { setState(EDITOR_DIRECT_SCALING); } + } else if (isAppCloneEntities) { + setState(EDITOR_CLONING); } else { setState(EDITOR_GRABBING); } @@ -836,6 +854,8 @@ } else { debug(side, "ERROR: Unexpected condition in EDITOR_HIGHLIGHTING! A"); } + } else if (isAppCloneEntities) { + setState(EDITOR_CLONING); } else { setState(EDITOR_GRABBING); } @@ -921,6 +941,21 @@ setState(EDITOR_GRABBING); } break; + case EDITOR_CLONING: + // Immediate transition out of state after cloning entities during state entry. + if (hand.valid() && hand.triggerClicked()) { + setState(EDITOR_GRABBING); + } else if (!hand.valid()) { + setState(EDITOR_IDLE); + } else if (!hand.triggerClicked()) { + if (intersection.entityID && intersection.editableEntity) { + highlightedEntityID = Entities.rootOf(intersection.entityID); + setState(EDITOR_HIGHLIGHTING); + } else { + setState(EDITOR_SEARCHING); + } + } + break; } if (DEBUG && editorState !== previousState) { From 59dc5ec3b8e1552d32901eb1767399cac4037474 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 1 Aug 2017 10:20:55 +1200 Subject: [PATCH 113/722] Refactor tool buttons --- scripts/vr-edit/modules/toolMenu.js | 100 +++++++++++++++------------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 7684fa66d3..87588105ae 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -17,8 +17,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { var menuOriginOverlay, menuPanelOverlay, - scaleButtonOverlay, - cloneButtonOverlay, + toolButtonOverlays = [], buttonHighlightOverlay, LEFT_HAND = 0, @@ -77,14 +76,17 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { HIGHLIGHT_Z_OFFSET = -0.002, - BUTTON_POSITIONS = [ - { x: 0.02, y: 0.02, z: 0.0 }, - { x: 0.06, y: 0.02, z: 0.0 } - ], - - BUTTON_COLORS = [ - { red: 0, green: 240, blue: 240 }, - { red: 240, green: 0, blue: 240 } + TOOL_BUTTONS = [ + { // Scale + position: { x: 0.02, y: 0.02, z: 0.0 }, + color: { red: 0, green: 240, blue: 240 }, + callback: "scale" + }, + { // Clone + position: { x: 0.06, y: 0.02, z: 0.0 }, + color: { red: 240, green: 0, blue: 240 }, + callback: "clone" + } ], isDisplaying = false, @@ -113,44 +115,47 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { setHand(side); function getEntityIDs() { - return [menuPanelOverlay, scaleButtonOverlay, cloneButtonOverlay]; + return [menuPanelOverlay].concat(toolButtonOverlays); } function update(intersectionOverlayID) { + var highlightedButton, + i, + length; + // Highlight button. - if ((intersectionOverlayID === scaleButtonOverlay || intersectionOverlayID === cloneButtonOverlay) - !== isHighlightingButton) { + highlightedButton = toolButtonOverlays.indexOf(intersectionOverlayID); + if ((highlightedButton !== -1) !== isHighlightingButton) { isHighlightingButton = !isHighlightingButton; - Overlays.editOverlay(buttonHighlightOverlay, { - localPosition: Vec3.sum({ x: 0, y: 0, z: HIGHLIGHT_Z_OFFSET }, - intersectionOverlayID === scaleButtonOverlay ? BUTTON_POSITIONS[0] : BUTTON_POSITIONS[1]), - visible: isHighlightingButton - }); + if (isHighlightingButton) { + Overlays.editOverlay(buttonHighlightOverlay, { + localPosition: Vec3.sum({ x: 0, y: 0, z: HIGHLIGHT_Z_OFFSET }, TOOL_BUTTONS[highlightedButton].position), + visible: true + }); + + } else { + Overlays.editOverlay(buttonHighlightOverlay, { + visible: false + }); + } } // Button click. if (!isHighlightingButton || controlHand.triggerClicked() !== isButtonPressed) { isButtonPressed = isHighlightingButton && controlHand.triggerClicked(); - if (isButtonPressed && intersectionOverlayID === scaleButtonOverlay) { - Overlays.editOverlay(scaleButtonOverlay, { - localPosition: Vec3.sum(BUTTON_POSITIONS[0], { x: 0, y: 0, z: 0.004 }) - }); - } else { - Overlays.editOverlay(scaleButtonOverlay, { - localPosition: BUTTON_POSITIONS[0] - }); - } - if (isButtonPressed && intersectionOverlayID === cloneButtonOverlay) { - Overlays.editOverlay(cloneButtonOverlay, { - localPosition: Vec3.sum(BUTTON_POSITIONS[1], { x: 0, y: 0, z: 0.004 }) - }); - } else { - Overlays.editOverlay(cloneButtonOverlay, { - localPosition: BUTTON_POSITIONS[1] - }); + for (i = 0, length = toolButtonOverlays.length; i < length; i += 1) { + if (isButtonPressed && intersectionOverlayID === toolButtonOverlays[i]) { + Overlays.editOverlay(toolButtonOverlays[i], { + localPosition: Vec3.sum(TOOL_BUTTONS[i].position, { x: 0, y: 0, z: 0.004 }) + }); + } else { + Overlays.editOverlay(toolButtonOverlays[i], { + localPosition: TOOL_BUTTONS[i].position + }); + } } if (isButtonPressed) { - setToolCallback(intersectionOverlayID === scaleButtonOverlay ? "scale" : "clone"); + setToolCallback(TOOL_BUTTONS[highlightedButton].callback); } } } @@ -158,7 +163,9 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { function display() { // Creates and shows menu entities. var handJointIndex, - properties; + properties, + i, + length; if (isDisplaying) { return; @@ -185,12 +192,11 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { menuPanelOverlay = Overlays.addOverlay("cube", properties); properties = Object.clone(BUTTON_PROPERTIES); properties.parentID = menuOriginOverlay; - properties.localPosition = BUTTON_POSITIONS[0]; - properties.color = BUTTON_COLORS[0]; - scaleButtonOverlay = Overlays.addOverlay("cube", properties); - properties.localPosition = BUTTON_POSITIONS[1]; - properties.color = BUTTON_COLORS[1]; - cloneButtonOverlay = Overlays.addOverlay("cube", properties); + for (i = 0, length = TOOL_BUTTONS.length; i < length; i += 1) { + properties.localPosition = TOOL_BUTTONS[i].position; + properties.color = TOOL_BUTTONS[i].color; + toolButtonOverlays[i] = Overlays.addOverlay("cube", properties); + } // Prepare button highlight overlay. properties = Object.clone(BUTTON_HIGHLIGHT_PROPERTIES); @@ -202,13 +208,17 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { function clear() { // Deletes menu entities. + var i, + length; + if (!isDisplaying) { return; } Overlays.deleteOverlay(buttonHighlightOverlay); - Overlays.deleteOverlay(cloneButtonOverlay); - Overlays.deleteOverlay(scaleButtonOverlay); + for (i = 0, length = toolButtonOverlays.length; i < length; i += 1) { + Overlays.deleteOverlay(toolButtonOverlays[i]); + } Overlays.deleteOverlay(menuPanelOverlay); Overlays.deleteOverlay(menuOriginOverlay); isDisplaying = false; From ca7c747c965f20da9a0b3b5756a4f92f8ed7722e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 1 Aug 2017 11:46:22 +1200 Subject: [PATCH 114/722] Create entities at hand position rather than in front of avatar --- scripts/vr-edit/modules/createPalette.js | 9 +++++---- scripts/vr-edit/modules/hand.js | 7 ++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index aea2d365b3..928cb48048 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -110,6 +110,7 @@ CreatePalette = function (side, leftInputs, rightInputs) { } function update(intersectionOverlayID) { + var CREATE_OFFSET = { x: 0, y: 0.05, z: -0.02 }; // Highlight cube. if (intersectionOverlayID === cubeOverlay !== isHighlightingCube) { isHighlightingCube = !isHighlightingCube; @@ -119,14 +120,14 @@ CreatePalette = function (side, leftInputs, rightInputs) { // Cube click. if (isHighlightingCube && controlHand.triggerClicked() !== isCubePressed) { isCubePressed = controlHand.triggerClicked(); - if (isCubePressed) { Overlays.editOverlay(cubeOverlay, { localPosition: Vec3.sum(CUBE_PROPERTIES.localPosition, { x: 0, y: 0, z: 0.01 }) }); - CUBE_ENTITY_PROPERTIES.position = - Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0.2, z: -1.0 })); - CUBE_ENTITY_PROPERTIES.rotation = MyAvatar.orientation; + CUBE_ENTITY_PROPERTIES.position = Vec3.sum(controlHand.palmPosition(), + Vec3.multiplyQbyV(controlHand.orientation(), + Vec3.sum({ x: 0, y: CUBE_ENTITY_PROPERTIES.dimensions.z / 2, z: 0 }, CREATE_OFFSET))); + CUBE_ENTITY_PROPERTIES.rotation = controlHand.orientation(); Entities.addEntity(CUBE_ENTITY_PROPERTIES); } else { Overlays.editOverlay(cubeOverlay, { diff --git a/scripts/vr-edit/modules/hand.js b/scripts/vr-edit/modules/hand.js index 0e2bf1c0b3..42d6751ed1 100644 --- a/scripts/vr-edit/modules/hand.js +++ b/scripts/vr-edit/modules/hand.js @@ -39,6 +39,7 @@ Hand = function (side) { handPose, handPosition, handOrientation, + palmPosition, intersection = {}; @@ -70,6 +71,10 @@ Hand = function (side) { return handOrientation; } + function getPalmPosition() { + return palmPosition; + } + function triggerPressed() { return isTriggerPressed; } @@ -88,7 +93,6 @@ Hand = function (side) { function update() { var gripValue, - palmPosition, overlayID, overlayIDs, overlayDistance, @@ -191,6 +195,7 @@ Hand = function (side) { valid: valid, position: position, orientation: orientation, + palmPosition: getPalmPosition, triggerPressed: triggerPressed, triggerClicked: triggerClicked, gripClicked: gripClicked, From f2cf2cab6eda2f42a6265640587d60d06dec6392 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 1 Aug 2017 16:02:04 +1200 Subject: [PATCH 115/722] Generalize tool selected --- scripts/vr-edit/vr-edit.js | 74 ++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 09ad63280f..da945a2668 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -19,10 +19,14 @@ // Application state isAppActive = false, - isAppScaleWithHandles = false, - isAppCloneEntities = false, dominantHand, + // Tool state + TOOL_NONE = 0, + TOOL_SCALE = 1, + TOOL_CLONE = 2, + toolSelected = TOOL_NONE, + // Primary objects Inputs, inputs = [], @@ -294,7 +298,7 @@ // State machine. STATE_MACHINE, highlightedEntityID = null, // Root entity of highlighted entity set. - wasAppScaleWithHandles = false, + wasScaleTool = false, isOtherEditorEditingEntityID = false, wasGripClicked = false, hoveredOverlayID = null, @@ -585,7 +589,7 @@ } function updateEditorSearching() { - if (isAppScaleWithHandles && intersection.overlayID !== hoveredOverlayID && otherEditor.isEditing()) { + if (toolSelected === TOOL_SCALE && intersection.overlayID !== hoveredOverlayID && otherEditor.isEditing()) { hoveredOverlayID = intersection.overlayID; otherEditor.hoverHandle(hoveredOverlayID); } @@ -597,20 +601,20 @@ function enterEditorHighlighting() { selection.select(highlightedEntityID); - if (!isAppScaleWithHandles || !otherEditor.isEditing(highlightedEntityID)) { + if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(highlightedEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), - isAppScaleWithHandles || otherEditor.isEditing(highlightedEntityID)); + toolSelected === TOOL_SCALE || otherEditor.isEditing(highlightedEntityID)); } isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID); - wasAppScaleWithHandles = isAppScaleWithHandles; + wasScaleTool = toolSelected === TOOL_SCALE; wasGripClicked = hand.gripClicked(); } function updateEditorHighlighting() { selection.select(highlightedEntityID); - if (!isAppScaleWithHandles || !otherEditor.isEditing(highlightedEntityID)) { + if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(highlightedEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), - isAppScaleWithHandles || otherEditor.isEditing(highlightedEntityID)); + toolSelected === TOOL_SCALE || otherEditor.isEditing(highlightedEntityID)); } else { highlights.clear(); } @@ -629,17 +633,17 @@ } else { laser.disable(); } - if (isAppScaleWithHandles) { + if (toolSelected === TOOL_SCALE) { handles.display(highlightedEntityID, selection.boundingBox(), selection.count() > 1); } startEditing(); - wasAppScaleWithHandles = isAppScaleWithHandles; + wasScaleTool = toolSelected === TOOL_SCALE; wasGripClicked = hand.gripClicked(); } function updateEditorGrabbing() { selection.select(highlightedEntityID); - if (isAppScaleWithHandles) { + if (toolSelected === TOOL_SCALE) { handles.display(highlightedEntityID, selection.boundingBox(), selection.count() > 1); } else { handles.clear(); @@ -760,9 +764,8 @@ function updateTool() { var isGripClicked = hand.gripClicked(); - if (!wasGripClicked && isGripClicked && (isAppScaleWithHandles || isAppCloneEntities)) { - isAppScaleWithHandles = false; - isAppCloneEntities = false; + if (!wasGripClicked && isGripClicked && (toolSelected !== TOOL_NONE)) { + toolSelected = TOOL_NONE; ui.clearToolIcon(); } wasGripClicked = isGripClicked; @@ -804,10 +807,10 @@ } else if (intersection.entityID && intersection.editableEntity && hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); if (otherEditor.isEditing(highlightedEntityID)) { - if (!isAppScaleWithHandles) { + if (toolSelected !== TOOL_SCALE) { setState(EDITOR_DIRECT_SCALING); } - } else if (isAppCloneEntities) { + } else if (toolSelected === TOOL_CLONE) { setState(EDITOR_CLONING); } else { setState(EDITOR_GRABBING); @@ -819,7 +822,8 @@ case EDITOR_HIGHLIGHTING: if (hand.valid() && intersection.entityID && intersection.editableEntity - && !(hand.triggerClicked() && (!otherEditor.isEditing(highlightedEntityID) || !isAppScaleWithHandles)) + && !(hand.triggerClicked() + && (!otherEditor.isEditing(highlightedEntityID) || toolSelected !== TOOL_SCALE)) && !(hand.triggerClicked() && intersection.overlayID && otherEditor.isHandle(intersection.overlayID))) { // No transition. doUpdateState = false; @@ -830,8 +834,8 @@ highlightedEntityID = Entities.rootOf(intersection.entityID); doUpdateState = true; } - if (isAppScaleWithHandles !== wasAppScaleWithHandles) { - wasAppScaleWithHandles = isAppScaleWithHandles; + if (toolSelected === TOOL_SCALE !== wasScaleTool) { + wasScaleTool = toolSelected === TOOL_SCALE; doUpdateState = true; } if (doUpdateState) { @@ -849,12 +853,12 @@ } else if (intersection.entityID && intersection.editableEntity && hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); // May be a different entityID. if (otherEditor.isEditing(highlightedEntityID)) { - if (!isAppScaleWithHandles) { + if (toolSelected !== TOOL_SCALE) { setState(EDITOR_DIRECT_SCALING); } else { debug(side, "ERROR: Unexpected condition in EDITOR_HIGHLIGHTING! A"); } - } else if (isAppCloneEntities) { + } else if (toolSelected === TOOL_CLONE) { setState(EDITOR_CLONING); } else { setState(EDITOR_GRABBING); @@ -869,9 +873,9 @@ if (hand.valid() && hand.triggerClicked() && !hand.gripClicked()) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // No transition. - if (isAppScaleWithHandles !== wasAppScaleWithHandles) { + if (toolSelected === TOOL_SCALE !== wasScaleTool) { updateState(); - wasAppScaleWithHandles = isAppScaleWithHandles; + wasScaleTool = toolSelected === TOOL_SCALE; } wasGripClicked = false; break; @@ -898,8 +902,8 @@ if (hand.valid() && hand.triggerClicked() && (otherEditor.isEditing(highlightedEntityID) || otherEditor.isHandle(intersection.overlayID))) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. - // Don't test isAppScaleWithHandles because this will eventually be a UI element and so not able to be - // changed while scaling with two hands. + // Don't test toolSelected === TOOL_SCALE because this is a UI element and so not able to be changed while + // scaling with two hands. // No transition. updateState(); break; @@ -921,8 +925,8 @@ case EDITOR_HANDLE_SCALING: if (hand.valid() && hand.triggerClicked() && otherEditor.isEditing(highlightedEntityID)) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. - // Don't test isAppScaleWithHandles because this will eventually be a UI element and so not able to be - // changed while scaling with two hands. + // Don't test toolSelected === TOOL_SCALE because this is a UI element and so not able to be changed while + // scaling with two hands. // No transition. updateState(); break; @@ -1051,13 +1055,11 @@ function setTool(tool) { switch (tool) { case "scale": - isAppScaleWithHandles = true; - isAppCloneEntities = false; + toolSelected = TOOL_SCALE; ui.setToolIcon(ui.SCALE_TOOL); break; case "clone": - isAppScaleWithHandles = false; - isAppCloneEntities = true; + toolSelected = TOOL_CLONE; ui.setToolIcon(ui.CLONE_TOOL); break; default: @@ -1101,11 +1103,13 @@ // Swap UI hands. ui.setHand(otherHand(dominantHand)); - if (isAppScaleWithHandles) { + switch (toolSelected) { + case TOOL_SCALE: ui.setToolIcon(ui.SCALE_TOOL); - } - if (isAppCloneEntities) { + break; + case TOOL_CLONE: ui.setToolIcon(ui.CLONE_TOOL); + break; } if (isAppActive) { From 12d911f29050cdf23015f4b0ee412de62e70b5e5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 2 Aug 2017 08:41:53 +1200 Subject: [PATCH 116/722] Add "group" bool button and icon --- scripts/vr-edit/modules/toolIcon.js | 5 ++++- scripts/vr-edit/modules/toolMenu.js | 5 +++++ scripts/vr-edit/vr-edit.js | 9 +++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index c9f955ecf5..7aa116af81 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -17,10 +17,12 @@ ToolIcon = function (side) { var SCALE_TOOL = 0, CLONE_TOOL = 1, + GROUP_TOOL = 2, ICON_COLORS = [ { red: 0, green: 240, blue: 240 }, - { red: 240, green: 0, blue: 240 } + { red: 240, green: 0, blue: 240 }, + { red: 240, green: 240, blue: 0 } ], LEFT_HAND = 0, @@ -100,6 +102,7 @@ ToolIcon = function (side) { return { SCALE_TOOL: SCALE_TOOL, CLONE_TOOL: CLONE_TOOL, + GROUP_TOOL: GROUP_TOOL, setHand: setHand, update: update, display: display, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 87588105ae..3af6063646 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -86,6 +86,11 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { position: { x: 0.06, y: 0.02, z: 0.0 }, color: { red: 240, green: 0, blue: 240 }, callback: "clone" + }, + { // Group + position: { x: 0.10, y: 0.02, z: 0.0 }, + color: { red: 240, green: 240, blue: 0 }, + callback: "group" } ], diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index da945a2668..dcfa702c46 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -25,6 +25,7 @@ TOOL_NONE = 0, TOOL_SCALE = 1, TOOL_CLONE = 2, + TOOL_GROUP = 3, toolSelected = TOOL_NONE, // Primary objects @@ -261,6 +262,7 @@ clearToolIcon: clearToolIcon, SCALE_TOOL: toolIcon.SCALE_TOOL, CLONE_TOOL: toolIcon.CLONE_TOOL, + GROUP_TOOL: toolIcon.GROUP_TOOL, display: display, update: update, clear: clear, @@ -1062,6 +1064,10 @@ toolSelected = TOOL_CLONE; ui.setToolIcon(ui.CLONE_TOOL); break; + case "group": + toolSelected = TOOL_GROUP; + ui.setToolIcon(ui.GROUP_TOOL); + break; default: debug("ERROR: Unexpected condition in setTool()!"); } @@ -1110,6 +1116,9 @@ case TOOL_CLONE: ui.setToolIcon(ui.CLONE_TOOL); break; + case TOOL_GROUP: + ui.setToolIcon(ui.GROUP_TOOL); + break; } if (isAppActive) { From a94b2b367b124b72615d42a3632576052158bb80 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 2 Aug 2017 09:16:24 +1200 Subject: [PATCH 117/722] Simplify grip click handling --- scripts/vr-edit/vr-edit.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index dcfa702c46..2719236a35 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -302,6 +302,7 @@ highlightedEntityID = null, // Root entity of highlighted entity set. wasScaleTool = false, isOtherEditorEditingEntityID = false, + isGripClicked = false, wasGripClicked = false, hoveredOverlayID = null, @@ -587,7 +588,6 @@ selection.clear(); hoveredOverlayID = intersection.overlayID; otherEditor.hoverHandle(hoveredOverlayID); - wasGripClicked = hand.gripClicked(); } function updateEditorSearching() { @@ -609,7 +609,6 @@ } isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID); wasScaleTool = toolSelected === TOOL_SCALE; - wasGripClicked = hand.gripClicked(); } function updateEditorHighlighting() { @@ -640,7 +639,6 @@ } startEditing(); wasScaleTool = toolSelected === TOOL_SCALE; - wasGripClicked = hand.gripClicked(); } function updateEditorGrabbing() { @@ -765,12 +763,10 @@ function updateTool() { - var isGripClicked = hand.gripClicked(); if (!wasGripClicked && isGripClicked && (toolSelected !== TOOL_NONE)) { toolSelected = TOOL_NONE; ui.clearToolIcon(); } - wasGripClicked = isGripClicked; } @@ -779,6 +775,7 @@ doUpdateState; intersection = getIntersection(); + isGripClicked = hand.gripClicked(); // State update. switch (editorState) { @@ -872,14 +869,14 @@ } break; case EDITOR_GRABBING: - if (hand.valid() && hand.triggerClicked() && !hand.gripClicked()) { + if (hand.valid() && hand.triggerClicked() && !isGripClicked) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // No transition. if (toolSelected === TOOL_SCALE !== wasScaleTool) { updateState(); wasScaleTool = toolSelected === TOOL_SCALE; } - wasGripClicked = false; + //updateTool(); Don't updateTool() because grip button is used to delete grabbed entity. break; } if (!hand.valid()) { @@ -891,7 +888,7 @@ } else { setState(EDITOR_SEARCHING); } - } else if (hand.gripClicked()) { + } else if (isGripClicked) { if (!wasGripClicked) { selection.deleteEntities(); setState(EDITOR_SEARCHING); @@ -908,6 +905,7 @@ // scaling with two hands. // No transition. updateState(); + // updateTool(); Don't updateTool() because this hand is currently using the scaling tool. break; } if (!hand.valid()) { @@ -931,6 +929,7 @@ // scaling with two hands. // No transition. updateState(); + updateTool(); break; } if (!hand.valid()) { @@ -964,6 +963,8 @@ break; } + wasGripClicked = isGripClicked; + if (DEBUG && editorState !== previousState) { debug(side, EDITOR_STATE_STRINGS[editorState]); } From 6d90b6d0fd37a985095b6dee64be402d7b675aa4 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 2 Aug 2017 11:49:10 +1200 Subject: [PATCH 118/722] Simplify trigger click usage --- scripts/vr-edit/vr-edit.js | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 2719236a35..a4454e83ba 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -302,6 +302,8 @@ highlightedEntityID = null, // Root entity of highlighted entity set. wasScaleTool = false, isOtherEditorEditingEntityID = false, + isTriggerClicked = false, + wasTriggerClicked = false, isGripClicked = false, wasGripClicked = false, hoveredOverlayID = null, @@ -775,6 +777,7 @@ doUpdateState; intersection = getIntersection(); + isTriggerClicked = hand.triggerClicked(); isGripClicked = hand.gripClicked(); // State update. @@ -788,7 +791,7 @@ break; case EDITOR_SEARCHING: if (hand.valid() && (!intersection.entityID || !intersection.editableEntity) - && !(intersection.overlayID && hand.triggerClicked() && otherEditor.isHandle(intersection.overlayID))) { + && !(intersection.overlayID && isTriggerClicked && otherEditor.isHandle(intersection.overlayID))) { // No transition. updateState(); updateTool(); @@ -796,14 +799,14 @@ } if (!hand.valid()) { setState(EDITOR_IDLE); - } else if (intersection.overlayID && hand.triggerClicked() + } else if (intersection.overlayID && isTriggerClicked && otherEditor.isHandle(intersection.overlayID)) { highlightedEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); - } else if (intersection.entityID && intersection.editableEntity && !hand.triggerClicked()) { + } else if (intersection.entityID && intersection.editableEntity && !isTriggerClicked) { highlightedEntityID = Entities.rootOf(intersection.entityID); setState(EDITOR_HIGHLIGHTING); - } else if (intersection.entityID && intersection.editableEntity && hand.triggerClicked()) { + } else if (intersection.entityID && intersection.editableEntity && isTriggerClicked) { highlightedEntityID = Entities.rootOf(intersection.entityID); if (otherEditor.isEditing(highlightedEntityID)) { if (toolSelected !== TOOL_SCALE) { @@ -821,9 +824,8 @@ case EDITOR_HIGHLIGHTING: if (hand.valid() && intersection.entityID && intersection.editableEntity - && !(hand.triggerClicked() - && (!otherEditor.isEditing(highlightedEntityID) || toolSelected !== TOOL_SCALE)) - && !(hand.triggerClicked() && intersection.overlayID && otherEditor.isHandle(intersection.overlayID))) { + && !(isTriggerClicked && (!otherEditor.isEditing(highlightedEntityID) || toolSelected !== TOOL_SCALE)) + && !(isTriggerClicked && intersection.overlayID && otherEditor.isHandle(intersection.overlayID))) { // No transition. doUpdateState = false; if (otherEditor.isEditing(highlightedEntityID) !== isOtherEditorEditingEntityID) { @@ -845,11 +847,11 @@ } if (!hand.valid()) { setState(EDITOR_IDLE); - } else if (intersection.overlayID && hand.triggerClicked() + } else if (intersection.overlayID && isTriggerClicked && otherEditor.isHandle(intersection.overlayID)) { highlightedEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); - } else if (intersection.entityID && intersection.editableEntity && hand.triggerClicked()) { + } else if (intersection.entityID && intersection.editableEntity && isTriggerClicked) { highlightedEntityID = Entities.rootOf(intersection.entityID); // May be a different entityID. if (otherEditor.isEditing(highlightedEntityID)) { if (toolSelected !== TOOL_SCALE) { @@ -869,7 +871,7 @@ } break; case EDITOR_GRABBING: - if (hand.valid() && hand.triggerClicked() && !isGripClicked) { + if (hand.valid() && isTriggerClicked && !isGripClicked) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // No transition. if (toolSelected === TOOL_SCALE !== wasScaleTool) { @@ -881,7 +883,7 @@ } if (!hand.valid()) { setState(EDITOR_IDLE); - } else if (!hand.triggerClicked()) { + } else if (!isTriggerClicked) { if (intersection.entityID && intersection.editableEntity) { highlightedEntityID = Entities.rootOf(intersection.entityID); setState(EDITOR_HIGHLIGHTING); @@ -898,7 +900,7 @@ } break; case EDITOR_DIRECT_SCALING: - if (hand.valid() && hand.triggerClicked() + if (hand.valid() && isTriggerClicked && (otherEditor.isEditing(highlightedEntityID) || otherEditor.isHandle(intersection.overlayID))) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // Don't test toolSelected === TOOL_SCALE because this is a UI element and so not able to be changed while @@ -910,7 +912,7 @@ } if (!hand.valid()) { setState(EDITOR_IDLE); - } else if (!hand.triggerClicked()) { + } else if (!isTriggerClicked) { if (!intersection.entityID || !intersection.editableEntity) { setState(EDITOR_SEARCHING); } else { @@ -923,7 +925,7 @@ } break; case EDITOR_HANDLE_SCALING: - if (hand.valid() && hand.triggerClicked() && otherEditor.isEditing(highlightedEntityID)) { + if (hand.valid() && isTriggerClicked && otherEditor.isEditing(highlightedEntityID)) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // Don't test toolSelected === TOOL_SCALE because this is a UI element and so not able to be changed while // scaling with two hands. @@ -934,7 +936,7 @@ } if (!hand.valid()) { setState(EDITOR_IDLE); - } else if (!hand.triggerClicked()) { + } else if (!isTriggerClicked) { if (!intersection.entityID || !intersection.editableEntity) { setState(EDITOR_SEARCHING); } else { @@ -948,11 +950,11 @@ break; case EDITOR_CLONING: // Immediate transition out of state after cloning entities during state entry. - if (hand.valid() && hand.triggerClicked()) { + if (hand.valid() && isTriggerClicked) { setState(EDITOR_GRABBING); } else if (!hand.valid()) { setState(EDITOR_IDLE); - } else if (!hand.triggerClicked()) { + } else if (!isTriggerClicked) { if (intersection.entityID && intersection.editableEntity) { highlightedEntityID = Entities.rootOf(intersection.entityID); setState(EDITOR_HIGHLIGHTING); @@ -963,6 +965,7 @@ break; } + wasTriggerClicked = isTriggerClicked; wasGripClicked = isGripClicked; if (DEBUG && editorState !== previousState) { From 7e1584a43efc4f8714bfa9cb237b387e08e85d50 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 2 Aug 2017 12:27:02 +1200 Subject: [PATCH 119/722] Add grouping state --- scripts/vr-edit/vr-edit.js | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index a4454e83ba..21a74a146c 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -293,8 +293,9 @@ EDITOR_DIRECT_SCALING = 4, // Scaling data are sent to other editor's EDITOR_GRABBING state. EDITOR_HANDLE_SCALING = 5, // "" EDITOR_CLONING = 6, + EDITOR_GROUPING = 7, EDITOR_STATE_STRINGS = ["EDITOR_IDLE", "EDITOR_SEARCHING", "EDITOR_HIGHLIGHTING", "EDITOR_GRABBING", - "EDITOR_DIRECT_SCALING", "EDITOR_HANDLE_SCALING", "EDITOR_CLONING"], + "EDITOR_DIRECT_SCALING", "EDITOR_HANDLE_SCALING", "EDITOR_CLONING", "EDITOR_GROUPING"], editorState = EDITOR_IDLE, // State machine. @@ -711,6 +712,14 @@ // Nothing to do. } + function enterEditorGrouping() { + // TODO: Add/remove highlightedEntityID to/from groups. + } + + function exitEditorGrouping() { + // Nothing to do. + } + STATE_MACHINE = { EDITOR_IDLE: { enter: enterEditorIdle, @@ -746,6 +755,11 @@ enter: enterEditorCloning, update: null, exit: exitEditorCloning + }, + EDITOR_GROUPING: { + enter: enterEditorGrouping, + update: null, + exit: exitEditorGrouping } }; @@ -814,6 +828,8 @@ } } else if (toolSelected === TOOL_CLONE) { setState(EDITOR_CLONING); + } else if (toolSelected === TOOL_GROUP) { + setState(EDITOR_GROUPING); } else { setState(EDITOR_GRABBING); } @@ -861,6 +877,8 @@ } } else if (toolSelected === TOOL_CLONE) { setState(EDITOR_CLONING); + } else if (toolSelected === TOOL_GROUP) { + setState(EDITOR_GROUPING); } else { setState(EDITOR_GRABBING); } @@ -922,6 +940,8 @@ } else if (!otherEditor.isEditing(highlightedEntityID)) { // Grab highlightEntityID that was scaling and has already been set. setState(EDITOR_GRABBING); + } else { + debug(side, "ERROR: Unexpected condition in EDITOR_DIRECT_SCALING!"); } break; case EDITOR_HANDLE_SCALING: @@ -946,6 +966,8 @@ } else if (!otherEditor.isEditing(highlightedEntityID)) { // Grab highlightEntityID that was scaling and has already been set. setState(EDITOR_GRABBING); + } else { + debug(side, "ERROR: Unexpected condition in EDITOR_HANDLE_SCALING!"); } break; case EDITOR_CLONING: @@ -961,6 +983,19 @@ } else { setState(EDITOR_SEARCHING); } + } else { + debug(side, "ERROR: Unexpected condition in EDITOR_CLONING!"); + } + break; + case EDITOR_GROUPING: + if (hand.valid() && isTriggerClicked) { + // No transition. + break; + } + if (!hand.valid()) { + setState(EDITOR_IDLE); + } else { + setState(EDITOR_SEARCHING); } break; } From c686cef4a01928c06f822a95680a3e665e3f0c8d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 2 Aug 2017 13:15:53 +1200 Subject: [PATCH 120/722] Collect grouping data --- scripts/vr-edit/modules/groups.js | 85 ++++++++++++++++++++++++++++ scripts/vr-edit/modules/selection.js | 1 + scripts/vr-edit/vr-edit.js | 71 ++++++++++++++++++++++- 3 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 scripts/vr-edit/modules/groups.js diff --git a/scripts/vr-edit/modules/groups.js b/scripts/vr-edit/modules/groups.js new file mode 100644 index 0000000000..3c6d301b0c --- /dev/null +++ b/scripts/vr-edit/modules/groups.js @@ -0,0 +1,85 @@ +// +// groups.js +// +// Created by David Rowe on 1 Aug 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global Groups */ + +Groups = function () { + // Groups and ungroups trees of entities. + + "use strict"; + + var groupRootEntityIDs = [], + groupSelectionDetails = []; + + if (!this instanceof Groups) { + return new Groups(); + } + + function add(selection) { + groupRootEntityIDs.push(selection[0].id); + groupSelectionDetails.push(Object.clone(selection)); + } + + function remove(selection) { + var index = groupRootEntityIDs.indexOf(selection[0].id); + + groupRootEntityIDs.splice(index, 1); + groupSelectionDetails.splice(index, 1); + } + + function toggle(selection) { + if (selection.length === 0) { + return; + } + + if (groupRootEntityIDs.indexOf(selection[0].id) === -1) { + add(selection); + } else { + remove(selection); + } + } + + function getGroups() { + return groupSelectionDetails; + } + + function count() { + return groupSelectionDetails.length; + } + + function group() { + // TODO + } + + function ungroup() { + // TODO + } + + function clear() { + groupRootEntityIDs = []; + groupSelectionDetails = []; + } + + function destroy() { + clear(); + } + + return { + toggle: toggle, + groups: getGroups, + count: count, + group: group, + ungroup: ungroup, + clear: clear, + destroy: destroy + }; +}; + +Groups.prototype = {}; diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index dc4d05a374..89e5221f81 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -31,6 +31,7 @@ Selection = function (side) { function traverseEntityTree(id, result) { // Recursively traverses tree of entities and their children, gather IDs and properties. + // The root entity is always the first entry. var children, properties, SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions", "parentID", "localPosition", diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 21a74a146c..d456a63087 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -37,9 +37,12 @@ editors = [], LEFT_HAND = 0, RIGHT_HAND = 1, + Grouping, + grouping, // Modules CreatePalette, + Groups, Hand, Handles, Highlights, @@ -61,6 +64,7 @@ // Modules Script.include("./modules/createPalette.js"); + Script.include("./modules/groups.js"); Script.include("./modules/hand.js"); Script.include("./modules/handles.js"); Script.include("./modules/highlights.js"); @@ -713,11 +717,12 @@ } function enterEditorGrouping() { - // TODO: Add/remove highlightedEntityID to/from groups. + highlights.display(intersection.handIntersected, selection.selection(), false); + grouping.toggle(selection.selection()); } function exitEditorGrouping() { - // Nothing to do. + highlights.clear(); } STATE_MACHINE = { @@ -1068,6 +1073,57 @@ }; + Grouping = function () { + // Grouping highlights and functions. + + var groups; + + if (!this instanceof Grouping) { + return new Grouping(); + } + + groups = new Groups(); + + function toggle(selection) { + groups.toggle(selection); + } + + function count() { + return groups.count(); + } + + function group() { + groups.group(); + } + + function ungroup() { + groups.ungroup(); + } + + function update(leftRootEntityID, rightRootEntityID) { + // TODO: Update grouping highlights. + } + + function clear() { + groups.clear(); + } + + function destroy() { + groups.destroy(); + } + + return { + toggle: toggle, + count: count, + group: group, + ungroup: ungroup, + update: update, + clear: clear, + destroy: destroy + }; + }; + + function update() { // Main update loop. updateTimer = null; @@ -1085,6 +1141,9 @@ editors[LEFT_HAND].apply(); editors[RIGHT_HAND].apply(); + // Grouping display. + grouping.update(editors[LEFT_HAND].rootEntityID(), editors[RIGHT_HAND].rootEntityID()); + updateTimer = Script.setTimeout(update, UPDATE_LOOP_TIMEOUT); } @@ -1203,6 +1262,9 @@ editors[LEFT_HAND].setReferences(inputs[LEFT_HAND], editors[RIGHT_HAND]); editors[RIGHT_HAND].setReferences(inputs[RIGHT_HAND], editors[LEFT_HAND]); + // Grouping object. + grouping = new Grouping(); + // Settings changes. MyAvatar.dominantHandChanged.connect(onDominantHandChanged); @@ -1230,6 +1292,11 @@ button = null; } + if (grouping) { + grouping.destroy(); + grouping = null; + } + if (editors[LEFT_HAND]) { editors[LEFT_HAND].destroy(); editors[LEFT_HAND] = null; From 65e57a9262ae8da94753996a2b5b4cd4cda7d606 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 2 Aug 2017 14:38:26 +1200 Subject: [PATCH 121/722] Highlight grouping entities --- scripts/vr-edit/modules/groups.js | 25 +++++++++-- scripts/vr-edit/modules/highlights.js | 13 +++--- scripts/vr-edit/vr-edit.js | 65 ++++++++++++++++++++++++--- 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/scripts/vr-edit/modules/groups.js b/scripts/vr-edit/modules/groups.js index 3c6d301b0c..5491b14b8c 100644 --- a/scripts/vr-edit/modules/groups.js +++ b/scripts/vr-edit/modules/groups.js @@ -46,8 +46,26 @@ Groups = function () { } } - function getGroups() { - return groupSelectionDetails; + function selection(excludes) { + var result = [], + i, + lengthI, + j, + lengthJ; + + for (i = 0, lengthI = groupRootEntityIDs.length; i < lengthI; i += 1) { + if (excludes.indexOf(groupRootEntityIDs[i]) === -1) { + for (j = 0, lengthJ = groupSelectionDetails[i].length; j < lengthJ; j += 1) { + result.push(groupSelectionDetails[i][j]); + } + } + } + + return result; + } + + function includes(rootEntityID) { + return groupRootEntityIDs.indexOf(rootEntityID) !== -1; } function count() { @@ -73,7 +91,8 @@ Groups = function () { return { toggle: toggle, - groups: getGroups, + selection: selection, + includes: includes, count: count, group: group, ungroup: ungroup, diff --git a/scripts/vr-edit/modules/highlights.js b/scripts/vr-edit/modules/highlights.js index ecde8ffa3e..6a0e789a0d 100644 --- a/scripts/vr-edit/modules/highlights.js +++ b/scripts/vr-edit/modules/highlights.js @@ -17,8 +17,9 @@ Highlights = function (side) { var handOverlay, entityOverlays = [], - GRAB_HIGHLIGHT_COLOR = { red: 240, green: 240, blue: 0 }, - SCALE_HIGHLIGHT_COLOR = { red: 0, green: 240, blue: 240 }, + HIGHLIGHT_COLOR = { red: 240, green: 240, blue: 0 }, + SCALE_COLOR = { red: 0, green: 240, blue: 240 }, + GROUP_COLOR = { red: 220, green: 60, blue: 220 }, HAND_HIGHLIGHT_ALPHA = 0.35, ENTITY_HIGHLIGHT_ALPHA = 0.8, HAND_HIGHLIGHT_DIMENSIONS = { x: 0.2, y: 0.2, z: 0.2 }, @@ -70,9 +71,8 @@ Highlights = function (side) { }); } - function display(handIntersected, selection, isScale) { - var overlayColor = isScale ? SCALE_HIGHLIGHT_COLOR : GRAB_HIGHLIGHT_COLOR, - i, + function display(handIntersected, selection, overlayColor) { + var i, length; // Show/hide hand overlay. @@ -114,6 +114,9 @@ Highlights = function (side) { } return { + HIGHLIGHT_COLOR: HIGHLIGHT_COLOR, + SCALE_COLOR: SCALE_COLOR, + GROUP_COLOR: GROUP_COLOR, display: display, clear: clear, destroy: destroy diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index d456a63087..1831031b09 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -612,7 +612,8 @@ selection.select(highlightedEntityID); if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(highlightedEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), - toolSelected === TOOL_SCALE || otherEditor.isEditing(highlightedEntityID)); + toolSelected === TOOL_SCALE || otherEditor.isEditing(highlightedEntityID) + ? highlights.SCALE_CLOR : highlights.HIGHLIGHT_COLOR); } isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID); wasScaleTool = toolSelected === TOOL_SCALE; @@ -622,7 +623,8 @@ selection.select(highlightedEntityID); if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(highlightedEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), - toolSelected === TOOL_SCALE || otherEditor.isEditing(highlightedEntityID)); + toolSelected === TOOL_SCALE || otherEditor.isEditing(highlightedEntityID) + ? highlights.SCALE_CLOR : highlights.HIGHLIGHT_COLOR); } else { highlights.clear(); } @@ -717,7 +719,9 @@ } function enterEditorGrouping() { - highlights.display(intersection.handIntersected, selection.selection(), false); + if (!grouping.includes(highlightedEntityID)) { + highlights.display(false, selection.selection(), highlights.GROUP_COLOR); + } grouping.toggle(selection.selection()); } @@ -1076,16 +1080,34 @@ Grouping = function () { // Grouping highlights and functions. - var groups; + var groups, + highlights, + exludedLeftRootEntityID = null, + exludedrightRootEntityID = null, + excludedRootEntityIDs = [], + hasHighlights = false, + hasSelectionChanged = false; if (!this instanceof Grouping) { return new Grouping(); } groups = new Groups(); + highlights = new Highlights(); function toggle(selection) { groups.toggle(selection); + if (groups.count() === 0) { + hasHighlights = false; + highlights.clear(); + } else { + hasHighlights = true; + hasSelectionChanged = true; + } + } + + function includes(rootEntityID) { + return groups.includes(rootEntityID); } function count() { @@ -1101,7 +1123,30 @@ } function update(leftRootEntityID, rightRootEntityID) { - // TODO: Update grouping highlights. + var hasExludedRootEntitiesChanged; + + hasExludedRootEntitiesChanged = leftRootEntityID !== exludedLeftRootEntityID + || rightRootEntityID !== exludedrightRootEntityID; + + if (!hasHighlights || (!hasSelectionChanged && !hasExludedRootEntitiesChanged)) { + return; + } + + if (hasExludedRootEntitiesChanged) { + excludedRootEntityIDs = []; + if (leftRootEntityID) { + excludedRootEntityIDs.push(leftRootEntityID); + } + if (rightRootEntityID) { + excludedRootEntityIDs.push(rightRootEntityID); + } + exludedLeftRootEntityID = leftRootEntityID; + exludedrightRootEntityID = rightRootEntityID; + } + + highlights.display(false, groups.selection(excludedRootEntityIDs), highlights.GROUP_COLOR); + + hasSelectionChanged = false; } function clear() { @@ -1109,11 +1154,19 @@ } function destroy() { - groups.destroy(); + if (groups) { + groups.destroy(); + groups = null; + } + if (highlights) { + highlights.destroy(); + highlights = null; + } } return { toggle: toggle, + includes: includes, count: count, group: group, ungroup: ungroup, From 26a14d093492e29a58320bec77cb68727f803ee5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 2 Aug 2017 14:41:42 +1200 Subject: [PATCH 122/722] Swap button and tool icon colours to match grouping highlights --- scripts/vr-edit/modules/toolIcon.js | 4 ++-- scripts/vr-edit/modules/toolMenu.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 7aa116af81..076c5997d4 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -21,8 +21,8 @@ ToolIcon = function (side) { ICON_COLORS = [ { red: 0, green: 240, blue: 240 }, - { red: 240, green: 0, blue: 240 }, - { red: 240, green: 240, blue: 0 } + { red: 240, green: 240, blue: 0 }, + { red: 220, green: 60, blue: 220 } ], LEFT_HAND = 0, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 3af6063646..c95ec1a9d8 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -84,12 +84,12 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { }, { // Clone position: { x: 0.06, y: 0.02, z: 0.0 }, - color: { red: 240, green: 0, blue: 240 }, + color: { red: 240, green: 240, blue: 0 }, callback: "clone" }, { // Group position: { x: 0.10, y: 0.02, z: 0.0 }, - color: { red: 240, green: 240, blue: 0 }, + color: { red: 220, green: 60, blue: 220 }, callback: "group" } ], From 53d755d8da7d149fd999cbabbc60fb26842e8f43 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 2 Aug 2017 14:45:19 +1200 Subject: [PATCH 123/722] Clear grouping selection when drop tool --- scripts/vr-edit/vr-edit.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 1831031b09..42a7d85b93 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -790,6 +790,7 @@ function updateTool() { if (!wasGripClicked && isGripClicked && (toolSelected !== TOOL_NONE)) { toolSelected = TOOL_NONE; + grouping.clear(); ui.clearToolIcon(); } } @@ -1151,6 +1152,7 @@ function clear() { groups.clear(); + highlights.clear(); } function destroy() { From fb1284ad615bce9f85a13c4be8cfaf6e2dd7ddf8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 2 Aug 2017 16:12:49 +1200 Subject: [PATCH 124/722] Clear grouping selection when choose another tool --- scripts/vr-edit/vr-edit.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 42a7d85b93..e61ad2311e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1208,6 +1208,10 @@ } function setTool(tool) { + if (toolSelected === TOOL_GROUP) { + grouping.clear(); + } + switch (tool) { case "scale": toolSelected = TOOL_SCALE; From 83f580c514ecd1509f29b8fff7a6ca388cc66875 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 2 Aug 2017 19:07:11 +1200 Subject: [PATCH 125/722] Generalize Tool menu UI --- scripts/vr-edit/modules/toolMenu.js | 238 ++++++++++++++++--------- scripts/vr-edit/utilities/utilities.js | 9 + scripts/vr-edit/vr-edit.js | 14 +- 3 files changed, 165 insertions(+), 96 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index c95ec1a9d8..c82b7426d6 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -10,30 +10,32 @@ /* global ToolMenu */ -ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { +ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { // Tool menu displayed on top of forearm. "use strict"; - var menuOriginOverlay, + var attachmentJointName, + + menuOriginOverlay, menuPanelOverlay, - toolButtonOverlays = [], - buttonHighlightOverlay, + + menuOverlays = [], + menuCallbacks = [], + highlightOverlay, LEFT_HAND = 0, AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), - controlJointName, - - CANVAS_SIZE = { x: 0.21, y: 0.13 }, - PANEL_ROOT_POSITION = { x: CANVAS_SIZE.x / 2, y: 0.15, z: -0.04 }, + CANVAS_SIZE = { x: 0.22, y: 0.13 }, + PANEL_ORIGIN_POSITION = { x: CANVAS_SIZE.x / 2, y: 0.15, z: -0.04 }, PANEL_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }), - lateralOffset, + panelLateralOffset, - PANEL_ORIGIN_PROPERTIES = { + MENU_ORIGIN_PROPERTIES = { dimensions: { x: 0.005, y: 0.005, z: 0.005 }, - localPosition: PANEL_ROOT_POSITION, + localPosition: PANEL_ORIGIN_POSITION, localRotation: PANEL_ROOT_ROTATION, color: { red: 255, blue: 0, green: 0 }, alpha: 1.0, @@ -46,59 +48,99 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { dimensions: { x: CANVAS_SIZE.x, y: CANVAS_SIZE.y, z: 0.01 }, localPosition: { x: CANVAS_SIZE.x / 2, y: CANVAS_SIZE.y / 2, z: 0.005 }, localRotation: ZERO_ROTATION, - color: { red: 192, green: 192, blue: 192 }, + color: { red: 164, green: 164, blue: 164 }, alpha: 1.0, solid: true, ignoreRayIntersection: false, visible: true }, - BUTTON_PROPERTIES = { - dimensions: { x: 0.03, y: 0.03, z: 0.01 }, - localRotation: ZERO_ROTATION, - alpha: 1.0, - solid: true, - ignoreRayIntersection: false, - visible: true + UI_ELEMENTS = { + "panel": { + overlay: "cube", + properties: { + dimensions: { x: 0.10, y: 0.12, z: 0.01 }, + localRotation: ZERO_ROTATION, + color: { red: 192, green: 192, blue: 192 }, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + } + }, + "button": { + overlay: "cube", + properties: { + dimensions: { x: 0.03, y: 0.03, z: 0.01 }, + localRotation: ZERO_ROTATION, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + } + } }, - BUTTON_HIGHLIGHT_PROPERTIES = { - dimensions: { x: 0.034, y: 0.034, z: 0.001 }, - localPosition: { x: 0.02, y: 0.02, z: -0.002 }, - localRotation: ZERO_ROTATION, - color: { red: 240, green: 240, blue: 0 }, - alpha: 0.8, - solid: false, - drawInFront: true, - ignoreRayIntersection: true, - visible: false - }, - - HIGHLIGHT_Z_OFFSET = -0.002, - - TOOL_BUTTONS = [ - { // Scale - position: { x: 0.02, y: 0.02, z: 0.0 }, - color: { red: 0, green: 240, blue: 240 }, - callback: "scale" + MENU_ITEMS = [ + { + // Background element + id: "toolsMenuPanel", + type: "panel", + properties: { + localPosition: { x: -0.055, y: 0.0, z: -0.005 } + } }, - { // Clone - position: { x: 0.06, y: 0.02, z: 0.0 }, - color: { red: 240, green: 240, blue: 0 }, - callback: "clone" + { + id: "scaleButton", + type: "button", + properties: { + localPosition: { x: -0.022, y: -0.04, z: -0.005 }, + color: { red: 0, green: 240, blue: 240 } + }, + callback: "scaleTool" }, - { // Group - position: { x: 0.10, y: 0.02, z: 0.0 }, - color: { red: 220, green: 60, blue: 220 }, - callback: "group" + { + id: "cloneButton", + type: "button", + properties: { + localPosition: { x: 0.022, y: -0.04, z: -0.005 }, + color: { red: 240, green: 240, blue: 0 } + }, + callback: "cloneTool" + }, + { + id: "groupButton", + type: "button", + properties: { + localPosition: { x: -0.022, y: 0.0, z: -0.005 }, + color: { red: 220, green: 60, blue: 220 } + }, + callback: "groupTool" } ], - isDisplaying = false, + HIGHLIGHT_PROPERTIES = { + xDelta: 0.004, + yDelta: 0.004, + zDimension: 0.001, + properties: { + localPosition: { x: 0, y: 0, z: -0.003 }, + localRotation: ZERO_ROTATION, + color: { red: 255, green: 255, blue: 0 }, + alpha: 0.8, + solid: false, + drawInFront: true, + ignoreRayIntersection: true, + visible: false + } + }, + highlightedItem = -1, isHighlightingButton = false, isButtonPressed = false, + isDisplaying = false, + // References. controlHand; @@ -113,54 +155,67 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { // Assumes UI is not displaying. side = hand; controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); - controlJointName = side === LEFT_HAND ? "LeftHand" : "RightHand"; - lateralOffset = side === LEFT_HAND ? -0.01 : 0.01; + attachmentJointName = side === LEFT_HAND ? "LeftHand" : "RightHand"; + panelLateralOffset = side === LEFT_HAND ? -0.01 : 0.01; } setHand(side); function getEntityIDs() { - return [menuPanelOverlay].concat(toolButtonOverlays); + return [menuPanelOverlay].concat(menuOverlays); } function update(intersectionOverlayID) { - var highlightedButton, + var intersectedItem, i, - length; + length, + CLICK_DELTA = 0.004; - // Highlight button. - highlightedButton = toolButtonOverlays.indexOf(intersectionOverlayID); - if ((highlightedButton !== -1) !== isHighlightingButton) { - isHighlightingButton = !isHighlightingButton; - if (isHighlightingButton) { - Overlays.editOverlay(buttonHighlightOverlay, { - localPosition: Vec3.sum({ x: 0, y: 0, z: HIGHLIGHT_Z_OFFSET }, TOOL_BUTTONS[highlightedButton].position), + // Highlight clickable item. + intersectedItem = menuOverlays.indexOf(intersectionOverlayID); + if (intersectedItem !== highlightedItem) { + if (intersectedItem !== -1 && menuCallbacks[intersectedItem] !== undefined) { + Overlays.editOverlay(highlightOverlay, { + parentID: menuOverlays[intersectedItem], + dimensions: { + x: UI_ELEMENTS[MENU_ITEMS[intersectedItem].type].properties.dimensions.x + HIGHLIGHT_PROPERTIES.xDelta, + y: UI_ELEMENTS[MENU_ITEMS[intersectedItem].type].properties.dimensions.y + HIGHLIGHT_PROPERTIES.yDelta, + z: HIGHLIGHT_PROPERTIES.zDimension + }, + localPosition: Vec3.sum(MENU_ITEMS[intersectedItem].properties, + HIGHLIGHT_PROPERTIES.properties.localPosition), + localRotation: HIGHLIGHT_PROPERTIES.properties.localRotation, // Needs to be set again for some reason. + color: HIGHLIGHT_PROPERTIES.properties.color, // "" visible: true }); - + isHighlightingButton = true; } else { - Overlays.editOverlay(buttonHighlightOverlay, { + Overlays.editOverlay(highlightOverlay, { visible: false }); + isHighlightingButton = false; } + highlightedItem = intersectedItem; } - // Button click. if (!isHighlightingButton || controlHand.triggerClicked() !== isButtonPressed) { isButtonPressed = isHighlightingButton && controlHand.triggerClicked(); - for (i = 0, length = toolButtonOverlays.length; i < length; i += 1) { - if (isButtonPressed && intersectionOverlayID === toolButtonOverlays[i]) { - Overlays.editOverlay(toolButtonOverlays[i], { - localPosition: Vec3.sum(TOOL_BUTTONS[i].position, { x: 0, y: 0, z: 0.004 }) - }); - } else { - Overlays.editOverlay(toolButtonOverlays[i], { - localPosition: TOOL_BUTTONS[i].position - }); + for (i = 0, length = menuOverlays.length; i < length; i += 1) { + if (menuCallbacks[intersectedItem] !== undefined) { + if (isButtonPressed && intersectedItem === menuOverlays[i]) { + Overlays.editOverlay(menuOverlays[i], { + localPosition: Vec3.sum(MENU_ITEMS[i].properties.LocalPosition, { x: 0, y: 0, z: CLICK_DELTA }) + }); + } else { + Overlays.editOverlay(menuOverlays[i], { + localPosition: MENU_ITEMS[i].properties.LocalPosition + }); + } + } } if (isButtonPressed) { - setToolCallback(TOOL_BUTTONS[highlightedButton].callback); + commandCallback(MENU_ITEMS[highlightedItem].callback); } } } @@ -169,6 +224,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { // Creates and shows menu entities. var handJointIndex, properties, + parentID, i, length; @@ -177,7 +233,7 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { } // Joint index. - handJointIndex = MyAvatar.getJointIndex(controlJointName); + handJointIndex = MyAvatar.getJointIndex(attachmentJointName); if (handJointIndex === -1) { // Don't display if joint isn't available (yet) to attach to. // User can clear this condition by toggling the app off and back on once avatar finishes loading. @@ -185,28 +241,32 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { return; } - // Calculate position to put menu. - properties = Object.clone(PANEL_ORIGIN_PROPERTIES); + // Menu origin. + properties = Object.clone(MENU_ORIGIN_PROPERTIES); properties.parentJointIndex = handJointIndex; - properties.localPosition = Vec3.sum(PANEL_ROOT_POSITION, { x: lateralOffset, y: 0, z: 0 }); + properties.localPosition = Vec3.sum(properties.localPosition, { x: panelLateralOffset, y: 0, z: 0 }); menuOriginOverlay = Overlays.addOverlay("sphere", properties); - // Create menu items. + // Panel background. properties = Object.clone(MENU_PANEL_PROPERTIES); properties.parentID = menuOriginOverlay; menuPanelOverlay = Overlays.addOverlay("cube", properties); - properties = Object.clone(BUTTON_PROPERTIES); - properties.parentID = menuOriginOverlay; - for (i = 0, length = TOOL_BUTTONS.length; i < length; i += 1) { - properties.localPosition = TOOL_BUTTONS[i].position; - properties.color = TOOL_BUTTONS[i].color; - toolButtonOverlays[i] = Overlays.addOverlay("cube", properties); + + // Menu items. + parentID = menuPanelOverlay; // Menu panel parents to background panel. + for (i = 0, length = MENU_ITEMS.length; i < length; i += 1) { + properties = Object.clone(UI_ELEMENTS[MENU_ITEMS[i].type].properties); + properties = Object.merge(properties, MENU_ITEMS[i].properties); + properties.parentID = parentID; + menuOverlays.push(Overlays.addOverlay(UI_ELEMENTS[MENU_ITEMS[i].type].overlay, properties)); + parentID = menuOverlays[0]; // Menu buttons parent to menu panel. + menuCallbacks.push(MENU_ITEMS[i].callback); } - // Prepare button highlight overlay. - properties = Object.clone(BUTTON_HIGHLIGHT_PROPERTIES); + // Prepare highlight overlay. + properties = Object.clone(HIGHLIGHT_PROPERTIES); properties.parentID = menuOriginOverlay; - buttonHighlightOverlay = Overlays.addOverlay("cube", properties); + highlightOverlay = Overlays.addOverlay("cube", properties); isDisplaying = true; } @@ -220,9 +280,9 @@ ToolMenu = function (side, leftInputs, rightInputs, setToolCallback) { return; } - Overlays.deleteOverlay(buttonHighlightOverlay); - for (i = 0, length = toolButtonOverlays.length; i < length; i += 1) { - Overlays.deleteOverlay(toolButtonOverlays[i]); + Overlays.deleteOverlay(highlightOverlay); + for (i = 0, length = menuOverlays.length; i < length; i += 1) { + Overlays.deleteOverlay(menuOverlays[i]); } Overlays.deleteOverlay(menuPanelOverlay); Overlays.deleteOverlay(menuOriginOverlay); diff --git a/scripts/vr-edit/utilities/utilities.js b/scripts/vr-edit/utilities/utilities.js index d737d820ea..cbf8206750 100644 --- a/scripts/vr-edit/utilities/utilities.js +++ b/scripts/vr-edit/utilities/utilities.js @@ -55,3 +55,12 @@ if (typeof Object.clone !== "function") { return JSON.parse(JSON.stringify(object)); }; } + +if (typeof Object.merge !== "function") { + Object.merge = function (objectA, objectB) { + var a = JSON.stringify(objectA), + b = JSON.stringify(objectB); + return JSON.parse(a.slice(0, -1) + "," + b.slice(1)); + }; +} + diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index e61ad2311e..4fd28f2ce9 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1207,26 +1207,26 @@ Settings.setValue(VR_EDIT_SETTING, isAppActive); } - function setTool(tool) { + function onUICommand(command) { if (toolSelected === TOOL_GROUP) { grouping.clear(); } - switch (tool) { - case "scale": + switch (command) { + case "scaleTool": toolSelected = TOOL_SCALE; ui.setToolIcon(ui.SCALE_TOOL); break; - case "clone": + case "cloneTool": toolSelected = TOOL_CLONE; ui.setToolIcon(ui.CLONE_TOOL); break; - case "group": + case "groupTool": toolSelected = TOOL_GROUP; ui.setToolIcon(ui.GROUP_TOOL); break; default: - debug("ERROR: Unexpected condition in setTool()!"); + debug("ERROR: Unexpected command in onUICommand()!"); } } @@ -1313,7 +1313,7 @@ inputs[RIGHT_HAND] = new Inputs(RIGHT_HAND); // UI object. - ui = new UI(otherHand(dominantHand), inputs[LEFT_HAND], inputs[RIGHT_HAND], setTool); + ui = new UI(otherHand(dominantHand), inputs[LEFT_HAND], inputs[RIGHT_HAND], onUICommand); // Editor objects. editors[LEFT_HAND] = new Editor(LEFT_HAND); From 25bbdba9878456c7577b3e80448509c0a9354fa7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 2 Aug 2017 22:17:02 +1200 Subject: [PATCH 126/722] Add Tool menu options panels, in particular grouping buttons --- scripts/vr-edit/modules/toolMenu.js | 108 ++++++++++++++++++++++++---- scripts/vr-edit/vr-edit.js | 24 +++++-- 2 files changed, 113 insertions(+), 19 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index c82b7426d6..3dc18bcd60 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -22,6 +22,8 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { menuOverlays = [], menuCallbacks = [], + optionsOverlays = [], + optionsCallbacks = [], highlightOverlay, LEFT_HAND = 0, @@ -81,6 +83,39 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { } }, + OPTON_PANELS = { + groupOptions: [ + { + // Background element + id: "toolsOptionsPanel", + type: "panel", + properties: { + localPosition: { x: 0.055, y: 0.0, z: -0.005 } + } + }, + { + id: "groupButton", + type: "button", + properties: { + dimensions: { x: 0.07, y: 0.03, z: 0.01 }, + localPosition: { x: 0, y: -0.025, z: -0.005 }, + color: { red: 64, green: 240, blue: 64 } + }, + callback: "groupButton" + }, + { + id: "ungroupButton", + type: "button", + properties: { + dimensions: { x: 0.07, y: 0.03, z: 0.01 }, + localPosition: { x: 0, y: 0.025, z: -0.005 }, + color: { red: 240, green: 64, blue: 64 } + }, + callback: "ungroupButton" + } + ] + }, + MENU_ITEMS = [ { // Background element @@ -115,6 +150,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { localPosition: { x: -0.022, y: 0.0, z: -0.005 }, color: { red: 220, green: 60, blue: 220 } }, + toolOptions: "groupOptions", callback: "groupTool" } ], @@ -136,6 +172,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { }, highlightedItem = -1, + highlightedSource = null, isHighlightingButton = false, isButtonPressed = false, @@ -162,30 +199,74 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { setHand(side); function getEntityIDs() { - return [menuPanelOverlay].concat(menuOverlays); + return [menuPanelOverlay].concat(menuOverlays).concat(optionsOverlays); + } + + function openOptions(toolOptions) { + var options, + properties, + parentID, + i, + length; + + // Close current panel, if any. + for (i = 0, length = optionsOverlays.length; i < length; i += 1) { + Overlays.deleteOverlay(optionsOverlays[i]); + optionsOverlays = []; + optionsCallbacks = []; + } + + // Open specified panel, if any. + if (toolOptions) { + options = OPTON_PANELS[toolOptions]; + parentID = menuPanelOverlay; // Menu panel parents to background panel. + for (i = 0, length = options.length; i < length; i += 1) { + properties = Object.clone(UI_ELEMENTS[options[i].type].properties); + properties = Object.merge(properties, options[i].properties); + properties.parentID = parentID; + optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[options[i].type].overlay, properties)); + parentID = optionsOverlays[0]; // Menu buttons parent to menu panel. + optionsCallbacks.push(options[i].callback); + } + } } function update(intersectionOverlayID) { var intersectedItem, + intersectionOverlays, + intersectionCallbacks, + parentProperties, i, length, CLICK_DELTA = 0.004; // Highlight clickable item. intersectedItem = menuOverlays.indexOf(intersectionOverlayID); - if (intersectedItem !== highlightedItem) { - if (intersectedItem !== -1 && menuCallbacks[intersectedItem] !== undefined) { + if (intersectedItem !== -1) { + intersectionOverlays = menuOverlays; + intersectionCallbacks = menuCallbacks; + } else { + intersectedItem = optionsOverlays.indexOf(intersectionOverlayID); + if (intersectedItem !== -1) { + intersectionOverlays = optionsOverlays; + intersectionCallbacks = optionsCallbacks; + } + } + + if (intersectedItem !== highlightedItem || intersectionOverlays !== highlightedSource) { + if (intersectedItem !== -1 && intersectionCallbacks[intersectedItem] !== undefined) { + parentProperties = Overlays.getProperties(intersectionOverlays[intersectedItem], + ["dimensions", "localPosition"]); Overlays.editOverlay(highlightOverlay, { - parentID: menuOverlays[intersectedItem], + parentID: intersectionOverlays[intersectedItem], dimensions: { - x: UI_ELEMENTS[MENU_ITEMS[intersectedItem].type].properties.dimensions.x + HIGHLIGHT_PROPERTIES.xDelta, - y: UI_ELEMENTS[MENU_ITEMS[intersectedItem].type].properties.dimensions.y + HIGHLIGHT_PROPERTIES.yDelta, + x: parentProperties.dimensions.x + HIGHLIGHT_PROPERTIES.xDelta, + y: parentProperties.dimensions.y + HIGHLIGHT_PROPERTIES.yDelta, z: HIGHLIGHT_PROPERTIES.zDimension }, - localPosition: Vec3.sum(MENU_ITEMS[intersectedItem].properties, - HIGHLIGHT_PROPERTIES.properties.localPosition), - localRotation: HIGHLIGHT_PROPERTIES.properties.localRotation, // Needs to be set again for some reason. - color: HIGHLIGHT_PROPERTIES.properties.color, // "" + localPosition: HIGHLIGHT_PROPERTIES.properties.localPosition, + localRotation: HIGHLIGHT_PROPERTIES.properties.localRotation, + color: HIGHLIGHT_PROPERTIES.properties.color, visible: true }); isHighlightingButton = true; @@ -196,6 +277,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { isHighlightingButton = false; } highlightedItem = intersectedItem; + highlightedSource = intersectionOverlays; } if (!isHighlightingButton || controlHand.triggerClicked() !== isButtonPressed) { @@ -211,11 +293,13 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { localPosition: MENU_ITEMS[i].properties.LocalPosition }); } - } } if (isButtonPressed) { - commandCallback(MENU_ITEMS[highlightedItem].callback); + if (intersectionOverlays === menuOverlays) { + openOptions(MENU_ITEMS[highlightedItem].toolOptions); + } + commandCallback(intersectionCallbacks[highlightedItem]); } } } diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 4fd28f2ce9..9cde84503c 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -214,16 +214,16 @@ toolIcon.clear(); } - function display() { - var uiEntityIDs; - - toolMenu.display(); - createPalette.display(); - - uiEntityIDs = [].concat(toolMenu.entityIDs(), createPalette.entityIDs()); + function setUIEntities() { + var uiEntityIDs = [].concat(toolMenu.entityIDs(), createPalette.entityIDs()); leftInputs.setUIEntities(side === RIGHT_HAND ? uiEntityIDs : []); rightInputs.setUIEntities(side === LEFT_HAND ? uiEntityIDs : []); + } + function display() { + toolMenu.display(); + createPalette.display(); + setUIEntities(); isDisplaying = true; } @@ -268,6 +268,7 @@ CLONE_TOOL: toolIcon.CLONE_TOOL, GROUP_TOOL: toolIcon.GROUP_TOOL, display: display, + updateUIEntities: setUIEntities, update: update, clear: clear, destroy: destroy @@ -1216,14 +1217,23 @@ case "scaleTool": toolSelected = TOOL_SCALE; ui.setToolIcon(ui.SCALE_TOOL); + ui.updateUIEntities(); break; case "cloneTool": toolSelected = TOOL_CLONE; ui.setToolIcon(ui.CLONE_TOOL); + ui.updateUIEntities(); break; case "groupTool": toolSelected = TOOL_GROUP; ui.setToolIcon(ui.GROUP_TOOL); + ui.updateUIEntities(); + break; + case "groupButton": + grouping.group(); + break; + case "ungroupButton": + grouping.ungroup(); break; default: debug("ERROR: Unexpected command in onUICommand()!"); From 37c1060080a9275d9a5f5f25a60c2d15daa1fba0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 3 Aug 2017 11:06:51 +1200 Subject: [PATCH 127/722] Fix button highlighting and clicking --- scripts/vr-edit/modules/toolMenu.js | 102 +++++++++++++++++----------- 1 file changed, 61 insertions(+), 41 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 3dc18bcd60..887e7b40da 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -24,6 +24,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { menuCallbacks = [], optionsOverlays = [], optionsCallbacks = [], + optionsItems, highlightOverlay, LEFT_HAND = 0, @@ -83,7 +84,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { } }, - OPTON_PANELS = { + OPTONS_PANELS = { groupOptions: [ { // Background element @@ -171,9 +172,16 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { } }, - highlightedItem = -1, + NONE = -1, + + intersectionOverlays, + intersectionCallbacks, + intersectionProperties, + highlightedItem = NONE, highlightedSource = null, isHighlightingButton = false, + pressedItem = NONE, + pressedSource = null, isButtonPressed = false, isDisplaying = false, @@ -203,8 +211,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { } function openOptions(toolOptions) { - var options, - properties, + var properties, parentID, i, length; @@ -218,43 +225,49 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { // Open specified panel, if any. if (toolOptions) { - options = OPTON_PANELS[toolOptions]; + optionsItems = OPTONS_PANELS[toolOptions]; parentID = menuPanelOverlay; // Menu panel parents to background panel. - for (i = 0, length = options.length; i < length; i += 1) { - properties = Object.clone(UI_ELEMENTS[options[i].type].properties); - properties = Object.merge(properties, options[i].properties); + for (i = 0, length = optionsItems.length; i < length; i += 1) { + properties = Object.clone(UI_ELEMENTS[optionsItems[i].type].properties); + properties = Object.merge(properties, optionsItems[i].properties); properties.parentID = parentID; - optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[options[i].type].overlay, properties)); + optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[optionsItems[i].type].overlay, properties)); parentID = optionsOverlays[0]; // Menu buttons parent to menu panel. - optionsCallbacks.push(options[i].callback); + optionsCallbacks.push(optionsItems[i].callback); } } } + function update(intersectionOverlayID) { var intersectedItem, - intersectionOverlays, - intersectionCallbacks, parentProperties, - i, - length, - CLICK_DELTA = 0.004; + BUTTON_PRESS_DELTA = 0.004; - // Highlight clickable item. - intersectedItem = menuOverlays.indexOf(intersectionOverlayID); - if (intersectedItem !== -1) { - intersectionOverlays = menuOverlays; - intersectionCallbacks = menuCallbacks; - } else { - intersectedItem = optionsOverlays.indexOf(intersectionOverlayID); + // Intersection details. + if (intersectionOverlayID) { + intersectedItem = menuOverlays.indexOf(intersectionOverlayID); if (intersectedItem !== -1) { - intersectionOverlays = optionsOverlays; - intersectionCallbacks = optionsCallbacks; + intersectionOverlays = menuOverlays; + intersectionCallbacks = menuCallbacks; + intersectionProperties = MENU_ITEMS; + } else { + intersectedItem = optionsOverlays.indexOf(intersectionOverlayID); + if (intersectedItem !== -1) { + intersectionOverlays = optionsOverlays; + intersectionCallbacks = optionsCallbacks; + intersectionProperties = optionsItems; + } } } + if (!intersectionOverlays) { + return; + } + // Highlight clickable item. if (intersectedItem !== highlightedItem || intersectionOverlays !== highlightedSource) { if (intersectedItem !== -1 && intersectionCallbacks[intersectedItem] !== undefined) { + // Highlight new button. parentProperties = Overlays.getProperties(intersectionOverlays[intersectedItem], ["dimensions", "localPosition"]); Overlays.editOverlay(highlightOverlay, { @@ -269,35 +282,42 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { color: HIGHLIGHT_PROPERTIES.properties.color, visible: true }); + highlightedItem = intersectedItem; isHighlightingButton = true; - } else { + } else if (highlightedItem !== NONE) { + // Un-highlight previous button. Overlays.editOverlay(highlightOverlay, { visible: false }); + highlightedItem = NONE; isHighlightingButton = false; } - highlightedItem = intersectedItem; highlightedSource = intersectionOverlays; } - if (!isHighlightingButton || controlHand.triggerClicked() !== isButtonPressed) { - isButtonPressed = isHighlightingButton && controlHand.triggerClicked(); - for (i = 0, length = menuOverlays.length; i < length; i += 1) { - if (menuCallbacks[intersectedItem] !== undefined) { - if (isButtonPressed && intersectedItem === menuOverlays[i]) { - Overlays.editOverlay(menuOverlays[i], { - localPosition: Vec3.sum(MENU_ITEMS[i].properties.LocalPosition, { x: 0, y: 0, z: CLICK_DELTA }) - }); - } else { - Overlays.editOverlay(menuOverlays[i], { - localPosition: MENU_ITEMS[i].properties.LocalPosition - }); - } - } + // Press button. + if (intersectedItem !== pressedItem || intersectionOverlays !== pressedSource + || controlHand.triggerClicked() !== isButtonPressed) { + if (pressedItem !== NONE) { + // Unpress previous button. + Overlays.editOverlay(intersectionOverlays[pressedItem], { + localPosition: intersectionProperties[pressedItem].properties.localPosition + }); + pressedItem = NONE; } + isButtonPressed = isHighlightingButton && controlHand.triggerClicked(); if (isButtonPressed) { + // Press new button. + Overlays.editOverlay(intersectionOverlays[intersectedItem], { + localPosition: Vec3.sum(intersectionProperties[intersectedItem].properties.localPosition, + { x: 0, y: 0, z: BUTTON_PRESS_DELTA }) + }); + pressedItem = intersectedItem; + pressedSource = intersectionOverlays; + + // Button press actions. if (intersectionOverlays === menuOverlays) { - openOptions(MENU_ITEMS[highlightedItem].toolOptions); + openOptions(intersectionProperties[highlightedItem].toolOptions); } commandCallback(intersectionCallbacks[highlightedItem]); } From c4eac1660c378ff5574e1079bfb0f40e5c6d0f46 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 3 Aug 2017 11:46:05 +1200 Subject: [PATCH 128/722] Fix app toggling and hand swapping issues --- scripts/vr-edit/modules/toolMenu.js | 39 ++++++++++++++++++---- scripts/vr-edit/vr-edit.js | 51 +++++++++++++---------------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 887e7b40da..266abf1596 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -44,7 +44,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { alpha: 1.0, parentID: AVATAR_SELF_ID, ignoreRayIntersection: true, - visible: true + visible: false }, MENU_PANEL_PROPERTIES = { @@ -177,12 +177,12 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { intersectionOverlays, intersectionCallbacks, intersectionProperties, - highlightedItem = NONE, - highlightedSource = null, - isHighlightingButton = false, - pressedItem = NONE, - pressedSource = null, - isButtonPressed = false, + highlightedItem, + highlightedSource, + isHighlightingButton, + pressedItem, + pressedSource, + isButtonPressed, isDisplaying = false, @@ -238,6 +238,9 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { } } + function clearTool() { + openOptions(); + } function update(intersectionOverlayID) { var intersectedItem, @@ -372,6 +375,17 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { properties.parentID = menuOriginOverlay; highlightOverlay = Overlays.addOverlay("cube", properties); + // Initial values. + intersectionOverlays = null; + intersectionCallbacks = null; + intersectionProperties = null; + highlightedItem = NONE; + highlightedSource = null; + isHighlightingButton = false; + pressedItem = NONE; + pressedSource = null; + isButtonPressed = false; + isDisplaying = true; } @@ -385,11 +399,21 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { } Overlays.deleteOverlay(highlightOverlay); + for (i = 0, length = optionsOverlays.length; i < length; i += 1) { + Overlays.deleteOverlay(optionsOverlays[i]); + } + optionsOverlays = []; + optionsCallbacks = []; + for (i = 0, length = menuOverlays.length; i < length; i += 1) { Overlays.deleteOverlay(menuOverlays[i]); } + menuOverlays = []; + menuCallbacks = []; + Overlays.deleteOverlay(menuPanelOverlay); Overlays.deleteOverlay(menuOriginOverlay); + isDisplaying = false; } @@ -400,6 +424,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { return { setHand: setHand, entityIDs: getEntityIDs, + clearTool: clearTool, update: update, display: display, clear: clear, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 9cde84503c..c810d7f712 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -212,6 +212,7 @@ function clearToolIcon() { toolIcon.clear(); + toolMenu.clearTool(); } function setUIEntities() { @@ -1240,6 +1241,23 @@ } } + function startApp() { + ui.display(); + update(); + } + + function stopApp() { + Script.clearTimeout(updateTimer); + updateTimer = null; + inputs[LEFT_HAND].clear(); + inputs[RIGHT_HAND].clear(); + ui.clear(); + grouping.clear(); + editors[LEFT_HAND].clear(); + editors[RIGHT_HAND].clear(); + toolSelected = TOOL_NONE; + } + function onAppButtonClicked() { // Application tablet/toolbar button clicked. isAppActive = !isAppActive; @@ -1247,16 +1265,9 @@ button.editProperties({ isActive: isAppActive }); if (isAppActive) { - ui.display(); - update(); + startApp(); } else { - Script.clearTimeout(updateTimer); - updateTimer = null; - inputs[LEFT_HAND].clear(); - inputs[RIGHT_HAND].clear(); - ui.clear(); - editors[LEFT_HAND].clear(); - editors[RIGHT_HAND].clear(); + stopApp(); } } @@ -1265,33 +1276,15 @@ if (isAppActive) { // Stop operations. - Script.clearTimeout(updateTimer); - updateTimer = null; - inputs[LEFT_HAND].clear(); - inputs[RIGHT_HAND].clear(); - ui.clear(); - editors[LEFT_HAND].clear(); - editors[RIGHT_HAND].clear(); + stopApp(); } // Swap UI hands. ui.setHand(otherHand(dominantHand)); - switch (toolSelected) { - case TOOL_SCALE: - ui.setToolIcon(ui.SCALE_TOOL); - break; - case TOOL_CLONE: - ui.setToolIcon(ui.CLONE_TOOL); - break; - case TOOL_GROUP: - ui.setToolIcon(ui.GROUP_TOOL); - break; - } if (isAppActive) { // Resume operations. - ui.display(); - update(); + startApp(); } } From 1b866fdf9e617c6e9da7a5b2cd529d7e20b64c2e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 3 Aug 2017 13:20:14 +1200 Subject: [PATCH 129/722] Enable/disable grouping buttons depending on current group selection --- scripts/vr-edit/modules/groups.js | 14 ++++-- scripts/vr-edit/modules/toolMenu.js | 67 +++++++++++++++++++++++++---- scripts/vr-edit/vr-edit.js | 15 ++++--- 3 files changed, 80 insertions(+), 16 deletions(-) diff --git a/scripts/vr-edit/modules/groups.js b/scripts/vr-edit/modules/groups.js index 5491b14b8c..c62615f25e 100644 --- a/scripts/vr-edit/modules/groups.js +++ b/scripts/vr-edit/modules/groups.js @@ -16,7 +16,8 @@ Groups = function () { "use strict"; var groupRootEntityIDs = [], - groupSelectionDetails = []; + groupSelectionDetails = [], + numberOfEntitiesSelected = 0; if (!this instanceof Groups) { return new Groups(); @@ -25,11 +26,13 @@ Groups = function () { function add(selection) { groupRootEntityIDs.push(selection[0].id); groupSelectionDetails.push(Object.clone(selection)); + numberOfEntitiesSelected += selection.length; } function remove(selection) { var index = groupRootEntityIDs.indexOf(selection[0].id); + numberOfEntitiesSelected -= groupSelectionDetails[index].length; groupRootEntityIDs.splice(index, 1); groupSelectionDetails.splice(index, 1); } @@ -68,10 +71,14 @@ Groups = function () { return groupRootEntityIDs.indexOf(rootEntityID) !== -1; } - function count() { + function groupsCount() { return groupSelectionDetails.length; } + function entitiesCount() { + return numberOfEntitiesSelected; + } + function group() { // TODO } @@ -93,7 +100,8 @@ Groups = function () { toggle: toggle, selection: selection, includes: includes, - count: count, + groupsCount: groupsCount, + entitiesCount: entitiesCount, group: group, ungroup: ungroup, clear: clear, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 266abf1596..168cde8254 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -24,7 +24,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { menuCallbacks = [], optionsOverlays = [], optionsCallbacks = [], - optionsItems, + optionsEnabled = [], highlightOverlay, LEFT_HAND = 0, @@ -100,8 +100,9 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { properties: { dimensions: { x: 0.07, y: 0.03, z: 0.01 }, localPosition: { x: 0, y: -0.025, z: -0.005 }, - color: { red: 64, green: 240, blue: 64 } + color: { red: 200, green: 200, blue: 200 } }, + enabledColor: { red: 64, green: 240, blue: 64 }, callback: "groupButton" }, { @@ -110,13 +111,17 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { properties: { dimensions: { x: 0.07, y: 0.03, z: 0.01 }, localPosition: { x: 0, y: 0.025, z: -0.005 }, - color: { red: 240, green: 64, blue: 64 } + color: { red: 200, green: 200, blue: 200 } }, + enabledColor: { red: 240, green: 64, blue: 64 }, callback: "ungroupButton" } ] }, + GROUP_BUTTON_INDEX = 1, + UNGROUP_BUTTON_INDEX = 2, + MENU_ITEMS = [ { // Background element @@ -174,8 +179,10 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { NONE = -1, + optionsItems, intersectionOverlays, intersectionCallbacks, + intersectionCallbacksEnabled, intersectionProperties, highlightedItem, highlightedSource, @@ -183,6 +190,8 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { pressedItem, pressedSource, isButtonPressed, + isGroupButtonEnabled, + isUngroupButtonEnabled, isDisplaying = false, @@ -221,6 +230,8 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { Overlays.deleteOverlay(optionsOverlays[i]); optionsOverlays = []; optionsCallbacks = []; + optionsEnabled = []; + optionsItems = null; } // Open specified panel, if any. @@ -234,18 +245,27 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[optionsItems[i].type].overlay, properties)); parentID = optionsOverlays[0]; // Menu buttons parent to menu panel. optionsCallbacks.push(optionsItems[i].callback); + optionsEnabled.push(true); } } + + // Special handling for Group options. + if (toolOptions === "groupOptions") { + optionsEnabled[GROUP_BUTTON_INDEX] = false; + optionsEnabled[UNGROUP_BUTTON_INDEX] = false; + } } function clearTool() { openOptions(); } - function update(intersectionOverlayID) { + function update(intersectionOverlayID, groupsCount, entitiesCount) { var intersectedItem, parentProperties, - BUTTON_PRESS_DELTA = 0.004; + BUTTON_PRESS_DELTA = 0.004, + enableGroupButton, + enableUngroupButton; // Intersection details. if (intersectionOverlayID) { @@ -253,12 +273,14 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { if (intersectedItem !== -1) { intersectionOverlays = menuOverlays; intersectionCallbacks = menuCallbacks; + intersectionCallbacksEnabled = null; intersectionProperties = MENU_ITEMS; } else { intersectedItem = optionsOverlays.indexOf(intersectionOverlayID); if (intersectedItem !== -1) { intersectionOverlays = optionsOverlays; intersectionCallbacks = optionsCallbacks; + intersectionCallbacksEnabled = optionsEnabled; intersectionProperties = optionsItems; } } @@ -309,7 +331,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { pressedItem = NONE; } isButtonPressed = isHighlightingButton && controlHand.triggerClicked(); - if (isButtonPressed) { + if (isButtonPressed && (intersectionCallbacksEnabled === null || intersectionCallbacksEnabled[intersectedItem])) { // Press new button. Overlays.editOverlay(intersectionOverlays[intersectedItem], { localPosition: Vec3.sum(intersectionProperties[intersectedItem].properties.localPosition, @@ -320,9 +342,34 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { // Button press actions. if (intersectionOverlays === menuOverlays) { - openOptions(intersectionProperties[highlightedItem].toolOptions); + openOptions(intersectionProperties[intersectedItem].toolOptions); } - commandCallback(intersectionCallbacks[highlightedItem]); + commandCallback(intersectionCallbacks[intersectedItem]); + } + } + + // Special handling for Group options. + if (optionsItems && optionsItems === OPTONS_PANELS.groupOptions) { + enableGroupButton = groupsCount > 1; + if (enableGroupButton !== isGroupButtonEnabled) { + isGroupButtonEnabled = enableGroupButton; + Overlays.editOverlay(optionsOverlays[GROUP_BUTTON_INDEX], { + color: isGroupButtonEnabled + ? OPTONS_PANELS.groupOptions[GROUP_BUTTON_INDEX].enabledColor + : OPTONS_PANELS.groupOptions[GROUP_BUTTON_INDEX].properties.color + }); + optionsEnabled[GROUP_BUTTON_INDEX] = enableGroupButton; + } + + enableUngroupButton = groupsCount === 1 && entitiesCount > 1; + if (enableUngroupButton !== isUngroupButtonEnabled) { + isUngroupButtonEnabled = enableUngroupButton; + Overlays.editOverlay(optionsOverlays[UNGROUP_BUTTON_INDEX], { + color: isUngroupButtonEnabled + ? OPTONS_PANELS.groupOptions[UNGROUP_BUTTON_INDEX].enabledColor + : OPTONS_PANELS.groupOptions[UNGROUP_BUTTON_INDEX].properties.color + }); + optionsEnabled[UNGROUP_BUTTON_INDEX] = enableUngroupButton; } } } @@ -376,8 +423,10 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { highlightOverlay = Overlays.addOverlay("cube", properties); // Initial values. + optionsItems = null; intersectionOverlays = null; intersectionCallbacks = null; + intersectionCallbacksEnabled = null; intersectionProperties = null; highlightedItem = NONE; highlightedSource = null; @@ -385,6 +434,8 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { pressedItem = NONE; pressedSource = null; isButtonPressed = false; + isGroupButtonEnabled = false; + isUngroupButtonEnabled = false; isDisplaying = true; } diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index c810d7f712..238fb5f48c 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -230,7 +230,7 @@ function update() { if (isDisplaying) { - toolMenu.update(getIntersection().overlayID); + toolMenu.update(getIntersection().overlayID, grouping.groupsCount(), grouping.entitiesCount()); createPalette.update(getIntersection().overlayID); toolIcon.update(); } @@ -1100,7 +1100,7 @@ function toggle(selection) { groups.toggle(selection); - if (groups.count() === 0) { + if (groups.groupsCount() === 0) { hasHighlights = false; highlights.clear(); } else { @@ -1113,8 +1113,12 @@ return groups.includes(rootEntityID); } - function count() { - return groups.count(); + function groupsCount() { + return groups.groupsCount(); + } + + function entitiesCount() { + return groups.entitiesCount(); } function group() { @@ -1171,7 +1175,8 @@ return { toggle: toggle, includes: includes, - count: count, + groupsCount: groupsCount, + entitiesCount: entitiesCount, group: group, ungroup: ungroup, update: update, From 1e1cb3a02eeb3495f771c535f90c1a2538a0e7bd Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 3 Aug 2017 13:24:38 +1200 Subject: [PATCH 130/722] Fix scale tool highlight color --- scripts/vr-edit/vr-edit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 238fb5f48c..6ddaeb5b97 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -615,7 +615,7 @@ if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(highlightedEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), toolSelected === TOOL_SCALE || otherEditor.isEditing(highlightedEntityID) - ? highlights.SCALE_CLOR : highlights.HIGHLIGHT_COLOR); + ? highlights.SCALE_COLOR : highlights.HIGHLIGHT_COLOR); } isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID); wasScaleTool = toolSelected === TOOL_SCALE; @@ -626,7 +626,7 @@ if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(highlightedEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), toolSelected === TOOL_SCALE || otherEditor.isEditing(highlightedEntityID) - ? highlights.SCALE_CLOR : highlights.HIGHLIGHT_COLOR); + ? highlights.SCALE_COLOR : highlights.HIGHLIGHT_COLOR); } else { highlights.clear(); } From 1ba658ee454b3f5c5a7f93a720b795ce5abf6e67 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 3 Aug 2017 19:52:39 +1200 Subject: [PATCH 131/722] Implement grouping and ungrouping --- scripts/vr-edit/modules/groups.js | 55 +++++++++++++++++++++++++++++-- scripts/vr-edit/vr-edit.js | 6 ++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/scripts/vr-edit/modules/groups.js b/scripts/vr-edit/modules/groups.js index c62615f25e..39217c8a76 100644 --- a/scripts/vr-edit/modules/groups.js +++ b/scripts/vr-edit/modules/groups.js @@ -80,16 +80,67 @@ Groups = function () { } function group() { - // TODO + var rootID, + i, + count; + + // Make the first entity in the first group the root and link the first entities of all other groups to it. + rootID = groupRootEntityIDs[0]; + for (i = 1, count = groupRootEntityIDs.length; i < count; i += 1) { + Entities.editEntity(groupRootEntityIDs[i], { + parentID: rootID + }); + } + + // Update selection. + groupRootEntityIDs.splice(1, groupRootEntityIDs.length - 1); + for (i = 1, count = groupSelectionDetails.length; i < count; i += 1) { + groupSelectionDetails[i][0].parentID = rootID; + groupSelectionDetails[0] = groupSelectionDetails[0].concat(groupSelectionDetails[i]); + } + groupSelectionDetails.splice(1, groupSelectionDetails.length - 1); } function ungroup() { - // TODO + var rootID, + childrenIDs, + childrenIDIndexes, + i, + count, + NULL_UUID = "{00000000-0000-0000-0000-000000000000}"; + + // Compile information on children. + rootID = groupRootEntityIDs[0]; + childrenIDs = []; + childrenIDIndexes = []; + for (i = 1, count = groupSelectionDetails[0].length; i < count; i += 1) { + if (groupSelectionDetails[0][i].parentID === rootID) { + childrenIDs.push(groupSelectionDetails[0][i].id); + childrenIDIndexes.push(i); + } + } + childrenIDIndexes.push(groupSelectionDetails[0].length); // Extra item at end to aid updating selection. + + // Unlink direct children from root entity. + for (i = 0, count = childrenIDs.length; i < count; i += 1) { + Entities.editEntity(childrenIDs[i], { + parentID: NULL_UUID + }); + } + + // Update selection. + groupRootEntityIDs = groupRootEntityIDs.concat(childrenIDs); + for (i = 0, count = childrenIDs.length; i < count; i += 1) { + groupSelectionDetails.push(groupSelectionDetails[0].slice(childrenIDIndexes[i], childrenIDIndexes[i + 1])); + groupSelectionDetails[i + 1][0].parentID = NULL_UUID; + } + groupSelectionDetails[0].splice(1, groupSelectionDetails[0].length - childrenIDIndexes[0]); } function clear() { groupRootEntityIDs = []; groupSelectionDetails = []; + numberOfEntitiesSelected = 0; } function destroy() { diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 6ddaeb5b97..4231865016 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1215,17 +1215,15 @@ } function onUICommand(command) { - if (toolSelected === TOOL_GROUP) { - grouping.clear(); - } - switch (command) { case "scaleTool": + grouping.clear(); toolSelected = TOOL_SCALE; ui.setToolIcon(ui.SCALE_TOOL); ui.updateUIEntities(); break; case "cloneTool": + grouping.clear(); toolSelected = TOOL_CLONE; ui.setToolIcon(ui.CLONE_TOOL); ui.updateUIEntities(); From e29bed7f0700ada16cb0262c9c9f222318be7b81 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 3 Aug 2017 20:26:21 +1200 Subject: [PATCH 132/722] Don't auto-grab if trigger fully pressed when laser starts intersecting --- scripts/vr-edit/vr-edit.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 4231865016..83a62b9cb9 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -816,8 +816,10 @@ setState(EDITOR_SEARCHING); break; case EDITOR_SEARCHING: - if (hand.valid() && (!intersection.entityID || !intersection.editableEntity) - && !(intersection.overlayID && isTriggerClicked && otherEditor.isHandle(intersection.overlayID))) { + if (hand.valid() + && (!intersection.entityID || !intersection.editableEntity) + && !(intersection.overlayID && !wasTriggerClicked && isTriggerClicked + && otherEditor.isHandle(intersection.overlayID))) { // No transition. updateState(); updateTool(); @@ -825,14 +827,14 @@ } if (!hand.valid()) { setState(EDITOR_IDLE); - } else if (intersection.overlayID && isTriggerClicked + } else if (intersection.overlayID && !wasTriggerClicked && isTriggerClicked && otherEditor.isHandle(intersection.overlayID)) { highlightedEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); - } else if (intersection.entityID && intersection.editableEntity && !isTriggerClicked) { + } else if (intersection.entityID && intersection.editableEntity && (wasTriggerClicked || !isTriggerClicked)) { highlightedEntityID = Entities.rootOf(intersection.entityID); setState(EDITOR_HIGHLIGHTING); - } else if (intersection.entityID && intersection.editableEntity && isTriggerClicked) { + } else if (intersection.entityID && intersection.editableEntity && !wasTriggerClicked && isTriggerClicked) { highlightedEntityID = Entities.rootOf(intersection.entityID); if (otherEditor.isEditing(highlightedEntityID)) { if (toolSelected !== TOOL_SCALE) { @@ -852,8 +854,10 @@ case EDITOR_HIGHLIGHTING: if (hand.valid() && intersection.entityID && intersection.editableEntity - && !(isTriggerClicked && (!otherEditor.isEditing(highlightedEntityID) || toolSelected !== TOOL_SCALE)) - && !(isTriggerClicked && intersection.overlayID && otherEditor.isHandle(intersection.overlayID))) { + && !(!wasTriggerClicked && isTriggerClicked + && (!otherEditor.isEditing(highlightedEntityID) || toolSelected !== TOOL_SCALE)) + && !(!wasTriggerClicked && isTriggerClicked && intersection.overlayID + && otherEditor.isHandle(intersection.overlayID))) { // No transition. doUpdateState = false; if (otherEditor.isEditing(highlightedEntityID) !== isOtherEditorEditingEntityID) { @@ -875,11 +879,11 @@ } if (!hand.valid()) { setState(EDITOR_IDLE); - } else if (intersection.overlayID && isTriggerClicked + } else if (intersection.overlayID && !wasTriggerClicked && isTriggerClicked && otherEditor.isHandle(intersection.overlayID)) { highlightedEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); - } else if (intersection.entityID && intersection.editableEntity && isTriggerClicked) { + } else if (intersection.entityID && intersection.editableEntity && !wasTriggerClicked && isTriggerClicked) { highlightedEntityID = Entities.rootOf(intersection.entityID); // May be a different entityID. if (otherEditor.isEditing(highlightedEntityID)) { if (toolSelected !== TOOL_SCALE) { From 110355796ce2e79b1afe6b5cb6d51ab3085078cc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 4 Aug 2017 08:55:43 +1200 Subject: [PATCH 133/722] Add delete tool --- scripts/vr-edit/modules/toolIcon.js | 5 ++++- scripts/vr-edit/modules/toolMenu.js | 9 +++++++++ scripts/vr-edit/vr-edit.js | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 076c5997d4..6eaf37e161 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -18,11 +18,13 @@ ToolIcon = function (side) { var SCALE_TOOL = 0, CLONE_TOOL = 1, GROUP_TOOL = 2, + DELETE_TOOL = 3, ICON_COLORS = [ { red: 0, green: 240, blue: 240 }, { red: 240, green: 240, blue: 0 }, - { red: 220, green: 60, blue: 220 } + { red: 220, green: 60, blue: 220 }, + { red: 240, green: 60, blue: 60 } ], LEFT_HAND = 0, @@ -103,6 +105,7 @@ ToolIcon = function (side) { SCALE_TOOL: SCALE_TOOL, CLONE_TOOL: CLONE_TOOL, GROUP_TOOL: GROUP_TOOL, + DELETE_TOOL: DELETE_TOOL, setHand: setHand, update: update, display: display, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 168cde8254..e98d38f3b7 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -158,6 +158,15 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { }, toolOptions: "groupOptions", callback: "groupTool" + }, + { + id: "deleteButton", + type: "button", + properties: { + localPosition: { x: 0.022, y: 0.04, z: -0.005 }, + color: { red: 240, green: 60, blue: 60 } + }, + callback: "deleteTool" } ], diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 83a62b9cb9..ee37bd7518 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -26,6 +26,7 @@ TOOL_SCALE = 1, TOOL_CLONE = 2, TOOL_GROUP = 3, + TOOL_DELETE = 4, toolSelected = TOOL_NONE, // Primary objects @@ -268,6 +269,7 @@ SCALE_TOOL: toolIcon.SCALE_TOOL, CLONE_TOOL: toolIcon.CLONE_TOOL, GROUP_TOOL: toolIcon.GROUP_TOOL, + DELETE_TOOL: toolIcon.DELETE_TOOL, display: display, updateUIEntities: setUIEntities, update: update, @@ -844,6 +846,10 @@ setState(EDITOR_CLONING); } else if (toolSelected === TOOL_GROUP) { setState(EDITOR_GROUPING); + } else if (toolSelected === TOOL_DELETE) { + setState(EDITOR_HIGHLIGHTING); + selection.deleteEntities(); + setState(EDITOR_SEARCHING); } else { setState(EDITOR_GRABBING); } @@ -895,6 +901,9 @@ setState(EDITOR_CLONING); } else if (toolSelected === TOOL_GROUP) { setState(EDITOR_GROUPING); + } else if (toolSelected === TOOL_DELETE) { + selection.deleteEntities(); + setState(EDITOR_SEARCHING); } else { setState(EDITOR_GRABBING); } @@ -1237,6 +1246,12 @@ ui.setToolIcon(ui.GROUP_TOOL); ui.updateUIEntities(); break; + case "deleteTool": + grouping.clear(); + toolSelected = TOOL_DELETE; + ui.setToolIcon(ui.DELETE_TOOL); + ui.updateUIEntities(); + break; case "groupButton": grouping.group(); break; From 4d8226ac02fa09662d84f681361224d3049b3549 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 4 Aug 2017 09:23:40 +1200 Subject: [PATCH 134/722] Abstract out some common constants missing from the API --- scripts/vr-edit/modules/createPalette.js | 10 ++++------ scripts/vr-edit/modules/groups.js | 7 +++---- scripts/vr-edit/modules/handles.js | 5 ++--- scripts/vr-edit/modules/highlights.js | 8 +++----- scripts/vr-edit/modules/laser.js | 3 +-- scripts/vr-edit/modules/toolIcon.js | 3 +-- scripts/vr-edit/modules/toolMenu.js | 12 +++++------- scripts/vr-edit/utilities/utilities.js | 21 +++++++++++++++------ 8 files changed, 34 insertions(+), 35 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 928cb48048..be9558e432 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -21,8 +21,6 @@ CreatePalette = function (side, leftInputs, rightInputs) { cubeHighlightOverlay, LEFT_HAND = 0, - AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", - ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), controlJointName, @@ -37,7 +35,7 @@ CreatePalette = function (side, leftInputs, rightInputs) { localRotation: PALETTE_ROOT_ROTATION, color: { red: 255, blue: 0, green: 0 }, alpha: 1.0, - parentID: AVATAR_SELF_ID, + parentID: Uuid.SELF, ignoreRayIntersection: true, visible: false }, @@ -45,7 +43,7 @@ CreatePalette = function (side, leftInputs, rightInputs) { PALETTE_PANEL_PROPERTIES = { dimensions: { x: CANVAS_SIZE.x, y: CANVAS_SIZE.y, z: 0.001 }, localPosition: { x: CANVAS_SIZE.x / 2, y: CANVAS_SIZE.y / 2, z: 0 }, - localRotation: ZERO_ROTATION, + localRotation: Quat.ZERO, color: { red: 192, green: 192, blue: 192 }, alpha: 0.3, solid: true, @@ -56,7 +54,7 @@ CreatePalette = function (side, leftInputs, rightInputs) { CUBE_PROPERTIES = { dimensions: { x: 0.03, y: 0.03, z: 0.03 }, localPosition: { x: 0.02, y: 0.02, z: 0.0 }, - localRotation: ZERO_ROTATION, + localRotation: Quat.ZERO, color: { red: 240, green: 0, blue: 0 }, alpha: 1.0, solid: true, @@ -67,7 +65,7 @@ CreatePalette = function (side, leftInputs, rightInputs) { CUBE_HIGHLIGHT_PROPERTIES = { dimensions: { x: 0.034, y: 0.034, z: 0.034 }, localPosition: { x: 0.02, y: 0.02, z: 0.0 }, - localRotation: ZERO_ROTATION, + localRotation: Quat.ZERO, color: { red: 240, green: 240, blue: 0 }, alpha: 0.8, solid: false, diff --git a/scripts/vr-edit/modules/groups.js b/scripts/vr-edit/modules/groups.js index 39217c8a76..036df1a71c 100644 --- a/scripts/vr-edit/modules/groups.js +++ b/scripts/vr-edit/modules/groups.js @@ -106,8 +106,7 @@ Groups = function () { childrenIDs, childrenIDIndexes, i, - count, - NULL_UUID = "{00000000-0000-0000-0000-000000000000}"; + count; // Compile information on children. rootID = groupRootEntityIDs[0]; @@ -124,7 +123,7 @@ Groups = function () { // Unlink direct children from root entity. for (i = 0, count = childrenIDs.length; i < count; i += 1) { Entities.editEntity(childrenIDs[i], { - parentID: NULL_UUID + parentID: Uuid.NULL }); } @@ -132,7 +131,7 @@ Groups = function () { groupRootEntityIDs = groupRootEntityIDs.concat(childrenIDs); for (i = 0, count = childrenIDs.length; i < count; i += 1) { groupSelectionDetails.push(groupSelectionDetails[0].slice(childrenIDIndexes[i], childrenIDIndexes[i + 1])); - groupSelectionDetails[i + 1][0].parentID = NULL_UUID; + groupSelectionDetails[i + 1][0].parentID = Uuid.NULL; } groupSelectionDetails[0].splice(1, groupSelectionDetails[0].length - childrenIDIndexes[0]); } diff --git a/scripts/vr-edit/modules/handles.js b/scripts/vr-edit/modules/handles.js index b9390b02d3..e32918160a 100644 --- a/scripts/vr-edit/modules/handles.js +++ b/scripts/vr-edit/modules/handles.js @@ -38,7 +38,6 @@ Handles = function (side) { FACE_HANDLE_OVERLAY_OFFSETS, FACE_HANDLE_OVERLAY_ROTATIONS, FACE_HANDLE_OVERLAY_SCALE_AXES, - ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), DISTANCE_MULTIPLIER_MULTIPLIER = 0.5, hoveredOverlayID = null, isVisible = false, @@ -154,7 +153,7 @@ Handles = function (side) { boundingBoxOverlay = Overlays.addOverlay("cube", { parentID: rootEntityID, localPosition: boundingBoxLocalCenter, - localRotation: ZERO_ROTATION, + localRotation: Quat.ZERO, dimensions: boundingBoxDimensions, color: BOUNDING_BOX_COLOR, alpha: BOUNDING_BOX_ALPHA, @@ -196,7 +195,7 @@ Handles = function (side) { parentID: rootEntityID, localPosition: Vec3.sum(boundingBoxLocalCenter, Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[i]], boundingBoxDimensions)), - localRotation: ZERO_ROTATION, + localRotation: Quat.ZERO, dimensions: cornerHandleDimensions, color: HANDLE_NORMAL_COLOR, alpha: HANDLE_NORMAL_ALPHA, diff --git a/scripts/vr-edit/modules/highlights.js b/scripts/vr-edit/modules/highlights.js index 6a0e789a0d..9f0dc44275 100644 --- a/scripts/vr-edit/modules/highlights.js +++ b/scripts/vr-edit/modules/highlights.js @@ -24,9 +24,7 @@ Highlights = function (side) { ENTITY_HIGHLIGHT_ALPHA = 0.8, HAND_HIGHLIGHT_DIMENSIONS = { x: 0.2, y: 0.2, z: 0.2 }, HAND_HIGHLIGHT_OFFSET = { x: 0.0, y: 0.11, z: 0.02 }, - LEFT_HAND = 0, - AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", - ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO); + LEFT_HAND = 0; if (!this instanceof Highlights) { return new Highlights(); @@ -34,7 +32,7 @@ Highlights = function (side) { handOverlay = Overlays.addOverlay("sphere", { dimensions: HAND_HIGHLIGHT_DIMENSIONS, - parentID: AVATAR_SELF_ID, + parentID: Uuid.SELF, parentJointIndex: MyAvatar.getJointIndex(side === LEFT_HAND ? "_CONTROLLER_LEFTHAND" : "_CONTROLLER_RIGHTHAND"), @@ -64,7 +62,7 @@ Highlights = function (side) { Overlays.editOverlay(entityOverlays[index], { parentID: details.id, localPosition: offset, - localRotation: ZERO_ROTATION, + localRotation: Quat.ZERO, dimensions: details.dimensions, color: overlayColor, visible: true diff --git a/scripts/vr-edit/modules/laser.js b/scripts/vr-edit/modules/laser.js index fefa13b8ff..991c9173b6 100644 --- a/scripts/vr-edit/modules/laser.js +++ b/scripts/vr-edit/modules/laser.js @@ -46,7 +46,6 @@ Laser = function (side) { specifiedLaserLength = null, LEFT_HAND = 0, - AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", uiEntityIDs = [], @@ -77,7 +76,7 @@ Laser = function (side) { glow: 1.0, ignoreRayIntersection: true, drawInFront: true, - parentID: AVATAR_SELF_ID, + parentID: Uuid.SELF, parentJointIndex: MyAvatar.getJointIndex(side === LEFT_HAND ? "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND" : "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"), diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 6eaf37e161..4f8f2da752 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -28,7 +28,6 @@ ToolIcon = function (side) { ], LEFT_HAND = 0, - AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", ICON_DIMENSIONS = { x: 0.1, y: 0.01, z: 0.1 }, ICON_POSITION = { x: 0, y: 0.01, z: 0 }, @@ -41,7 +40,7 @@ ToolIcon = function (side) { localRotation: ICON_ROTATION, solid: true, alpha: 1.0, - parentID: AVATAR_SELF_ID, + parentID: Uuid.SELF, ignoreRayIntersection: false, visible: true }, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index e98d38f3b7..3c5fab0e23 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -28,8 +28,6 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { highlightOverlay, LEFT_HAND = 0, - AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", - ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO), CANVAS_SIZE = { x: 0.22, y: 0.13 }, PANEL_ORIGIN_POSITION = { x: CANVAS_SIZE.x / 2, y: 0.15, z: -0.04 }, @@ -42,7 +40,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { localRotation: PANEL_ROOT_ROTATION, color: { red: 255, blue: 0, green: 0 }, alpha: 1.0, - parentID: AVATAR_SELF_ID, + parentID: Uuid.SELF, ignoreRayIntersection: true, visible: false }, @@ -50,7 +48,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { MENU_PANEL_PROPERTIES = { dimensions: { x: CANVAS_SIZE.x, y: CANVAS_SIZE.y, z: 0.01 }, localPosition: { x: CANVAS_SIZE.x / 2, y: CANVAS_SIZE.y / 2, z: 0.005 }, - localRotation: ZERO_ROTATION, + localRotation: Quat.ZERO, color: { red: 164, green: 164, blue: 164 }, alpha: 1.0, solid: true, @@ -63,7 +61,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { overlay: "cube", properties: { dimensions: { x: 0.10, y: 0.12, z: 0.01 }, - localRotation: ZERO_ROTATION, + localRotation: Quat.ZERO, color: { red: 192, green: 192, blue: 192 }, alpha: 1.0, solid: true, @@ -75,7 +73,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { overlay: "cube", properties: { dimensions: { x: 0.03, y: 0.03, z: 0.01 }, - localRotation: ZERO_ROTATION, + localRotation: Quat.ZERO, alpha: 1.0, solid: true, ignoreRayIntersection: false, @@ -176,7 +174,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { zDimension: 0.001, properties: { localPosition: { x: 0, y: 0, z: -0.003 }, - localRotation: ZERO_ROTATION, + localRotation: Quat.ZERO, color: { red: 255, green: 255, blue: 0 }, alpha: 0.8, solid: false, diff --git a/scripts/vr-edit/utilities/utilities.js b/scripts/vr-edit/utilities/utilities.js index cbf8206750..62343c5436 100644 --- a/scripts/vr-edit/utilities/utilities.js +++ b/scripts/vr-edit/utilities/utilities.js @@ -20,15 +20,26 @@ if (typeof Vec3.max !== "function") { }; } +if (typeof Quat.ZERO !== "object") { + Quat.ZERO = Quat.fromVec3Radians(Vec3.ZERO); +} + +if (typeof Uuid.NULL !== "string") { + Uuid.NULL = "{00000000-0000-0000-0000-000000000000}"; +} + +if (typeof Uuid.SELF !== "string") { + Uuid.SELF = "{00000000-0000-0000-0000-000000000001}"; +} + if (typeof Entities.rootOf !== "function") { Entities.rootOf = function (entityID) { var rootEntityID, entityProperties, - PARENT_PROPERTIES = ["parentID"], - NULL_UUID = "{00000000-0000-0000-0000-000000000000}"; + PARENT_PROPERTIES = ["parentID"]; rootEntityID = entityID; entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); - while (entityProperties.parentID && entityProperties.parentID !== NULL_UUID) { + while (entityProperties.parentID && entityProperties.parentID !== Uuid.NULL) { rootEntityID = entityProperties.parentID; entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); } @@ -40,10 +51,9 @@ if (typeof Entities.hasEditableRoot !== "function") { Entities.hasEditableRoot = function (entityID) { var EDITIBLE_ENTITY_QUERY_PROPERTYES = ["parentID", "visible", "locked", "type"], NONEDITABLE_ENTITY_TYPES = ["Unknown", "Zone", "Light"], - NULL_UUID = "{00000000-0000-0000-0000-000000000000}", properties; properties = Entities.getEntityProperties(entityID, EDITIBLE_ENTITY_QUERY_PROPERTYES); - while (properties.parentID && properties.parentID !== NULL_UUID) { + while (properties.parentID && properties.parentID !== Uuid.NULL) { properties = Entities.getEntityProperties(properties.parentID, EDITIBLE_ENTITY_QUERY_PROPERTYES); } return properties.visible && !properties.locked && NONEDITABLE_ENTITY_TYPES.indexOf(properties.type) === -1; @@ -63,4 +73,3 @@ if (typeof Object.merge !== "function") { return JSON.parse(a.slice(0, -1) + "," + b.slice(1)); }; } - From 2237290b2e2d592436024a1b68cd2c823bf284b3 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 4 Aug 2017 13:22:48 +1200 Subject: [PATCH 135/722] Add labels to buttons --- scripts/vr-edit/modules/toolMenu.js | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 3c5fab0e23..e082da08ae 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -79,6 +79,24 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { ignoreRayIntersection: false, visible: true } + }, + "label": { + overlay: "text3d", + properties: { + dimensions: { x: 0.03, y: 0.0075 }, + localPosition: { x: 0.0, y: 0.0, z: -0.005 }, + localRotation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 180 }), + topMargin: 0, + leftMargin: 0, + color: { red: 128, green: 128, blue: 128 }, + alpha: 1.0, + lineHeight: 0.007, + backgroundAlpha: 0, + ignoreRayIntersection: true, + isFacingAvatar: false, + drawInFront: true, + visible: true + } } }, @@ -100,6 +118,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { localPosition: { x: 0, y: -0.025, z: -0.005 }, color: { red: 200, green: 200, blue: 200 } }, + label: " GROUP", enabledColor: { red: 64, green: 240, blue: 64 }, callback: "groupButton" }, @@ -111,6 +130,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { localPosition: { x: 0, y: 0.025, z: -0.005 }, color: { red: 200, green: 200, blue: 200 } }, + label: "UNGROUP", enabledColor: { red: 240, green: 64, blue: 64 }, callback: "ungroupButton" } @@ -136,6 +156,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { localPosition: { x: -0.022, y: -0.04, z: -0.005 }, color: { red: 0, green: 240, blue: 240 } }, + label: " SCALE", callback: "scaleTool" }, { @@ -145,6 +166,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { localPosition: { x: 0.022, y: -0.04, z: -0.005 }, color: { red: 240, green: 240, blue: 0 } }, + label: " CLONE", callback: "cloneTool" }, { @@ -154,6 +176,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { localPosition: { x: -0.022, y: 0.0, z: -0.005 }, color: { red: 220, green: 60, blue: 220 } }, + label: " GROUP", toolOptions: "groupOptions", callback: "groupTool" }, @@ -164,6 +187,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { localPosition: { x: 0.022, y: 0.04, z: -0.005 }, color: { red: 240, green: 60, blue: 60 } }, + label: " DELETE", callback: "deleteTool" } ], @@ -250,6 +274,12 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { properties = Object.merge(properties, optionsItems[i].properties); properties.parentID = parentID; optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[optionsItems[i].type].overlay, properties)); + if (optionsItems[i].label) { + properties = Object.clone(UI_ELEMENTS.label.properties); + properties.text = optionsItems[i].label; + properties.parentID = optionsOverlays[optionsOverlays.length - 1]; + Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); + } parentID = optionsOverlays[0]; // Menu buttons parent to menu panel. optionsCallbacks.push(optionsItems[i].callback); optionsEnabled.push(true); @@ -420,6 +450,12 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { properties = Object.merge(properties, MENU_ITEMS[i].properties); properties.parentID = parentID; menuOverlays.push(Overlays.addOverlay(UI_ELEMENTS[MENU_ITEMS[i].type].overlay, properties)); + if (MENU_ITEMS[i].label) { + properties = Object.clone(UI_ELEMENTS.label.properties); + properties.text = MENU_ITEMS[i].label; + properties.parentID = menuOverlays[menuOverlays.length - 1]; + Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); + } parentID = menuOverlays[0]; // Menu buttons parent to menu panel. menuCallbacks.push(MENU_ITEMS[i].callback); } From ed497afdc36e48c06db0dc836ae478125fb90868 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 4 Aug 2017 14:08:57 +1200 Subject: [PATCH 136/722] Add "Color" tool button, icon, and empty options panel --- scripts/vr-edit/modules/toolIcon.js | 5 ++++- scripts/vr-edit/modules/toolMenu.js | 23 +++++++++++++++++++++-- scripts/vr-edit/vr-edit.js | 15 ++++++++++++++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 4f8f2da752..3b67d7790b 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -18,12 +18,14 @@ ToolIcon = function (side) { var SCALE_TOOL = 0, CLONE_TOOL = 1, GROUP_TOOL = 2, - DELETE_TOOL = 3, + COLOR_TOOL = 3, + DELETE_TOOL = 4, ICON_COLORS = [ { red: 0, green: 240, blue: 240 }, { red: 240, green: 240, blue: 0 }, { red: 220, green: 60, blue: 220 }, + { red: 220, green: 220, blue: 220 }, { red: 240, green: 60, blue: 60 } ], @@ -104,6 +106,7 @@ ToolIcon = function (side) { SCALE_TOOL: SCALE_TOOL, CLONE_TOOL: CLONE_TOOL, GROUP_TOOL: GROUP_TOOL, + COLOR_TOOL: COLOR_TOOL, DELETE_TOOL: DELETE_TOOL, setHand: setHand, update: update, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index e082da08ae..9ae428de90 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -103,8 +103,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { OPTONS_PANELS = { groupOptions: [ { - // Background element - id: "toolsOptionsPanel", + id: "groupOptionsPanel", type: "panel", properties: { localPosition: { x: 0.055, y: 0.0, z: -0.005 } @@ -134,6 +133,15 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { enabledColor: { red: 240, green: 64, blue: 64 }, callback: "ungroupButton" } + ], + colorOptions: [ + { + id: "colorOptionsPanel", + type: "panel", + properties: { + localPosition: { x: 0.055, y: 0.0, z: -0.005 } + } + } ] }, @@ -180,6 +188,17 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { toolOptions: "groupOptions", callback: "groupTool" }, + { + id: "colorButton", + type: "button", + properties: { + localPosition: { x: 0.022, y: 0.0, z: -0.005 }, + color: { red: 220, green: 220, blue: 220 } + }, + label: " COLOR", + toolOptions: "colorOptions", + callback: "colorTool" + }, { id: "deleteButton", type: "button", diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index ee37bd7518..7d45fd4dc4 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -26,7 +26,8 @@ TOOL_SCALE = 1, TOOL_CLONE = 2, TOOL_GROUP = 3, - TOOL_DELETE = 4, + TOOL_COLOR = 4, + TOOL_DELETE = 5, toolSelected = TOOL_NONE, // Primary objects @@ -269,6 +270,7 @@ SCALE_TOOL: toolIcon.SCALE_TOOL, CLONE_TOOL: toolIcon.CLONE_TOOL, GROUP_TOOL: toolIcon.GROUP_TOOL, + COLOR_TOOL: toolIcon.COLOR_TOOL, DELETE_TOOL: toolIcon.DELETE_TOOL, display: display, updateUIEntities: setUIEntities, @@ -846,6 +848,9 @@ setState(EDITOR_CLONING); } else if (toolSelected === TOOL_GROUP) { setState(EDITOR_GROUPING); + } else if (toolSelected === TOOL_COLOR) { + // TODO + print("$$$$$$$ apply color"); } else if (toolSelected === TOOL_DELETE) { setState(EDITOR_HIGHLIGHTING); selection.deleteEntities(); @@ -901,6 +906,9 @@ setState(EDITOR_CLONING); } else if (toolSelected === TOOL_GROUP) { setState(EDITOR_GROUPING); + } else if (toolSelected === TOOL_COLOR) { + // TODO + print("$$$$$$$ apply color"); } else if (toolSelected === TOOL_DELETE) { selection.deleteEntities(); setState(EDITOR_SEARCHING); @@ -1246,6 +1254,11 @@ ui.setToolIcon(ui.GROUP_TOOL); ui.updateUIEntities(); break; + case "colorTool": + toolSelected = TOOL_COLOR; + ui.setToolIcon(ui.COLOR_TOOL); + ui.updateUIEntities(); + break; case "deleteTool": grouping.clear(); toolSelected = TOOL_DELETE; From d0143c2c19f3d25a67717bcfec511250a51b2b50 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 4 Aug 2017 17:33:00 +1200 Subject: [PATCH 137/722] Add color swatch buttons and "current color" circle --- scripts/vr-edit/modules/toolMenu.js | 161 ++++++++++++++++++++++++++-- scripts/vr-edit/vr-edit.js | 2 +- 2 files changed, 152 insertions(+), 11 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 9ae428de90..7a17a0fede 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -22,9 +22,15 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { menuOverlays = [], menuCallbacks = [], + optionsOverlays = [], + optionsOverlaysIDs = [], + optionsCommands = [], + optionsCommandsParameters = [], optionsCallbacks = [], + optionsCallbacksParameters = [], optionsEnabled = [], + highlightOverlay, LEFT_HAND = 0, @@ -97,6 +103,19 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { drawInFront: true, visible: true } + }, + "circle": { + overlay: "circle3d", + properties: { + size: 0.015, + localPosition: { x: 0.0, y: 0.0, z: -0.01 }, + localRotation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 180 }), + color: { red: 128, green: 128, blue: 128 }, + alpha: 1.0, + solid: true, + ignoreRayIntersection: true, + visible: true + } } }, @@ -141,6 +160,66 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { properties: { localPosition: { x: 0.055, y: 0.0, z: -0.005 } } + }, + { + id: "colorSwatch1", + type: "button", + properties: { + dimensions: { x: 0.02, y: 0.02, z: 0.01 }, + localPosition: { x: -0.035, y: 0.02, z: -0.005 }, + color: { red: 255, green: 0, blue: 0 } + }, + command: "setCurrentColor", + commandParameter: "colorSwatch1.color", + callback: "setColor", + callbackParameter: "colorSwatch1.color" + }, + { + id: "colorSwatch2", + type: "button", + properties: { + dimensions: { x: 0.02, y: 0.02, z: 0.01 }, + localPosition: { x: -0.01, y: 0.02, z: -0.005 }, + color: { red: 0, green: 255, blue: 0 } + }, + command: "setCurrentColor", + commandParameter: "colorSwatch2.color", + callback: "setColor", + callbackParameter: "colorSwatch2.color" + }, + { + id: "colorSwatch3", + type: "button", + properties: { + dimensions: { x: 0.02, y: 0.02, z: 0.01 }, + localPosition: { x: -0.035, y: 0.045, z: -0.005 }, + color: { red: 0, green: 0, blue: 255 } + }, + command: "setCurrentColor", + commandParameter: "colorSwatch3.color", + callback: "setColor", + callbackParameter: "colorSwatch3.color" + }, + { + id: "colorSwatch4", + type: "button", + properties: { + dimensions: { x: 0.02, y: 0.02, z: 0.01 }, + localPosition: { x: -0.01, y: 0.045, z: -0.005 }, + color: { red: 255, green: 255, blue: 255 } + }, + command: "setCurrentColor", + commandParameter: "colorSwatch4.color", + callback: "setColor", + callbackParameter: "colorSwatch4.color" + }, + { + id: "currentColor", + type: "circle", + properties: { + localPosition: { x: 0.025, y: 0.0325, z: -0.007 }, + color: { red: 128, green: 128, blue: 128 } + } } ] }, @@ -231,8 +310,12 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { optionsItems, intersectionOverlays, + intersectionOverlaysIDs, + intersectionCommands, + intersectionCommandsParameters, intersectionCallbacks, - intersectionCallbacksEnabled, + intersectionCallbacksParameters, + intersectionEnabled, intersectionProperties, highlightedItem, highlightedSource, @@ -278,11 +361,15 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { // Close current panel, if any. for (i = 0, length = optionsOverlays.length; i < length; i += 1) { Overlays.deleteOverlay(optionsOverlays[i]); - optionsOverlays = []; - optionsCallbacks = []; - optionsEnabled = []; - optionsItems = null; } + optionsOverlays = []; + optionsOverlaysIDs = []; + optionsCommands = []; + optionsCommandsParameters = []; + optionsCallbacks = []; + optionsCallbacksParameters = []; + optionsEnabled = []; + optionsItems = null; // Open specified panel, if any. if (toolOptions) { @@ -293,6 +380,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { properties = Object.merge(properties, optionsItems[i].properties); properties.parentID = parentID; optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[optionsItems[i].type].overlay, properties)); + optionsOverlaysIDs.push(optionsItems[i].id); if (optionsItems[i].label) { properties = Object.clone(UI_ELEMENTS.label.properties); properties.text = optionsItems[i].label; @@ -300,7 +388,10 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); } parentID = optionsOverlays[0]; // Menu buttons parent to menu panel. + optionsCommands.push(optionsItems[i].command); + optionsCommandsParameters.push(optionsItems[i].commandParameter); optionsCallbacks.push(optionsItems[i].callback); + optionsCallbacksParameters.push(optionsItems[i].callbackParameter); optionsEnabled.push(true); } } @@ -316,10 +407,37 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { openOptions(); } + function evaluateParameter(parameter) { + var parameters, + overlayID, + overlayProperty; + + parameters = parameter.split("."); + overlayID = parameters[0]; + overlayProperty = parameters[1]; + + return Overlays.getProperty(optionsOverlays[optionsOverlaysIDs.indexOf(overlayID)], overlayProperty); + } + + function peformCommand(command, parameter) { + switch (command) { + case "setCurrentColor": + Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], { + color: parameter + }); + break; + default: + // TODO: Log error. + } + + } + function update(intersectionOverlayID, groupsCount, entitiesCount) { var intersectedItem, parentProperties, BUTTON_PRESS_DELTA = 0.004, + commandParameter, + callbackParameter, enableGroupButton, enableUngroupButton; @@ -328,15 +446,23 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { intersectedItem = menuOverlays.indexOf(intersectionOverlayID); if (intersectedItem !== -1) { intersectionOverlays = menuOverlays; + intersectionOverlaysIDs = null; + intersectionCommands = null; + intersectionCommandsParameters = null; intersectionCallbacks = menuCallbacks; - intersectionCallbacksEnabled = null; + intersectionCallbacksParameters = null; + intersectionEnabled = null; intersectionProperties = MENU_ITEMS; } else { intersectedItem = optionsOverlays.indexOf(intersectionOverlayID); if (intersectedItem !== -1) { intersectionOverlays = optionsOverlays; + intersectionOverlaysIDs = optionsOverlaysIDs; + intersectionCommands = optionsCommands; + intersectionCommandsParameters = optionsCommandsParameters; intersectionCallbacks = optionsCallbacks; - intersectionCallbacksEnabled = optionsEnabled; + intersectionCallbacksParameters = optionsCallbacksParameters; + intersectionEnabled = optionsEnabled; intersectionProperties = optionsItems; } } @@ -387,7 +513,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { pressedItem = NONE; } isButtonPressed = isHighlightingButton && controlHand.triggerClicked(); - if (isButtonPressed && (intersectionCallbacksEnabled === null || intersectionCallbacksEnabled[intersectedItem])) { + if (isButtonPressed && (intersectionEnabled === null || intersectionEnabled[intersectedItem])) { // Press new button. Overlays.editOverlay(intersectionOverlays[intersectedItem], { localPosition: Vec3.sum(intersectionProperties[intersectedItem].properties.localPosition, @@ -400,7 +526,18 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { if (intersectionOverlays === menuOverlays) { openOptions(intersectionProperties[intersectedItem].toolOptions); } - commandCallback(intersectionCallbacks[intersectedItem]); + if (intersectionCommands && intersectionCommands[intersectedItem]) { + if (intersectionCommandsParameters && intersectionCommandsParameters[intersectedItem]) { + commandParameter = evaluateParameter(intersectionCommandsParameters[intersectedItem]); + } + peformCommand(intersectionCommands[intersectedItem], commandParameter); + } + if (intersectionCallbacks[intersectedItem]) { + if (intersectionCallbacksParameters && intersectionCallbacksParameters[intersectedItem]) { + callbackParameter = evaluateParameter(intersectionCallbacksParameters[intersectedItem]); + } + commandCallback(intersectionCallbacks[intersectedItem], callbackParameter); + } } } @@ -487,8 +624,12 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { // Initial values. optionsItems = null; intersectionOverlays = null; + intersectionOverlaysIDs = null; + intersectionCommands = null; + intersectionCommandsParameters = null; intersectionCallbacks = null; - intersectionCallbacksEnabled = null; + intersectionCallbacksParameters = null; + intersectionEnabled = null; intersectionProperties = null; highlightedItem = NONE; highlightedSource = null; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 7d45fd4dc4..f0f828398d 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1235,7 +1235,7 @@ Settings.setValue(VR_EDIT_SETTING, isAppActive); } - function onUICommand(command) { + function onUICommand(command, parameter) { switch (command) { case "scaleTool": grouping.clear(); From 75b481adab869529cbc54037a8e8bd9927d80cd4 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 4 Aug 2017 17:52:53 +1200 Subject: [PATCH 138/722] Update icon color as current color is changed --- scripts/vr-edit/modules/toolIcon.js | 5 +++++ scripts/vr-edit/vr-edit.js | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 3b67d7790b..41a6294820 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -90,6 +90,10 @@ ToolIcon = function (side) { } } + function setColor(color) { + Overlays.editOverlay(iconOverlay, { color: color }); + } + function clear() { // Deletes current icon. if (iconOverlay) { @@ -111,6 +115,7 @@ ToolIcon = function (side) { setHand: setHand, update: update, display: display, + setColor: setColor, clear: clear, destroy: destroy }; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index f0f828398d..7af8be3fb6 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -212,6 +212,10 @@ toolIcon.display(icon); } + function setToolColor(color) { + toolIcon.setColor(color); + } + function clearToolIcon() { toolIcon.clear(); toolMenu.clearTool(); @@ -266,6 +270,7 @@ return { setHand: setHand, setToolIcon: setToolIcon, + setToolColor: setToolColor, clearToolIcon: clearToolIcon, SCALE_TOOL: toolIcon.SCALE_TOOL, CLONE_TOOL: toolIcon.CLONE_TOOL, @@ -1271,6 +1276,9 @@ case "ungroupButton": grouping.ungroup(); break; + case "setColor": + ui.setToolColor(parameter); + break; default: debug("ERROR: Unexpected command in onUICommand()!"); } From ceba5769e0e7c001620181ad170a4938f3919ebe Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 4 Aug 2017 18:07:42 +1200 Subject: [PATCH 139/722] Apply color to entities when click them --- scripts/vr-edit/modules/selection.js | 10 ++++++++++ scripts/vr-edit/vr-edit.js | 8 ++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 89e5221f81..7234553a9d 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -327,6 +327,15 @@ Selection = function (side) { rootEntityID = selection[0].id; } + function applyColor(color) { + // Entities without a color property simply ignore the edit. + for (i = 0, length = selection.length; i < length; i += 1) { + Entities.editEntity(selection[i].id, { + color: color + }); + } + } + function clear() { selection = []; selectedEntityID = null; @@ -361,6 +370,7 @@ Selection = function (side) { finishHandleScaling: finishHandleScaling, finishEditing: finishEditing, cloneEntities: cloneEntities, + applyColor: applyColor, deleteEntities: deleteEntities, clear: clear, destroy: destroy diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 7af8be3fb6..72232d6101 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -29,6 +29,7 @@ TOOL_COLOR = 4, TOOL_DELETE = 5, toolSelected = TOOL_NONE, + colorToolColor = { red: 128, green: 128, blue: 128 }, // Primary objects Inputs, @@ -854,8 +855,7 @@ } else if (toolSelected === TOOL_GROUP) { setState(EDITOR_GROUPING); } else if (toolSelected === TOOL_COLOR) { - // TODO - print("$$$$$$$ apply color"); + selection.applyColor(colorToolColor); } else if (toolSelected === TOOL_DELETE) { setState(EDITOR_HIGHLIGHTING); selection.deleteEntities(); @@ -912,8 +912,7 @@ } else if (toolSelected === TOOL_GROUP) { setState(EDITOR_GROUPING); } else if (toolSelected === TOOL_COLOR) { - // TODO - print("$$$$$$$ apply color"); + selection.applyColor(colorToolColor); } else if (toolSelected === TOOL_DELETE) { selection.deleteEntities(); setState(EDITOR_SEARCHING); @@ -1278,6 +1277,7 @@ break; case "setColor": ui.setToolColor(parameter); + colorToolColor = parameter; break; default: debug("ERROR: Unexpected command in onUICommand()!"); From 24f36c1ae50bfe750534b18c327885caff9c07d7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 5 Aug 2017 12:07:33 +1200 Subject: [PATCH 140/722] Default current color and remember current color over tool/app toggles --- scripts/vr-edit/modules/toolMenu.js | 31 ++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 7a17a0fede..c241ad9740 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -31,6 +31,8 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { optionsCallbacksParameters = [], optionsEnabled = [], + optionsSettings = {}, + highlightOverlay, LEFT_HAND = 0, @@ -217,8 +219,13 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { id: "currentColor", type: "circle", properties: { - localPosition: { x: 0.025, y: 0.0325, z: -0.007 }, - color: { red: 128, green: 128, blue: 128 } + localPosition: { x: 0.025, y: 0.0325, z: -0.007 } + }, + setting: { + key: "VREdit.colorTool.currentColor", + property: "color", + defaultValue: { red: 128, green: 128, blue: 128 }, + callback: "setColor" } } ] @@ -355,6 +362,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { function openOptions(toolOptions) { var properties, parentID, + value, i, length; @@ -379,6 +387,15 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { properties = Object.clone(UI_ELEMENTS[optionsItems[i].type].properties); properties = Object.merge(properties, optionsItems[i].properties); properties.parentID = parentID; + if (optionsItems[i].setting) { + optionsSettings[optionsItems[i].id] = { key: optionsItems[i].setting.key }; + value = Settings.getValue(optionsItems[i].setting.key); + if (value === "") { + value = optionsItems[i].setting.defaultValue; + } + properties[optionsItems[i].setting.property] = value; + commandCallback(optionsItems[i].setting.callback, value); // Apply setting. + } optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[optionsItems[i].type].overlay, properties)); optionsOverlaysIDs.push(optionsItems[i].id); if (optionsItems[i].label) { @@ -425,6 +442,9 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], { color: parameter }); + if (optionsSettings.currentColor) { + Settings.setValue(optionsSettings.currentColor.key, parameter); + } break; default: // TODO: Log error. @@ -523,9 +543,6 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { pressedSource = intersectionOverlays; // Button press actions. - if (intersectionOverlays === menuOverlays) { - openOptions(intersectionProperties[intersectedItem].toolOptions); - } if (intersectionCommands && intersectionCommands[intersectedItem]) { if (intersectionCommandsParameters && intersectionCommandsParameters[intersectedItem]) { commandParameter = evaluateParameter(intersectionCommandsParameters[intersectedItem]); @@ -538,6 +555,10 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { } commandCallback(intersectionCallbacks[intersectedItem], callbackParameter); } + // Open options panel after changing tool so that options values can be applied to the tool. + if (intersectionOverlays === menuOverlays) { + openOptions(intersectionProperties[intersectedItem].toolOptions); + } } } From 23fab65f2759701ce81e0d2c2f5f3a8c92cae646 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 5 Aug 2017 12:08:36 +1200 Subject: [PATCH 141/722] Tidying --- scripts/vr-edit/modules/selection.js | 3 +++ scripts/vr-edit/vr-edit.js | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 7234553a9d..5425fb7153 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -329,6 +329,9 @@ Selection = function (side) { function applyColor(color) { // Entities without a color property simply ignore the edit. + var i, + length; + for (i = 0, length = selection.length; i < length; i += 1) { Entities.editEntity(selection[i].id, { color: color diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 72232d6101..4c26f46d0d 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -217,7 +217,7 @@ toolIcon.setColor(color); } - function clearToolIcon() { + function clearTool() { toolIcon.clear(); toolMenu.clearTool(); } @@ -272,7 +272,7 @@ setHand: setHand, setToolIcon: setToolIcon, setToolColor: setToolColor, - clearToolIcon: clearToolIcon, + clearTool: clearTool, SCALE_TOOL: toolIcon.SCALE_TOOL, CLONE_TOOL: toolIcon.CLONE_TOOL, GROUP_TOOL: toolIcon.GROUP_TOOL, @@ -803,7 +803,7 @@ if (!wasGripClicked && isGripClicked && (toolSelected !== TOOL_NONE)) { toolSelected = TOOL_NONE; grouping.clear(); - ui.clearToolIcon(); + ui.clearTool(); } } From e6456ca5010f1f5724fd7c7a38924a04e58f016a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 5 Aug 2017 17:08:56 +1200 Subject: [PATCH 142/722] Tidy and fix options handling --- scripts/vr-edit/modules/toolMenu.js | 108 ++++++++++------------------ scripts/vr-edit/vr-edit.js | 2 + 2 files changed, 38 insertions(+), 72 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index c241ad9740..a915fd6093 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -10,7 +10,7 @@ /* global ToolMenu */ -ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { +ToolMenu = function (side, leftInputs, rightInputs, doCallback) { // Tool menu displayed on top of forearm. "use strict"; @@ -21,16 +21,10 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { menuPanelOverlay, menuOverlays = [], - menuCallbacks = [], optionsOverlays = [], - optionsOverlaysIDs = [], - optionsCommands = [], - optionsCommandsParameters = [], - optionsCallbacks = [], - optionsCallbacksParameters = [], + optionsOverlaysIDs = [], // Text ids (names) of options overlays. optionsEnabled = [], - optionsSettings = {}, highlightOverlay, @@ -224,8 +218,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { setting: { key: "VREdit.colorTool.currentColor", property: "color", - defaultValue: { red: 128, green: 128, blue: 128 }, - callback: "setColor" + defaultValue: { red: 128, green: 128, blue: 128 } } } ] @@ -236,7 +229,6 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { MENU_ITEMS = [ { - // Background element id: "toolsMenuPanel", type: "panel", properties: { @@ -283,7 +275,8 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { }, label: " COLOR", toolOptions: "colorOptions", - callback: "colorTool" + callback: "colorTool", + callbackParameter: "currentColor.color" }, { id: "deleteButton", @@ -317,13 +310,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { optionsItems, intersectionOverlays, - intersectionOverlaysIDs, - intersectionCommands, - intersectionCommandsParameters, - intersectionCallbacks, - intersectionCallbacksParameters, intersectionEnabled, - intersectionProperties, highlightedItem, highlightedSource, isHighlightingButton, @@ -371,11 +358,8 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { Overlays.deleteOverlay(optionsOverlays[i]); } optionsOverlays = []; + optionsOverlaysIDs = []; - optionsCommands = []; - optionsCommandsParameters = []; - optionsCallbacks = []; - optionsCallbacksParameters = []; optionsEnabled = []; optionsItems = null; @@ -394,7 +378,9 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { value = optionsItems[i].setting.defaultValue; } properties[optionsItems[i].setting.property] = value; - commandCallback(optionsItems[i].setting.callback, value); // Apply setting. + if (optionsItems[i].setting.callback) { + doCallback(optionsItems[i].setting.callback, value); + } } optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[optionsItems[i].type].overlay, properties)); optionsOverlaysIDs.push(optionsItems[i].id); @@ -405,10 +391,6 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); } parentID = optionsOverlays[0]; // Menu buttons parent to menu panel. - optionsCommands.push(optionsItems[i].command); - optionsCommandsParameters.push(optionsItems[i].commandParameter); - optionsCallbacks.push(optionsItems[i].callback); - optionsCallbacksParameters.push(optionsItems[i].callbackParameter); optionsEnabled.push(true); } } @@ -436,7 +418,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { return Overlays.getProperty(optionsOverlays[optionsOverlaysIDs.indexOf(overlayID)], overlayProperty); } - function peformCommand(command, parameter) { + function doCommand(command, parameter) { switch (command) { case "setCurrentColor": Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], { @@ -453,11 +435,11 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { } function update(intersectionOverlayID, groupsCount, entitiesCount) { - var intersectedItem, + var intersectedItem = -1, + intersectionItems, parentProperties, BUTTON_PRESS_DELTA = 0.004, - commandParameter, - callbackParameter, + parameterValue, enableGroupButton, enableUngroupButton; @@ -465,25 +447,15 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { if (intersectionOverlayID) { intersectedItem = menuOverlays.indexOf(intersectionOverlayID); if (intersectedItem !== -1) { + intersectionItems = MENU_ITEMS; intersectionOverlays = menuOverlays; - intersectionOverlaysIDs = null; - intersectionCommands = null; - intersectionCommandsParameters = null; - intersectionCallbacks = menuCallbacks; - intersectionCallbacksParameters = null; intersectionEnabled = null; - intersectionProperties = MENU_ITEMS; } else { intersectedItem = optionsOverlays.indexOf(intersectionOverlayID); if (intersectedItem !== -1) { + intersectionItems = optionsItems; intersectionOverlays = optionsOverlays; - intersectionOverlaysIDs = optionsOverlaysIDs; - intersectionCommands = optionsCommands; - intersectionCommandsParameters = optionsCommandsParameters; - intersectionCallbacks = optionsCallbacks; - intersectionCallbacksParameters = optionsCallbacksParameters; intersectionEnabled = optionsEnabled; - intersectionProperties = optionsItems; } } } @@ -493,8 +465,8 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { // Highlight clickable item. if (intersectedItem !== highlightedItem || intersectionOverlays !== highlightedSource) { - if (intersectedItem !== -1 && intersectionCallbacks[intersectedItem] !== undefined) { - // Highlight new button. + if (intersectedItem !== -1 && intersectionItems[intersectedItem].callback !== undefined) { + // Highlight new button. (The existence of a callback infers that the item is a button.) parentProperties = Overlays.getProperties(intersectionOverlays[intersectedItem], ["dimensions", "localPosition"]); Overlays.editOverlay(highlightOverlay, { @@ -527,37 +499,38 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { || controlHand.triggerClicked() !== isButtonPressed) { if (pressedItem !== NONE) { // Unpress previous button. - Overlays.editOverlay(intersectionOverlays[pressedItem], { - localPosition: intersectionProperties[pressedItem].properties.localPosition - }); + if (intersectionItems) { + Overlays.editOverlay(intersectionOverlays[pressedItem], { + localPosition: intersectionItems[pressedItem].properties.localPosition + }); + } pressedItem = NONE; } isButtonPressed = isHighlightingButton && controlHand.triggerClicked(); if (isButtonPressed && (intersectionEnabled === null || intersectionEnabled[intersectedItem])) { // Press new button. Overlays.editOverlay(intersectionOverlays[intersectedItem], { - localPosition: Vec3.sum(intersectionProperties[intersectedItem].properties.localPosition, + localPosition: Vec3.sum(intersectionItems[intersectedItem].properties.localPosition, { x: 0, y: 0, z: BUTTON_PRESS_DELTA }) }); pressedItem = intersectedItem; pressedSource = intersectionOverlays; // Button press actions. - if (intersectionCommands && intersectionCommands[intersectedItem]) { - if (intersectionCommandsParameters && intersectionCommandsParameters[intersectedItem]) { - commandParameter = evaluateParameter(intersectionCommandsParameters[intersectedItem]); - } - peformCommand(intersectionCommands[intersectedItem], commandParameter); - } - if (intersectionCallbacks[intersectedItem]) { - if (intersectionCallbacksParameters && intersectionCallbacksParameters[intersectedItem]) { - callbackParameter = evaluateParameter(intersectionCallbacksParameters[intersectedItem]); - } - commandCallback(intersectionCallbacks[intersectedItem], callbackParameter); - } - // Open options panel after changing tool so that options values can be applied to the tool. if (intersectionOverlays === menuOverlays) { - openOptions(intersectionProperties[intersectedItem].toolOptions); + openOptions(intersectionItems[intersectedItem].toolOptions); + } + if (intersectionItems[intersectedItem].command) { + if (intersectionItems[intersectedItem].callbackParameter) { + parameterValue = evaluateParameter(intersectionItems[intersectedItem].commandParameter); + } + doCommand(intersectionItems[intersectedItem].command, parameterValue); + } + if (intersectionItems[intersectedItem].callback) { + if (intersectionItems[intersectedItem].callbackParameter) { + parameterValue = evaluateParameter(intersectionItems[intersectedItem].callbackParameter); + } + doCallback(intersectionItems[intersectedItem].callback, parameterValue); } } } @@ -634,7 +607,6 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); } parentID = menuOverlays[0]; // Menu buttons parent to menu panel. - menuCallbacks.push(MENU_ITEMS[i].callback); } // Prepare highlight overlay. @@ -645,13 +617,7 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { // Initial values. optionsItems = null; intersectionOverlays = null; - intersectionOverlaysIDs = null; - intersectionCommands = null; - intersectionCommandsParameters = null; - intersectionCallbacks = null; - intersectionCallbacksParameters = null; intersectionEnabled = null; - intersectionProperties = null; highlightedItem = NONE; highlightedSource = null; isHighlightingButton = false; @@ -678,13 +644,11 @@ ToolMenu = function (side, leftInputs, rightInputs, commandCallback) { Overlays.deleteOverlay(optionsOverlays[i]); } optionsOverlays = []; - optionsCallbacks = []; for (i = 0, length = menuOverlays.length; i < length; i += 1) { Overlays.deleteOverlay(menuOverlays[i]); } menuOverlays = []; - menuCallbacks = []; Overlays.deleteOverlay(menuPanelOverlay); Overlays.deleteOverlay(menuOriginOverlay); diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 4c26f46d0d..745cb15d04 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1261,6 +1261,8 @@ case "colorTool": toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); + ui.setToolColor(parameter); + colorToolColor = parameter; ui.updateUIEntities(); break; case "deleteTool": From bff3ad342da01a4ddc608db4b72af9848eeda8de Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 5 Aug 2017 17:37:40 +1200 Subject: [PATCH 143/722] Tidy command and callback handling --- scripts/vr-edit/modules/toolMenu.js | 92 +++++++++++++++++++---------- 1 file changed, 61 insertions(+), 31 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index a915fd6093..bfde776509 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -134,7 +134,9 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { }, label: " GROUP", enabledColor: { red: 64, green: 240, blue: 64 }, - callback: "groupButton" + callback: { + method: "groupButton" + } }, { id: "ungroupButton", @@ -146,7 +148,9 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { }, label: "UNGROUP", enabledColor: { red: 240, green: 64, blue: 64 }, - callback: "ungroupButton" + callback: { + method: "ungroupButton" + } } ], colorOptions: [ @@ -165,10 +169,14 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { localPosition: { x: -0.035, y: 0.02, z: -0.005 }, color: { red: 255, green: 0, blue: 0 } }, - command: "setCurrentColor", - commandParameter: "colorSwatch1.color", - callback: "setColor", - callbackParameter: "colorSwatch1.color" + command: { + method: "setCurrentColor", + parameter: "colorSwatch1.color" + }, + callback: { + method: "setColor", + parameter: "colorSwatch1.color" + } }, { id: "colorSwatch2", @@ -178,10 +186,14 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { localPosition: { x: -0.01, y: 0.02, z: -0.005 }, color: { red: 0, green: 255, blue: 0 } }, - command: "setCurrentColor", - commandParameter: "colorSwatch2.color", - callback: "setColor", - callbackParameter: "colorSwatch2.color" + command: { + method: "setCurrentColor", + parameter: "colorSwatch2.color" + }, + callback: { + method: "setColor", + parameter: "colorSwatch2.color" + } }, { id: "colorSwatch3", @@ -191,10 +203,14 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { localPosition: { x: -0.035, y: 0.045, z: -0.005 }, color: { red: 0, green: 0, blue: 255 } }, - command: "setCurrentColor", - commandParameter: "colorSwatch3.color", - callback: "setColor", - callbackParameter: "colorSwatch3.color" + command: { + method: "setCurrentColor", + parameter: "colorSwatch3.color" + }, + callback: { + method: "setColor", + parameter: "colorSwatch3.color" + } }, { id: "colorSwatch4", @@ -204,10 +220,14 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { localPosition: { x: -0.01, y: 0.045, z: -0.005 }, color: { red: 255, green: 255, blue: 255 } }, - command: "setCurrentColor", - commandParameter: "colorSwatch4.color", - callback: "setColor", - callbackParameter: "colorSwatch4.color" + command: { + method: "setCurrentColor", + parameter: "colorSwatch4.color" + }, + callback: { + method: "setColor", + parameter: "colorSwatch4.color" + } }, { id: "currentColor", @@ -243,7 +263,9 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { color: { red: 0, green: 240, blue: 240 } }, label: " SCALE", - callback: "scaleTool" + callback: { + method: "scaleTool" + } }, { id: "cloneButton", @@ -253,7 +275,9 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { color: { red: 240, green: 240, blue: 0 } }, label: " CLONE", - callback: "cloneTool" + callback: { + method: "cloneTool" + } }, { id: "groupButton", @@ -264,7 +288,9 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { }, label: " GROUP", toolOptions: "groupOptions", - callback: "groupTool" + callback: { + method: "groupTool" + } }, { id: "colorButton", @@ -275,8 +301,10 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { }, label: " COLOR", toolOptions: "colorOptions", - callback: "colorTool", - callbackParameter: "currentColor.color" + callback: { + method: "colorTool", + parameter: "currentColor.color" + } }, { id: "deleteButton", @@ -286,7 +314,9 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { color: { red: 240, green: 60, blue: 60 } }, label: " DELETE", - callback: "deleteTool" + callback: { + method: "deleteTool" + } } ], @@ -379,7 +409,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { } properties[optionsItems[i].setting.property] = value; if (optionsItems[i].setting.callback) { - doCallback(optionsItems[i].setting.callback, value); + doCallback(optionsItems[i].setting.callback.method, value); } } optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[optionsItems[i].type].overlay, properties)); @@ -521,16 +551,16 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { openOptions(intersectionItems[intersectedItem].toolOptions); } if (intersectionItems[intersectedItem].command) { - if (intersectionItems[intersectedItem].callbackParameter) { - parameterValue = evaluateParameter(intersectionItems[intersectedItem].commandParameter); + if (intersectionItems[intersectedItem].command.parameter) { + parameterValue = evaluateParameter(intersectionItems[intersectedItem].command.parameter); } - doCommand(intersectionItems[intersectedItem].command, parameterValue); + doCommand(intersectionItems[intersectedItem].command.method, parameterValue); } if (intersectionItems[intersectedItem].callback) { - if (intersectionItems[intersectedItem].callbackParameter) { - parameterValue = evaluateParameter(intersectionItems[intersectedItem].callbackParameter); + if (intersectionItems[intersectedItem].callback.parameter) { + parameterValue = evaluateParameter(intersectionItems[intersectedItem].callback.parameter); } - doCallback(intersectionItems[intersectedItem].callback, parameterValue); + doCallback(intersectionItems[intersectedItem].callback.method, parameterValue); } } } From 4e87c1302023d6c6e401dbb0294b3cd0fd4f588b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 5 Aug 2017 17:52:54 +1200 Subject: [PATCH 144/722] Tidy group buttons' enabling --- scripts/vr-edit/modules/toolMenu.js | 38 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index bfde776509..ea07ea52c4 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -244,9 +244,6 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { ] }, - GROUP_BUTTON_INDEX = 1, - UNGROUP_BUTTON_INDEX = 2, - MENU_ITEMS = [ { id: "toolsMenuPanel", @@ -347,8 +344,11 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { pressedItem, pressedSource, isButtonPressed, + isGroupButtonEnabled, isUngroupButtonEnabled, + groupButtonIndex, + ungroupButtonIndex, isDisplaying = false, @@ -427,8 +427,8 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { // Special handling for Group options. if (toolOptions === "groupOptions") { - optionsEnabled[GROUP_BUTTON_INDEX] = false; - optionsEnabled[UNGROUP_BUTTON_INDEX] = false; + optionsEnabled[groupButtonIndex] = false; + optionsEnabled[ungroupButtonIndex] = false; } } @@ -570,23 +570,23 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { enableGroupButton = groupsCount > 1; if (enableGroupButton !== isGroupButtonEnabled) { isGroupButtonEnabled = enableGroupButton; - Overlays.editOverlay(optionsOverlays[GROUP_BUTTON_INDEX], { + Overlays.editOverlay(optionsOverlays[groupButtonIndex], { color: isGroupButtonEnabled - ? OPTONS_PANELS.groupOptions[GROUP_BUTTON_INDEX].enabledColor - : OPTONS_PANELS.groupOptions[GROUP_BUTTON_INDEX].properties.color + ? OPTONS_PANELS.groupOptions[groupButtonIndex].enabledColor + : OPTONS_PANELS.groupOptions[groupButtonIndex].properties.color }); - optionsEnabled[GROUP_BUTTON_INDEX] = enableGroupButton; + optionsEnabled[groupButtonIndex] = enableGroupButton; } enableUngroupButton = groupsCount === 1 && entitiesCount > 1; if (enableUngroupButton !== isUngroupButtonEnabled) { isUngroupButtonEnabled = enableUngroupButton; - Overlays.editOverlay(optionsOverlays[UNGROUP_BUTTON_INDEX], { + Overlays.editOverlay(optionsOverlays[ungroupButtonIndex], { color: isUngroupButtonEnabled - ? OPTONS_PANELS.groupOptions[UNGROUP_BUTTON_INDEX].enabledColor - : OPTONS_PANELS.groupOptions[UNGROUP_BUTTON_INDEX].properties.color + ? OPTONS_PANELS.groupOptions[ungroupButtonIndex].enabledColor + : OPTONS_PANELS.groupOptions[ungroupButtonIndex].properties.color }); - optionsEnabled[UNGROUP_BUTTON_INDEX] = enableUngroupButton; + optionsEnabled[ungroupButtonIndex] = enableUngroupButton; } } } @@ -596,6 +596,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { var handJointIndex, properties, parentID, + id, i, length; @@ -657,6 +658,17 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { isGroupButtonEnabled = false; isUngroupButtonEnabled = false; + // Special handling for Group options. + for (i = 0, length = OPTONS_PANELS.groupOptions.length; i < length; i += 1) { + id = OPTONS_PANELS.groupOptions[i].id; + if (id === "groupButton") { + groupButtonIndex = i; + } + if (id === "ungroupButton") { + ungroupButtonIndex = i; + } + } + isDisplaying = true; } From 0f44e36128685b0968cff16085d189f4c3a2abdc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 5 Aug 2017 18:04:39 +1200 Subject: [PATCH 145/722] Fix buttons sometimes staying pressed --- scripts/vr-edit/modules/toolMenu.js | 30 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index ea07ea52c4..77d52ec7bb 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -341,7 +341,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { highlightedItem, highlightedSource, isHighlightingButton, - pressedItem, + pressedItem = null, pressedSource, isButtonPressed, @@ -468,6 +468,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { var intersectedItem = -1, intersectionItems, parentProperties, + localPosition, BUTTON_PRESS_DELTA = 0.004, parameterValue, enableGroupButton, @@ -524,27 +525,28 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { highlightedSource = intersectionOverlays; } - // Press button. - if (intersectedItem !== pressedItem || intersectionOverlays !== pressedSource + // Press/unpress button. + if ((pressedItem && intersectedItem !== pressedItem.index) || intersectionOverlays !== pressedSource || controlHand.triggerClicked() !== isButtonPressed) { - if (pressedItem !== NONE) { + if (pressedItem) { // Unpress previous button. - if (intersectionItems) { - Overlays.editOverlay(intersectionOverlays[pressedItem], { - localPosition: intersectionItems[pressedItem].properties.localPosition - }); - } - pressedItem = NONE; + Overlays.editOverlay(intersectionOverlays[pressedItem.index], { + localPosition: pressedItem.localPosition + }); + pressedItem = null; } isButtonPressed = isHighlightingButton && controlHand.triggerClicked(); if (isButtonPressed && (intersectionEnabled === null || intersectionEnabled[intersectedItem])) { // Press new button. + localPosition = intersectionItems[intersectedItem].properties.localPosition; Overlays.editOverlay(intersectionOverlays[intersectedItem], { - localPosition: Vec3.sum(intersectionItems[intersectedItem].properties.localPosition, - { x: 0, y: 0, z: BUTTON_PRESS_DELTA }) + localPosition: Vec3.sum(localPosition, { x: 0, y: 0, z: BUTTON_PRESS_DELTA }) }); - pressedItem = intersectedItem; pressedSource = intersectionOverlays; + pressedItem = { + index: intersectedItem, + localPosition: localPosition + }; // Button press actions. if (intersectionOverlays === menuOverlays) { @@ -652,7 +654,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { highlightedItem = NONE; highlightedSource = null; isHighlightingButton = false; - pressedItem = NONE; + pressedItem = null; pressedSource = null; isButtonPressed = false; isGroupButtonEnabled = false; From 1231491a722d218549ae5ee78c758e36badaa5ae Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 7 Aug 2017 17:29:58 +1200 Subject: [PATCH 146/722] Add color picker --- scripts/vr-edit/modules/selection.js | 37 +++++++++++++++++++++---- scripts/vr-edit/modules/toolIcon.js | 5 +++- scripts/vr-edit/modules/toolMenu.js | 20 ++++++++++++-- scripts/vr-edit/vr-edit.js | 41 ++++++++++++++++++++++++++-- 4 files changed, 92 insertions(+), 11 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 5425fb7153..9836a1fed6 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -23,7 +23,8 @@ Selection = function (side) { scaleCenter, scaleRootOffset, scaleRootOrientation, - ENTITY_TYPE = "entity"; + ENTITY_TYPE = "entity", + ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "ParticleEffect"]; if (!this instanceof Selection) { return new Selection(side); @@ -329,14 +330,39 @@ Selection = function (side) { function applyColor(color) { // Entities without a color property simply ignore the edit. - var i, + var properties, + isError = true, + i, length; for (i = 0, length = selection.length; i < length; i += 1) { - Entities.editEntity(selection[i].id, { - color: color - }); + properties = Entities.getEntityProperties(selection[i].id, "color"); + if (ENTITY_TYPES_WITH_COLOR.indexOf(properties.type) !== -1) { + Entities.editEntity(selection[i].id, { + color: color + }); + isError = false; + } } + + if (isError) { + // TODO + print("TODO: Error beep"); + } + } + + function getColor(entityID) { + var properties; + + properties = Entities.getEntityProperties(entityID, "color"); + if (ENTITY_TYPES_WITH_COLOR.indexOf(properties.type) === -1) { + // Some entities don't have a color property. + // TODO + print("TODO: Error beep"); + return null; + } + + return properties.color; } function clear() { @@ -374,6 +400,7 @@ Selection = function (side) { finishEditing: finishEditing, cloneEntities: cloneEntities, applyColor: applyColor, + getColor: getColor, deleteEntities: deleteEntities, clear: clear, destroy: destroy diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 41a6294820..a223fdb923 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -19,13 +19,15 @@ ToolIcon = function (side) { CLONE_TOOL = 1, GROUP_TOOL = 2, COLOR_TOOL = 3, - DELETE_TOOL = 4, + PICK_COLOR_TOOL = 4, + DELETE_TOOL = 5, ICON_COLORS = [ { red: 0, green: 240, blue: 240 }, { red: 240, green: 240, blue: 0 }, { red: 220, green: 60, blue: 220 }, { red: 220, green: 220, blue: 220 }, + { red: 0, green: 0, blue: 0 }, { red: 240, green: 60, blue: 60 } ], @@ -111,6 +113,7 @@ ToolIcon = function (side) { CLONE_TOOL: CLONE_TOOL, GROUP_TOOL: GROUP_TOOL, COLOR_TOOL: COLOR_TOOL, + PICK_COLOR_TOOL: PICK_COLOR_TOOL, DELETE_TOOL: DELETE_TOOL, setHand: setHand, update: update, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 77d52ec7bb..322e75801b 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -103,7 +103,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { "circle": { overlay: "circle3d", properties: { - size: 0.015, + size: 0.01, localPosition: { x: 0.0, y: 0.0, z: -0.01 }, localRotation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 180 }), color: { red: 128, green: 128, blue: 128 }, @@ -233,13 +233,26 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { id: "currentColor", type: "circle", properties: { - localPosition: { x: 0.025, y: 0.0325, z: -0.007 } + localPosition: { x: 0.025, y: 0.02, z: -0.007 } }, setting: { key: "VREdit.colorTool.currentColor", property: "color", defaultValue: { red: 128, green: 128, blue: 128 } } + }, + { + id: "pickColor", + type: "button", + properties: { + dimensions: { x: 0.04, y: 0.02, z: 0.01 }, + localPosition: { x: 0.025, y: 0.045, z: -0.005 }, + color: { red: 255, green: 255, blue: 255 } + }, + label: " PICK", + callback: { + method: "pickColorTool" + } } ] }, @@ -662,7 +675,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { // Special handling for Group options. for (i = 0, length = OPTONS_PANELS.groupOptions.length; i < length; i += 1) { - id = OPTONS_PANELS.groupOptions[i].id; + id = OPTONS_PANELS.groupOptions[i].id; if (id === "groupButton") { groupButtonIndex = i; } @@ -708,6 +721,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { setHand: setHand, entityIDs: getEntityIDs, clearTool: clearTool, + doCommand: doCommand, update: update, display: display, clear: clear, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 745cb15d04..34cf042398 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -27,7 +27,8 @@ TOOL_CLONE = 2, TOOL_GROUP = 3, TOOL_COLOR = 4, - TOOL_DELETE = 5, + TOOL_PICK_COLOR = 5, + TOOL_DELETE = 6, toolSelected = TOOL_NONE, colorToolColor = { red: 128, green: 128, blue: 128 }, @@ -243,6 +244,10 @@ } } + function doPickColor(color) { + toolMenu.doCommand("setCurrentColor", color); + } + function clear() { leftInputs.setUIEntities([]); rightInputs.setUIEntities([]); @@ -277,9 +282,11 @@ CLONE_TOOL: toolIcon.CLONE_TOOL, GROUP_TOOL: toolIcon.GROUP_TOOL, COLOR_TOOL: toolIcon.COLOR_TOOL, + PICK_COLOR_TOOL: toolIcon.PICK_COLOR_TOOL, DELETE_TOOL: toolIcon.DELETE_TOOL, display: display, updateUIEntities: setUIEntities, + doPickColor: doPickColor, update: update, clear: clear, destroy: destroy @@ -810,7 +817,8 @@ function update() { var previousState = editorState, - doUpdateState; + doUpdateState, + color; intersection = getIntersection(); isTriggerClicked = hand.triggerClicked(); @@ -856,6 +864,16 @@ setState(EDITOR_GROUPING); } else if (toolSelected === TOOL_COLOR) { selection.applyColor(colorToolColor); + } else if (toolSelected === TOOL_PICK_COLOR) { + color = selection.getColor(intersection.entityID); + if (color) { + colorToolColor = color; + ui.doPickColor(colorToolColor); + ui.setToolColor(colorToolColor); + } + toolSelected = TOOL_COLOR; + ui.setToolIcon(ui.COLOR_TOOL); + ui.setToolColor(colorToolColor); } else if (toolSelected === TOOL_DELETE) { setState(EDITOR_HIGHLIGHTING); selection.deleteEntities(); @@ -913,6 +931,16 @@ setState(EDITOR_GROUPING); } else if (toolSelected === TOOL_COLOR) { selection.applyColor(colorToolColor); + } else if (toolSelected === TOOL_PICK_COLOR) { + color = selection.getColor(intersection.entityID); + if (color) { + colorToolColor = color; + ui.doPickColor(colorToolColor); + ui.setToolColor(colorToolColor); + } + toolSelected = TOOL_COLOR; + ui.setToolIcon(ui.COLOR_TOOL); + ui.setToolColor(colorToolColor); } else if (toolSelected === TOOL_DELETE) { selection.deleteEntities(); setState(EDITOR_SEARCHING); @@ -1265,6 +1293,11 @@ colorToolColor = parameter; ui.updateUIEntities(); break; + case "pickColorTool": + toolSelected = TOOL_PICK_COLOR; + ui.setToolIcon(ui.PICK_COLOR_TOOL); + ui.updateUIEntities(); + break; case "deleteTool": grouping.clear(); toolSelected = TOOL_DELETE; @@ -1278,6 +1311,10 @@ grouping.ungroup(); break; case "setColor": + if (toolSelected === TOOL_PICK_COLOR) { + toolSelected = TOOL_COLOR; + ui.setToolIcon(ui.COLOR_TOOL); + } ui.setToolColor(parameter); colorToolColor = parameter; break; From 6621b43fae91a2a12b7d30e341ff32d370eb8d82 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 8 Aug 2017 16:34:45 +1200 Subject: [PATCH 147/722] Make some swatches start with "no color"; color them when clicked --- scripts/vr-edit/modules/toolMenu.js | 94 ++++++++++++++++++----------- scripts/vr-edit/vr-edit.js | 2 +- 2 files changed, 61 insertions(+), 35 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 322e75801b..3385d1fd36 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -82,6 +82,17 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { visible: true } }, + "swatch": { + overlay: "cube", + properties: { + dimensions: { x: 0.03, y: 0.03, z: 0.01 }, + localRotation: Quat.ZERO, + alpha: 1.0, + solid: false, // False indicates "no color assigned" + ignoreRayIntersection: false, + visible: true + } + }, "label": { overlay: "text3d", properties: { @@ -163,69 +174,57 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { }, { id: "colorSwatch1", - type: "button", + type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, localPosition: { x: -0.035, y: 0.02, z: -0.005 }, - color: { red: 255, green: 0, blue: 0 } + color: { red: 255, green: 0, blue: 0 }, + solid: true }, command: { - method: "setCurrentColor", - parameter: "colorSwatch1.color" - }, - callback: { - method: "setColor", + method: "setColorPerSwatch", parameter: "colorSwatch1.color" } }, { id: "colorSwatch2", - type: "button", + type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, localPosition: { x: -0.01, y: 0.02, z: -0.005 }, - color: { red: 0, green: 255, blue: 0 } + color: { red: 0, green: 255, blue: 0 }, + solid: true }, command: { - method: "setCurrentColor", - parameter: "colorSwatch2.color" - }, - callback: { - method: "setColor", + method: "setColorPerSwatch", parameter: "colorSwatch2.color" } }, { id: "colorSwatch3", - type: "button", + type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, localPosition: { x: -0.035, y: 0.045, z: -0.005 }, - color: { red: 0, green: 0, blue: 255 } + color: { red: 128, green: 128, blue: 128 }, + solid: false }, command: { - method: "setCurrentColor", - parameter: "colorSwatch3.color" - }, - callback: { - method: "setColor", + method: "setColorPerSwatch", parameter: "colorSwatch3.color" } }, { id: "colorSwatch4", - type: "button", + type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, localPosition: { x: -0.01, y: 0.045, z: -0.005 }, - color: { red: 255, green: 255, blue: 255 } + color: { red: 128, green: 128, blue: 128 }, + solid: false }, command: { - method: "setCurrentColor", - parameter: "colorSwatch4.color" - }, - callback: { - method: "setColor", + method: "setColorPerSwatch", parameter: "colorSwatch4.color" } }, @@ -462,8 +461,34 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { } function doCommand(command, parameter) { + var parameters, + hasColor, + value; + switch (command) { - case "setCurrentColor": + case "setColorPerSwatch": + parameters = parameter.split("."); + hasColor = Overlays.getProperty(optionsOverlays[optionsOverlaysIDs.indexOf(parameters[0])], "solid"); + if (hasColor) { + // Swatch has a color; set current fill color to swatch color. + value = evaluateParameter(parameter); + Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], { + color: value + }); + if (optionsSettings.currentColor) { + Settings.setValue(optionsSettings.currentColor.key, value); + } + doCallback("setColor", value); + } else { + // Swatch has no color; set swatch color to current fill color. + value = Overlays.getProperty(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], "color"); + Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf(parameters[0])], { + color: value, + solid: true + }); + } + break; + case "setColorFromPick": Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], { color: parameter }); @@ -474,7 +499,6 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { default: // TODO: Log error. } - } function update(intersectionOverlayID, groupsCount, entitiesCount) { @@ -483,6 +507,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { parentProperties, localPosition, BUTTON_PRESS_DELTA = 0.004, + parameter, parameterValue, enableGroupButton, enableUngroupButton; @@ -509,8 +534,9 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { // Highlight clickable item. if (intersectedItem !== highlightedItem || intersectionOverlays !== highlightedSource) { - if (intersectedItem !== -1 && intersectionItems[intersectedItem].callback !== undefined) { - // Highlight new button. (The existence of a callback infers that the item is a button.) + if (intersectedItem !== -1 && (intersectionItems[intersectedItem].command !== undefined + || intersectionItems[intersectedItem].callback !== undefined)) { + // Highlight new button. (The existence of a command or callback infers that the item is a button.) parentProperties = Overlays.getProperties(intersectionOverlays[intersectedItem], ["dimensions", "localPosition"]); Overlays.editOverlay(highlightOverlay, { @@ -567,9 +593,9 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { } if (intersectionItems[intersectedItem].command) { if (intersectionItems[intersectedItem].command.parameter) { - parameterValue = evaluateParameter(intersectionItems[intersectedItem].command.parameter); + parameter = intersectionItems[intersectedItem].command.parameter; } - doCommand(intersectionItems[intersectedItem].command.method, parameterValue); + doCommand(intersectionItems[intersectedItem].command.method, parameter); } if (intersectionItems[intersectedItem].callback) { if (intersectionItems[intersectedItem].callback.parameter) { diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 34cf042398..576ae44093 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -245,7 +245,7 @@ } function doPickColor(color) { - toolMenu.doCommand("setCurrentColor", color); + toolMenu.doCommand("setColorFromPick", color); } function clear() { From 0b8ea29193b1a329c9f392635dfd4d4a1ac1650f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 8 Aug 2017 20:37:41 +1200 Subject: [PATCH 148/722] Clear swatch with grip click --- scripts/vr-edit/modules/hand.js | 13 +++++++ scripts/vr-edit/modules/toolMenu.js | 56 +++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/scripts/vr-edit/modules/hand.js b/scripts/vr-edit/modules/hand.js index 42d6751ed1..14994c226d 100644 --- a/scripts/vr-edit/modules/hand.js +++ b/scripts/vr-edit/modules/hand.js @@ -21,6 +21,7 @@ Hand = function (side) { controllerGrip, isGripClicked = false, + isGripClickedHandled = false, GRIP_ON_VALUE = 0.99, GRIP_OFF_VALUE = 0.95, @@ -87,6 +88,11 @@ Hand = function (side) { return isGripClicked; } + function setGripClickedHandled() { + isGripClicked = false; + isGripClickedHandled = true; + } + function getIntersection() { return intersection; } @@ -126,6 +132,12 @@ Hand = function (side) { } else { isGripClicked = gripValue > GRIP_ON_VALUE; } + // Grip clicked may be being handled by UI. + if (isGripClicked) { + isGripClicked = !isGripClickedHandled; + } else { + isGripClickedHandled = false; + } // Hand-overlay intersection, if any. overlayID = null; @@ -199,6 +211,7 @@ Hand = function (side) { triggerPressed: triggerPressed, triggerClicked: triggerClicked, gripClicked: gripClicked, + setGripClickedHandled: setGripClickedHandled, intersection: getIntersection, update: update, clear: clear, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 3385d1fd36..5722c61bde 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -58,6 +58,8 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { visible: true }, + NO_SWATCH_COLOR = { red: 128, green: 128, blue: 128 }, + UI_ELEMENTS = { "panel": { overlay: "cube", @@ -87,8 +89,9 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { properties: { dimensions: { x: 0.03, y: 0.03, z: 0.01 }, localRotation: Quat.ZERO, + color: NO_SWATCH_COLOR, alpha: 1.0, - solid: false, // False indicates "no color assigned" + solid: false, // False indicates "no swatch color assigned" ignoreRayIntersection: false, visible: true } @@ -184,6 +187,10 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { command: { method: "setColorPerSwatch", parameter: "colorSwatch1.color" + }, + onGripClicked: { + method: "clearSwatch", + parameter: "colorSwatch1" } }, { @@ -198,6 +205,10 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { command: { method: "setColorPerSwatch", parameter: "colorSwatch2.color" + }, + onGripClicked: { + method: "clearSwatch", + parameter: "colorSwatch2" } }, { @@ -205,13 +216,15 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: 0.045, z: -0.005 }, - color: { red: 128, green: 128, blue: 128 }, - solid: false + localPosition: { x: -0.035, y: 0.045, z: -0.005 } }, command: { method: "setColorPerSwatch", parameter: "colorSwatch3.color" + }, + onGripClicked: { + method: "clearSwatch", + parameter: "colorSwatch3" } }, { @@ -219,13 +232,15 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.01, y: 0.045, z: -0.005 }, - color: { red: 128, green: 128, blue: 128 }, - solid: false + localPosition: { x: -0.01, y: 0.045, z: -0.005 } }, command: { method: "setColorPerSwatch", parameter: "colorSwatch4.color" + }, + onGripClicked: { + method: "clearSwatch", + parameter: "colorSwatch4" } }, { @@ -356,6 +371,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { pressedItem = null, pressedSource, isButtonPressed, + isGripClicked, isGroupButtonEnabled, isUngroupButtonEnabled, @@ -501,6 +517,19 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { } } + function doGripClicked(command, parameter) { + switch (command) { + case "clearSwatch": + Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf(parameter)], { + color: NO_SWATCH_COLOR, + solid: false + }); + break; + default: + // TODO: Log error. + } + } + function update(intersectionOverlayID, groupsCount, entitiesCount) { var intersectedItem = -1, intersectionItems, @@ -606,6 +635,18 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { } } + // Grip click. + if (controlHand.gripClicked() !== isGripClicked) { + isGripClicked = !isGripClicked; + if (isGripClicked && intersectionItems && intersectedItem && intersectionItems[intersectedItem].onGripClicked) { + controlHand.setGripClickedHandled(); + if (intersectionItems[intersectedItem].onGripClicked.parameter) { + parameter = intersectionItems[intersectedItem].onGripClicked.parameter; + } + doGripClicked(intersectionItems[intersectedItem].onGripClicked.method, parameter); + } + } + // Special handling for Group options. if (optionsItems && optionsItems === OPTONS_PANELS.groupOptions) { enableGroupButton = groupsCount > 1; @@ -696,6 +737,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { pressedItem = null; pressedSource = null; isButtonPressed = false; + isGripClicked = false; isGroupButtonEnabled = false; isUngroupButtonEnabled = false; From 42284796a153b886bc15ede7421dd4daead6c296 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 8 Aug 2017 21:44:54 +1200 Subject: [PATCH 149/722] Remember swatch colors --- scripts/vr-edit/modules/toolMenu.js | 50 +++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 5722c61bde..19698ef4c4 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -184,6 +184,11 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { color: { red: 255, green: 0, blue: 0 }, solid: true }, + setting: { + key: "VREdit.colorTool.swatch1Color", + property: "color" + // Default value is set in properties, above. + }, command: { method: "setColorPerSwatch", parameter: "colorSwatch1.color" @@ -202,6 +207,11 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { color: { red: 0, green: 255, blue: 0 }, solid: true }, + setting: { + key: "VREdit.colorTool.swatch2Color", + property: "color" + // Default value is set in properties, above. + }, command: { method: "setColorPerSwatch", parameter: "colorSwatch2.color" @@ -217,6 +227,12 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, localPosition: { x: -0.035, y: 0.045, z: -0.005 } + // Default to empty swatch. + }, + setting: { + key: "VREdit.colorTool.swatch3Color", + property: "color" + // Default value is set in properties, above. }, command: { method: "setColorPerSwatch", @@ -233,6 +249,12 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, localPosition: { x: -0.01, y: 0.045, z: -0.005 } + // Default to empty swatch. + }, + setting: { + key: "VREdit.colorTool.swatch4Color", + property: "color" + // Default value is set in properties, above. }, command: { method: "setColorPerSwatch", @@ -435,9 +457,15 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { if (value === "") { value = optionsItems[i].setting.defaultValue; } - properties[optionsItems[i].setting.property] = value; - if (optionsItems[i].setting.callback) { - doCallback(optionsItems[i].setting.callback.method, value); + if (value) { + properties[optionsItems[i].setting.property] = value; + if (optionsItems[i].type === "swatch") { + // Special case for when swatch color is defined. + properties.solid = true; + } + if (optionsItems[i].setting.callback) { + doCallback(optionsItems[i].setting.callback.method, value); + } } } optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[optionsItems[i].type].overlay, properties)); @@ -478,13 +506,15 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { function doCommand(command, parameter) { var parameters, + overlayID, hasColor, value; switch (command) { case "setColorPerSwatch": parameters = parameter.split("."); - hasColor = Overlays.getProperty(optionsOverlays[optionsOverlaysIDs.indexOf(parameters[0])], "solid"); + overlayID = optionsOverlaysIDs.indexOf(parameters[0]); + hasColor = Overlays.getProperty(optionsOverlays[overlayID], "solid"); if (hasColor) { // Swatch has a color; set current fill color to swatch color. value = evaluateParameter(parameter); @@ -498,10 +528,13 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { } else { // Swatch has no color; set swatch color to current fill color. value = Overlays.getProperty(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], "color"); - Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf(parameters[0])], { + Overlays.editOverlay(optionsOverlays[overlayID], { color: value, solid: true }); + if (optionsSettings[parameters[0]]) { + Settings.setValue(optionsSettings[parameters[0]].key, value); + } } break; case "setColorFromPick": @@ -518,12 +551,17 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { } function doGripClicked(command, parameter) { + var overlayID; switch (command) { case "clearSwatch": - Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf(parameter)], { + overlayID = optionsOverlaysIDs.indexOf(parameter); + Overlays.editOverlay(optionsOverlays[overlayID], { color: NO_SWATCH_COLOR, solid: false }); + if (optionsSettings[parameter]) { + Settings.setValue(optionsSettings[parameter].key, null); // Deleted settings value. + } break; default: // TODO: Log error. From 046ce353fd4a9b8160c5069b2965f671fe5b5edf Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 8 Aug 2017 21:56:01 +1200 Subject: [PATCH 150/722] Fix highlight overlay getting deleted --- scripts/vr-edit/modules/toolMenu.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 19698ef4c4..d96c12096e 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -434,6 +434,9 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { length; // Close current panel, if any. + Overlays.editOverlay(highlightOverlay, { + parentID: menuOriginOverlay + }); for (i = 0, length = optionsOverlays.length; i < length; i += 1) { Overlays.deleteOverlay(optionsOverlays[i]); } From 22432671ca1d15eae53ff89bc877faa9e422ad52 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 9 Aug 2017 09:26:33 +1200 Subject: [PATCH 151/722] Fix newly created entity not being grabbed --- scripts/vr-edit/modules/createPalette.js | 8 +++++--- scripts/vr-edit/modules/toolMenu.js | 8 ++++---- scripts/vr-edit/vr-edit.js | 24 +++++++++++++++++++----- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index be9558e432..846c82dbcf 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -10,7 +10,7 @@ /* global CreatePalette */ -CreatePalette = function (side, leftInputs, rightInputs) { +CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { // Tool menu displayed on top of forearm. "use strict"; @@ -108,7 +108,8 @@ CreatePalette = function (side, leftInputs, rightInputs) { } function update(intersectionOverlayID) { - var CREATE_OFFSET = { x: 0, y: 0.05, z: -0.02 }; + var entityID, + CREATE_OFFSET = { x: 0, y: 0.05, z: -0.02 }; // Highlight cube. if (intersectionOverlayID === cubeOverlay !== isHighlightingCube) { isHighlightingCube = !isHighlightingCube; @@ -126,7 +127,8 @@ CreatePalette = function (side, leftInputs, rightInputs) { Vec3.multiplyQbyV(controlHand.orientation(), Vec3.sum({ x: 0, y: CUBE_ENTITY_PROPERTIES.dimensions.z / 2, z: 0 }, CREATE_OFFSET))); CUBE_ENTITY_PROPERTIES.rotation = controlHand.orientation(); - Entities.addEntity(CUBE_ENTITY_PROPERTIES); + entityID = Entities.addEntity(CUBE_ENTITY_PROPERTIES); + uiCommandCallback("autoGrab"); } else { Overlays.editOverlay(cubeOverlay, { localPosition: CUBE_PROPERTIES.localPosition diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index d96c12096e..63c0037e43 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -10,7 +10,7 @@ /* global ToolMenu */ -ToolMenu = function (side, leftInputs, rightInputs, doCallback) { +ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Tool menu displayed on top of forearm. "use strict"; @@ -467,7 +467,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { properties.solid = true; } if (optionsItems[i].setting.callback) { - doCallback(optionsItems[i].setting.callback.method, value); + uiCommandCallback(optionsItems[i].setting.callback.method, value); } } } @@ -527,7 +527,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { if (optionsSettings.currentColor) { Settings.setValue(optionsSettings.currentColor.key, value); } - doCallback("setColor", value); + uiCommandCallback("setColor", value); } else { // Swatch has no color; set swatch color to current fill color. value = Overlays.getProperty(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], "color"); @@ -671,7 +671,7 @@ ToolMenu = function (side, leftInputs, rightInputs, doCallback) { if (intersectionItems[intersectedItem].callback.parameter) { parameterValue = evaluateParameter(intersectionItems[intersectedItem].callback.parameter); } - doCallback(intersectionItems[intersectedItem].callback.method, parameterValue); + uiCommandCallback(intersectionItems[intersectedItem].callback.method, parameterValue); } } } diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 576ae44093..baea506446 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -179,7 +179,7 @@ }; - UI = function (side, leftInputs, rightInputs, setToolCallback) { + UI = function (side, leftInputs, rightInputs, uiCommandCallback) { // Tool menu and Create palette. var // Primary objects. @@ -197,8 +197,8 @@ } toolIcon = new ToolIcon(otherHand(side)); - toolMenu = new ToolMenu(side, leftInputs, rightInputs, setToolCallback); - createPalette = new CreatePalette(side, leftInputs, rightInputs); + toolMenu = new ToolMenu(side, leftInputs, rightInputs, uiCommandCallback); + createPalette = new CreatePalette(side, leftInputs, rightInputs, uiCommandCallback); getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; @@ -331,6 +331,7 @@ isGripClicked = false, wasGripClicked = false, hoveredOverlayID = null, + isAutoGrab = false, // Position values. initialHandOrientationInverse, @@ -382,6 +383,11 @@ handles.hover(overlayID); } + function enableAutoGrab() { + // Used to grab entity created from Create palette. + isAutoGrab = true; + } + function isHandle(overlayID) { return handles.isHandle(overlayID); } @@ -849,10 +855,12 @@ && otherEditor.isHandle(intersection.overlayID)) { highlightedEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); - } else if (intersection.entityID && intersection.editableEntity && (wasTriggerClicked || !isTriggerClicked)) { + } else if (intersection.entityID && intersection.editableEntity + && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab) { highlightedEntityID = Entities.rootOf(intersection.entityID); setState(EDITOR_HIGHLIGHTING); - } else if (intersection.entityID && intersection.editableEntity && !wasTriggerClicked && isTriggerClicked) { + } else if (intersection.entityID && intersection.editableEntity + && (!wasTriggerClicked || isAutoGrab) && isTriggerClicked) { highlightedEntityID = Entities.rootOf(intersection.entityID); if (otherEditor.isEditing(highlightedEntityID)) { if (toolSelected !== TOOL_SCALE) { @@ -1067,6 +1075,7 @@ wasTriggerClicked = isTriggerClicked; wasGripClicked = isGripClicked; + isAutoGrab = isAutoGrab && isTriggerClicked; if (DEBUG && editorState !== previousState) { debug(side, EDITOR_STATE_STRINGS[editorState]); @@ -1115,6 +1124,7 @@ return { setReferences: setReferences, hoverHandle: hoverHandle, + enableAutoGrab: enableAutoGrab, isHandle: isHandle, isEditing: isEditing, isScaling: isScaling, @@ -1318,6 +1328,10 @@ ui.setToolColor(parameter); colorToolColor = parameter; break; + case "autoGrab": + editors[LEFT_HAND].enableAutoGrab(); + editors[RIGHT_HAND].enableAutoGrab(); + break; default: debug("ERROR: Unexpected command in onUICommand()!"); } From 28f9f9e4d0593ff6fd5750ce2f3c506ed0ea09d1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 9 Aug 2017 11:02:33 +1200 Subject: [PATCH 152/722] Add a "Physics" button and panel --- scripts/vr-edit/modules/toolMenu.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 63c0037e43..65647740fd 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -290,6 +290,15 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { method: "pickColorTool" } } + ], + physicsOptions: [ + { + id: "physicsOptionsPanel", + type: "panel", + properties: { + localPosition: { x: 0.055, y: 0.0, z: -0.005 } + } + } ] }, @@ -352,6 +361,19 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { parameter: "currentColor.color" } }, + { + id: "physicsButton", + type: "button", + properties: { + localPosition: { x: -0.022, y: 0.04, z: -0.005 }, + color: { red: 60, green: 60, blue: 240 } + }, + label: "PHYSICS", + toolOptions: "physicsOptions", + callback: { + method: "physicsTool" + } + }, { id: "deleteButton", type: "button", From c79931106d45f34e0ec922d3d56da10d998707bb Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 9 Aug 2017 12:06:35 +1200 Subject: [PATCH 153/722] Add basic element of slider --- scripts/vr-edit/modules/selection.js | 5 +++++ scripts/vr-edit/modules/toolIcon.js | 5 ++++- scripts/vr-edit/modules/toolMenu.js | 22 ++++++++++++++++++++++ scripts/vr-edit/vr-edit.js | 18 ++++++++++++++++-- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 9836a1fed6..2b1e7e5a37 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -365,6 +365,10 @@ Selection = function (side) { return properties.color; } + function applyPhysics() { + // TODO + } + function clear() { selection = []; selectedEntityID = null; @@ -401,6 +405,7 @@ Selection = function (side) { cloneEntities: cloneEntities, applyColor: applyColor, getColor: getColor, + applyPhysics: applyPhysics, deleteEntities: deleteEntities, clear: clear, destroy: destroy diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index a223fdb923..6ff748ac59 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -20,7 +20,8 @@ ToolIcon = function (side) { GROUP_TOOL = 2, COLOR_TOOL = 3, PICK_COLOR_TOOL = 4, - DELETE_TOOL = 5, + PHYSICS_TOOL = 5, + DELETE_TOOL = 6, ICON_COLORS = [ { red: 0, green: 240, blue: 240 }, @@ -28,6 +29,7 @@ ToolIcon = function (side) { { red: 220, green: 60, blue: 220 }, { red: 220, green: 220, blue: 220 }, { red: 0, green: 0, blue: 0 }, + { red: 60, green: 60, blue: 240 }, { red: 240, green: 60, blue: 60 } ], @@ -114,6 +116,7 @@ ToolIcon = function (side) { GROUP_TOOL: GROUP_TOOL, COLOR_TOOL: COLOR_TOOL, PICK_COLOR_TOOL: PICK_COLOR_TOOL, + PHYSICS_TOOL: PHYSICS_TOOL, DELETE_TOOL: DELETE_TOOL, setHand: setHand, update: update, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 65647740fd..a90cb4f17c 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -126,6 +126,18 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { ignoreRayIntersection: true, visible: true } + }, + "slider": { + overlay: "cube", + properties: { + dimensions: { x: 0.03, y: 0.1, z: 0.01 }, + localRotation: Quat.ZERO, + color: { red: 128, green: 128, blue: 255 }, + alpha: 0.1, + solid: true, + ignoreRayIntersection: false, + visible: true + } } }, @@ -298,6 +310,16 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: 0.055, y: 0.0, z: -0.005 } } + }, + { + id: "physicsSlider", + type: "slider", + properties: { + localPosition: { x: -0.02, y: 0.0, z: -0.005 } + }, + callback: { + method: "setSliderValue" + } } ] }, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index baea506446..7b93fd98b3 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -28,7 +28,8 @@ TOOL_GROUP = 3, TOOL_COLOR = 4, TOOL_PICK_COLOR = 5, - TOOL_DELETE = 6, + TOOL_PHYSICS = 6, + TOOL_DELETE = 7, toolSelected = TOOL_NONE, colorToolColor = { red: 128, green: 128, blue: 128 }, @@ -283,6 +284,7 @@ GROUP_TOOL: toolIcon.GROUP_TOOL, COLOR_TOOL: toolIcon.COLOR_TOOL, PICK_COLOR_TOOL: toolIcon.PICK_COLOR_TOOL, + PHYSICS_TOOL: toolIcon.PHYSICS_TOOL, DELETE_TOOL: toolIcon.DELETE_TOOL, display: display, updateUIEntities: setUIEntities, @@ -882,6 +884,8 @@ toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); ui.setToolColor(colorToolColor); + } else if (toolSelected === TOOL_PHYSICS) { + selection.applyPhysics(); } else if (toolSelected === TOOL_DELETE) { setState(EDITOR_HIGHLIGHTING); selection.deleteEntities(); @@ -949,6 +953,8 @@ toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); ui.setToolColor(colorToolColor); + } else if (toolSelected === TOOL_PHYSICS) { + selection.applyPhysics(); } else if (toolSelected === TOOL_DELETE) { selection.deleteEntities(); setState(EDITOR_SEARCHING); @@ -1308,6 +1314,11 @@ ui.setToolIcon(ui.PICK_COLOR_TOOL); ui.updateUIEntities(); break; + case "physicsTool": + toolSelected = TOOL_PHYSICS; + ui.setToolIcon(ui.PHYSICS_TOOL); + ui.updateUIEntities(); + break; case "deleteTool": grouping.clear(); toolSelected = TOOL_DELETE; @@ -1332,8 +1343,11 @@ editors[LEFT_HAND].enableAutoGrab(); editors[RIGHT_HAND].enableAutoGrab(); break; + case "setSliderValue": + print("$$$$$$$ setSliderValue = " + JSON.stringify(parameter)); + break; default: - debug("ERROR: Unexpected command in onUICommand()!"); + debug("ERROR: Unexpected command in onUICommand()! " + command); } } From 3d4cec63cd3a361dbea08e872112c405b96315ad Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 9 Aug 2017 14:36:17 +1200 Subject: [PATCH 154/722] Calculate slider value from laser intersection --- scripts/vr-edit/modules/toolMenu.js | 29 ++++++++++++++++++++++++----- scripts/vr-edit/vr-edit.js | 12 +++++++++--- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index a90cb4f17c..8c3f1dd8d9 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -615,7 +615,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } - function update(intersectionOverlayID, groupsCount, entitiesCount) { + function update(intersection, groupsCount, entitiesCount) { var intersectedItem = -1, intersectionItems, parentProperties, @@ -624,17 +624,21 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { parameter, parameterValue, enableGroupButton, - enableUngroupButton; + enableUngroupButton, + sliderProperties, + overlayDimensions, + basePoint, + fraction; // Intersection details. - if (intersectionOverlayID) { - intersectedItem = menuOverlays.indexOf(intersectionOverlayID); + if (intersection.overlayID) { + intersectedItem = menuOverlays.indexOf(intersection.overlayID); if (intersectedItem !== -1) { intersectionItems = MENU_ITEMS; intersectionOverlays = menuOverlays; intersectionEnabled = null; } else { - intersectedItem = optionsOverlays.indexOf(intersectionOverlayID); + intersectedItem = optionsOverlays.indexOf(intersection.overlayID); if (intersectedItem !== -1) { intersectionItems = optionsItems; intersectionOverlays = optionsOverlays; @@ -732,6 +736,21 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } + // Slider update. + if (intersectionItems && intersectionItems[intersectedItem].type === "slider" && controlHand.triggerClicked()) { + sliderProperties = Overlays.getProperties(intersection.overlayID, ["position", "orientation"]); + overlayDimensions = intersectionItems[intersectedItem].properties.dimensions; + if (overlayDimensions === undefined) { + overlayDimensions = UI_ELEMENTS.slider.properties.dimensions; + } + basePoint = Vec3.sum(sliderProperties.position, + Vec3.multiplyQbyV(sliderProperties.orientation, { x: 0, y: overlayDimensions.y / 2, z: 0 })); + fraction = Vec3.dot(Vec3.subtract(basePoint, intersection.intersection), + Vec3.multiplyQbyV(sliderProperties.orientation, Vec3.UNIT_Y)) / overlayDimensions.y; + + uiCommandCallback(intersectionItems[intersectedItem].callback.method, fraction); + } + // Special handling for Group options. if (optionsItems && optionsItems === OPTONS_PANELS.groupOptions) { enableGroupButton = groupsCount > 1; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 7b93fd98b3..94b155e38c 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -238,9 +238,12 @@ } function update() { + var intersection; + if (isDisplaying) { - toolMenu.update(getIntersection().overlayID, grouping.groupsCount(), grouping.entitiesCount()); - createPalette.update(getIntersection().overlayID); + intersection = getIntersection(); + toolMenu.update(intersection, grouping.groupsCount(), grouping.entitiesCount()); + createPalette.update(intersection.overlayID); toolIcon.update(); } } @@ -1344,7 +1347,10 @@ editors[RIGHT_HAND].enableAutoGrab(); break; case "setSliderValue": - print("$$$$$$$ setSliderValue = " + JSON.stringify(parameter)); + if (parameter !== undefined) { + // TODO + print("setSliderValue = " + parameter); + } break; default: debug("ERROR: Unexpected command in onUICommand()! " + command); From 938b09c1a42d26a6787666cebea20be10daee8f7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 9 Aug 2017 15:19:25 +1200 Subject: [PATCH 155/722] Slider display per intersection value --- scripts/vr-edit/modules/toolMenu.js | 54 +++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 8c3f1dd8d9..95306c3326 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -24,6 +24,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsOverlays = [], optionsOverlaysIDs = [], // Text ids (names) of options overlays. + optionsOverlaysAuxiliaries = [], optionsEnabled = [], optionsSettings = {}, @@ -133,11 +134,37 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.03, y: 0.1, z: 0.01 }, localRotation: Quat.ZERO, color: { red: 128, green: 128, blue: 255 }, - alpha: 0.1, + alpha: 0.0, solid: true, ignoreRayIntersection: false, visible: true } + }, + "sliderValue": { + overlay: "cube", + properties: { + dimensions: { x: 0.03, y: 0.03, z: 0.01 }, + localPosition: { x: 0, y: 0.035, z: 0 }, + localRotation: Quat.ZERO, + color: { red: 100, green: 240, blue: 100 }, + alpha: 1.0, + solid: true, + ignoreRayIntersection: true, + visible: true + } + }, + "sliderRemainder": { + overlay: "cube", + properties: { + dimensions: { x: 0.03, y: 0.07, z: 0.01 }, + localPosition: { x: 0, y: -0.015, z: 0 }, + localRotation: Quat.ZERO, + color: { red: 64, green: 64, blue: 64 }, + alpha: 1.0, + solid: true, + ignoreRayIntersection: true, + visible: true + } } }, @@ -487,6 +514,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsOverlays = []; optionsOverlaysIDs = []; + optionsOverlaysAuxiliaries = []; optionsEnabled = []; optionsItems = null; @@ -523,6 +551,16 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties.parentID = optionsOverlays[optionsOverlays.length - 1]; Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); } + if (optionsItems[i].type === "slider") { + optionsOverlaysAuxiliaries[i] = {}; + properties = Object.clone(UI_ELEMENTS.sliderValue.properties); + properties.parentID = optionsOverlays[optionsOverlays.length - 1]; + optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.sliderValue.overlay, properties); + properties = Object.clone(UI_ELEMENTS.sliderRemainder.properties); + properties.parentID = optionsOverlays[optionsOverlays.length - 1]; + optionsOverlaysAuxiliaries[i].remainder = Overlays.addOverlay(UI_ELEMENTS.sliderRemainder.overlay, + properties); + } parentID = optionsOverlays[0]; // Menu buttons parent to menu panel. optionsEnabled.push(true); } @@ -628,7 +666,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { sliderProperties, overlayDimensions, basePoint, - fraction; + fraction, + otherFraction; // Intersection details. if (intersection.overlayID) { @@ -747,6 +786,16 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Vec3.multiplyQbyV(sliderProperties.orientation, { x: 0, y: overlayDimensions.y / 2, z: 0 })); fraction = Vec3.dot(Vec3.subtract(basePoint, intersection.intersection), Vec3.multiplyQbyV(sliderProperties.orientation, Vec3.UNIT_Y)) / overlayDimensions.y; + otherFraction = 1.0 - fraction; + + Overlays.editOverlay(optionsOverlaysAuxiliaries[intersectedItem].value, { + localPosition: { x: 0, y: (0.5 - fraction / 2) * overlayDimensions.y, z: 0 }, + dimensions: { x: overlayDimensions.x, y: fraction * overlayDimensions.y, z: overlayDimensions.z } + }); + Overlays.editOverlay(optionsOverlaysAuxiliaries[intersectedItem].remainder, { + localPosition: { x: 0, y: (-0.5 + otherFraction / 2) * overlayDimensions.y, z: 0 }, + dimensions: { x: overlayDimensions.x, y: otherFraction * overlayDimensions.y, z: overlayDimensions.z } + }); uiCommandCallback(intersectionItems[intersectedItem].callback.method, fraction); } @@ -871,6 +920,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Overlays.deleteOverlay(highlightOverlay); for (i = 0, length = optionsOverlays.length; i < length; i += 1) { Overlays.deleteOverlay(optionsOverlays[i]); + // Any auxiliary overlays parented to this overlay are automatically deleted. } optionsOverlays = []; From c19ec5822048889efb2b8a6268468cbd9a0d3f8a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 9 Aug 2017 15:51:37 +1200 Subject: [PATCH 156/722] Halve hand highlight and grab sphere radius --- scripts/vr-edit/modules/hand.js | 2 +- scripts/vr-edit/modules/highlights.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/hand.js b/scripts/vr-edit/modules/hand.js index 14994c226d..92e8b06e10 100644 --- a/scripts/vr-edit/modules/hand.js +++ b/scripts/vr-edit/modules/hand.js @@ -31,7 +31,7 @@ Hand = function (side) { TRIGGER_OFF_VALUE = 0.1, // Per handControllerGrab.js. TRIGGER_CLICKED_VALUE = 1.0, - NEAR_GRAB_RADIUS = 0.1, // Per handControllerGrab.js. + NEAR_GRAB_RADIUS = 0.05, // Different from handControllerGrab.js's value of 0.1. NEAR_HOVER_RADIUS = 0.025, LEFT_HAND = 0, diff --git a/scripts/vr-edit/modules/highlights.js b/scripts/vr-edit/modules/highlights.js index 9f0dc44275..333a68f04e 100644 --- a/scripts/vr-edit/modules/highlights.js +++ b/scripts/vr-edit/modules/highlights.js @@ -22,7 +22,7 @@ Highlights = function (side) { GROUP_COLOR = { red: 220, green: 60, blue: 220 }, HAND_HIGHLIGHT_ALPHA = 0.35, ENTITY_HIGHLIGHT_ALPHA = 0.8, - HAND_HIGHLIGHT_DIMENSIONS = { x: 0.2, y: 0.2, z: 0.2 }, + HAND_HIGHLIGHT_DIMENSIONS = { x: 0.1, y: 0.1, z: 0.1 }, HAND_HIGHLIGHT_OFFSET = { x: 0.0, y: 0.11, z: 0.02 }, LEFT_HAND = 0; From 1c2d3ced898c3f42d893b23753b036830380e89b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 9 Aug 2017 17:28:28 +1200 Subject: [PATCH 157/722] Add some further entities to the Create palette --- scripts/vr-edit/modules/createPalette.js | 214 +++++++++++++++++------ 1 file changed, 160 insertions(+), 54 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 846c82dbcf..0ee7b6e4f7 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -17,8 +17,8 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { var paletteOriginOverlay, palettePanelOverlay, - cubeOverlay, - cubeHighlightOverlay, + highlightOverlay, + paletteItemOverlays = [], LEFT_HAND = 0, @@ -51,18 +51,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true }, - CUBE_PROPERTIES = { - dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: 0.02, y: 0.02, z: 0.0 }, - localRotation: Quat.ZERO, - color: { red: 240, green: 0, blue: 0 }, - alpha: 1.0, - solid: true, - ignoreRayIntersection: false, - visible: true - }, - - CUBE_HIGHLIGHT_PROPERTIES = { + HIGHLIGHT_PROPERTIES = { dimensions: { x: 0.034, y: 0.034, z: 0.034 }, localPosition: { x: 0.02, y: 0.02, z: 0.0 }, localRotation: Quat.ZERO, @@ -74,16 +63,99 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: false }, - CUBE_ENTITY_PROPERTIES = { - type: "Box", - dimensions: { x: 0.2, y: 0.2, z: 0.2 }, - color: { red: 192, green: 192, blue: 192 } - }, + PALETTE_ITEMS = [ + { + overlay: { + type: "cube", + properties: { + dimensions: { x: 0.03, y: 0.03, z: 0.03 }, + localPosition: { x: 0.02, y: 0.02, z: 0.0 }, + localRotation: Quat.ZERO, + color: { red: 240, green: 0, blue: 0 }, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + } + }, + entity: { + type: "Box", + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + color: { red: 192, green: 192, blue: 192 } + } + }, + { + overlay: { + type: "shape", + properties: { + shape: "Cylinder", + dimensions: { x: 0.03, y: 0.03, z: 0.03 }, + localPosition: { x: 0.06, y: 0.02, z: 0.0 }, + localRotation: Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }), + color: { red: 240, green: 0, blue: 0 }, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + } + }, + entity: { + type: "Shape", + shape: "Cylinder", + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + color: { red: 192, green: 192, blue: 192 } + } + }, + { + overlay: { + type: "shape", + properties: { + shape: "Cone", + dimensions: { x: 0.03, y: 0.03, z: 0.03 }, + localPosition: { x: 0.10, y: 0.02, z: 0.0 }, + localRotation: Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }), + color: { red: 240, green: 0, blue: 0 }, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + } + }, + entity: { + type: "Shape", + shape: "Cone", + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + color: { red: 192, green: 192, blue: 192 } + } + }, + { + overlay: { + type: "sphere", + properties: { + dimensions: { x: 0.03, y: 0.03, z: 0.03 }, + localPosition: { x: 0.14, y: 0.02, z: 0.0 }, + localRotation: Quat.ZERO, + color: { red: 240, green: 0, blue: 0 }, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + } + }, + entity: { + type: "Sphere", + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + color: { red: 192, green: 192, blue: 192 } + } + } + ], isDisplaying = false, - isHighlightingCube = false, - isCubePressed = false, + NONE = -1, + highlightedItem = NONE, + pressedItem = NONE, + wasTriggerClicked = false, // References. controlHand; @@ -104,43 +176,70 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { setHand(side); function getEntityIDs() { - return [palettePanelOverlay, cubeOverlay]; + return [palettePanelOverlay].concat(paletteItemOverlays); } function update(intersectionOverlayID) { - var entityID, - CREATE_OFFSET = { x: 0, y: 0.05, z: -0.02 }; + var itemIndex, + isTriggerClicked, + properties, + PRESS_DELTA = { x: 0, y: 0, z: 0.01 }, + CREATE_OFFSET = { x: 0, y: 0.05, z: -0.02 }, + INVERSE_HAND_BASIS_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }); + // Highlight cube. - if (intersectionOverlayID === cubeOverlay !== isHighlightingCube) { - isHighlightingCube = !isHighlightingCube; - Overlays.editOverlay(cubeHighlightOverlay, { visible: isHighlightingCube }); + itemIndex = paletteItemOverlays.indexOf(intersectionOverlayID); + if (itemIndex !== NONE) { + if (highlightedItem !== itemIndex) { + Overlays.editOverlay(highlightOverlay, { + parentID: intersectionOverlayID, + localPosition: Vec3.ZERO, + visible: true + }); + highlightedItem = itemIndex; + } + } else { + Overlays.editOverlay(highlightOverlay, { + visible: false + }); + highlightedItem = NONE; } - // Cube click. - if (isHighlightingCube && controlHand.triggerClicked() !== isCubePressed) { - isCubePressed = controlHand.triggerClicked(); - if (isCubePressed) { - Overlays.editOverlay(cubeOverlay, { - localPosition: Vec3.sum(CUBE_PROPERTIES.localPosition, { x: 0, y: 0, z: 0.01 }) - }); - CUBE_ENTITY_PROPERTIES.position = Vec3.sum(controlHand.palmPosition(), - Vec3.multiplyQbyV(controlHand.orientation(), - Vec3.sum({ x: 0, y: CUBE_ENTITY_PROPERTIES.dimensions.z / 2, z: 0 }, CREATE_OFFSET))); - CUBE_ENTITY_PROPERTIES.rotation = controlHand.orientation(); - entityID = Entities.addEntity(CUBE_ENTITY_PROPERTIES); - uiCommandCallback("autoGrab"); - } else { - Overlays.editOverlay(cubeOverlay, { - localPosition: CUBE_PROPERTIES.localPosition - }); - } + // Unpress currently pressed item. + if (pressedItem !== NONE && pressedItem !== itemIndex) { + Overlays.editOverlay(paletteItemOverlays[pressedItem], { + localPosition: PALETTE_ITEMS[pressedItem].overlay.properties.localPosition + }); + pressedItem = NONE; } + + // Press item and create new entity. + isTriggerClicked = controlHand.triggerClicked(); + if (highlightedItem !== NONE && pressedItem === NONE && isTriggerClicked && !wasTriggerClicked) { + Overlays.editOverlay(paletteItemOverlays[itemIndex], { + localPosition: Vec3.sum(PALETTE_ITEMS[itemIndex].overlay.properties.localPosition, PRESS_DELTA) + }); + pressedItem = itemIndex; + + properties = Object.clone(PALETTE_ITEMS[itemIndex].entity); + properties.position = Vec3.sum(controlHand.palmPosition(), + Vec3.multiplyQbyV(controlHand.orientation(), + Vec3.sum({ x: 0, y: properties.dimensions.z / 2, z: 0 }, CREATE_OFFSET))); + properties.rotation = Quat.multiply(controlHand.orientation(), INVERSE_HAND_BASIS_ROTATION); + Entities.addEntity(properties); + + uiCommandCallback("autoGrab"); // TODO: Could pass entity ID through to autoGrab. + } + + wasTriggerClicked = isTriggerClicked; } function display() { // Creates and shows menu entities. var handJointIndex, - properties; + properties, + i, + length; if (isDisplaying) { return; @@ -161,30 +260,37 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { properties.localPosition = Vec3.sum(PALETTE_ROOT_POSITION, { x: lateralOffset, y: 0, z: 0 }); paletteOriginOverlay = Overlays.addOverlay("sphere", properties); - // Create palette items. + // Create palette. properties = Object.clone(PALETTE_PANEL_PROPERTIES); properties.parentID = paletteOriginOverlay; palettePanelOverlay = Overlays.addOverlay("cube", properties); - properties = Object.clone(CUBE_PROPERTIES); - properties.parentID = paletteOriginOverlay; - cubeOverlay = Overlays.addOverlay("cube", properties); + for (i = 0, length = PALETTE_ITEMS.length; i < length; i += 1) { + properties = Object.clone(PALETTE_ITEMS[i].overlay.properties); + properties.parentID = paletteOriginOverlay; + paletteItemOverlays[i] = Overlays.addOverlay(PALETTE_ITEMS[i].overlay.type, properties); + } // Prepare cube highlight overlay. - properties = Object.clone(CUBE_HIGHLIGHT_PROPERTIES); + properties = Object.clone(HIGHLIGHT_PROPERTIES); properties.parentID = paletteOriginOverlay; - cubeHighlightOverlay = Overlays.addOverlay("cube", properties); + highlightOverlay = Overlays.addOverlay("cube", properties); isDisplaying = true; } function clear() { // Deletes menu entities. + var i, + length; + if (!isDisplaying) { return; } - Overlays.deleteOverlay(cubeHighlightOverlay); - Overlays.deleteOverlay(cubeOverlay); + Overlays.deleteOverlay(highlightOverlay); + for (i = 0, length = paletteItemOverlays.length; i < length; i += 1) { + Overlays.deleteOverlay(paletteItemOverlays[i]); + } Overlays.deleteOverlay(palettePanelOverlay); Overlays.deleteOverlay(paletteOriginOverlay); From 556569819dd59956e026c4358d949cfccb911805 Mon Sep 17 00:00:00 2001 From: vladest Date: Wed, 9 Aug 2017 14:00:19 +0200 Subject: [PATCH 158/722] 1st stage of moving to async dialogs --- interface/src/Application.cpp | 165 +++++++++-------- interface/src/Application.h | 1 - interface/src/ModelPackager.cpp | 6 +- interface/src/ModelPropertiesDialog.cpp | 2 +- interface/src/assets/ATPAssetMigrator.cpp | 172 +++++++++--------- .../scripting/WindowScriptingInterface.cpp | 4 +- .../trackers/src/trackers/EyeTracker.cpp | 8 +- libraries/ui/src/OffscreenUi.cpp | 33 +++- libraries/ui/src/OffscreenUi.h | 23 ++- 9 files changed, 244 insertions(+), 170 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e5fb9949db..a72faad851 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -894,7 +894,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(scriptEngines, &ScriptEngines::scriptLoadError, scriptEngines, [](const QString& filename, const QString& error){ - OffscreenUi::warning(nullptr, "Error Loading Script", filename + " failed to load."); + OffscreenUi::asyncWarning(nullptr, "Error Loading Script", filename + " failed to load."); }, Qt::QueuedConnection); #ifdef _WIN32 @@ -1689,7 +1689,7 @@ void Application::domainConnectionRefused(const QString& reasonMessage, int reas case DomainHandler::ConnectionRefusedReason::Unknown: { QString message = "Unable to connect to the location you are visiting.\n"; message += reasonMessage; - OffscreenUi::warning("", message); + OffscreenUi::asyncWarning("", message); break; } default: @@ -3626,7 +3626,7 @@ bool Application::acceptSnapshot(const QString& urlString) { DependencyManager::get()->handleLookupString(snapshotData->getURL().toString()); } } else { - OffscreenUi::warning("", "No location details were found in the file\n" + + OffscreenUi::asyncWarning("", "No location details were found in the file\n" + snapshotPath + "\nTry dragging in an authentic Hifi snapshot."); } return true; @@ -5997,7 +5997,7 @@ void Application::setSessionUUID(const QUuid& sessionUUID) const { bool Application::askToSetAvatarUrl(const QString& url) { QUrl realUrl(url); if (realUrl.isLocalFile()) { - OffscreenUi::warning("", "You can not use local files for avatar components."); + OffscreenUi::asyncWarning("", "You can not use local files for avatar components."); return false; } @@ -6009,41 +6009,61 @@ bool Application::askToSetAvatarUrl(const QString& url) { QString modelName = fstMapping["name"].toString(); QString modelLicense = fstMapping["license"].toString(); - bool agreeToLicence = true; // assume true + bool agreeToLicense = true; // assume true + //create set avatar callback + auto setAvatar = [=] (QString url, QString modelName) { + auto offscreenUi = DependencyManager::get(); + + QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { + auto offscreenUi = DependencyManager::get(); + QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); + + bool ok = (QMessageBox::Ok == answer); + if (ok) { + getMyAvatar()->useFullAvatarURL(url, modelName); + emit fullAvatarURLChanged(url, modelName); + } else { + qCDebug(interfaceapp) << "Declined to use the avatar: " << url; + } + }); + OffscreenUi::asyncQuestion("Set Avatar", + "Would you like to use '" + modelName + "' for your avatar?", + QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); + }; + if (!modelLicense.isEmpty()) { - // word wrap the licence text to fit in a reasonable shaped message box. + // word wrap the license text to fit in a reasonable shaped message box. const int MAX_CHARACTERS_PER_LINE = 90; modelLicense = simpleWordWrap(modelLicense, MAX_CHARACTERS_PER_LINE); - agreeToLicence = QMessageBox::Yes == OffscreenUi::question("Avatar Usage License", + auto offscreenUi = DependencyManager::get(); + QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=, &agreeToLicense] (QMessageBox::StandardButton answer) { + auto offscreenUi = DependencyManager::get(); + QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); + + agreeToLicense = (answer == QMessageBox::Yes); + if (agreeToLicense) { + switch (modelType) { + case FSTReader::HEAD_AND_BODY_MODEL: { + setAvatar(url, modelName); + break; + } + default: + OffscreenUi::asyncWarning("", modelName + "Does not support a head and body as required."); + break; + } + } else { + qCDebug(interfaceapp) << "Declined to agree to avatar license: " << url; + } + + //auto offscreenUi = DependencyManager::get(); + }); + + OffscreenUi::asyncQuestion("Avatar Usage License", modelLicense + "\nDo you agree to these terms?", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - } - - bool ok = false; - - if (!agreeToLicence) { - qCDebug(interfaceapp) << "Declined to agree to avatar license: " << url; } else { - switch (modelType) { - - case FSTReader::HEAD_AND_BODY_MODEL: - ok = QMessageBox::Ok == OffscreenUi::question("Set Avatar", - "Would you like to use '" + modelName + "' for your avatar?", - QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); - break; - - default: - OffscreenUi::warning("", modelName + "Does not support a head and body as required."); - break; - } - } - - if (ok) { - getMyAvatar()->useFullAvatarURL(url, modelName); - emit fullAvatarURLChanged(url, modelName); - } else { - qCDebug(interfaceapp) << "Declined to use the avatar: " << url; + setAvatar(url, modelName); } return true; @@ -6051,8 +6071,6 @@ bool Application::askToSetAvatarUrl(const QString& url) { bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { - QMessageBox::StandardButton reply; - QString shortName = scriptFilenameOrURL; QUrl scriptURL { scriptFilenameOrURL }; @@ -6063,16 +6081,23 @@ bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { shortName = shortName.mid(startIndex, endIndex - startIndex); } + auto offscreenUi = DependencyManager::get(); + QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { + const QString& fileName = scriptFilenameOrURL; + if (answer == QMessageBox::Yes) { + qCDebug(interfaceapp) << "Chose to run the script: " << fileName; + DependencyManager::get()->loadScript(fileName); + } else { + qCDebug(interfaceapp) << "Declined to run the script: " << scriptFilenameOrURL; + } + auto offscreenUi = DependencyManager::get(); + QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); + }); + QString message = "Would you like to run this script:\n" + shortName; - reply = OffscreenUi::question(getWindow(), "Run Script", message, QMessageBox::Yes | QMessageBox::No); + OffscreenUi::asyncQuestion(getWindow(), "Run Script", message, QMessageBox::Yes | QMessageBox::No); - if (reply == QMessageBox::Yes) { - qCDebug(interfaceapp) << "Chose to run the script: " << scriptFilenameOrURL; - DependencyManager::get()->loadScript(scriptFilenameOrURL); - } else { - qCDebug(interfaceapp) << "Declined to run the script: " << scriptFilenameOrURL; - } return true; } @@ -6109,22 +6134,29 @@ bool Application::askToWearAvatarAttachmentUrl(const QString& url) { name = nameValue.toString(); } - // display confirmation dialog - if (displayAvatarAttachmentConfirmationDialog(name)) { - - // add attachment to avatar - auto myAvatar = getMyAvatar(); - assert(myAvatar); - auto attachmentDataVec = myAvatar->getAttachmentData(); - AttachmentData attachmentData; - attachmentData.fromJson(jsonObject); - attachmentDataVec.push_back(attachmentData); - myAvatar->setAttachmentData(attachmentDataVec); - - } else { - qCDebug(interfaceapp) << "User declined to wear the avatar attachment: " << url; - } + auto offscreenUi = DependencyManager::get(); + QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { + auto offscreenUi = DependencyManager::get(); + QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); + if (answer == QMessageBox::Yes) { + // add attachment to avatar + auto myAvatar = getMyAvatar(); + assert(myAvatar); + auto attachmentDataVec = myAvatar->getAttachmentData(); + AttachmentData attachmentData; + attachmentData.fromJson(jsonObject); + attachmentDataVec.push_back(attachmentData); + myAvatar->setAttachmentData(attachmentDataVec); + } else { + qCDebug(interfaceapp) << "User declined to wear the avatar attachment: " << url; + } + }); + auto avatarAttachmentConfirmationTitle = tr("Avatar Attachment Confirmation"); + auto avatarAttachmentConfirmationMessage = tr("Would you like to wear '%1' on your avatar?").arg(name); + OffscreenUi::asyncQuestion(avatarAttachmentConfirmationTitle, + avatarAttachmentConfirmationMessage, + QMessageBox::Ok | QMessageBox::Cancel); } else { // json parse error auto avatarAttachmentParseErrorString = tr("Error parsing attachment JSON from url: \"%1\""); @@ -6142,20 +6174,7 @@ bool Application::askToWearAvatarAttachmentUrl(const QString& url) { void Application::displayAvatarAttachmentWarning(const QString& message) const { auto avatarAttachmentWarningTitle = tr("Avatar Attachment Failure"); - OffscreenUi::warning(avatarAttachmentWarningTitle, message); -} - -bool Application::displayAvatarAttachmentConfirmationDialog(const QString& name) const { - auto avatarAttachmentConfirmationTitle = tr("Avatar Attachment Confirmation"); - auto avatarAttachmentConfirmationMessage = tr("Would you like to wear '%1' on your avatar?").arg(name); - auto reply = OffscreenUi::question(avatarAttachmentConfirmationTitle, - avatarAttachmentConfirmationMessage, - QMessageBox::Ok | QMessageBox::Cancel); - if (QMessageBox::Ok == reply) { - return true; - } else { - return false; - } + OffscreenUi::asyncWarning(avatarAttachmentWarningTitle, message); } void Application::showDialog(const QUrl& widgetUrl, const QUrl& tabletUrl, const QString& name) const { @@ -6722,7 +6741,7 @@ void Application::setPreviousScriptLocation(const QString& location) { void Application::loadScriptURLDialog() const { QString newScript = OffscreenUi::getText(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); if (QUrl(newScript).scheme() == "atp") { - OffscreenUi::warning("Error Loading Script", "Cannot load client script over ATP"); + OffscreenUi::asyncWarning("Error Loading Script", "Cannot load client script over ATP"); } else if (!newScript.isEmpty()) { DependencyManager::get()->loadScript(newScript.trimmed()); } @@ -6836,7 +6855,7 @@ void Application::notifyPacketVersionMismatch() { QString message = "The location you are visiting is running an incompatible server version.\n"; message += "Content may not display properly."; - OffscreenUi::warning("", message); + OffscreenUi::asyncWarning("", message); } } @@ -6845,7 +6864,7 @@ void Application::checkSkeleton() const { qCDebug(interfaceapp) << "MyAvatar model has no skeleton"; QString message = "Your selected avatar body has no skeleton.\n\nThe default body will be loaded..."; - OffscreenUi::warning("", message); + OffscreenUi::asyncWarning("", message); getMyAvatar()->useFullAvatarURL(AvatarData::defaultFullAvatarModelUrl(), DEFAULT_FULL_AVATAR_MODEL_NAME); } else { diff --git a/interface/src/Application.h b/interface/src/Application.h index f8eb393f9e..21ccfd30f0 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -428,7 +428,6 @@ private slots: bool askToWearAvatarAttachmentUrl(const QString& url); void displayAvatarAttachmentWarning(const QString& message) const; - bool displayAvatarAttachmentConfirmationDialog(const QString& name) const; void setSessionUUID(const QUuid& sessionUUID) const; diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index 44768db93c..5f4c7526e0 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -79,7 +79,7 @@ bool ModelPackager::loadModel() { if (_modelFile.completeSuffix().contains("fst")) { QFile fst(_modelFile.filePath()); if (!fst.open(QFile::ReadOnly | QFile::Text)) { - OffscreenUi::warning(NULL, + OffscreenUi::asyncWarning(NULL, QString("ModelPackager::loadModel()"), QString("Could not open FST file %1").arg(_modelFile.filePath()), QMessageBox::Ok); @@ -98,7 +98,7 @@ bool ModelPackager::loadModel() { // open the fbx file QFile fbx(_fbxInfo.filePath()); if (!_fbxInfo.exists() || !_fbxInfo.isFile() || !fbx.open(QIODevice::ReadOnly)) { - OffscreenUi::warning(NULL, + OffscreenUi::asyncWarning(NULL, QString("ModelPackager::loadModel()"), QString("Could not open FBX file %1").arg(_fbxInfo.filePath()), QMessageBox::Ok); @@ -408,7 +408,7 @@ bool ModelPackager::copyTextures(const QString& oldDir, const QDir& newDir) { } if (!errors.isEmpty()) { - OffscreenUi::warning(nullptr, "ModelPackager::copyTextures()", + OffscreenUi::asyncWarning(nullptr, "ModelPackager::copyTextures()", "Missing textures:" + errors); qCDebug(interfaceapp) << "ModelPackager::copyTextures():" << errors; return false; diff --git a/interface/src/ModelPropertiesDialog.cpp b/interface/src/ModelPropertiesDialog.cpp index d41a913c95..ae352974ae 100644 --- a/interface/src/ModelPropertiesDialog.cpp +++ b/interface/src/ModelPropertiesDialog.cpp @@ -201,7 +201,7 @@ void ModelPropertiesDialog::chooseTextureDirectory() { return; } if (!directory.startsWith(_basePath)) { - OffscreenUi::warning(NULL, "Invalid texture directory", "Texture directory must be child of base path."); + OffscreenUi::asyncWarning(NULL, "Invalid texture directory", "Texture directory must be child of base path."); return; } _textureDirectory->setText(directory.length() == _basePath.length() ? "." : directory.mid(_basePath.length() + 1)); diff --git a/interface/src/assets/ATPAssetMigrator.cpp b/interface/src/assets/ATPAssetMigrator.cpp index 667c2587b0..63147c8798 100644 --- a/interface/src/assets/ATPAssetMigrator.cpp +++ b/interface/src/assets/ATPAssetMigrator.cpp @@ -53,105 +53,109 @@ void ATPAssetMigrator::loadEntityServerFile() { " continue?\n\nMake sure you are connected to the right domain." }; - auto button = OffscreenUi::question(_dialogParent, MESSAGE_BOX_TITLE, MIGRATION_CONFIRMATION_TEXT, - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - - if (button == QMessageBox::No) { - return; - } - - // try to open the file at the given filename - QFile modelsFile { filename }; - - if (modelsFile.open(QIODevice::ReadOnly)) { - QByteArray compressedJsonData = modelsFile.readAll(); - QByteArray jsonData; - - if (!gunzip(compressedJsonData, jsonData)) { - OffscreenUi::warning(_dialogParent, "Error", "The file at" + filename + "was not in gzip format."); - } - - QJsonDocument modelsJSON = QJsonDocument::fromJson(jsonData); - _entitiesArray = modelsJSON.object()["Entities"].toArray(); - - for (auto jsonValue : _entitiesArray) { - QJsonObject entityObject = jsonValue.toObject(); - QString modelURLString = entityObject.value(MODEL_URL_KEY).toString(); - QString compoundURLString = entityObject.value(COMPOUND_SHAPE_URL_KEY).toString(); + auto offscreenUi = DependencyManager::get(); - for (int i = 0; i < 2; ++i) { - bool isModelURL = (i == 0); - quint8 replacementType = i; - auto migrationURLString = (isModelURL) ? modelURLString : compoundURLString; + QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { + auto offscreenUi = DependencyManager::get(); + QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); - if (!migrationURLString.isEmpty()) { - QUrl migrationURL = QUrl(migrationURLString); + if (QMessageBox::Yes == answer) { + // try to open the file at the given filename + QFile modelsFile { filename }; - if (!_ignoredUrls.contains(migrationURL) - && (migrationURL.scheme() == URL_SCHEME_HTTP || migrationURL.scheme() == URL_SCHEME_HTTPS - || migrationURL.scheme() == URL_SCHEME_FILE || migrationURL.scheme() == URL_SCHEME_FTP)) { + if (modelsFile.open(QIODevice::ReadOnly)) { + QByteArray compressedJsonData = modelsFile.readAll(); + QByteArray jsonData; - if (_pendingReplacements.contains(migrationURL)) { - // we already have a request out for this asset, just store the QJsonValueRef - // so we can do the hash replacement when the request comes back - _pendingReplacements.insert(migrationURL, { jsonValue, replacementType }); - } else if (_uploadedAssets.contains(migrationURL)) { - // we already have a hash for this asset - // so just do the replacement immediately - if (isModelURL) { - entityObject[MODEL_URL_KEY] = _uploadedAssets.value(migrationURL).toString(); - } else { - entityObject[COMPOUND_SHAPE_URL_KEY] = _uploadedAssets.value(migrationURL).toString(); - } + if (!gunzip(compressedJsonData, jsonData)) { + OffscreenUi::asyncWarning(_dialogParent, "Error", "The file at" + filename + "was not in gzip format."); + } - jsonValue = entityObject; - } else if (wantsToMigrateResource(migrationURL)) { - auto request = - DependencyManager::get()->createResourceRequest(this, migrationURL); + QJsonDocument modelsJSON = QJsonDocument::fromJson(jsonData); + _entitiesArray = modelsJSON.object()["Entities"].toArray(); - if (request) { - qCDebug(asset_migrator) << "Requesting" << migrationURL << "for ATP asset migration"; + for (auto jsonValue : _entitiesArray) { + QJsonObject entityObject = jsonValue.toObject(); + QString modelURLString = entityObject.value(MODEL_URL_KEY).toString(); + QString compoundURLString = entityObject.value(COMPOUND_SHAPE_URL_KEY).toString(); - // add this combination of QUrl and QJsonValueRef to our multi hash so we can change the URL - // to an ATP one once ready - _pendingReplacements.insert(migrationURL, { jsonValue, (isModelURL ? 0 : 1)}); + for (int i = 0; i < 2; ++i) { + bool isModelURL = (i == 0); + quint8 replacementType = i; + auto migrationURLString = (isModelURL) ? modelURLString : compoundURLString; - connect(request, &ResourceRequest::finished, this, [=]() { - if (request->getResult() == ResourceRequest::Success) { - migrateResource(request); + if (!migrationURLString.isEmpty()) { + QUrl migrationURL = QUrl(migrationURLString); + + if (!_ignoredUrls.contains(migrationURL) + && (migrationURL.scheme() == URL_SCHEME_HTTP || migrationURL.scheme() == URL_SCHEME_HTTPS + || migrationURL.scheme() == URL_SCHEME_FILE || migrationURL.scheme() == URL_SCHEME_FTP)) { + + if (_pendingReplacements.contains(migrationURL)) { + // we already have a request out for this asset, just store the QJsonValueRef + // so we can do the hash replacement when the request comes back + _pendingReplacements.insert(migrationURL, { jsonValue, replacementType }); + } else if (_uploadedAssets.contains(migrationURL)) { + // we already have a hash for this asset + // so just do the replacement immediately + if (isModelURL) { + entityObject[MODEL_URL_KEY] = _uploadedAssets.value(migrationURL).toString(); + } else { + entityObject[COMPOUND_SHAPE_URL_KEY] = _uploadedAssets.value(migrationURL).toString(); + } + + jsonValue = entityObject; + } else if (wantsToMigrateResource(migrationURL)) { + auto request = + DependencyManager::get()->createResourceRequest(this, migrationURL); + + if (request) { + qCDebug(asset_migrator) << "Requesting" << migrationURL << "for ATP asset migration"; + + // add this combination of QUrl and QJsonValueRef to our multi hash so we can change the URL + // to an ATP one once ready + _pendingReplacements.insert(migrationURL, { jsonValue, (isModelURL ? 0 : 1)}); + + connect(request, &ResourceRequest::finished, this, [=]() { + if (request->getResult() == ResourceRequest::Success) { + migrateResource(request); + } else { + ++_errorCount; + _pendingReplacements.remove(migrationURL); + qWarning() << "Could not retrieve asset at" << migrationURL.toString(); + + checkIfFinished(); + } + request->deleteLater(); + }); + + request->send(); } else { ++_errorCount; - _pendingReplacements.remove(migrationURL); - qWarning() << "Could not retrieve asset at" << migrationURL.toString(); - - checkIfFinished(); + qWarning() << "Count not create request for asset at" << migrationURL.toString(); } - request->deleteLater(); - }); - request->send(); - } else { - ++_errorCount; - qWarning() << "Count not create request for asset at" << migrationURL.toString(); + } else { + _ignoredUrls.insert(migrationURL); + } } - - } else { - _ignoredUrls.insert(migrationURL); - } } + } + } + + _doneReading = true; + + checkIfFinished(); + + } else { + OffscreenUi::asyncWarning(_dialogParent, "Error", + "There was a problem loading that entity-server file for ATP asset migration. Please try again"); } - } - - _doneReading = true; - - checkIfFinished(); - - } else { - OffscreenUi::warning(_dialogParent, "Error", - "There was a problem loading that entity-server file for ATP asset migration. Please try again"); - } + }); + OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, MIGRATION_CONFIRMATION_TEXT, + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); } } @@ -314,11 +318,11 @@ void ATPAssetMigrator::saveEntityServerFile() { OffscreenUi::information(_dialogParent, "Success", infoMessage); } else { - OffscreenUi::warning(_dialogParent, "Error", "Could not gzip JSON data for new entities file."); + OffscreenUi::asyncWarning(_dialogParent, "Error", "Could not gzip JSON data for new entities file."); } } else { - OffscreenUi::warning(_dialogParent, "Error", + OffscreenUi::asyncWarning(_dialogParent, "Error", QString("Could not open file at %1 to write new entities file to.").arg(saveName)); } } diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 84f4cbbbd8..36381b3626 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -59,7 +59,7 @@ WindowScriptingInterface::WindowScriptingInterface() { QUrl url(urlString); emit svoImportRequested(url.url()); } else { - OffscreenUi::warning("Import SVO Error", "You need to be running edit.js to import entities."); + OffscreenUi::asyncWarning("Import SVO Error", "You need to be running edit.js to import entities."); } }); @@ -103,7 +103,7 @@ void WindowScriptingInterface::raiseMainWindow() { /// \param const QString& message message to display /// \return QScriptValue::UndefinedValue void WindowScriptingInterface::alert(const QString& message) { - OffscreenUi::warning("", message); + OffscreenUi::asyncWarning("", message); } /// Display a confirmation box with the options 'Yes' and 'No' diff --git a/libraries/trackers/src/trackers/EyeTracker.cpp b/libraries/trackers/src/trackers/EyeTracker.cpp index 8733461dbb..e641abc630 100644 --- a/libraries/trackers/src/trackers/EyeTracker.cpp +++ b/libraries/trackers/src/trackers/EyeTracker.cpp @@ -136,7 +136,7 @@ void EyeTracker::onStreamStarted() { qCWarning(interfaceapp) << "Eye Tracker: Error starting streaming:" << smiReturnValueToString(result); // Display error dialog unless SMI SDK has already displayed an error message. if (result != SMI_ERROR_HMD_NOT_SUPPORTED) { - OffscreenUi::warning(nullptr, "Eye Tracker Error", smiReturnValueToString(result)); + OffscreenUi::asyncWarning(nullptr, "Eye Tracker Error", smiReturnValueToString(result)); } } else { qCDebug(interfaceapp) << "Eye Tracker: Started streaming"; @@ -149,7 +149,7 @@ void EyeTracker::onStreamStarted() { result = smi_loadCalibration(HIGH_FIDELITY_EYE_TRACKER_CALIBRATION); if (result != SMI_RET_SUCCESS) { qCWarning(interfaceapp) << "Eye Tracker: Error loading calibration:" << smiReturnValueToString(result); - OffscreenUi::warning(nullptr, "Eye Tracker Error", "Error loading calibration" + OffscreenUi::asyncWarning(nullptr, "Eye Tracker Error", "Error loading calibration" + smiReturnValueToString(result)); } else { qCDebug(interfaceapp) << "Eye Tracker: Loaded calibration"; @@ -165,7 +165,7 @@ void EyeTracker::setEnabled(bool enabled, bool simulate) { int result = smi_setCallback(eyeTrackerCallback); if (result != SMI_RET_SUCCESS) { qCWarning(interfaceapp) << "Eye Tracker: Error setting callback:" << smiReturnValueToString(result); - OffscreenUi::warning(nullptr, "Eye Tracker Error", smiReturnValueToString(result)); + OffscreenUi::asyncWarning(nullptr, "Eye Tracker Error", smiReturnValueToString(result)); } else { _isInitialized = true; } @@ -270,7 +270,7 @@ void EyeTracker::calibrate(int points) { } if (result != SMI_RET_SUCCESS) { - OffscreenUi::warning(nullptr, "Eye Tracker Error", "Calibration error: " + smiReturnValueToString(result)); + OffscreenUi::asyncWarning(nullptr, "Eye Tracker Error", "Calibration error: " + smiReturnValueToString(result)); } } #endif diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 135729653e..75f13b0dbd 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -91,6 +91,12 @@ QObject* OffscreenUi::getFlags() { return offscreenFlags; } +void OffscreenUi::removeModalDialog(QObject* modal) { + if (modal) { + _modalDialogListeners.removeOne(modal); + } +} + void OffscreenUi::create(QOpenGLContext* context) { OffscreenQmlSurface::create(context); auto myContext = getSurfaceContext(); @@ -204,6 +210,9 @@ private slots: void onSelected(int button) { _result = button; _finished = true; + auto offscreenUi = DependencyManager::get(); + emit offscreenUi->response(static_cast(_result.toInt())); + offscreenUi->removeModalDialog(qobject_cast(this)); disconnect(_dialog); } }; @@ -263,6 +272,21 @@ QMessageBox::StandardButton OffscreenUi::messageBox(Icon icon, const QString& ti return static_cast(waitForMessageBoxResult(createMessageBox(icon, title, text, buttons, defaultButton))); } +void OffscreenUi::asyncMessageBox(Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "asyncMessageBox", + Q_ARG(Icon, icon), + Q_ARG(QString, title), + Q_ARG(QString, text), + Q_ARG(QMessageBox::StandardButtons, buttons), + Q_ARG(QMessageBox::StandardButton, defaultButton)); + } + + MessageBoxListener* messageBoxListener = new MessageBoxListener(createMessageBox(icon, title, text, buttons, defaultButton)); + QObject* modalDialog = qobject_cast(messageBoxListener); + _modalDialogListeners.push_back(modalDialog); +} + QMessageBox::StandardButton OffscreenUi::critical(const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { return DependencyManager::get()->messageBox(OffscreenUi::Icon::ICON_CRITICAL, title, text, buttons, defaultButton); @@ -275,11 +299,18 @@ QMessageBox::StandardButton OffscreenUi::question(const QString& title, const QS QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { return DependencyManager::get()->messageBox(OffscreenUi::Icon::ICON_QUESTION, title, text, buttons, defaultButton); } +void OffscreenUi::asyncQuestion(const QString& title, const QString& text, + QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { + DependencyManager::get()->asyncMessageBox(OffscreenUi::Icon::ICON_QUESTION, title, text, buttons, defaultButton); +} QMessageBox::StandardButton OffscreenUi::warning(const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { return DependencyManager::get()->messageBox(OffscreenUi::Icon::ICON_WARNING, title, text, buttons, defaultButton); } - +void OffscreenUi::asyncWarning(const QString& title, const QString& text, + QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { + DependencyManager::get()->asyncMessageBox(OffscreenUi::Icon::ICON_WARNING, title, text, buttons, defaultButton); +} class InputDialogListener : public ModalDialogListener { diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index f228f28d5e..726b7897a0 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -71,6 +71,7 @@ public: // Message box compatibility Q_INVOKABLE QMessageBox::StandardButton messageBox(Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); + Q_INVOKABLE void asyncMessageBox(Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); // Must be called from the main thread QQuickItem* createMessageBox(Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); // Must be called from the main thread @@ -94,12 +95,23 @@ public: QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) { return question(title, text, buttons, defaultButton); } + + static void asyncQuestion(void* ignored, const QString& title, const QString& text, + QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) { + return asyncQuestion(title, text, buttons, defaultButton); + } /// Same design as QMessageBox::warning(), will block, returns result static QMessageBox::StandardButton warning(void* ignored, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) { return warning(title, text, buttons, defaultButton); } + static void asyncWarning(void* ignored, const QString& title, const QString& text, + QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) { + return asyncWarning(title, text, buttons, defaultButton); + } static QMessageBox::StandardButton critical(const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, @@ -110,9 +122,15 @@ public: static QMessageBox::StandardButton question(const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + static void asyncQuestion (const QString& title, const QString& text, + QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); static QMessageBox::StandardButton warning(const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + static void asyncWarning(const QString& title, const QString& text, + QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); Q_INVOKABLE QString fileOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QString fileSaveDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); @@ -153,9 +171,11 @@ public: static QVariant getCustomInfo(const Icon icon, const QString& title, const QVariantMap& config, bool* ok = 0); unsigned int getMenuUserDataId() const; - signals: void showDesktop(); + void response(QMessageBox::StandardButton response); + public slots: + void removeModalDialog(QObject* modal); private: QString fileDialog(const QVariantMap& properties); @@ -163,6 +183,7 @@ private: QQuickItem* _desktop { nullptr }; QQuickItem* _toolWindow { nullptr }; + QList _modalDialogListeners; std::unordered_map _pressedKeys; VrMenu* _vrMenu { nullptr }; QQueue> _queuedMenuInitializers; From 616f15864a8059c9223189670dd6ae4f1f5bbac7 Mon Sep 17 00:00:00 2001 From: vladest Date: Wed, 9 Aug 2017 14:49:15 +0200 Subject: [PATCH 159/722] Ported an some currently not used code --- interface/src/assets/ATPAssetMigrator.cpp | 138 ++++++++++++++-------- 1 file changed, 88 insertions(+), 50 deletions(-) diff --git a/interface/src/assets/ATPAssetMigrator.cpp b/interface/src/assets/ATPAssetMigrator.cpp index 63147c8798..b3711a2707 100644 --- a/interface/src/assets/ATPAssetMigrator.cpp +++ b/interface/src/assets/ATPAssetMigrator.cpp @@ -47,6 +47,37 @@ void ATPAssetMigrator::loadEntityServerFile() { if (!filename.isEmpty()) { qCDebug(asset_migrator) << "Selected filename for ATP asset migration: " << filename; + auto migrateResources = [=](QUrl migrationURL, QJsonValueRef jsonValue, bool isModelURL) { + auto request = + DependencyManager::get()->createResourceRequest(this, migrationURL); + + if (request) { + qCDebug(asset_migrator) << "Requesting" << migrationURL << "for ATP asset migration"; + + // add this combination of QUrl and QJsonValueRef to our multi hash so we can change the URL + // to an ATP one once ready + _pendingReplacements.insert(migrationURL, { jsonValue, (isModelURL ? 0 : 1)}); + + connect(request, &ResourceRequest::finished, this, [=]() { + if (request->getResult() == ResourceRequest::Success) { + migrateResource(request); + } else { + ++_errorCount; + _pendingReplacements.remove(migrationURL); + qWarning() << "Could not retrieve asset at" << migrationURL.toString(); + + checkIfFinished(); + } + request->deleteLater(); + }); + + request->send(); + } else { + ++_errorCount; + qWarning() << "Count not create request for asset at" << migrationURL.toString(); + } + + }; static const QString MIGRATION_CONFIRMATION_TEXT { "The ATP Asset Migration process will scan the selected entity-server file,\nupload discovered resources to the"\ " current asset-server\nand then save a new entity-server file with the ATP URLs.\n\nAre you ready to"\ @@ -54,7 +85,6 @@ void ATPAssetMigrator::loadEntityServerFile() { }; auto offscreenUi = DependencyManager::get(); - QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { auto offscreenUi = DependencyManager::get(); QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); @@ -88,60 +118,68 @@ void ATPAssetMigrator::loadEntityServerFile() { QUrl migrationURL = QUrl(migrationURLString); if (!_ignoredUrls.contains(migrationURL) - && (migrationURL.scheme() == URL_SCHEME_HTTP || migrationURL.scheme() == URL_SCHEME_HTTPS - || migrationURL.scheme() == URL_SCHEME_FILE || migrationURL.scheme() == URL_SCHEME_FTP)) { - - if (_pendingReplacements.contains(migrationURL)) { - // we already have a request out for this asset, just store the QJsonValueRef - // so we can do the hash replacement when the request comes back - _pendingReplacements.insert(migrationURL, { jsonValue, replacementType }); - } else if (_uploadedAssets.contains(migrationURL)) { - // we already have a hash for this asset - // so just do the replacement immediately - if (isModelURL) { - entityObject[MODEL_URL_KEY] = _uploadedAssets.value(migrationURL).toString(); - } else { - entityObject[COMPOUND_SHAPE_URL_KEY] = _uploadedAssets.value(migrationURL).toString(); - } - - jsonValue = entityObject; - } else if (wantsToMigrateResource(migrationURL)) { - auto request = - DependencyManager::get()->createResourceRequest(this, migrationURL); - - if (request) { - qCDebug(asset_migrator) << "Requesting" << migrationURL << "for ATP asset migration"; - - // add this combination of QUrl and QJsonValueRef to our multi hash so we can change the URL - // to an ATP one once ready - _pendingReplacements.insert(migrationURL, { jsonValue, (isModelURL ? 0 : 1)}); - - connect(request, &ResourceRequest::finished, this, [=]() { - if (request->getResult() == ResourceRequest::Success) { - migrateResource(request); - } else { - ++_errorCount; - _pendingReplacements.remove(migrationURL); - qWarning() << "Could not retrieve asset at" << migrationURL.toString(); - - checkIfFinished(); - } - request->deleteLater(); - }); - - request->send(); - } else { - ++_errorCount; - qWarning() << "Count not create request for asset at" << migrationURL.toString(); - } + && (migrationURL.scheme() == URL_SCHEME_HTTP || migrationURL.scheme() == URL_SCHEME_HTTPS + || migrationURL.scheme() == URL_SCHEME_FILE || migrationURL.scheme() == URL_SCHEME_FTP)) { + if (_pendingReplacements.contains(migrationURL)) { + // we already have a request out for this asset, just store the QJsonValueRef + // so we can do the hash replacement when the request comes back + _pendingReplacements.insert(migrationURL, { jsonValue, replacementType }); + } else if (_uploadedAssets.contains(migrationURL)) { + // we already have a hash for this asset + // so just do the replacement immediately + if (isModelURL) { + entityObject[MODEL_URL_KEY] = _uploadedAssets.value(migrationURL).toString(); } else { - _ignoredUrls.insert(migrationURL); + entityObject[COMPOUND_SHAPE_URL_KEY] = _uploadedAssets.value(migrationURL).toString(); + } + + jsonValue = entityObject; + } else { + + static bool hasAskedForCompleteMigration { false }; + static bool wantsCompleteMigration { false }; + + if (!hasAskedForCompleteMigration) { + // this is the first resource migration - ask the user if they just want to migrate everything + static const QString COMPLETE_MIGRATION_TEXT { "Do you want to migrate all assets found in this entity-server file?\n"\ + "Select \"Yes\" to upload all discovered assets to the current asset-server immediately.\n"\ + "Select \"No\" to be prompted for each discovered asset." + }; + auto offscreenUi = DependencyManager::get(); + QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { + auto offscreenUi = DependencyManager::get(); + QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); + if (answer == QMessageBox::Yes) { + wantsCompleteMigration = true; + migrateResources(migrationURL, jsonValue, isModelURL); + } else { + QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { + auto offscreenUi = DependencyManager::get(); + QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); + if (answer == QMessageBox::Yes) { + migrateResources(migrationURL, jsonValue, isModelURL); + } else { + _ignoredUrls.insert(migrationURL); + } + }); + OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, + "Would you like to migrate the following resource?\n" + migrationURL.toString(), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + } + }); + OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, COMPLETE_MIGRATION_TEXT, + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + hasAskedForCompleteMigration = true; + } + if (wantsCompleteMigration) { + migrateResources(migrationURL, jsonValue, isModelURL); } } + } } } - } _doneReading = true; @@ -150,7 +188,7 @@ void ATPAssetMigrator::loadEntityServerFile() { } else { OffscreenUi::asyncWarning(_dialogParent, "Error", - "There was a problem loading that entity-server file for ATP asset migration. Please try again"); + "There was a problem loading that entity-server file for ATP asset migration. Please try again"); } } }); From 1aeaee8153db8d40abcc59627418b189e702b7dd Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 10 Aug 2017 09:04:28 +1200 Subject: [PATCH 160/722] Rename slider to barSlider --- scripts/vr-edit/modules/toolMenu.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 95306c3326..d86f84bf08 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -128,7 +128,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, - "slider": { + "barSlider": { overlay: "cube", properties: { dimensions: { x: 0.03, y: 0.1, z: 0.01 }, @@ -140,7 +140,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, - "sliderValue": { + "barSliderValue": { overlay: "cube", properties: { dimensions: { x: 0.03, y: 0.03, z: 0.01 }, @@ -153,7 +153,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, - "sliderRemainder": { + "barSliderRemainder": { overlay: "cube", properties: { dimensions: { x: 0.03, y: 0.07, z: 0.01 }, @@ -340,7 +340,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, { id: "physicsSlider", - type: "slider", + type: "barSlider", properties: { localPosition: { x: -0.02, y: 0.0, z: -0.005 } }, @@ -551,14 +551,14 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties.parentID = optionsOverlays[optionsOverlays.length - 1]; Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); } - if (optionsItems[i].type === "slider") { + if (optionsItems[i].type === "barSlider") { optionsOverlaysAuxiliaries[i] = {}; - properties = Object.clone(UI_ELEMENTS.sliderValue.properties); + properties = Object.clone(UI_ELEMENTS.barSliderValue.properties); properties.parentID = optionsOverlays[optionsOverlays.length - 1]; - optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.sliderValue.overlay, properties); - properties = Object.clone(UI_ELEMENTS.sliderRemainder.properties); + optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.barSliderValue.overlay, properties); + properties = Object.clone(UI_ELEMENTS.barSliderRemainder.properties); properties.parentID = optionsOverlays[optionsOverlays.length - 1]; - optionsOverlaysAuxiliaries[i].remainder = Overlays.addOverlay(UI_ELEMENTS.sliderRemainder.overlay, + optionsOverlaysAuxiliaries[i].remainder = Overlays.addOverlay(UI_ELEMENTS.barSliderRemainder.overlay, properties); } parentID = optionsOverlays[0]; // Menu buttons parent to menu panel. @@ -775,12 +775,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } - // Slider update. - if (intersectionItems && intersectionItems[intersectedItem].type === "slider" && controlHand.triggerClicked()) { + // Bar slider update. + if (intersectionItems && intersectionItems[intersectedItem].type === "barSlider" && controlHand.triggerClicked()) { sliderProperties = Overlays.getProperties(intersection.overlayID, ["position", "orientation"]); overlayDimensions = intersectionItems[intersectedItem].properties.dimensions; if (overlayDimensions === undefined) { - overlayDimensions = UI_ELEMENTS.slider.properties.dimensions; + overlayDimensions = UI_ELEMENTS.barSlider.properties.dimensions; } basePoint = Vec3.sum(sliderProperties.position, Vec3.multiplyQbyV(sliderProperties.orientation, { x: 0, y: overlayDimensions.y / 2, z: 0 })); From 397527d3f3468aaa4a5bbb523334e6ebd04402cd Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 10 Aug 2017 10:23:13 +1200 Subject: [PATCH 161/722] Add image UI element --- scripts/vr-edit/modules/toolMenu.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index d86f84bf08..02b299ce75 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -128,6 +128,19 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, + "image": { + overlay: "image3d", + properties: { + dimensions: { x: 0.1, y: 0.1 }, + localPosition: { x: 0, y: 0, z: 0 }, + localRotation: Quat.ZERO, + color: { red: 255, green: 255, blue: 255 }, + alpha: 1.0, + ignoreRayIntersection: true, + isFacingAvatar: false, + visible: true + } + }, "barSlider": { overlay: "cube", properties: { @@ -526,6 +539,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.clone(UI_ELEMENTS[optionsItems[i].type].properties); properties = Object.merge(properties, optionsItems[i].properties); properties.parentID = parentID; + if (properties.url) { + properties.url = Script.resolvePath(properties.url); + } if (optionsItems[i].setting) { optionsSettings[optionsItems[i].id] = { key: optionsItems[i].setting.key }; value = Settings.getValue(optionsItems[i].setting.key); From e1adb3a20ed063b22dfe66c55fc648e2c72f46f4 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 10 Aug 2017 12:44:30 +1200 Subject: [PATCH 162/722] Add image slider (for H, S, V or similar values) --- scripts/vr-edit/assets/slider-white-alpha.png | Bin 0 -> 564 bytes scripts/vr-edit/assets/slider-white.png | Bin 0 -> 187 bytes scripts/vr-edit/modules/toolMenu.js | 132 ++++++++++++++++-- 3 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 scripts/vr-edit/assets/slider-white-alpha.png create mode 100644 scripts/vr-edit/assets/slider-white.png diff --git a/scripts/vr-edit/assets/slider-white-alpha.png b/scripts/vr-edit/assets/slider-white-alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..d61221d492a2c879057ec23d578ffda36258780e GIT binary patch literal 564 zcmeAS@N?(olHy`uVBq!ia0y~yU=U$oU`XL$V_;xtTP<*gfq{XsILO_JVcj{ImkbOH zEa{HEjtmSN`?>!lvNA9*a29w(7BevL9R^{>|GE>&Eb)~Fh>-{no(Yl+0AwM$RFB~}1x1dqJ zjD@#myYh>mdv7?jYPKq`_;9pu!J%~PCa(D)fi21z51M5cD$ir945^T3b)H_7?jG=P zt*m3Kc^Qjn+-<>-id6T2hf8H0TgA&*MCaWW{Gjj3{8hV2cJJZM8WL6Q%R!4y~9?$}0*k z_I;2S-yODE*X7^=`N-`y@~qDNHXw(uk#%HFf5V}>bIMB%3B}W!G_9Ms)ZcO}j9Tk= zLomc5&z&Q4R^Ls*5Q{AL3kL+Po7nhn3z`(1=o8SIdTN2JV=Hr6OMu?=-)}jz-fUKG yQSqz-u{J7aBy5*;WZwOTL+VB*yT;Ty24_aq{j)-^$ucl7FnGH9xvX!lvNA9*a29w(7BevL9R^{>bP0 Hl+XkKX5mD1 literal 0 HcmV?d00001 diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 02b299ce75..b7141b5440 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -144,9 +144,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { "barSlider": { overlay: "cube", properties: { - dimensions: { x: 0.03, y: 0.1, z: 0.01 }, + dimensions: { x: 0.02, y: 0.1, z: 0.01 }, localRotation: Quat.ZERO, - color: { red: 128, green: 128, blue: 255 }, + color: { red: 128, green: 128, blue: 128 }, alpha: 0.0, solid: true, ignoreRayIntersection: false, @@ -156,7 +156,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { "barSliderValue": { overlay: "cube", properties: { - dimensions: { x: 0.03, y: 0.03, z: 0.01 }, + dimensions: { x: 0.02, y: 0.03, z: 0.01 }, localPosition: { x: 0, y: 0.035, z: 0 }, localRotation: Quat.ZERO, color: { red: 100, green: 240, blue: 100 }, @@ -169,7 +169,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { "barSliderRemainder": { overlay: "cube", properties: { - dimensions: { x: 0.03, y: 0.07, z: 0.01 }, + dimensions: { x: 0.02, y: 0.07, z: 0.01 }, localPosition: { x: 0, y: -0.015, z: 0 }, localRotation: Quat.ZERO, color: { red: 64, green: 64, blue: 64 }, @@ -178,6 +178,34 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { ignoreRayIntersection: true, visible: true } + }, + "imageSlider": { + overlay: "cube", + properties: { + dimensions: { x: 0.02, y: 0.1, z: 0.01 }, + localRotation: Quat.ZERO, + color: { red: 128, green: 128, blue: 128 }, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + }, + useBaseColor: false, + imageURL: null, + imageOverlayURL: null + }, + "sliderPointer": { + overlay: "shape", + properties: { + shape: "Cone", + dimensions: { x: 0.01, y: 0.01, z: 0.01 }, + localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }), + color: { red: 180, green: 180, blue: 180 }, + alpha: 1.0, + solid: true, + ignoreRayIntersection: true, + visible: true + } } }, @@ -360,6 +388,20 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { callback: { method: "setSliderValue" } + }, + { + id: "colorSlider", + type: "imageSlider", + properties: { + localPosition: { x: 0.02, y: 0.0, z: -0.005 }, + color: { red: 255, green: 0, blue: 0 } + }, + useBaseColor: true, + imageURL: "../assets/slider-white.png", + imageOverlayURL: "../assets/slider-white-alpha.png", + callback: { + method: "setSliderValue" + } } ] }, @@ -512,8 +554,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { function openOptions(toolOptions) { var properties, + childProperties, + auxiliaryProperties, parentID, value, + imageOffset, + IMAGE_OFFSET = 0.0005, i, length; @@ -567,15 +613,62 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties.parentID = optionsOverlays[optionsOverlays.length - 1]; Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); } + if (optionsItems[i].type === "barSlider") { optionsOverlaysAuxiliaries[i] = {}; - properties = Object.clone(UI_ELEMENTS.barSliderValue.properties); - properties.parentID = optionsOverlays[optionsOverlays.length - 1]; - optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.barSliderValue.overlay, properties); - properties = Object.clone(UI_ELEMENTS.barSliderRemainder.properties); - properties.parentID = optionsOverlays[optionsOverlays.length - 1]; + auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderValue.properties); + auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; + optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.barSliderValue.overlay, + auxiliaryProperties); + auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderRemainder.properties); + auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; optionsOverlaysAuxiliaries[i].remainder = Overlays.addOverlay(UI_ELEMENTS.barSliderRemainder.overlay, - properties); + auxiliaryProperties); + } + if (optionsItems[i].type === "imageSlider") { + imageOffset = 0.0; + + // Primary image. + if (optionsItems[i].imageURL) { + childProperties = Object.clone(UI_ELEMENTS.image.properties); + childProperties.url = Script.resolvePath(optionsItems[i].imageURL); + childProperties.scale = properties.dimensions.y; + imageOffset += IMAGE_OFFSET; + childProperties.emissive = true; + if (optionsItems[i].useBaseColor) { + childProperties.color = properties.color; + } + childProperties.localPosition = { x: 0, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; + childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; + Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); + } + + // Overlay image. + if (optionsItems[i].imageOverlayURL) { + childProperties = Object.clone(UI_ELEMENTS.image.properties); + childProperties.url = Script.resolvePath(optionsItems[i].imageOverlayURL); + childProperties.drawInFront = true; // TODO: Work-around for rendering bug; remove when bug fixed. + childProperties.scale = properties.dimensions.y; + imageOffset += IMAGE_OFFSET; + childProperties.localPosition = { x: 0, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; + childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; + Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); + } + + // Value pointers. + optionsOverlaysAuxiliaries[i] = {}; + optionsOverlaysAuxiliaries[i].offset = + { x: -properties.dimensions.x / 2, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; + auxiliaryProperties = Object.clone(UI_ELEMENTS.sliderPointer.properties); + auxiliaryProperties.localPosition = optionsOverlaysAuxiliaries[i].offset; + auxiliaryProperties.drawInFront = true; // TODO: Accommodate work-around above; remove when bug fixed. + auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; + optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.sliderPointer.overlay, + auxiliaryProperties); + auxiliaryProperties.localPosition = { x: 0, y: properties.dimensions.x, z: 0 }; + auxiliaryProperties.localRotation = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }); + auxiliaryProperties.parentID = optionsOverlaysAuxiliaries[i].value; + Overlays.addOverlay(UI_ELEMENTS.sliderPointer.overlay, auxiliaryProperties); } parentID = optionsOverlays[0]; // Menu buttons parent to menu panel. optionsEnabled.push(true); @@ -816,6 +909,25 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { uiCommandCallback(intersectionItems[intersectedItem].callback.method, fraction); } + // Image slider update. + if (intersectionItems && intersectionItems[intersectedItem].type === "imageSlider" && controlHand.triggerClicked()) { + sliderProperties = Overlays.getProperties(intersection.overlayID, ["position", "orientation"]); + overlayDimensions = intersectionItems[intersectedItem].properties.dimensions; + if (overlayDimensions === undefined) { + overlayDimensions = UI_ELEMENTS.imageSlider.properties.dimensions; + } + basePoint = Vec3.sum(sliderProperties.position, + Vec3.multiplyQbyV(sliderProperties.orientation, { x: 0, y: overlayDimensions.y / 2, z: 0 })); + fraction = Vec3.dot(Vec3.subtract(basePoint, intersection.intersection), + Vec3.multiplyQbyV(sliderProperties.orientation, Vec3.UNIT_Y)) / overlayDimensions.y; + Overlays.editOverlay(optionsOverlaysAuxiliaries[intersectedItem].value, { + localPosition: Vec3.sum(optionsOverlaysAuxiliaries[intersectedItem].offset, + { x: 0, y: (0.5 - fraction) * overlayDimensions.y, z: 0 }) + }); + + uiCommandCallback(intersectionItems[intersectedItem].callback.method, fraction); + } + // Special handling for Group options. if (optionsItems && optionsItems === OPTONS_PANELS.groupOptions) { enableGroupButton = groupsCount > 1; From cc6464494665964e94aee8d356dfb7adab71ea92 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 10 Aug 2017 15:02:19 +1200 Subject: [PATCH 163/722] Don't press sliders down when click on them --- scripts/vr-edit/modules/toolMenu.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index b7141b5440..da26a97e48 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -209,6 +209,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, + BUTTON_UI_ELEMENTS = ["button", "swatch"], + OPTONS_PANELS = { groupOptions: [ { @@ -818,7 +820,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true }); highlightedItem = intersectedItem; - isHighlightingButton = true; + isHighlightingButton = BUTTON_UI_ELEMENTS.indexOf(intersectionItems[intersectedItem].type) !== NONE; } else if (highlightedItem !== NONE) { // Un-highlight previous button. Overlays.editOverlay(highlightOverlay, { From 9442a667b4c9913355d589453712b0bddd07d189 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 10 Aug 2017 15:32:48 +1200 Subject: [PATCH 164/722] Raise sliders on hover --- scripts/vr-edit/modules/toolMenu.js | 38 ++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index da26a97e48..afd7b75355 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -210,6 +210,10 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, BUTTON_UI_ELEMENTS = ["button", "swatch"], + BUTTON_PRESS_DELTA = { x: 0, y: 0, z: 0.004 }, + + SLIDER_UI_ELEMENTS = ["barSlider", "imageSlider"], + SLIDER_RAISE_DELTA = { x: 0, y: 0, z: 0.004 }, OPTONS_PANELS = { groupOptions: [ @@ -516,8 +520,10 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { intersectionOverlays, intersectionEnabled, highlightedItem, + highlightedItems, highlightedSource, isHighlightingButton, + isHighlightingSlider, pressedItem = null, pressedSource, isButtonPressed, @@ -769,7 +775,6 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { intersectionItems, parentProperties, localPosition, - BUTTON_PRESS_DELTA = 0.004, parameter, parameterValue, enableGroupButton, @@ -819,15 +824,41 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: HIGHLIGHT_PROPERTIES.properties.color, visible: true }); + // Lower old slider. + if (isHighlightingSlider) { + localPosition = highlightedItems[highlightedItem].properties.localPosition; + Overlays.editOverlay(highlightedSource[highlightedItem], { + localPosition: localPosition + }); + } + // Update status variables. highlightedItem = intersectedItem; - isHighlightingButton = BUTTON_UI_ELEMENTS.indexOf(intersectionItems[intersectedItem].type) !== NONE; + highlightedItems = intersectionItems; + isHighlightingButton = BUTTON_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; + isHighlightingSlider = SLIDER_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; + // Raise new slider. + if (isHighlightingSlider) { + localPosition = intersectionItems[highlightedItem].properties.localPosition; + Overlays.editOverlay(intersectionOverlays[highlightedItem], { + localPosition: Vec3.subtract(localPosition, SLIDER_RAISE_DELTA) + }); + } } else if (highlightedItem !== NONE) { // Un-highlight previous button. Overlays.editOverlay(highlightOverlay, { visible: false }); + // Lower slider. + if (isHighlightingSlider) { + localPosition = highlightedItems[highlightedItem].properties.localPosition; + Overlays.editOverlay(highlightedSource[highlightedItem], { + localPosition: localPosition + }); + } + // Update status variables. highlightedItem = NONE; isHighlightingButton = false; + isHighlightingSlider = false; } highlightedSource = intersectionOverlays; } @@ -847,7 +878,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Press new button. localPosition = intersectionItems[intersectedItem].properties.localPosition; Overlays.editOverlay(intersectionOverlays[intersectedItem], { - localPosition: Vec3.sum(localPosition, { x: 0, y: 0, z: BUTTON_PRESS_DELTA }) + localPosition: Vec3.sum(localPosition, BUTTON_PRESS_DELTA) }); pressedSource = intersectionOverlays; pressedItem = { @@ -1017,6 +1048,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightedItem = NONE; highlightedSource = null; isHighlightingButton = false; + isHighlightingSlider = false; pressedItem = null; pressedSource = null; isButtonPressed = false; From 594d144210ac7ee545f39b8f08b0d8683ce43845 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 10 Aug 2017 15:45:05 +1200 Subject: [PATCH 165/722] Finesse sliders to produce values that fully got to 0.0 and 1.0 --- scripts/vr-edit/modules/toolMenu.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index afd7b75355..2095117771 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -770,6 +770,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } + function adjustSliderFraction(fraction) { + // Makes slider values achieve and saturate at 0.0 and 1.0. + return Math.min(1.0, Math.max(0.0, fraction * 1.01 - 0.005)); + } + function update(intersection, groupsCount, entitiesCount) { var intersectedItem = -1, intersectionItems, @@ -928,6 +933,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Vec3.multiplyQbyV(sliderProperties.orientation, { x: 0, y: overlayDimensions.y / 2, z: 0 })); fraction = Vec3.dot(Vec3.subtract(basePoint, intersection.intersection), Vec3.multiplyQbyV(sliderProperties.orientation, Vec3.UNIT_Y)) / overlayDimensions.y; + fraction = adjustSliderFraction(fraction); otherFraction = 1.0 - fraction; Overlays.editOverlay(optionsOverlaysAuxiliaries[intersectedItem].value, { @@ -953,6 +959,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Vec3.multiplyQbyV(sliderProperties.orientation, { x: 0, y: overlayDimensions.y / 2, z: 0 })); fraction = Vec3.dot(Vec3.subtract(basePoint, intersection.intersection), Vec3.multiplyQbyV(sliderProperties.orientation, Vec3.UNIT_Y)) / overlayDimensions.y; + fraction = adjustSliderFraction(fraction); Overlays.editOverlay(optionsOverlaysAuxiliaries[intersectedItem].value, { localPosition: Vec3.sum(optionsOverlaysAuxiliaries[intersectedItem].offset, { x: 0, y: (0.5 - fraction) * overlayDimensions.y, z: 0 }) From a6af6e0bff547e6444d9009103df06244b16b550 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 10 Aug 2017 16:27:19 +1200 Subject: [PATCH 166/722] Add extra color swatches for user tests --- scripts/vr-edit/modules/toolMenu.js | 108 +++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 8 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 2095117771..781c3fb7a4 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -266,8 +266,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: 0.02, z: -0.005 }, - color: { red: 255, green: 0, blue: 0 }, + localPosition: { x: -0.035, y: -0.03, z: -0.005 }, + color: { red: 0, green: 255, blue: 255 }, solid: true }, setting: { @@ -289,8 +289,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.01, y: 0.02, z: -0.005 }, - color: { red: 0, green: 255, blue: 0 }, + localPosition: { x: -0.01, y: -0.03, z: -0.005 }, + color: { red: 255, green: 0, blue: 255 }, solid: true }, setting: { @@ -312,8 +312,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: 0.045, z: -0.005 } - // Default to empty swatch. + localPosition: { x: -0.035, y: -0.005, z: -0.005 }, + color: { red: 255, green: 255, blue: 0 }, + solid: true }, setting: { key: "VREdit.colorTool.swatch3Color", @@ -334,8 +335,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.01, y: 0.045, z: -0.005 } - // Default to empty swatch. + localPosition: { x: -0.01, y: -0.005, z: -0.005 }, + color: { red: 255, green: 0, blue: 0 }, + solid: true }, setting: { key: "VREdit.colorTool.swatch4Color", @@ -351,6 +353,96 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { parameter: "colorSwatch4" } }, + { + id: "colorSwatch5", + type: "swatch", + properties: { + dimensions: { x: 0.02, y: 0.02, z: 0.01 }, + localPosition: { x: -0.035, y: 0.02, z: -0.005 }, + color: { red: 0, green: 255, blue: 0 }, + solid: true + }, + setting: { + key: "VREdit.colorTool.swatch5Color", + property: "color" + // Default value is set in properties, above. + }, + command: { + method: "setColorPerSwatch", + parameter: "colorSwatch5.color" + }, + onGripClicked: { + method: "clearSwatch", + parameter: "colorSwatch5" + } + }, + { + id: "colorSwatch6", + type: "swatch", + properties: { + dimensions: { x: 0.02, y: 0.02, z: 0.01 }, + localPosition: { x: -0.01, y: 0.02, z: -0.005 }, + color: { red: 0, green: 0, blue: 255 }, + solid: true + }, + setting: { + key: "VREdit.colorTool.swatch6Color", + property: "color" + // Default value is set in properties, above. + }, + command: { + method: "setColorPerSwatch", + parameter: "colorSwatch6.color" + }, + onGripClicked: { + method: "clearSwatch", + parameter: "colorSwatch6" + } + }, + { + id: "colorSwatch7", + type: "swatch", + properties: { + dimensions: { x: 0.02, y: 0.02, z: 0.01 }, + localPosition: { x: -0.035, y: 0.045, z: -0.005 } + // Default to empty swatch. + }, + setting: { + key: "VREdit.colorTool.swatch7Color", + property: "color" + // Default value is set in properties, above. + }, + command: { + method: "setColorPerSwatch", + parameter: "colorSwatch7.color" + }, + onGripClicked: { + method: "clearSwatch", + parameter: "colorSwatch7" + } + }, + { + id: "colorSwatch8", + type: "swatch", + properties: { + dimensions: { x: 0.02, y: 0.02, z: 0.01 }, + localPosition: { x: -0.01, y: 0.045, z: -0.005 } + // Default to empty swatch. + }, + setting: { + key: "VREdit.colorTool.swatch8Color", + property: "color" + // Default value is set in properties, above. + }, + command: { + method: "setColorPerSwatch", + parameter: "colorSwatch8.color" + }, + onGripClicked: { + method: "clearSwatch", + parameter: "colorSwatch8" + } + }, { id: "currentColor", type: "circle", From 4b3f7d6614b64be31984e98d8e0182e99e69d870 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 10 Aug 2017 16:46:39 +1200 Subject: [PATCH 167/722] Reorient Tools menu and slightly adjust Create palette position --- scripts/vr-edit/modules/createPalette.js | 2 +- scripts/vr-edit/modules/toolMenu.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 0ee7b6e4f7..e8d5fe8989 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -25,7 +25,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { controlJointName, CANVAS_SIZE = { x: 0.21, y: 0.13 }, - PALETTE_ROOT_POSITION = { x: -CANVAS_SIZE.x / 2, y: 0.15, z: 0.09 }, + PALETTE_ROOT_POSITION = { x: -CANVAS_SIZE.x / 2, y: 0.15, z: 0.11 }, PALETTE_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 180 }), lateralOffset, diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 781c3fb7a4..bd9d926dee 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -33,8 +33,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { LEFT_HAND = 0, CANVAS_SIZE = { x: 0.22, y: 0.13 }, - PANEL_ORIGIN_POSITION = { x: CANVAS_SIZE.x / 2, y: 0.15, z: -0.04 }, - PANEL_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }), + PANEL_ORIGIN_POSITION = { x: -0.005 - CANVAS_SIZE.x / 2, y: 0.15, z: -CANVAS_SIZE.x / 2 }, + PANEL_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: -90, z: 180 }), panelLateralOffset, MENU_ORIGIN_PROPERTIES = { From b58cac1c5626489a4f76a6fb4a21953c1f9afee3 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 10 Aug 2017 17:28:43 +1200 Subject: [PATCH 168/722] Tidying --- scripts/vr-edit/modules/createPalette.js | 2 +- scripts/vr-edit/modules/toolMenu.js | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index e8d5fe8989..659e90d8cb 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -247,7 +247,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { // Joint index. handJointIndex = MyAvatar.getJointIndex(controlJointName); - if (handJointIndex === -1) { + if (handJointIndex === NONE) { // Don't display if joint isn't available (yet) to attach to. // User can clear this condition by toggling the app off and back on once avatar finishes loading. // TODO: Log error. diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index bd9d926dee..b921c8f7bb 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -868,7 +868,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } function update(intersection, groupsCount, entitiesCount) { - var intersectedItem = -1, + var intersectedItem = NONE, intersectionItems, parentProperties, localPosition, @@ -885,13 +885,13 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Intersection details. if (intersection.overlayID) { intersectedItem = menuOverlays.indexOf(intersection.overlayID); - if (intersectedItem !== -1) { + if (intersectedItem !== NONE) { intersectionItems = MENU_ITEMS; intersectionOverlays = menuOverlays; intersectionEnabled = null; } else { intersectedItem = optionsOverlays.indexOf(intersection.overlayID); - if (intersectedItem !== -1) { + if (intersectedItem !== NONE) { intersectionItems = optionsItems; intersectionOverlays = optionsOverlays; intersectionEnabled = optionsEnabled; @@ -904,7 +904,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Highlight clickable item. if (intersectedItem !== highlightedItem || intersectionOverlays !== highlightedSource) { - if (intersectedItem !== -1 && (intersectionItems[intersectedItem].command !== undefined + if (intersectedItem !== NONE && (intersectionItems[intersectedItem].command !== undefined || intersectionItems[intersectedItem].callback !== undefined)) { // Highlight new button. (The existence of a command or callback infers that the item is a button.) parentProperties = Overlays.getProperties(intersectionOverlays[intersectedItem], @@ -1027,7 +1027,6 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Vec3.multiplyQbyV(sliderProperties.orientation, Vec3.UNIT_Y)) / overlayDimensions.y; fraction = adjustSliderFraction(fraction); otherFraction = 1.0 - fraction; - Overlays.editOverlay(optionsOverlaysAuxiliaries[intersectedItem].value, { localPosition: { x: 0, y: (0.5 - fraction / 2) * overlayDimensions.y, z: 0 }, dimensions: { x: overlayDimensions.x, y: fraction * overlayDimensions.y, z: overlayDimensions.z } @@ -1101,7 +1100,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Joint index. handJointIndex = MyAvatar.getJointIndex(attachmentJointName); - if (handJointIndex === -1) { + if (handJointIndex === NONE) { // Don't display if joint isn't available (yet) to attach to. // User can clear this condition by toggling the app off and back on once avatar finishes loading. // TODO: Log error. From 68e9d335cdf29b1c65e77514a7222d80e5a3e7e5 Mon Sep 17 00:00:00 2001 From: vladest Date: Thu, 10 Aug 2017 10:45:11 +0200 Subject: [PATCH 169/722] Added async information and critical dialogs --- interface/src/assets/ATPAssetMigrator.cpp | 2 +- interface/src/ui/AddressBarDialog.cpp | 4 ++-- libraries/ui/src/OffscreenUi.cpp | 13 ++++++++++++- libraries/ui/src/OffscreenUi.h | 16 ++++++++++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/interface/src/assets/ATPAssetMigrator.cpp b/interface/src/assets/ATPAssetMigrator.cpp index b3711a2707..45459683e8 100644 --- a/interface/src/assets/ATPAssetMigrator.cpp +++ b/interface/src/assets/ATPAssetMigrator.cpp @@ -354,7 +354,7 @@ void ATPAssetMigrator::saveEntityServerFile() { infoMessage += "You can re-attempt migration on those models\nby restarting this process with the newly saved file."; } - OffscreenUi::information(_dialogParent, "Success", infoMessage); + OffscreenUi::asyncInformation(_dialogParent, "Success", infoMessage); } else { OffscreenUi::asyncWarning(_dialogParent, "Error", "Could not gzip JSON data for new entities file."); } diff --git a/interface/src/ui/AddressBarDialog.cpp b/interface/src/ui/AddressBarDialog.cpp index 8aaaac1a57..d7b59ba912 100644 --- a/interface/src/ui/AddressBarDialog.cpp +++ b/interface/src/ui/AddressBarDialog.cpp @@ -73,11 +73,11 @@ void AddressBarDialog::loadForward() { } void AddressBarDialog::displayAddressOfflineMessage() { - OffscreenUi::critical("", "That user or place is currently offline"); + OffscreenUi::asyncCritical("", "That user or place is currently offline"); } void AddressBarDialog::displayAddressNotFoundMessage() { - OffscreenUi::critical("", "There is no address information for that user or place"); + OffscreenUi::asyncCritical("", "There is no address information for that user or place"); } void AddressBarDialog::observeShownChanged(bool visible) { diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 75f13b0dbd..fd557d74c6 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -172,7 +172,7 @@ protected: virtual QVariant waitForResult() { while (!_finished) { - QCoreApplication::processEvents(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); } return _result; } @@ -295,6 +295,17 @@ QMessageBox::StandardButton OffscreenUi::information(const QString& title, const QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { return DependencyManager::get()->messageBox(OffscreenUi::Icon::ICON_INFORMATION, title, text, buttons, defaultButton); } + +void OffscreenUi::asyncCritical(const QString& title, const QString& text, + QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { + DependencyManager::get()->asyncMessageBox(OffscreenUi::Icon::ICON_CRITICAL, title, text, buttons, defaultButton); +} + +void OffscreenUi::asyncInformation(const QString& title, const QString& text, + QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { + DependencyManager::get()->asyncMessageBox(OffscreenUi::Icon::ICON_INFORMATION, title, text, buttons, defaultButton); +} + QMessageBox::StandardButton OffscreenUi::question(const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { return DependencyManager::get()->messageBox(OffscreenUi::Icon::ICON_QUESTION, title, text, buttons, defaultButton); diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 726b7897a0..09f5d0b863 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -89,6 +89,16 @@ public: QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) { return information(title, text, buttons, defaultButton); } + static void asyncCritical(void* ignored, const QString& title, const QString& text, + QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) { + return asyncCritical(title, text, buttons, defaultButton); + } + static void asyncInformation(void* ignored, const QString& title, const QString& text, + QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) { + return asyncInformation(title, text, buttons, defaultButton); + } /// Same design as QMessageBox::question(), will block, returns result static QMessageBox::StandardButton question(void* ignored, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, @@ -119,6 +129,12 @@ public: static QMessageBox::StandardButton information(const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + static void asyncCritical(const QString& title, const QString& text, + QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + static void asyncInformation(const QString& title, const QString& text, + QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); static QMessageBox::StandardButton question(const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); From 20c7e17fb65b55ba4f653bce2d18c66bb2c23cbe Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 10 Aug 2017 21:13:40 +1200 Subject: [PATCH 170/722] Tidy up logging --- scripts/vr-edit/modules/createPalette.js | 4 +- scripts/vr-edit/modules/toolIcon.js | 4 +- scripts/vr-edit/modules/toolMenu.js | 8 ++-- scripts/vr-edit/vr-edit.js | 58 ++++++++++++++---------- 4 files changed, 41 insertions(+), 33 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 659e90d8cb..4df39eb7ae 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -10,7 +10,7 @@ /* global CreatePalette */ -CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { +CreatePalette = function (App, side, leftInputs, rightInputs, uiCommandCallback) { // Tool menu displayed on top of forearm. "use strict"; @@ -250,7 +250,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { if (handJointIndex === NONE) { // Don't display if joint isn't available (yet) to attach to. // User can clear this condition by toggling the app off and back on once avatar finishes loading. - // TODO: Log error. + App.log(side, "ERROR: CreatePalette: Hand joint index isn't available!"); return; } diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 6ff748ac59..ea729271c0 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -10,7 +10,7 @@ /* global ToolIcon */ -ToolIcon = function (side) { +ToolIcon = function (App, side) { // Tool icon displayed on non-dominant hand. "use strict"; @@ -80,7 +80,7 @@ ToolIcon = function (side) { if (handJointIndex === -1) { // Don't display if joint isn't available (yet) to attach to. // User can clear this condition by toggling the app off and back on once avatar finishes loading. - // TODO: Log error. + App.log(side, "ERROR: ToolIcon: Hand joint index isn't available!"); return; } diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index b921c8f7bb..b61056a063 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -10,7 +10,7 @@ /* global ToolMenu */ -ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { +ToolMenu = function (App, side, leftInputs, rightInputs, uiCommandCallback) { // Tool menu displayed on top of forearm. "use strict"; @@ -840,7 +840,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } break; default: - // TODO: Log error. + App.log(side, "ERROR: ToolMenu: Unexpected command! " + command); } } @@ -858,7 +858,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } break; default: - // TODO: Log error. + App.log(side, "ERROR: ToolMenu: Unexpected command! " + command); } } @@ -1103,7 +1103,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (handJointIndex === NONE) { // Don't display if joint isn't available (yet) to attach to. // User can clear this condition by toggling the app off and back on once avatar finishes loading. - // TODO: Log error. + App.log(side, "ERROR: ToolMenu: Hand joint index isn't available!"); return; } diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 94b155e38c..b395596572 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -34,6 +34,7 @@ colorToolColor = { red: 128, green: 128, blue: 128 }, // Primary objects + App, Inputs, inputs = [], UI, @@ -79,21 +80,22 @@ Script.include("./modules/toolMenu.js"); - function log(message) { - print(APP_NAME + ": " + message); + function log(side, message) { + // Optional parameter: side. + var hand = "", + HAND_LETTERS = ["L", "R"]; + if (side === 0 || side === 1) { + hand = HAND_LETTERS[side] + " "; + } else { + message = side; + } + print(APP_NAME + ": " + hand + message); } function debug(side, message) { // Optional parameter: side. - var hand = "", - HAND_LETTERS = ["L", "R"]; if (DEBUG) { - if (side === 0 || side === 1) { - hand = HAND_LETTERS[side] + " "; - } else { - message = side; - } - log(hand + message); + log(side, message); } } @@ -101,6 +103,12 @@ return (hand + 1) % 2; } + App = { + log: log, + debug: debug, + DEBUG: DEBUG + }; + Inputs = function (side) { // A hand plus a laser. @@ -180,7 +188,7 @@ }; - UI = function (side, leftInputs, rightInputs, uiCommandCallback) { + UI = function (App, side, leftInputs, rightInputs, uiCommandCallback) { // Tool menu and Create palette. var // Primary objects. @@ -197,9 +205,9 @@ return new UI(); } - toolIcon = new ToolIcon(otherHand(side)); - toolMenu = new ToolMenu(side, leftInputs, rightInputs, uiCommandCallback); - createPalette = new CreatePalette(side, leftInputs, rightInputs, uiCommandCallback); + toolIcon = new ToolIcon(App, otherHand(side)); + toolMenu = new ToolMenu(App, side, leftInputs, rightInputs, uiCommandCallback); + createPalette = new CreatePalette(App, side, leftInputs, rightInputs, uiCommandCallback); getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; @@ -807,8 +815,8 @@ STATE_MACHINE[EDITOR_STATE_STRINGS[editorState]].exit(); STATE_MACHINE[EDITOR_STATE_STRINGS[state]].enter(); editorState = state; - } else if (DEBUG) { - log("ERROR: Null state transition: " + state + "!"); + } else { + log(side, "ERROR: Editor: Null state transition: " + state + "!"); } } @@ -897,7 +905,7 @@ setState(EDITOR_GRABBING); } } else { - debug(side, "ERROR: Unexpected condition in EDITOR_SEARCHING!"); + log(side, "ERROR: Editor: Unexpected condition in EDITOR_SEARCHING!"); } break; case EDITOR_HIGHLIGHTING: @@ -938,7 +946,7 @@ if (toolSelected !== TOOL_SCALE) { setState(EDITOR_DIRECT_SCALING); } else { - debug(side, "ERROR: Unexpected condition in EDITOR_HIGHLIGHTING! A"); + log(side, "ERROR: Editor: Unexpected condition A in EDITOR_HIGHLIGHTING!"); } } else if (toolSelected === TOOL_CLONE) { setState(EDITOR_CLONING); @@ -967,7 +975,7 @@ } else if (!intersection.entityID || !intersection.editableEntity) { setState(EDITOR_SEARCHING); } else { - debug(side, "ERROR: Unexpected condition in EDITOR_HIGHLIGHTING! B"); + log(side, "ERROR: Editor: Unexpected condition B in EDITOR_HIGHLIGHTING!"); } break; case EDITOR_GRABBING: @@ -996,7 +1004,7 @@ setState(EDITOR_SEARCHING); } } else { - debug(side, "ERROR: Unexpected condition in EDITOR_GRABBING!"); + log(side, "ERROR: Editor: Unexpected condition in EDITOR_GRABBING!"); } break; case EDITOR_DIRECT_SCALING: @@ -1023,7 +1031,7 @@ // Grab highlightEntityID that was scaling and has already been set. setState(EDITOR_GRABBING); } else { - debug(side, "ERROR: Unexpected condition in EDITOR_DIRECT_SCALING!"); + log(side, "ERROR: Editor: Unexpected condition in EDITOR_DIRECT_SCALING!"); } break; case EDITOR_HANDLE_SCALING: @@ -1049,7 +1057,7 @@ // Grab highlightEntityID that was scaling and has already been set. setState(EDITOR_GRABBING); } else { - debug(side, "ERROR: Unexpected condition in EDITOR_HANDLE_SCALING!"); + log(side, "ERROR: Editor: Unexpected condition in EDITOR_HANDLE_SCALING!"); } break; case EDITOR_CLONING: @@ -1066,7 +1074,7 @@ setState(EDITOR_SEARCHING); } } else { - debug(side, "ERROR: Unexpected condition in EDITOR_CLONING!"); + log(side, "ERROR: Editor: Unexpected condition in EDITOR_CLONING!"); } break; case EDITOR_GROUPING: @@ -1353,7 +1361,7 @@ } break; default: - debug("ERROR: Unexpected command in onUICommand()! " + command); + log("ERROR: Unexpected command in onUICommand(): " + command); } } @@ -1432,7 +1440,7 @@ inputs[RIGHT_HAND] = new Inputs(RIGHT_HAND); // UI object. - ui = new UI(otherHand(dominantHand), inputs[LEFT_HAND], inputs[RIGHT_HAND], onUICommand); + ui = new UI(App, otherHand(dominantHand), inputs[LEFT_HAND], inputs[RIGHT_HAND], onUICommand); // Editor objects. editors[LEFT_HAND] = new Editor(LEFT_HAND); From 6d61c188f8f7c6d2e97013c3d5266c5daecb1d41 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 10 Aug 2017 21:22:11 +1200 Subject: [PATCH 171/722] Tighten up grabbing created entity --- scripts/vr-edit/modules/createPalette.js | 2 +- scripts/vr-edit/vr-edit.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 4df39eb7ae..ec1d4ed6d9 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -228,7 +228,7 @@ CreatePalette = function (App, side, leftInputs, rightInputs, uiCommandCallback) properties.rotation = Quat.multiply(controlHand.orientation(), INVERSE_HAND_BASIS_ROTATION); Entities.addEntity(properties); - uiCommandCallback("autoGrab"); // TODO: Could pass entity ID through to autoGrab. + uiCommandCallback("autoGrab"); } wasTriggerClicked = isTriggerClicked; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index b395596572..74dcb30e0a 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1351,8 +1351,11 @@ colorToolColor = parameter; break; case "autoGrab": - editors[LEFT_HAND].enableAutoGrab(); - editors[RIGHT_HAND].enableAutoGrab(); + if (dominantHand === LEFT_HAND) { + editors[LEFT_HAND].enableAutoGrab(); + } else { + editors[RIGHT_HAND].enableAutoGrab(); + } break; case "setSliderValue": if (parameter !== undefined) { From 6977c35b6e32523b72b71d371d1fc43d7168bbac Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 10 Aug 2017 22:17:46 +1200 Subject: [PATCH 172/722] Reduce number of Entities.getEntityProperties() calls --- scripts/vr-edit/utilities/utilities.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/utilities/utilities.js b/scripts/vr-edit/utilities/utilities.js index 62343c5436..d5f9d556f8 100644 --- a/scripts/vr-edit/utilities/utilities.js +++ b/scripts/vr-edit/utilities/utilities.js @@ -48,15 +48,34 @@ if (typeof Entities.rootOf !== "function") { } if (typeof Entities.hasEditableRoot !== "function") { + Entities.hasEditableRootCache = { + CACHE_ENTRY_EXPIRY_TIME: 5000 // ms + }; + Entities.hasEditableRoot = function (entityID) { + if (Entities.hasEditableRootCache[entityID]) { + if (Date.now() - Entities.hasEditableRootCache[entityID].timeStamp + < Entities.hasEditableRootCache.CACHE_ENTRY_EXPIRY_TIME) { + return Entities.hasEditableRootCache[entityID].hasEditableRoot; + } + delete Entities.hasEditableRootCache[entityID]; + } + var EDITIBLE_ENTITY_QUERY_PROPERTYES = ["parentID", "visible", "locked", "type"], NONEDITABLE_ENTITY_TYPES = ["Unknown", "Zone", "Light"], - properties; + properties, + isEditable; + properties = Entities.getEntityProperties(entityID, EDITIBLE_ENTITY_QUERY_PROPERTYES); while (properties.parentID && properties.parentID !== Uuid.NULL) { properties = Entities.getEntityProperties(properties.parentID, EDITIBLE_ENTITY_QUERY_PROPERTYES); } - return properties.visible && !properties.locked && NONEDITABLE_ENTITY_TYPES.indexOf(properties.type) === -1; + isEditable = properties.visible && !properties.locked && NONEDITABLE_ENTITY_TYPES.indexOf(properties.type) === -1; + Entities.hasEditableRootCache[entityID] = { + hasEditableRoot: isEditable, + timeStamp: Date.now() + }; + return isEditable; }; } From badd57c16506b47ab4f3426083e9902460f023a0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 11 Aug 2017 09:26:05 +1200 Subject: [PATCH 173/722] Tidying --- scripts/vr-edit/vr-edit.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 74dcb30e0a..6400ced0fb 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -431,7 +431,7 @@ selection.startEditing(); } - function stopEditing() { + function finishEditing() { selection.finishEditing(); } @@ -698,7 +698,7 @@ } function exitEditorGrabbing() { - stopEditing(); + finishEditing(); handles.clear(); laser.clearLength(); laser.enable(); @@ -924,7 +924,7 @@ highlightedEntityID = Entities.rootOf(intersection.entityID); doUpdateState = true; } - if (toolSelected === TOOL_SCALE !== wasScaleTool) { + if ((toolSelected === TOOL_SCALE) !== wasScaleTool) { wasScaleTool = toolSelected === TOOL_SCALE; doUpdateState = true; } @@ -982,7 +982,7 @@ if (hand.valid() && isTriggerClicked && !isGripClicked) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // No transition. - if (toolSelected === TOOL_SCALE !== wasScaleTool) { + if ((toolSelected === TOOL_SCALE) !== wasScaleTool) { updateState(); wasScaleTool = toolSelected === TOOL_SCALE; } From 37f57852da593b9f1413925183b95f6fe4d8212f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 11 Aug 2017 09:56:47 +1200 Subject: [PATCH 174/722] Set hand highlight sphere size from grab radius --- scripts/vr-edit/modules/hand.js | 5 +++++ scripts/vr-edit/modules/highlights.js | 11 +++++++++-- scripts/vr-edit/vr-edit.js | 2 ++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/hand.js b/scripts/vr-edit/modules/hand.js index 92e8b06e10..35073173ff 100644 --- a/scripts/vr-edit/modules/hand.js +++ b/scripts/vr-edit/modules/hand.js @@ -97,6 +97,10 @@ Hand = function (side) { return intersection; } + function getNearGrabRadius() { + return NEAR_GRAB_RADIUS; + } + function update() { var gripValue, overlayID, @@ -213,6 +217,7 @@ Hand = function (side) { gripClicked: gripClicked, setGripClickedHandled: setGripClickedHandled, intersection: getIntersection, + getNearGrabRadius: getNearGrabRadius, update: update, clear: clear, destroy: destroy diff --git a/scripts/vr-edit/modules/highlights.js b/scripts/vr-edit/modules/highlights.js index 333a68f04e..5e88575a73 100644 --- a/scripts/vr-edit/modules/highlights.js +++ b/scripts/vr-edit/modules/highlights.js @@ -22,7 +22,6 @@ Highlights = function (side) { GROUP_COLOR = { red: 220, green: 60, blue: 220 }, HAND_HIGHLIGHT_ALPHA = 0.35, ENTITY_HIGHLIGHT_ALPHA = 0.8, - HAND_HIGHLIGHT_DIMENSIONS = { x: 0.1, y: 0.1, z: 0.1 }, HAND_HIGHLIGHT_OFFSET = { x: 0.0, y: 0.11, z: 0.02 }, LEFT_HAND = 0; @@ -31,7 +30,7 @@ Highlights = function (side) { } handOverlay = Overlays.addOverlay("sphere", { - dimensions: HAND_HIGHLIGHT_DIMENSIONS, + dimension: Vec3.ZERO, parentID: Uuid.SELF, parentJointIndex: MyAvatar.getJointIndex(side === LEFT_HAND ? "_CONTROLLER_LEFTHAND" @@ -44,6 +43,13 @@ Highlights = function (side) { visible: false }); + function setHandHighlightRadius(radius) { + var dimension = 2 * radius; + Overlays.editOverlay(handOverlay, { + dimensions: { x: dimension, y: dimension, z: dimension } + }); + } + function maybeAddEntityOverlay(index) { if (index >= entityOverlays.length) { entityOverlays.push(Overlays.addOverlay("cube", { @@ -115,6 +121,7 @@ Highlights = function (side) { HIGHLIGHT_COLOR: HIGHLIGHT_COLOR, SCALE_COLOR: SCALE_COLOR, GROUP_COLOR: GROUP_COLOR, + setHandHighlightRadius: setHandHighlightRadius, display: display, clear: clear, destroy: destroy diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 6400ced0fb..6da49060e4 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -388,6 +388,8 @@ otherEditor = editor; // Object. laserOffset = laser.handOffset(); // Value. + + highlights.setHandHighlightRadius(hand.getNearGrabRadius()); } From c34344161ee5965f837fa3e18bc79df74a936bcd Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 11 Aug 2017 10:09:52 +1200 Subject: [PATCH 175/722] Improve App object handling --- scripts/vr-edit/modules/createPalette.js | 4 ++-- scripts/vr-edit/modules/toolIcon.js | 4 ++-- scripts/vr-edit/modules/toolMenu.js | 4 ++-- scripts/vr-edit/vr-edit.js | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index ec1d4ed6d9..d1c52d41f9 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -8,9 +8,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global CreatePalette */ +/* global App, CreatePalette */ -CreatePalette = function (App, side, leftInputs, rightInputs, uiCommandCallback) { +CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { // Tool menu displayed on top of forearm. "use strict"; diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index ea729271c0..9b0d81052d 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -8,9 +8,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global ToolIcon */ +/* global App, ToolIcon */ -ToolIcon = function (App, side) { +ToolIcon = function (side) { // Tool icon displayed on non-dominant hand. "use strict"; diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index b61056a063..4198e9b7b0 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -8,9 +8,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global ToolMenu */ +/* global App, ToolMenu */ -ToolMenu = function (App, side, leftInputs, rightInputs, uiCommandCallback) { +ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Tool menu displayed on top of forearm. "use strict"; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 6da49060e4..187bfd3fb7 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -188,7 +188,7 @@ }; - UI = function (App, side, leftInputs, rightInputs, uiCommandCallback) { + UI = function (side, leftInputs, rightInputs, uiCommandCallback) { // Tool menu and Create palette. var // Primary objects. @@ -205,9 +205,9 @@ return new UI(); } - toolIcon = new ToolIcon(App, otherHand(side)); - toolMenu = new ToolMenu(App, side, leftInputs, rightInputs, uiCommandCallback); - createPalette = new CreatePalette(App, side, leftInputs, rightInputs, uiCommandCallback); + toolIcon = new ToolIcon(otherHand(side)); + toolMenu = new ToolMenu(side, leftInputs, rightInputs, uiCommandCallback); + createPalette = new CreatePalette(side, leftInputs, rightInputs, uiCommandCallback); getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; @@ -1445,7 +1445,7 @@ inputs[RIGHT_HAND] = new Inputs(RIGHT_HAND); // UI object. - ui = new UI(App, otherHand(dominantHand), inputs[LEFT_HAND], inputs[RIGHT_HAND], onUICommand); + ui = new UI(otherHand(dominantHand), inputs[LEFT_HAND], inputs[RIGHT_HAND], onUICommand); // Editor objects. editors[LEFT_HAND] = new Editor(LEFT_HAND); From 821f5de3e325d9b19c4c31b3a9f7a0a28444cc41 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 11 Aug 2017 10:20:36 +1200 Subject: [PATCH 176/722] Further reduce number of Entites.getEntityProperties() calls --- scripts/vr-edit/utilities/utilities.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/scripts/vr-edit/utilities/utilities.js b/scripts/vr-edit/utilities/utilities.js index d5f9d556f8..8dacec8c2d 100644 --- a/scripts/vr-edit/utilities/utilities.js +++ b/scripts/vr-edit/utilities/utilities.js @@ -33,7 +33,19 @@ if (typeof Uuid.SELF !== "string") { } if (typeof Entities.rootOf !== "function") { + Entities.rootOfCache = { + CACHE_ENTRY_EXPIRY_TIME: 1000 // ms + }; + Entities.rootOf = function (entityID) { + if (Entities.rootOfCache[entityID]) { + if (Date.now() - Entities.rootOfCache[entityID].timeStamp + < Entities.rootOfCache.CACHE_ENTRY_EXPIRY_TIME) { + return Entities.rootOfCache[entityID].rootOf; + } + delete Entities.rootOfCache[entityID]; + } + var rootEntityID, entityProperties, PARENT_PROPERTIES = ["parentID"]; @@ -43,6 +55,11 @@ if (typeof Entities.rootOf !== "function") { rootEntityID = entityProperties.parentID; entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); } + + Entities.rootOfCache[entityID] = { + rootOf: rootEntityID, + timeStamp: Date.now() + }; return rootEntityID; }; } @@ -71,6 +88,7 @@ if (typeof Entities.hasEditableRoot !== "function") { properties = Entities.getEntityProperties(properties.parentID, EDITIBLE_ENTITY_QUERY_PROPERTYES); } isEditable = properties.visible && !properties.locked && NONEDITABLE_ENTITY_TYPES.indexOf(properties.type) === -1; + Entities.hasEditableRootCache[entityID] = { hasEditableRoot: isEditable, timeStamp: Date.now() From 0b063926b6f63807cc25f76415565d2597e21fbd Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 11 Aug 2017 11:41:33 +1200 Subject: [PATCH 177/722] Move image slider to Color options --- scripts/vr-edit/modules/toolMenu.js | 34 +++++++++++++++-------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 4198e9b7b0..6bb2b8f9e5 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -182,7 +182,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { "imageSlider": { overlay: "cube", properties: { - dimensions: { x: 0.02, y: 0.1, z: 0.01 }, + dimensions: { x: 0.01, y: 0.05, z: 0.01 }, localRotation: Quat.ZERO, color: { red: 128, green: 128, blue: 128 }, alpha: 1.0, @@ -198,7 +198,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { overlay: "shape", properties: { shape: "Cone", - dimensions: { x: 0.01, y: 0.01, z: 0.01 }, + dimensions: { x: 0.005, y: 0.005, z: 0.005 }, localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }), color: { red: 180, green: 180, blue: 180 }, alpha: 1.0, @@ -261,6 +261,20 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: { x: 0.055, y: 0.0, z: -0.005 } } }, + { + id: "colorSlider", + type: "imageSlider", + properties: { + localPosition: { x: 0.025, y: -0.025, z: -0.005 }, + color: { red: 255, green: 0, blue: 0 } + }, + useBaseColor: true, + imageURL: "../assets/slider-white.png", + imageOverlayURL: "../assets/slider-white-alpha.png", + callback: { + method: "setSliderValue" + } + }, { id: "colorSwatch1", type: "swatch", @@ -486,20 +500,6 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { callback: { method: "setSliderValue" } - }, - { - id: "colorSlider", - type: "imageSlider", - properties: { - localPosition: { x: 0.02, y: 0.0, z: -0.005 }, - color: { red: 255, green: 0, blue: 0 } - }, - useBaseColor: true, - imageURL: "../assets/slider-white.png", - imageOverlayURL: "../assets/slider-white-alpha.png", - callback: { - method: "setSliderValue" - } } ] }, @@ -732,6 +732,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (optionsItems[i].imageURL) { childProperties = Object.clone(UI_ELEMENTS.image.properties); childProperties.url = Script.resolvePath(optionsItems[i].imageURL); + delete childProperties.dimensions; childProperties.scale = properties.dimensions.y; imageOffset += IMAGE_OFFSET; childProperties.emissive = true; @@ -748,6 +749,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { childProperties = Object.clone(UI_ELEMENTS.image.properties); childProperties.url = Script.resolvePath(optionsItems[i].imageOverlayURL); childProperties.drawInFront = true; // TODO: Work-around for rendering bug; remove when bug fixed. + delete childProperties.dimensions; childProperties.scale = properties.dimensions.y; imageOffset += IMAGE_OFFSET; childProperties.localPosition = { x: 0, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; From c82a4d6d56e639b98ca9067812f835ddc701bafa Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 11 Aug 2017 13:15:07 +1200 Subject: [PATCH 178/722] Layout mockup of Physics options --- scripts/vr-edit/modules/toolMenu.js | 68 ++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 6bb2b8f9e5..3a1a60fca1 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -491,15 +491,74 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: { x: 0.055, y: 0.0, z: -0.005 } } }, + { - id: "physicsSlider", + id: "gravityToggle", + type: "panel", + properties: { + localPosition: { x: -0.0325, y: -0.03, z: -0.005 }, + dimensions: { x: 0.03, y: 0.02, z: 0.01 } + } + }, + { + id: "grabToggle", + type: "panel", + properties: { + localPosition: { x: -0.0325, y: -0.005, z: -0.005 }, + dimensions: { x: 0.03, y: 0.02, z: 0.01 } + } + }, + { + id: "collideToggle", + type: "panel", + properties: { + localPosition: { x: -0.0325, y: 0.02, z: -0.005 }, + dimensions: { x: 0.03, y: 0.02, z: 0.01 } + } + }, + + { + id: "presets", + type: "panel", + properties: { + localPosition: { x: 0.016, y: -0.03, z: -0.005 }, + dimensions: { x: 0.06, y: 0.02, z: 0.01 } + } + }, + { + id: "gravitySlider", type: "barSlider", properties: { - localPosition: { x: -0.02, y: 0.0, z: -0.005 } + localPosition: { x: -0.007, y: 0.016, z: -0.005 }, + dimensions: { x: 0.014, y: 0.06, z: 0.01 } }, callback: { method: "setSliderValue" } + }, + { + id: "bounceSlider", + type: "panel", + properties: { + localPosition: { x: 0.009, y: 0.016, z: -0.005 }, + dimensions: { x: 0.014, y: 0.06, z: 0.01 } + } + }, + { + id: "dampingSlider", + type: "panel", + properties: { + localPosition: { x: 0.024, y: 0.016, z: -0.005 }, + dimensions: { x: 0.014, y: 0.06, z: 0.01 } + } + }, + { + id: "densitySlider", + type: "panel", + properties: { + localPosition: { x: 0.039, y: 0.016, z: -0.005 }, + dimensions: { x: 0.014, y: 0.06, z: 0.01 } + } } ] }, @@ -715,12 +774,17 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } if (optionsItems[i].type === "barSlider") { + // Initial value = 0. optionsOverlaysAuxiliaries[i] = {}; auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderValue.properties); + auxiliaryProperties.dimensions = Vec3.multiplyVbyV({ x: 1, y: 0, z: 0 }, properties.dimensions); + auxiliaryProperties.localPosition = Vec3.ZERO; auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.barSliderValue.overlay, auxiliaryProperties); auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderRemainder.properties); + auxiliaryProperties.dimensions = properties.dimensions; + auxiliaryProperties.localPosition = Vec3.ZERO; auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; optionsOverlaysAuxiliaries[i].remainder = Overlays.addOverlay(UI_ELEMENTS.barSliderRemainder.overlay, auxiliaryProperties); From ee225e6a8d15c6e88c84b188ffd116a47c75a5e5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 11 Aug 2017 18:23:20 +1200 Subject: [PATCH 179/722] Implement toggle buttons --- scripts/vr-edit/modules/toolMenu.js | 100 +++++++++++++++++++++++++--- scripts/vr-edit/vr-edit.js | 2 +- 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 3a1a60fca1..042635bbc7 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -85,6 +85,19 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, + "toggleButton": { + overlay: "cube", + properties: { + dimensions: { x: 0.03, y: 0.03, z: 0.01 }, + localRotation: Quat.ZERO, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + }, + onColor: { red: 100, green: 240, blue: 100 }, + offColor: { red: 64, green: 64, blue: 64 } + }, "swatch": { overlay: "cube", properties: { @@ -209,7 +222,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, - BUTTON_UI_ELEMENTS = ["button", "swatch"], + BUTTON_UI_ELEMENTS = ["button", "toggleButton", "swatch"], BUTTON_PRESS_DELTA = { x: 0, y: 0, z: 0.004 }, SLIDER_UI_ELEMENTS = ["barSlider", "imageSlider"], @@ -492,28 +505,70 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, + { + id: "propertiesLabel", + type: "label", + properties: { + text: "PROPERTIES", + lineHeight: 0.005, + localPosition: { x: -0.0325, y: -0.0475, z: -0.0075} + } + }, { id: "gravityToggle", - type: "panel", + type: "toggleButton", properties: { localPosition: { x: -0.0325, y: -0.03, z: -0.005 }, dimensions: { x: 0.03, y: 0.02, z: 0.01 } + }, + label: "GRAVITY", + setting: { + key: "VREdit.physicsTool.gravityOn", + // No property + defaultValue: false, + command: "setGravity" + }, + command: { + method: "setGravity", + parameter: "gravityToggle" } }, { id: "grabToggle", - type: "panel", + type: "toggleButton", properties: { localPosition: { x: -0.0325, y: -0.005, z: -0.005 }, dimensions: { x: 0.03, y: 0.02, z: 0.01 } + }, + label: " GRAB", + setting: { + key: "VREdit.physicsTool.grabOn", + // No property + defaultValue: false, + command: "setGrab" + }, + command: { + method: "setGrab", + parameter: "grabToggle" } }, { id: "collideToggle", - type: "panel", + type: "toggleButton", properties: { localPosition: { x: -0.0325, y: 0.02, z: -0.005 }, dimensions: { x: 0.03, y: 0.02, z: 0.01 } + }, + label: "COLLIDE", + setting: { + key: "VREdit.physicsTool.collideOn", + // No property + defaultValue: false, + command: "setCollide" + }, + command: { + method: "setCollide", + parameter: "collideToggle" } }, @@ -753,14 +808,21 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (value === "") { value = optionsItems[i].setting.defaultValue; } - if (value) { + if (value !== "") { properties[optionsItems[i].setting.property] = value; if (optionsItems[i].type === "swatch") { // Special case for when swatch color is defined. properties.solid = true; } - if (optionsItems[i].setting.callback) { - uiCommandCallback(optionsItems[i].setting.callback.method, value); + if (optionsItems[i].type === "toggleButton") { + // Store value in optionsSettings rather than using overlay property. + optionsSettings[optionsItems[i].id].value = value; + properties.color = value + ? UI_ELEMENTS[optionsItems[i].type].onColor + : UI_ELEMENTS[optionsItems[i].type].offColor; + } + if (optionsItems[i].setting.command) { + uiCommandCallback(optionsItems[i].setting.command, value); } } } @@ -866,15 +928,16 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { function doCommand(command, parameter) { var parameters, - overlayID, + index, hasColor, value; switch (command) { + case "setColorPerSwatch": parameters = parameter.split("."); - overlayID = optionsOverlaysIDs.indexOf(parameters[0]); - hasColor = Overlays.getProperty(optionsOverlays[overlayID], "solid"); + index = optionsOverlaysIDs.indexOf(parameters[0]); + hasColor = Overlays.getProperty(optionsOverlays[index], "solid"); if (hasColor) { // Swatch has a color; set current fill color to swatch color. value = evaluateParameter(parameter); @@ -888,7 +951,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } else { // Swatch has no color; set swatch color to current fill color. value = Overlays.getProperty(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], "color"); - Overlays.editOverlay(optionsOverlays[overlayID], { + Overlays.editOverlay(optionsOverlays[index], { color: value, solid: true }); @@ -897,6 +960,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } break; + case "setColorFromPick": Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], { color: parameter @@ -905,6 +969,20 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Settings.setValue(optionsSettings.currentColor.key, parameter); } break; + + case "setGravity": + case "setGrab": + case "setCollide": + value = !optionsSettings[parameter].value; + optionsSettings[parameter].value = value; + Settings.setValue(optionsSettings[parameter].key, value); + index = optionsOverlaysIDs.indexOf(parameter); + Overlays.editOverlay(optionsOverlays[index], { + color: value ? UI_ELEMENTS[optionsItems[index].type].onColor : UI_ELEMENTS[optionsItems[index].type].offColor + }); + uiCommandCallback(command, value); + break; + default: App.log(side, "ERROR: ToolMenu: Unexpected command! " + command); } diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 187bfd3fb7..1935bd0443 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1366,7 +1366,7 @@ } break; default: - log("ERROR: Unexpected command in onUICommand(): " + command); + log("ERROR: Unexpected command in onUICommand(): " + command + ", " + parameter); } } From a96d9ab85b76ffe63c7a520890de90b60820b5ab Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 11 Aug 2017 22:11:34 +1200 Subject: [PATCH 180/722] Apply physics toggle button values --- scripts/vr-edit/modules/selection.js | 57 +++++++++++++++++++++++----- scripts/vr-edit/vr-edit.js | 33 +++++++++++++++- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 2b1e7e5a37..561dafedb5 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -24,7 +24,10 @@ Selection = function (side) { scaleRootOffset, scaleRootOrientation, ENTITY_TYPE = "entity", - ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "ParticleEffect"]; + ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "ParticleEffect"], + DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD + DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.02, z: 0 }; + if (!this instanceof Selection) { return new Selection(side); @@ -36,7 +39,7 @@ Selection = function (side) { var children, properties, SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions", "parentID", "localPosition", - "dynamic", "collisionless"], + "dynamic", "collisionless", "userData"], i, length; @@ -50,7 +53,8 @@ Selection = function (side) { rotation: properties.rotation, dimensions: properties.dimensions, dynamic: properties.dynamic, - collisionless: properties.collisionless + collisionless: properties.collisionless, + userData: properties.userData }); children = Entities.getChildrenIDs(id); @@ -187,8 +191,6 @@ Selection = function (side) { function finishEditing() { var firstDynamicEntityID = null, properties, - VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD - VELOCITY_KICK = { x: 0, y: 0.02, z: 0 }, count, i; @@ -206,8 +208,8 @@ Selection = function (side) { // If dynamic with gravity, and velocity is zero, give the entity set a little kick to set off physics. if (firstDynamicEntityID) { properties = Entities.getEntityProperties(firstDynamicEntityID, ["velocity", "gravity"]); - if (Vec3.length(properties.gravity) > 0 && Vec3.length(properties.velocity) < VELOCITY_THRESHOLD) { - Entities.editEntity(firstDynamicEntityID, { velocity: VELOCITY_KICK }); + if (Vec3.length(properties.gravity) > 0 && Vec3.length(properties.velocity) < DYNAMIC_VELOCITY_THRESHOLD) { + Entities.editEntity(firstDynamicEntityID, { velocity: DYNAMIC_VELOCITY_KICK }); } } } @@ -365,8 +367,45 @@ Selection = function (side) { return properties.color; } - function applyPhysics() { - // TODO + function updatePhysicsUserData(userDataString, physicsUserData) { + var userData = {}; + + if (userDataString !== "") { + try { + userData = JSON.parse(userDataString); + } catch (e) { + App.log(side, "ERROR: Invalid userData in entity being updated! " + userDataString); + } + } + + if (!userData.hasOwnProperty("grabbableKey")) { + userData.grabbableKey = {}; + } + userData.grabbableKey.grabbable = physicsUserData.grabbableKey.grabbable; + + return JSON.stringify(userData); + } + + function applyPhysics(physicsProperties) { + // Applies physics to the current selection (i.e., the selection made when entity was trigger-clicked to apply physics). + var properties, + i, + length; + + properties = Object.clone(physicsProperties); + + for (i = 0, length = selection.length; i < length; i += 1) { + properties.userData = updatePhysicsUserData(selection[i].userData, physicsProperties.userData); + Entities.editEntity(selection[i].id, properties); + } + + if (physicsProperties.dynamic) { + // Give dynamic entities with zero a little kick to set off physics. + properties = Entities.getEntityProperties(selection[0].id, ["velocity"]); + if (Vec3.length(properties.velocity) < DYNAMIC_VELOCITY_THRESHOLD) { + Entities.editEntity(selection[0].id, { velocity: DYNAMIC_VELOCITY_KICK }); + } + } } function clear() { diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 1935bd0443..d24c7a986e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -32,6 +32,7 @@ TOOL_DELETE = 7, toolSelected = TOOL_NONE, colorToolColor = { red: 128, green: 128, blue: 128 }, + physicsToolPhysics = { userData: { grabbableKey: {} } }, // Primary objects App, @@ -886,6 +887,7 @@ } else if (toolSelected === TOOL_GROUP) { setState(EDITOR_GROUPING); } else if (toolSelected === TOOL_COLOR) { + setState(EDITOR_HIGHLIGHTING); selection.applyColor(colorToolColor); } else if (toolSelected === TOOL_PICK_COLOR) { color = selection.getColor(intersection.entityID); @@ -898,7 +900,8 @@ ui.setToolIcon(ui.COLOR_TOOL); ui.setToolColor(colorToolColor); } else if (toolSelected === TOOL_PHYSICS) { - selection.applyPhysics(); + setState(EDITOR_HIGHLIGHTING); + selection.applyPhysics(physicsToolPhysics); } else if (toolSelected === TOOL_DELETE) { setState(EDITOR_HIGHLIGHTING); selection.deleteEntities(); @@ -967,7 +970,7 @@ ui.setToolIcon(ui.COLOR_TOOL); ui.setToolColor(colorToolColor); } else if (toolSelected === TOOL_PHYSICS) { - selection.applyPhysics(); + selection.applyPhysics(physicsToolPhysics); } else if (toolSelected === TOOL_DELETE) { selection.deleteEntities(); setState(EDITOR_SEARCHING); @@ -1338,12 +1341,14 @@ ui.setToolIcon(ui.DELETE_TOOL); ui.updateUIEntities(); break; + case "groupButton": grouping.group(); break; case "ungroupButton": grouping.ungroup(); break; + case "setColor": if (toolSelected === TOOL_PICK_COLOR) { toolSelected = TOOL_COLOR; @@ -1352,6 +1357,29 @@ ui.setToolColor(parameter); colorToolColor = parameter; break; + + case "setGravity": + if (parameter) { + physicsToolPhysics.gravity = { x: 0, y: -9.8, z: 0 }; // Earth gravity. + physicsToolPhysics.dynamic = true; + } else { + physicsToolPhysics.gravity = Vec3.ZERO; + physicsToolPhysics.dynamic = false; + } + break; + case "setGrab": + physicsToolPhysics.userData.grabbableKey.grabbable = parameter; + break; + case "setCollide": + if (parameter) { + physicsToolPhysics.collisionless = false; + physicsToolPhysics.collidesWith = "static,dynamic,kinematic,myAvatar,otherAvatar"; + } else { + physicsToolPhysics.collisionless = true; + physicsToolPhysics.collidesWith = ""; + } + break; + case "autoGrab": if (dominantHand === LEFT_HAND) { editors[LEFT_HAND].enableAutoGrab(); @@ -1359,6 +1387,7 @@ editors[RIGHT_HAND].enableAutoGrab(); } break; + case "setSliderValue": if (parameter !== undefined) { // TODO From 2bf0ea1dff6ab2d4ada712e4be7c31d71c556867 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 11 Aug 2017 22:31:04 +1200 Subject: [PATCH 181/722] Fix display artifact that occurs when overlay dimension is 0 --- scripts/vr-edit/modules/toolMenu.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 042635bbc7..1dd0eb486c 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -227,6 +227,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { SLIDER_UI_ELEMENTS = ["barSlider", "imageSlider"], SLIDER_RAISE_DELTA = { x: 0, y: 0, z: 0.004 }, + MIN_BAR_SLIDER_DIMENSION = 0.0001, + OPTONS_PANELS = { groupOptions: [ @@ -839,8 +841,16 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Initial value = 0. optionsOverlaysAuxiliaries[i] = {}; auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderValue.properties); - auxiliaryProperties.dimensions = Vec3.multiplyVbyV({ x: 1, y: 0, z: 0 }, properties.dimensions); - auxiliaryProperties.localPosition = Vec3.ZERO; + auxiliaryProperties.dimensions = { + x: properties.dimensions.x, + y: MIN_BAR_SLIDER_DIMENSION, // Avoid visual artifact if 0. + z: properties.dimensions.z + }; + auxiliaryProperties.localPosition = { + x: 0, + y: properties.dimensions.y / 2, + z: 0 + }; auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.barSliderValue.overlay, auxiliaryProperties); @@ -1173,11 +1183,19 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { otherFraction = 1.0 - fraction; Overlays.editOverlay(optionsOverlaysAuxiliaries[intersectedItem].value, { localPosition: { x: 0, y: (0.5 - fraction / 2) * overlayDimensions.y, z: 0 }, - dimensions: { x: overlayDimensions.x, y: fraction * overlayDimensions.y, z: overlayDimensions.z } + dimensions: { + x: overlayDimensions.x, + y: Math.max(fraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION), // Avoid visual artifact if 0. + z: overlayDimensions.z + } }); Overlays.editOverlay(optionsOverlaysAuxiliaries[intersectedItem].remainder, { localPosition: { x: 0, y: (-0.5 + otherFraction / 2) * overlayDimensions.y, z: 0 }, - dimensions: { x: overlayDimensions.x, y: otherFraction * overlayDimensions.y, z: overlayDimensions.z } + dimensions: { + x: overlayDimensions.x, + y: Math.max(otherFraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION), // Avoid visual artifact if 0. + z: overlayDimensions.z + } }); uiCommandCallback(intersectionItems[intersectedItem].callback.method, fraction); From 541402b559f6872a10da521c36076753e5735d7c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 12 Aug 2017 14:56:22 +1200 Subject: [PATCH 182/722] Distinguish properly between intersected and root entities --- scripts/vr-edit/modules/selection.js | 26 ++++--- scripts/vr-edit/vr-edit.js | 104 ++++++++++++++++----------- 2 files changed, 80 insertions(+), 50 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 561dafedb5..d5965b3552 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -16,7 +16,7 @@ Selection = function (side) { "use strict"; var selection = [], - selectedEntityID = null, + intersectedEntityID = null, rootEntityID = null, rootPosition, rootOrientation, @@ -65,12 +65,14 @@ Selection = function (side) { } } - function select(entityID) { + function select(intersectionEntityID) { var entityProperties, PARENT_PROPERTIES = ["parentID", "position", "rotation", "dymamic", "collisionless"]; + intersectedEntityID = intersectionEntityID; + // Find root parent. - rootEntityID = Entities.rootOf(entityID); + rootEntityID = Entities.rootOf(intersectedEntityID); // Selection position and orientation is that of the root entity. entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); @@ -80,8 +82,10 @@ Selection = function (side) { // Find all children. selection = []; traverseEntityTree(rootEntityID, selection); + } - selectedEntityID = entityID; + function getIntersectedEntityID() { + return intersectedEntityID; } function getRootEntityID() { @@ -263,7 +267,7 @@ Selection = function (side) { } function finishDirectScaling() { - select(selectedEntityID); // Refresh. + select(intersectedEntityID); // Refresh. } function startHandleScaling() { @@ -296,19 +300,23 @@ Selection = function (side) { } function finishHandleScaling() { - select(selectedEntityID); // Refresh. + select(intersectedEntityID); // Refresh. } function cloneEntities() { var parentIDIndexes = [], + intersectedEntityIndex = 0, parentID, properties, i, j, length; - // Map parent IDs. + // Map parent IDs; find intersectedEntityID's index. for (i = 1, length = selection.length; i < length; i += 1) { + if (selection[i].id === intersectedEntityID) { + intersectedEntityIndex = i; + } parentID = selection[i].parentID; for (j = 0; j < i; j += 1) { if (parentID === selection[j].id) { @@ -327,6 +335,7 @@ Selection = function (side) { selection[i].id = Entities.addEntity(properties); } + intersectedEntityID = selection[intersectedEntityIndex].id; rootEntityID = selection[0].id; } @@ -410,7 +419,7 @@ Selection = function (side) { function clear() { selection = []; - selectedEntityID = null; + intersectedEntityID = null; rootEntityID = null; } @@ -429,6 +438,7 @@ Selection = function (side) { select: select, selection: getSelection, count: count, + intersectedEntityID: getIntersectedEntityID, rootEntityID: getRootEntityID, boundingBox: getBoundingBox, getPositionAndOrientation: getPositionAndOrientation, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index d24c7a986e..9664a346cb 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -337,7 +337,8 @@ // State machine. STATE_MACHINE, - highlightedEntityID = null, // Root entity of highlighted entity set. + intersectedEntityID = null, // Intersected entity of highlighted entity set. + rootEntityID = null, // Root entity of highlighted entity set. wasScaleTool = false, isOtherEditorEditingEntityID = false, isTriggerClicked = false, @@ -408,18 +409,22 @@ return handles.isHandle(overlayID); } - function isEditing(rootEntityID) { - // rootEntityID is an optional parameter. + function isEditing(aRootEntityID) { + // aRootEntityID is an optional parameter. return editorState > EDITOR_HIGHLIGHTING - && (rootEntityID === undefined || rootEntityID === selection.rootEntityID()); + && (aRootEntityID === undefined || aRootEntityID === rootEntityID); } function isScaling() { return editorState === EDITOR_DIRECT_SCALING || editorState === EDITOR_HANDLE_SCALING; } - function rootEntityID() { - return selection.rootEntityID(); + function getIntersectedEntityID() { + return intersectedEntityID; + } + + function getRootEntityID() { + return rootEntityID; } @@ -634,6 +639,8 @@ function enterEditorSearching() { selection.clear(); + intersectedEntityID = null; + rootEntityID = null; hoveredOverlayID = intersection.overlayID; otherEditor.hoverHandle(hoveredOverlayID); } @@ -650,21 +657,21 @@ } function enterEditorHighlighting() { - selection.select(highlightedEntityID); - if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(highlightedEntityID)) { + selection.select(intersectedEntityID); + if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(rootEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), - toolSelected === TOOL_SCALE || otherEditor.isEditing(highlightedEntityID) + toolSelected === TOOL_SCALE || otherEditor.isEditing(rootEntityID) ? highlights.SCALE_COLOR : highlights.HIGHLIGHT_COLOR); } - isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID); + isOtherEditorEditingEntityID = otherEditor.isEditing(rootEntityID); wasScaleTool = toolSelected === TOOL_SCALE; } function updateEditorHighlighting() { - selection.select(highlightedEntityID); - if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(highlightedEntityID)) { + selection.select(intersectedEntityID); + if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(rootEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), - toolSelected === TOOL_SCALE || otherEditor.isEditing(highlightedEntityID) + toolSelected === TOOL_SCALE || otherEditor.isEditing(rootEntityID) ? highlights.SCALE_COLOR : highlights.HIGHLIGHT_COLOR); } else { highlights.clear(); @@ -678,23 +685,23 @@ } function enterEditorGrabbing() { - selection.select(highlightedEntityID); // For when transitioning from EDITOR_SEARCHING. + selection.select(intersectedEntityID); // For when transitioning from EDITOR_SEARCHING. if (intersection.laserIntersected) { laser.setLength(laser.length()); } else { laser.disable(); } if (toolSelected === TOOL_SCALE) { - handles.display(highlightedEntityID, selection.boundingBox(), selection.count() > 1); + handles.display(rootEntityID, selection.boundingBox(), selection.count() > 1); } startEditing(); wasScaleTool = toolSelected === TOOL_SCALE; } function updateEditorGrabbing() { - selection.select(highlightedEntityID); + selection.select(intersectedEntityID); if (toolSelected === TOOL_SCALE) { - handles.display(highlightedEntityID, selection.boundingBox(), selection.count() > 1); + handles.display(rootEntityID, selection.boundingBox(), selection.count() > 1); } else { handles.clear(); } @@ -708,7 +715,7 @@ } function enterEditorDirectScaling() { - selection.select(highlightedEntityID); // In case need to transition to EDITOR_GRABBING. + selection.select(intersectedEntityID); // In case need to transition to EDITOR_GRABBING. isScalingWithHand = intersection.handIntersected; if (intersection.laserIntersected) { laser.setLength(laser.length()); @@ -729,7 +736,7 @@ } function enterEditorHandleScaling() { - selection.select(highlightedEntityID); // In case need to transition to EDITOR_GRABBING. + selection.select(intersectedEntityID); // In case need to transition to EDITOR_GRABBING. isScalingWithHand = intersection.handIntersected; if (intersection.laserIntersected) { laser.setLength(laser.length()); @@ -750,9 +757,11 @@ } function enterEditorCloning() { - selection.select(highlightedEntityID); // For when transitioning from EDITOR_SEARCHING. + selection.select(intersectedEntityID); // For when transitioning from EDITOR_SEARCHING. selection.cloneEntities(); - highlightedEntityID = selection.rootEntityID(); + intersectedEntityID = selection.intersectedEntityID(); + rootEntityID = selection.rootEntityID(); + intersectedEntityID = rootEntityID; } function exitEditorCloning() { @@ -760,7 +769,7 @@ } function enterEditorGrouping() { - if (!grouping.includes(highlightedEntityID)) { + if (!grouping.includes(rootEntityID)) { highlights.display(false, selection.selection(), highlights.GROUP_COLOR); } grouping.toggle(selection.selection()); @@ -869,16 +878,19 @@ setState(EDITOR_IDLE); } else if (intersection.overlayID && !wasTriggerClicked && isTriggerClicked && otherEditor.isHandle(intersection.overlayID)) { - highlightedEntityID = otherEditor.rootEntityID(); + intersectedEntityID = otherEditor.intersectedEntityID(); + rootEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && intersection.editableEntity && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab) { - highlightedEntityID = Entities.rootOf(intersection.entityID); + intersectedEntityID = intersection.entityID; + rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); } else if (intersection.entityID && intersection.editableEntity && (!wasTriggerClicked || isAutoGrab) && isTriggerClicked) { - highlightedEntityID = Entities.rootOf(intersection.entityID); - if (otherEditor.isEditing(highlightedEntityID)) { + intersectedEntityID = intersection.entityID; + rootEntityID = Entities.rootOf(intersectedEntityID); + if (otherEditor.isEditing(rootEntityID)) { if (toolSelected !== TOOL_SCALE) { setState(EDITOR_DIRECT_SCALING); } @@ -917,16 +929,17 @@ if (hand.valid() && intersection.entityID && intersection.editableEntity && !(!wasTriggerClicked && isTriggerClicked - && (!otherEditor.isEditing(highlightedEntityID) || toolSelected !== TOOL_SCALE)) + && (!otherEditor.isEditing(rootEntityID) || toolSelected !== TOOL_SCALE)) && !(!wasTriggerClicked && isTriggerClicked && intersection.overlayID && otherEditor.isHandle(intersection.overlayID))) { // No transition. doUpdateState = false; - if (otherEditor.isEditing(highlightedEntityID) !== isOtherEditorEditingEntityID) { + if (otherEditor.isEditing(rootEntityID) !== isOtherEditorEditingEntityID) { doUpdateState = true; } - if (Entities.rootOf(intersection.entityID) !== highlightedEntityID) { - highlightedEntityID = Entities.rootOf(intersection.entityID); + if (Entities.rootOf(intersection.entityID) !== rootEntityID) { + intersectedEntityID = intersection.entityID; + rootEntityID = Entities.rootOf(intersectedEntityID); doUpdateState = true; } if ((toolSelected === TOOL_SCALE) !== wasScaleTool) { @@ -943,11 +956,13 @@ setState(EDITOR_IDLE); } else if (intersection.overlayID && !wasTriggerClicked && isTriggerClicked && otherEditor.isHandle(intersection.overlayID)) { - highlightedEntityID = otherEditor.rootEntityID(); + intersectedEntityID = otherEditor.intersectedEntityID(); + rootEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && intersection.editableEntity && !wasTriggerClicked && isTriggerClicked) { - highlightedEntityID = Entities.rootOf(intersection.entityID); // May be a different entityID. - if (otherEditor.isEditing(highlightedEntityID)) { + intersectedEntityID = intersection.entityID; // May be a different entityID. + rootEntityID = Entities.rootOf(intersectedEntityID); + if (otherEditor.isEditing(rootEntityID)) { if (toolSelected !== TOOL_SCALE) { setState(EDITOR_DIRECT_SCALING); } else { @@ -998,7 +1013,8 @@ setState(EDITOR_IDLE); } else if (!isTriggerClicked) { if (intersection.entityID && intersection.editableEntity) { - highlightedEntityID = Entities.rootOf(intersection.entityID); + intersectedEntityID = intersection.entityID; + rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); } else { setState(EDITOR_SEARCHING); @@ -1014,7 +1030,7 @@ break; case EDITOR_DIRECT_SCALING: if (hand.valid() && isTriggerClicked - && (otherEditor.isEditing(highlightedEntityID) || otherEditor.isHandle(intersection.overlayID))) { + && (otherEditor.isEditing(rootEntityID) || otherEditor.isHandle(intersection.overlayID))) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // Don't test toolSelected === TOOL_SCALE because this is a UI element and so not able to be changed while // scaling with two hands. @@ -1029,10 +1045,11 @@ if (!intersection.entityID || !intersection.editableEntity) { setState(EDITOR_SEARCHING); } else { - highlightedEntityID = Entities.rootOf(intersection.entityID); + intersectedEntityID = intersection.entityID; + rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); } - } else if (!otherEditor.isEditing(highlightedEntityID)) { + } else if (!otherEditor.isEditing(rootEntityID)) { // Grab highlightEntityID that was scaling and has already been set. setState(EDITOR_GRABBING); } else { @@ -1040,7 +1057,7 @@ } break; case EDITOR_HANDLE_SCALING: - if (hand.valid() && isTriggerClicked && otherEditor.isEditing(highlightedEntityID)) { + if (hand.valid() && isTriggerClicked && otherEditor.isEditing(rootEntityID)) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // Don't test toolSelected === TOOL_SCALE because this is a UI element and so not able to be changed while // scaling with two hands. @@ -1055,10 +1072,11 @@ if (!intersection.entityID || !intersection.editableEntity) { setState(EDITOR_SEARCHING); } else { - highlightedEntityID = Entities.rootOf(intersection.entityID); + intersectedEntityID = intersection.entityID; + rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); } - } else if (!otherEditor.isEditing(highlightedEntityID)) { + } else if (!otherEditor.isEditing(rootEntityID)) { // Grab highlightEntityID that was scaling and has already been set. setState(EDITOR_GRABBING); } else { @@ -1073,7 +1091,8 @@ setState(EDITOR_IDLE); } else if (!isTriggerClicked) { if (intersection.entityID && intersection.editableEntity) { - highlightedEntityID = Entities.rootOf(intersection.entityID); + intersectedEntityID = intersection.entityID; + rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); } else { setState(EDITOR_SEARCHING); @@ -1150,7 +1169,8 @@ isHandle: isHandle, isEditing: isEditing, isScaling: isScaling, - rootEntityID: rootEntityID, + intersectedEntityID: getIntersectedEntityID, + rootEntityID: getRootEntityID, startDirectScaling: startDirectScaling, updateDirectScaling: updateDirectScaling, stopDirectScaling: stopDirectScaling, From c281d5f94bd4cd8064859cebb3f2151962872ff4 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 12 Aug 2017 15:46:53 +1200 Subject: [PATCH 183/722] Highlight and color single entities with Color tool --- scripts/vr-edit/modules/highlights.js | 17 +++++++++++----- scripts/vr-edit/modules/selection.js | 28 +++++++++++++++++++++++---- scripts/vr-edit/vr-edit.js | 15 ++++++++++---- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/scripts/vr-edit/modules/highlights.js b/scripts/vr-edit/modules/highlights.js index 5e88575a73..7c8cb0e99f 100644 --- a/scripts/vr-edit/modules/highlights.js +++ b/scripts/vr-edit/modules/highlights.js @@ -75,7 +75,8 @@ Highlights = function (side) { }); } - function display(handIntersected, selection, overlayColor) { + function display(handIntersected, selection, entityIndex, overlayColor) { + // Displays highlight for just entityIndex if non-null, otherwise highlights whole selection. var i, length; @@ -85,10 +86,16 @@ Highlights = function (side) { visible: handIntersected }); - // Add/edit entity overlay. - for (i = 0, length = selection.length; i < length; i += 1) { - maybeAddEntityOverlay(i); - editEntityOverlay(i, selection[i], overlayColor); + if (entityIndex !== null) { + // Add/edit entity overlay for just entityIndex. + maybeAddEntityOverlay(0); + editEntityOverlay(0, selection[entityIndex], overlayColor); + } else { + // Add/edit entity overlays for all entities in selection. + for (i = 0, length = selection.length; i < length; i += 1) { + maybeAddEntityOverlay(i); + editEntityOverlay(i, selection[i], overlayColor); + } } // Delete extra entity overlays. diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index d5965b3552..935841698f 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -17,6 +17,7 @@ Selection = function (side) { var selection = [], intersectedEntityID = null, + intersectedEntityIndex, rootEntityID = null, rootPosition, rootOrientation, @@ -57,6 +58,10 @@ Selection = function (side) { userData: properties.userData }); + if (id === intersectedEntityID) { + intersectedEntityIndex = result.length - 1; + } + children = Entities.getChildrenIDs(id); for (i = 0, length = children.length; i < length; i += 1) { if (Entities.getNestableType(children[i]) === ENTITY_TYPE) { @@ -88,6 +93,10 @@ Selection = function (side) { return intersectedEntityID; } + function getIntersectedEntityIndex() { + return intersectedEntityIndex; + } + function getRootEntityID() { return rootEntityID; } @@ -339,17 +348,27 @@ Selection = function (side) { rootEntityID = selection[0].id; } - function applyColor(color) { + function applyColor(color, isApplyToAll) { // Entities without a color property simply ignore the edit. var properties, isError = true, i, length; - for (i = 0, length = selection.length; i < length; i += 1) { - properties = Entities.getEntityProperties(selection[i].id, "color"); + if (isApplyToAll) { + for (i = 0, length = selection.length; i < length; i += 1) { + properties = Entities.getEntityProperties(selection[i].id, "color"); + if (ENTITY_TYPES_WITH_COLOR.indexOf(properties.type) !== -1) { + Entities.editEntity(selection[i].id, { + color: color + }); + isError = false; + } + } + } else { + properties = Entities.getEntityProperties(intersectedEntityID, "type"); if (ENTITY_TYPES_WITH_COLOR.indexOf(properties.type) !== -1) { - Entities.editEntity(selection[i].id, { + Entities.editEntity(intersectedEntityID, { color: color }); isError = false; @@ -439,6 +458,7 @@ Selection = function (side) { selection: getSelection, count: count, intersectedEntityID: getIntersectedEntityID, + intersectedEntityIndex: getIntersectedEntityIndex, rootEntityID: getRootEntityID, boundingBox: getBoundingBox, getPositionAndOrientation: getPositionAndOrientation, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 9664a346cb..471bd2c24e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -660,6 +660,7 @@ selection.select(intersectedEntityID); if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(rootEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), + toolSelected === TOOL_COLOR ? selection.intersectedEntityIndex() : null, toolSelected === TOOL_SCALE || otherEditor.isEditing(rootEntityID) ? highlights.SCALE_COLOR : highlights.HIGHLIGHT_COLOR); } @@ -671,6 +672,7 @@ selection.select(intersectedEntityID); if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(rootEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), + toolSelected === TOOL_COLOR ? selection.intersectedEntityIndex() : null, toolSelected === TOOL_SCALE || otherEditor.isEditing(rootEntityID) ? highlights.SCALE_COLOR : highlights.HIGHLIGHT_COLOR); } else { @@ -770,7 +772,7 @@ function enterEditorGrouping() { if (!grouping.includes(rootEntityID)) { - highlights.display(false, selection.selection(), highlights.GROUP_COLOR); + highlights.display(false, selection.selection(), null, highlights.GROUP_COLOR); } grouping.toggle(selection.selection()); } @@ -849,6 +851,7 @@ function update() { var previousState = editorState, doUpdateState, + doRefreshSelection, color; intersection = getIntersection(); @@ -900,7 +903,7 @@ setState(EDITOR_GROUPING); } else if (toolSelected === TOOL_COLOR) { setState(EDITOR_HIGHLIGHTING); - selection.applyColor(colorToolColor); + selection.applyColor(colorToolColor, false); } else if (toolSelected === TOOL_PICK_COLOR) { color = selection.getColor(intersection.entityID); if (color) { @@ -946,6 +949,10 @@ wasScaleTool = toolSelected === TOOL_SCALE; doUpdateState = true; } + if (toolSelected === TOOL_COLOR && intersection.entityID !== intersectedEntityID) { + intersectedEntityID = intersection.entityID; + doUpdateState = true; + } if (doUpdateState) { updateState(); } @@ -973,7 +980,7 @@ } else if (toolSelected === TOOL_GROUP) { setState(EDITOR_GROUPING); } else if (toolSelected === TOOL_COLOR) { - selection.applyColor(colorToolColor); + selection.applyColor(colorToolColor, false); } else if (toolSelected === TOOL_PICK_COLOR) { color = selection.getColor(intersection.entityID); if (color) { @@ -1256,7 +1263,7 @@ exludedrightRootEntityID = rightRootEntityID; } - highlights.display(false, groups.selection(excludedRootEntityIDs), highlights.GROUP_COLOR); + highlights.display(false, groups.selection(excludedRootEntityIDs), null, highlights.GROUP_COLOR); hasSelectionChanged = false; } From a5407398e701c799a2c10a03c835ba04b93cd318 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 12 Aug 2017 16:21:55 +1200 Subject: [PATCH 184/722] Fix Group tool highlights not being cleared when switch to other tools --- scripts/vr-edit/vr-edit.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 471bd2c24e..ca2d429ab5 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1346,6 +1346,7 @@ ui.updateUIEntities(); break; case "colorTool": + grouping.clear(); toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); ui.setToolColor(parameter); @@ -1353,11 +1354,13 @@ ui.updateUIEntities(); break; case "pickColorTool": + grouping.clear(); toolSelected = TOOL_PICK_COLOR; ui.setToolIcon(ui.PICK_COLOR_TOOL); ui.updateUIEntities(); break; case "physicsTool": + grouping.clear(); toolSelected = TOOL_PHYSICS; ui.setToolIcon(ui.PHYSICS_TOOL); ui.updateUIEntities(); From bc00bcd02ceed1b0565d16bc3cbf047bc84c98d4 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 12 Aug 2017 16:59:28 +1200 Subject: [PATCH 185/722] For development testing, apply physics to the entity intersected --- scripts/vr-edit/modules/selection.js | 10 ++++++++++ scripts/vr-edit/vr-edit.js | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 935841698f..1682ce0bbc 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -415,6 +415,15 @@ Selection = function (side) { } function applyPhysics(physicsProperties) { + // TODO: For development testing, apply physics to the currently intersected entity. + var properties; + + properties = Object.clone(physicsProperties); + properties.userData = updatePhysicsUserData(selection[intersectedEntityIndex].userData, physicsProperties.userData); + Entities.editEntity(selection[intersectedEntityIndex].id, properties); + + // TODO: Original functionality applied physics to all entities in the selection. + /* // Applies physics to the current selection (i.e., the selection made when entity was trigger-clicked to apply physics). var properties, i, @@ -434,6 +443,7 @@ Selection = function (side) { Entities.editEntity(selection[0].id, { velocity: DYNAMIC_VELOCITY_KICK }); } } + */ } function clear() { diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index ca2d429ab5..c10bb748f7 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -851,7 +851,6 @@ function update() { var previousState = editorState, doUpdateState, - doRefreshSelection, color; intersection = getIntersection(); @@ -953,6 +952,12 @@ intersectedEntityID = intersection.entityID; doUpdateState = true; } + // TODO: For development testing, update intersectedEntityID so that physics can be applied to it. + if ((toolSelected === TOOL_COLOR || toolSelected === TOOL_PHYSICS) + && intersection.entityID !== intersectedEntityID) { + intersectedEntityID = intersection.entityID; + doUpdateState = true; + } if (doUpdateState) { updateState(); } From 71993972a214f618c5e3a3895a52c3ab2a16a8f1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 13 Aug 2017 14:04:01 +1200 Subject: [PATCH 186/722] Improve ungrouping behavior --- scripts/vr-edit/modules/groups.js | 131 ++++++++++++++++++------------ 1 file changed, 80 insertions(+), 51 deletions(-) diff --git a/scripts/vr-edit/modules/groups.js b/scripts/vr-edit/modules/groups.js index 036df1a71c..8cfb2b971e 100644 --- a/scripts/vr-edit/modules/groups.js +++ b/scripts/vr-edit/modules/groups.js @@ -15,26 +15,26 @@ Groups = function () { "use strict"; - var groupRootEntityIDs = [], - groupSelectionDetails = [], - numberOfEntitiesSelected = 0; + var rootEntityIDs = [], + selections = [], + entitiesSelectedCount = 0; if (!this instanceof Groups) { return new Groups(); } function add(selection) { - groupRootEntityIDs.push(selection[0].id); - groupSelectionDetails.push(Object.clone(selection)); - numberOfEntitiesSelected += selection.length; + rootEntityIDs.push(selection[0].id); + selections.push(Object.clone(selection)); + entitiesSelectedCount += selection.length; } function remove(selection) { - var index = groupRootEntityIDs.indexOf(selection[0].id); + var index = rootEntityIDs.indexOf(selection[0].id); - numberOfEntitiesSelected -= groupSelectionDetails[index].length; - groupRootEntityIDs.splice(index, 1); - groupSelectionDetails.splice(index, 1); + entitiesSelectedCount -= selections[index].length; + rootEntityIDs.splice(index, 1); + selections.splice(index, 1); } function toggle(selection) { @@ -42,7 +42,7 @@ Groups = function () { return; } - if (groupRootEntityIDs.indexOf(selection[0].id) === -1) { + if (rootEntityIDs.indexOf(selection[0].id) === -1) { add(selection); } else { remove(selection); @@ -56,10 +56,10 @@ Groups = function () { j, lengthJ; - for (i = 0, lengthI = groupRootEntityIDs.length; i < lengthI; i += 1) { - if (excludes.indexOf(groupRootEntityIDs[i]) === -1) { - for (j = 0, lengthJ = groupSelectionDetails[i].length; j < lengthJ; j += 1) { - result.push(groupSelectionDetails[i][j]); + for (i = 0, lengthI = rootEntityIDs.length; i < lengthI; i += 1) { + if (excludes.indexOf(rootEntityIDs[i]) === -1) { + for (j = 0, lengthJ = selections[i].length; j < lengthJ; j += 1) { + result.push(selections[i][j]); } } } @@ -68,78 +68,107 @@ Groups = function () { } function includes(rootEntityID) { - return groupRootEntityIDs.indexOf(rootEntityID) !== -1; + return rootEntityIDs.indexOf(rootEntityID) !== -1; } function groupsCount() { - return groupSelectionDetails.length; + return selections.length; } function entitiesCount() { - return numberOfEntitiesSelected; + return entitiesSelectedCount; } function group() { + // Groups all selections into one. var rootID, i, count; // Make the first entity in the first group the root and link the first entities of all other groups to it. - rootID = groupRootEntityIDs[0]; - for (i = 1, count = groupRootEntityIDs.length; i < count; i += 1) { - Entities.editEntity(groupRootEntityIDs[i], { + rootID = rootEntityIDs[0]; + for (i = 1, count = rootEntityIDs.length; i < count; i += 1) { + Entities.editEntity(rootEntityIDs[i], { parentID: rootID }); } // Update selection. - groupRootEntityIDs.splice(1, groupRootEntityIDs.length - 1); - for (i = 1, count = groupSelectionDetails.length; i < count; i += 1) { - groupSelectionDetails[i][0].parentID = rootID; - groupSelectionDetails[0] = groupSelectionDetails[0].concat(groupSelectionDetails[i]); + rootEntityIDs.splice(1, rootEntityIDs.length - 1); + for (i = 1, count = selections.length; i < count; i += 1) { + selections[i][0].parentID = rootID; + selections[0] = selections[0].concat(selections[i]); } - groupSelectionDetails.splice(1, groupSelectionDetails.length - 1); + selections.splice(1, selections.length - 1); } function ungroup() { + // Ungroups the first and assumed to be only selection. + // If the first entity in the selection has a mix of solo and group children then just the group children are unlinked, + // otherwise all are unlinked. var rootID, - childrenIDs, - childrenIDIndexes, + childrenIDs = [], + childrenIndexes = [], + childrenIndexIsGroup = [], + previousWasGroup, + hasSoloChildren = false, + hasGroupChildren = false, + isUngroupAll, i, count; - // Compile information on children. - rootID = groupRootEntityIDs[0]; - childrenIDs = []; - childrenIDIndexes = []; - for (i = 1, count = groupSelectionDetails[0].length; i < count; i += 1) { - if (groupSelectionDetails[0][i].parentID === rootID) { - childrenIDs.push(groupSelectionDetails[0][i].id); - childrenIDIndexes.push(i); + function updateGroupInformation() { + var childrenIndexesLength = childrenIndexes.length; + if (childrenIndexesLength > 1) { + previousWasGroup = childrenIndexes[childrenIndexesLength - 2] < i - 1; + childrenIndexIsGroup.push(previousWasGroup); + if (previousWasGroup) { + hasGroupChildren = true; + } else { + hasSoloChildren = true; + } } } - childrenIDIndexes.push(groupSelectionDetails[0].length); // Extra item at end to aid updating selection. - // Unlink direct children from root entity. - for (i = 0, count = childrenIDs.length; i < count; i += 1) { - Entities.editEntity(childrenIDs[i], { - parentID: Uuid.NULL - }); + if (entitiesSelectedCount === 0) { + App.log("ERROR: Groups: Nothing to ungroup!"); + return; + } + if (entitiesSelectedCount === 1) { + App.log("ERROR: Groups: Cannot ungroup sole entity!"); + return; } - // Update selection. - groupRootEntityIDs = groupRootEntityIDs.concat(childrenIDs); - for (i = 0, count = childrenIDs.length; i < count; i += 1) { - groupSelectionDetails.push(groupSelectionDetails[0].slice(childrenIDIndexes[i], childrenIDIndexes[i + 1])); - groupSelectionDetails[i + 1][0].parentID = Uuid.NULL; + // Compile information on immediate children. + rootID = rootEntityIDs[0]; + for (i = 1, count = selections[0].length; i < count; i += 1) { + if (selections[0][i].parentID === rootID) { + childrenIDs.push(selections[0][i].id); + childrenIndexes.push(i); + updateGroupInformation(); + } + } + childrenIndexes.push(selections[0].length); // Special extra item at end to aid updating selection. + updateGroupInformation(); + + // Unlink children. + isUngroupAll = hasSoloChildren !== hasGroupChildren; + for (i = childrenIDs.length - 1; i >= 0; i -= 1) { + if (isUngroupAll || childrenIndexIsGroup[i]) { + Entities.editEntity(childrenIDs[i], { + parentID: Uuid.NULL + }); + rootEntityIDs.push(childrenIDs[i]); + selections[0][childrenIndexes[i]].parentID = Uuid.NULL; + selections.push(selections[0].splice(childrenIndexes[i], childrenIndexes[i + 1] - childrenIndexes[i])); + } } - groupSelectionDetails[0].splice(1, groupSelectionDetails[0].length - childrenIDIndexes[0]); } function clear() { - groupRootEntityIDs = []; - groupSelectionDetails = []; - numberOfEntitiesSelected = 0; + rootEntityIDs = []; + selections = []; + entitiesSelectedCount = 0; } function destroy() { From b604b1bbac75f8833dbf8d82d12143839cbf6f55 Mon Sep 17 00:00:00 2001 From: vladest Date: Sun, 13 Aug 2017 11:02:29 +0200 Subject: [PATCH 187/722] Async file dialogs #1 --- libraries/ui/src/OffscreenUi.cpp | 47 ++++++++++++++++++++++++++++++++ libraries/ui/src/OffscreenUi.h | 5 +++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index fd557d74c6..4027c4c5b6 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -680,6 +680,31 @@ QString OffscreenUi::fileDialog(const QVariantMap& properties) { return result.toUrl().toLocalFile(); } +QQuickItem *OffscreenUi::fileDialogAsync(const QVariantMap& properties) { + QVariant buildDialogResult; + bool invokeResult; + auto tabletScriptingInterface = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode()) { + invokeResult = QMetaObject::invokeMethod(_desktop, "fileDialog", + Q_RETURN_ARG(QVariant, buildDialogResult), + Q_ARG(QVariant, QVariant::fromValue(properties))); + } else { + QQuickItem* tabletRoot = tablet->getTabletRoot(); + invokeResult = QMetaObject::invokeMethod(tabletRoot, "fileDialog", + Q_RETURN_ARG(QVariant, buildDialogResult), + Q_ARG(QVariant, QVariant::fromValue(properties))); + emit tabletScriptingInterface->tabletNotification(); + } + + if (!invokeResult) { + qWarning() << "Failed to create file open dialog"; + return nullptr; + } + + return qvariant_cast(buildDialogResult); +} + QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { if (QThread::currentThread() != thread()) { QString result; @@ -702,6 +727,28 @@ QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, return fileDialog(map); } +QString OffscreenUi::fileOpenDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { + if (QThread::currentThread() != thread()) { + QString result; + BLOCKING_INVOKE_METHOD(this, "fileOpenDialogAsync", + Q_RETURN_ARG(QString, result), + Q_ARG(QString, caption), + Q_ARG(QString, dir), + Q_ARG(QString, filter), + Q_ARG(QString*, selectedFilter), + Q_ARG(QFileDialog::Options, options)); + return result; + } + + // FIXME support returning the selected filter... somehow? + QVariantMap map; + map.insert("caption", caption); + map.insert("dir", QUrl::fromLocalFile(dir)); + map.insert("filter", filter); + map.insert("options", static_cast(options)); + return fileDialog(map); +} + QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { if (QThread::currentThread() != thread()) { QString result; diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 09f5d0b863..90902dc417 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -149,6 +149,7 @@ public: QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); Q_INVOKABLE QString fileOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE QString fileOpenDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QString fileSaveDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QString existingDirectoryDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); @@ -190,11 +191,13 @@ public: signals: void showDesktop(); void response(QMessageBox::StandardButton response); - public slots: + void fileDialogResponse(QString response); +public slots: void removeModalDialog(QObject* modal); private: QString fileDialog(const QVariantMap& properties); + QQuickItem* fileDialogAsync(const QVariantMap &properties); QString assetDialog(const QVariantMap& properties); QQuickItem* _desktop { nullptr }; From 441ba157083cae37cf0789db5ae5c26be96ad45f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 14 Aug 2017 17:23:02 +1200 Subject: [PATCH 188/722] Fix swatch colors --- scripts/vr-edit/modules/toolMenu.js | 56 +++++++++++------------------ 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 1dd0eb486c..ea8106c769 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -295,14 +295,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: -0.03, z: -0.005 }, - color: { red: 0, green: 255, blue: 255 }, - solid: true + localPosition: { x: -0.035, y: -0.03, z: -0.005 } }, setting: { key: "VREdit.colorTool.swatch1Color", - property: "color" - // Default value is set in properties, above. + property: "color", + defaultValue: { red: 0, green: 255, blue: 255 } }, command: { method: "setColorPerSwatch", @@ -318,14 +316,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.01, y: -0.03, z: -0.005 }, - color: { red: 255, green: 0, blue: 255 }, - solid: true + localPosition: { x: -0.01, y: -0.03, z: -0.005 } }, setting: { key: "VREdit.colorTool.swatch2Color", - property: "color" - // Default value is set in properties, above. + property: "color", + defaultValue: { red: 255, green: 0, blue: 255 } }, command: { method: "setColorPerSwatch", @@ -341,14 +337,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: -0.005, z: -0.005 }, - color: { red: 255, green: 255, blue: 0 }, - solid: true + localPosition: { x: -0.035, y: -0.005, z: -0.005 } }, setting: { key: "VREdit.colorTool.swatch3Color", - property: "color" - // Default value is set in properties, above. + property: "color", + defaultValue: { red: 255, green: 255, blue: 0 } }, command: { method: "setColorPerSwatch", @@ -364,14 +358,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.01, y: -0.005, z: -0.005 }, - color: { red: 255, green: 0, blue: 0 }, - solid: true + localPosition: { x: -0.01, y: -0.005, z: -0.005 } }, setting: { key: "VREdit.colorTool.swatch4Color", - property: "color" - // Default value is set in properties, above. + property: "color", + defaultValue: { red: 255, green: 0, blue: 0 } }, command: { method: "setColorPerSwatch", @@ -387,14 +379,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: 0.02, z: -0.005 }, - color: { red: 0, green: 255, blue: 0 }, - solid: true + localPosition: { x: -0.035, y: 0.02, z: -0.005 } }, setting: { key: "VREdit.colorTool.swatch5Color", - property: "color" - // Default value is set in properties, above. + property: "color", + defaultValue: { red: 0, green: 255, blue: 0 } }, command: { method: "setColorPerSwatch", @@ -410,14 +400,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.01, y: 0.02, z: -0.005 }, - color: { red: 0, green: 0, blue: 255 }, - solid: true + localPosition: { x: -0.01, y: 0.02, z: -0.005 } }, setting: { key: "VREdit.colorTool.swatch6Color", - property: "color" - // Default value is set in properties, above. + property: "color", + defaultValue: { red: 0, green: 0, blue: 255 } }, command: { method: "setColorPerSwatch", @@ -434,12 +422,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, localPosition: { x: -0.035, y: 0.045, z: -0.005 } - // Default to empty swatch. }, setting: { key: "VREdit.colorTool.swatch7Color", property: "color" - // Default value is set in properties, above. + // Default to empty swatch. }, command: { method: "setColorPerSwatch", @@ -456,12 +443,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, localPosition: { x: -0.01, y: 0.045, z: -0.005 } - // Default to empty swatch. }, setting: { key: "VREdit.colorTool.swatch8Color", property: "color" - // Default value is set in properties, above. + // Default to empty swatch. }, command: { method: "setColorPerSwatch", @@ -807,7 +793,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (optionsItems[i].setting) { optionsSettings[optionsItems[i].id] = { key: optionsItems[i].setting.key }; value = Settings.getValue(optionsItems[i].setting.key); - if (value === "") { + if (value === "" && optionsItems[i].setting.defaultValue) { value = optionsItems[i].setting.defaultValue; } if (value !== "") { From ac7aa609f292abc38773ad4451609add290a00e8 Mon Sep 17 00:00:00 2001 From: vladest Date: Mon, 14 Aug 2017 16:23:53 +0200 Subject: [PATCH 189/722] Open file async implemented --- interface/src/Application.cpp | 15 +++++++++------ libraries/ui/src/OffscreenUi.cpp | 23 +++++++++++++++++------ libraries/ui/src/OffscreenUi.h | 8 +++++--- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 408e4e5bd1..966cead203 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6774,12 +6774,15 @@ void Application::openUrl(const QUrl& url) const { void Application::loadDialog() { auto scriptEngines = DependencyManager::get(); - QString fileNameString = OffscreenUi::getOpenFileName( - _glWidget, tr("Open Script"), getPreviousScriptLocation(), tr("JavaScript Files (*.js)")); - if (!fileNameString.isEmpty() && QFile(fileNameString).exists()) { - setPreviousScriptLocation(QFileInfo(fileNameString).absolutePath()); - DependencyManager::get()->loadScript(fileNameString, true, false, false, true); // Don't load from cache - } + auto offscreenUi = DependencyManager::get(); + connect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, this, [=] (QString response) { + if (!response.isEmpty() && QFile(response).exists()) { + setPreviousScriptLocation(QFileInfo(response).absolutePath()); + DependencyManager::get()->loadScript(response, true, false, false, true); // Don't load from cache + } + }); + OffscreenUi::getOpenFileNameAsync(_glWidget, tr("Open Script"), getPreviousScriptLocation(), + tr("JavaScript Files (*.js)")); } QString Application::getPreviousScriptLocation() { diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 4027c4c5b6..8ea92d2b1e 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -645,6 +645,9 @@ private slots: void onSelectedFile(QVariant file) { _result = file; _finished = true; + auto offscreenUi = DependencyManager::get(); + emit offscreenUi->fileDialogResponse(_result.toUrl().toLocalFile()); + offscreenUi->removeModalDialog(qobject_cast(this)); disconnect(_dialog); } }; @@ -680,7 +683,7 @@ QString OffscreenUi::fileDialog(const QVariantMap& properties) { return result.toUrl().toLocalFile(); } -QQuickItem *OffscreenUi::fileDialogAsync(const QVariantMap& properties) { +void OffscreenUi::fileDialogAsync(const QVariantMap& properties) { QVariant buildDialogResult; bool invokeResult; auto tabletScriptingInterface = DependencyManager::get(); @@ -699,10 +702,14 @@ QQuickItem *OffscreenUi::fileDialogAsync(const QVariantMap& properties) { if (!invokeResult) { qWarning() << "Failed to create file open dialog"; - return nullptr; + return; } - return qvariant_cast(buildDialogResult); + FileDialogListener* fileDialogListener = new FileDialogListener(qvariant_cast(buildDialogResult)); + QObject* fileModalDialog = qobject_cast(fileDialogListener); + _modalDialogListeners.push_back(fileModalDialog); + + return; } QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { @@ -727,7 +734,7 @@ QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, return fileDialog(map); } -QString OffscreenUi::fileOpenDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { +void OffscreenUi::fileOpenDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { if (QThread::currentThread() != thread()) { QString result; BLOCKING_INVOKE_METHOD(this, "fileOpenDialogAsync", @@ -737,7 +744,7 @@ QString OffscreenUi::fileOpenDialogAsync(const QString& caption, const QString& Q_ARG(QString, filter), Q_ARG(QString*, selectedFilter), Q_ARG(QFileDialog::Options, options)); - return result; + return; } // FIXME support returning the selected filter... somehow? @@ -746,7 +753,7 @@ QString OffscreenUi::fileOpenDialogAsync(const QString& caption, const QString& map.insert("dir", QUrl::fromLocalFile(dir)); map.insert("filter", filter); map.insert("options", static_cast(options)); - return fileDialog(map); + fileDialogAsync(map); } QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { @@ -799,6 +806,10 @@ QString OffscreenUi::getOpenFileName(void* ignored, const QString &caption, cons return DependencyManager::get()->fileOpenDialog(caption, dir, filter, selectedFilter, options); } +void OffscreenUi::getOpenFileNameAsync(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { + return DependencyManager::get()->fileOpenDialogAsync(caption, dir, filter, selectedFilter, options); +} + QString OffscreenUi::getSaveFileName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { return DependencyManager::get()->fileSaveDialog(caption, dir, filter, selectedFilter, options); } diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 90902dc417..335645ce06 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -149,14 +149,16 @@ public: QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); Q_INVOKABLE QString fileOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); - Q_INVOKABLE QString fileOpenDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE void fileOpenDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QString fileSaveDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QString existingDirectoryDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QString assetOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); // Compatibility with QFileDialog::getOpenFileName - static QString getOpenFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + static QString getOpenFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + static void getOpenFileNameAsync(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + // Compatibility with QFileDialog::getSaveFileName static QString getSaveFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); // Compatibility with QFileDialog::getExistingDirectory @@ -197,7 +199,7 @@ public slots: private: QString fileDialog(const QVariantMap& properties); - QQuickItem* fileDialogAsync(const QVariantMap &properties); + void fileDialogAsync(const QVariantMap &properties); QString assetDialog(const QVariantMap& properties); QQuickItem* _desktop { nullptr }; From 504857a1b8f2e8e00fe56c661d47969590652e2a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 15 Aug 2017 10:23:36 +1200 Subject: [PATCH 190/722] Set up Physics sliders --- scripts/vr-edit/modules/toolMenu.js | 66 ++++++++++++++++++++++++++--- scripts/vr-edit/vr-edit.js | 26 ++++++++++++ 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index ea8106c769..fb80330dc5 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -498,8 +498,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "label", properties: { text: "PROPERTIES", - lineHeight: 0.005, - localPosition: { x: -0.0325, y: -0.0475, z: -0.0075} + lineHeight: 0.0045, + localPosition: { x: -0.031, y: -0.0475, z: -0.0075} } }, { @@ -560,6 +560,15 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, + { + id: "propertiesLabel", + type: "label", + properties: { + text: "PRESETS", + lineHeight: 0.0045, + localPosition: { x: 0.002, y: -0.0475, z: -0.0075 } + } + }, { id: "presets", type: "panel", @@ -576,31 +585,76 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.014, y: 0.06, z: 0.01 } }, callback: { - method: "setSliderValue" + method: "setGravityValue" + } + }, + { + id: "gravityLabel", + type: "label", + properties: { + text: "GRAVITY", + lineHeight: 0.0045, + localPosition: { x: -0.003, y: 0.052, z: -0.0075 } } }, { id: "bounceSlider", - type: "panel", + type: "barSlider", properties: { localPosition: { x: 0.009, y: 0.016, z: -0.005 }, dimensions: { x: 0.014, y: 0.06, z: 0.01 } + }, + callback: { + method: "setBounceValue" + } + }, + { + id: "bounceLabel", + type: "label", + properties: { + text: "BOUNCE", + lineHeight: 0.0045, + localPosition: { x: 0.015, y: 0.057, z: -0.0075 } } }, { id: "dampingSlider", - type: "panel", + type: "barSlider", properties: { localPosition: { x: 0.024, y: 0.016, z: -0.005 }, dimensions: { x: 0.014, y: 0.06, z: 0.01 } + }, + callback: { + method: "setDampingValue" + } + }, + { + id: "dampingLabel", + type: "label", + properties: { + text: "DAMPING", + lineHeight: 0.0045, + localPosition: { x: 0.030, y: 0.052, z: -0.0075 } } }, { id: "densitySlider", - type: "panel", + type: "barSlider", properties: { localPosition: { x: 0.039, y: 0.016, z: -0.005 }, dimensions: { x: 0.014, y: 0.06, z: 0.01 } + }, + callback: { + method: "setDensityValue" + } + }, + { + id: "densityLabel", + type: "label", + properties: { + text: "DENSITY", + lineHeight: 0.0045, + localPosition: { x: 0.045, y: 0.057, z: -0.0075 } } } ] diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index c10bb748f7..3d0ec93b4d 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1415,6 +1415,31 @@ } break; + case "setGravityValue": + if (parameter !== undefined) { + // TODO + print("setGravityValue = " + parameter); + } + break; + case "setBounceValue": + if (parameter !== undefined) { + // TODO + print("setBounceValue = " + parameter); + } + break; + case "setDampingValue": + if (parameter !== undefined) { + // TODO + print("setDampingValue = " + parameter); + } + break; + case "setDensityValue": + if (parameter !== undefined) { + // TODO + print("setDensityValue = " + parameter); + } + break; + case "autoGrab": if (dominantHand === LEFT_HAND) { editors[LEFT_HAND].enableAutoGrab(); @@ -1429,6 +1454,7 @@ print("setSliderValue = " + parameter); } break; + default: log("ERROR: Unexpected command in onUICommand(): " + command + ", " + parameter); } From 3a1fc1f11c6b2ef2c8b282b6f6ece8d3bad7bb44 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 15 Aug 2017 17:44:42 +1200 Subject: [PATCH 191/722] Remember Physics slider values --- scripts/vr-edit/modules/toolMenu.js | 122 ++++++++++++++++++---------- scripts/vr-edit/vr-edit.js | 6 +- 2 files changed, 83 insertions(+), 45 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index fb80330dc5..d93567ef72 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -227,7 +227,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { SLIDER_UI_ELEMENTS = ["barSlider", "imageSlider"], SLIDER_RAISE_DELTA = { x: 0, y: 0, z: 0.004 }, - MIN_BAR_SLIDER_DIMENSION = 0.0001, + MIN_BAR_SLIDER_DIMENSION = 0.0001, // Avoid visual artifact for 0 slider values. OPTONS_PANELS = { @@ -512,12 +512,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { label: "GRAVITY", setting: { key: "VREdit.physicsTool.gravityOn", - // No property defaultValue: false, - command: "setGravity" + callback: "setGravityOn" }, command: { - method: "setGravity", + method: "setGravityOn", parameter: "gravityToggle" } }, @@ -531,12 +530,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { label: " GRAB", setting: { key: "VREdit.physicsTool.grabOn", - // No property defaultValue: false, - command: "setGrab" + callback: "setGrabOn" }, command: { - method: "setGrab", + method: "setGrabOn", parameter: "grabToggle" } }, @@ -550,12 +548,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { label: "COLLIDE", setting: { key: "VREdit.physicsTool.collideOn", - // No property defaultValue: false, - command: "setCollide" + callback: "setCollideOn" }, command: { - method: "setCollide", + method: "setCollideOn", parameter: "collideToggle" } }, @@ -584,8 +581,13 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: { x: -0.007, y: 0.016, z: -0.005 }, dimensions: { x: 0.014, y: 0.06, z: 0.01 } }, - callback: { - method: "setGravityValue" + setting: { + key: "VREdit.physicsTool.gravity", + defaultValue: 0, // Slider value in range 0.0 .. 1.0. TODO: Default value. + callback: "setGravity" + }, + command: { + method: "setGravity" } }, { @@ -604,8 +606,13 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: { x: 0.009, y: 0.016, z: -0.005 }, dimensions: { x: 0.014, y: 0.06, z: 0.01 } }, - callback: { - method: "setBounceValue" + setting: { + key: "VREdit.physicsTool.bounce", + defaultValue: 0, // Slider value in range 0.0 .. 1.0. TODO: Default value. + callback: "setBounce" + }, + command: { + method: "setBounce" } }, { @@ -624,8 +631,13 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: { x: 0.024, y: 0.016, z: -0.005 }, dimensions: { x: 0.014, y: 0.06, z: 0.01 } }, - callback: { - method: "setDampingValue" + setting: { + key: "VREdit.physicsTool.damping", + defaultValue: 0, // Slider value in range 0.0 .. 1.0. TODO: Default value. + callback: "setDamping" + }, + command: { + method: "setDamping" } }, { @@ -644,8 +656,13 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: { x: 0.039, y: 0.016, z: -0.005 }, dimensions: { x: 0.014, y: 0.06, z: 0.01 } }, - callback: { - method: "setDensityValue" + setting: { + key: "VREdit.physicsTool.density", + defaultValue: 0, // Slider value in range 0.0 .. 1.0. TODO: Default value. + callback: "setDensity" + }, + command: { + method: "setDensity" } }, { @@ -847,7 +864,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (optionsItems[i].setting) { optionsSettings[optionsItems[i].id] = { key: optionsItems[i].setting.key }; value = Settings.getValue(optionsItems[i].setting.key); - if (value === "" && optionsItems[i].setting.defaultValue) { + if (value === "" && optionsItems[i].setting.defaultValue !== undefined) { value = optionsItems[i].setting.defaultValue; } if (value !== "") { @@ -863,8 +880,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { ? UI_ELEMENTS[optionsItems[i].type].onColor : UI_ELEMENTS[optionsItems[i].type].offColor; } - if (optionsItems[i].setting.command) { - uiCommandCallback(optionsItems[i].setting.command, value); + if (optionsItems[i].type === "barSlider") { + // Store value in optionsSettings rather than using overlay property. + optionsSettings[optionsItems[i].id].value = value; + } + if (optionsItems[i].setting.callback) { + uiCommandCallback(optionsItems[i].setting.callback, value); } } } @@ -878,29 +899,29 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } if (optionsItems[i].type === "barSlider") { - // Initial value = 0. optionsOverlaysAuxiliaries[i] = {}; auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderValue.properties); + auxiliaryProperties.localPosition = { x: 0, y: (0.5 - value / 2) * properties.dimensions.y, z: 0 }; auxiliaryProperties.dimensions = { x: properties.dimensions.x, - y: MIN_BAR_SLIDER_DIMENSION, // Avoid visual artifact if 0. + y: Math.max(value * properties.dimensions.y, MIN_BAR_SLIDER_DIMENSION), z: properties.dimensions.z }; - auxiliaryProperties.localPosition = { - x: 0, - y: properties.dimensions.y / 2, - z: 0 - }; auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.barSliderValue.overlay, auxiliaryProperties); auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderRemainder.properties); - auxiliaryProperties.dimensions = properties.dimensions; - auxiliaryProperties.localPosition = Vec3.ZERO; + auxiliaryProperties.localPosition = { x: 0, y: (-0.5 + (1.0 - value) / 2) * properties.dimensions.y, z: 0 }; + auxiliaryProperties.dimensions = { + x: properties.dimensions.x, + y: Math.max((1.0 - value) * properties.dimensions.y, MIN_BAR_SLIDER_DIMENSION), + z: properties.dimensions.z + }; auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; optionsOverlaysAuxiliaries[i].remainder = Overlays.addOverlay(UI_ELEMENTS.barSliderRemainder.overlay, auxiliaryProperties); } + if (optionsItems[i].type === "imageSlider") { imageOffset = 0.0; @@ -1020,9 +1041,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } break; - case "setGravity": - case "setGrab": - case "setCollide": + case "setGravityOn": + case "setGrabOn": + case "setCollideOn": value = !optionsSettings[parameter].value; optionsSettings[parameter].value = value; Settings.setValue(optionsSettings[parameter].key, value); @@ -1033,6 +1054,23 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { uiCommandCallback(command, value); break; + case "setGravity": + Settings.setValue(optionsSettings.gravitySlider.key, parameter); + uiCommandCallback("setGravity", parameter); + break; + case "setBounce": + Settings.setValue(optionsSettings.bounceSlider.key, parameter); + uiCommandCallback("setBounce", parameter); + break; + case "setDamping": + Settings.setValue(optionsSettings.dampingSlider.key, parameter); + uiCommandCallback("setDamping", parameter); + break; + case "setDensity": + Settings.setValue(optionsSettings.densitySlider.key, parameter); + uiCommandCallback("setDensity", parameter); + break; + default: App.log(side, "ERROR: ToolMenu: Unexpected command! " + command); } @@ -1182,9 +1220,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { openOptions(intersectionItems[intersectedItem].toolOptions); } if (intersectionItems[intersectedItem].command) { - if (intersectionItems[intersectedItem].command.parameter) { - parameter = intersectionItems[intersectedItem].command.parameter; - } + parameter = intersectionItems[intersectedItem].id; doCommand(intersectionItems[intersectedItem].command.method, parameter); } if (intersectionItems[intersectedItem].callback) { @@ -1225,7 +1261,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: { x: 0, y: (0.5 - fraction / 2) * overlayDimensions.y, z: 0 }, dimensions: { x: overlayDimensions.x, - y: Math.max(fraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION), // Avoid visual artifact if 0. + y: Math.max(fraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION), z: overlayDimensions.z } }); @@ -1233,12 +1269,13 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: { x: 0, y: (-0.5 + otherFraction / 2) * overlayDimensions.y, z: 0 }, dimensions: { x: overlayDimensions.x, - y: Math.max(otherFraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION), // Avoid visual artifact if 0. + y: Math.max(otherFraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION), z: overlayDimensions.z } }); - - uiCommandCallback(intersectionItems[intersectedItem].callback.method, fraction); + if (intersectionItems[intersectedItem].command) { + doCommand(intersectionItems[intersectedItem].command.method, fraction); + } } // Image slider update. @@ -1257,8 +1294,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: Vec3.sum(optionsOverlaysAuxiliaries[intersectedItem].offset, { x: 0, y: (0.5 - fraction) * overlayDimensions.y, z: 0 }) }); - - uiCommandCallback(intersectionItems[intersectedItem].callback.method, fraction); + if (intersectionItems[intersectedItem].callback) { + uiCommandCallback(intersectionItems[intersectedItem].callback.method, fraction); + } } // Special handling for Group options. diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 3d0ec93b4d..d0d307c07e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1393,7 +1393,7 @@ colorToolColor = parameter; break; - case "setGravity": + case "setGravityOn": if (parameter) { physicsToolPhysics.gravity = { x: 0, y: -9.8, z: 0 }; // Earth gravity. physicsToolPhysics.dynamic = true; @@ -1402,10 +1402,10 @@ physicsToolPhysics.dynamic = false; } break; - case "setGrab": + case "setGrabOn": physicsToolPhysics.userData.grabbableKey.grabbable = parameter; break; - case "setCollide": + case "setCollideOn": if (parameter) { physicsToolPhysics.collisionless = false; physicsToolPhysics.collidesWith = "static,dynamic,kinematic,myAvatar,otherAvatar"; From 8cf18c1b033bb4f24b7fa4072c80c4ecc29168f5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 15 Aug 2017 17:47:59 +1200 Subject: [PATCH 192/722] Tidy applying color swatches --- scripts/vr-edit/modules/toolMenu.js | 37 ++++++++++------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index d93567ef72..0df93cce3c 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -303,8 +303,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { defaultValue: { red: 0, green: 255, blue: 255 } }, command: { - method: "setColorPerSwatch", - parameter: "colorSwatch1.color" + method: "setColorPerSwatch" }, onGripClicked: { method: "clearSwatch", @@ -324,8 +323,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { defaultValue: { red: 255, green: 0, blue: 255 } }, command: { - method: "setColorPerSwatch", - parameter: "colorSwatch2.color" + method: "setColorPerSwatch" }, onGripClicked: { method: "clearSwatch", @@ -345,8 +343,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { defaultValue: { red: 255, green: 255, blue: 0 } }, command: { - method: "setColorPerSwatch", - parameter: "colorSwatch3.color" + method: "setColorPerSwatch" }, onGripClicked: { method: "clearSwatch", @@ -366,8 +363,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { defaultValue: { red: 255, green: 0, blue: 0 } }, command: { - method: "setColorPerSwatch", - parameter: "colorSwatch4.color" + method: "setColorPerSwatch" }, onGripClicked: { method: "clearSwatch", @@ -387,8 +383,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { defaultValue: { red: 0, green: 255, blue: 0 } }, command: { - method: "setColorPerSwatch", - parameter: "colorSwatch5.color" + method: "setColorPerSwatch" }, onGripClicked: { method: "clearSwatch", @@ -408,8 +403,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { defaultValue: { red: 0, green: 0, blue: 255 } }, command: { - method: "setColorPerSwatch", - parameter: "colorSwatch6.color" + method: "setColorPerSwatch" }, onGripClicked: { method: "clearSwatch", @@ -429,8 +423,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Default to empty swatch. }, command: { - method: "setColorPerSwatch", - parameter: "colorSwatch7.color" + method: "setColorPerSwatch" }, onGripClicked: { method: "clearSwatch", @@ -450,8 +443,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Default to empty swatch. }, command: { - method: "setColorPerSwatch", - parameter: "colorSwatch8.color" + method: "setColorPerSwatch" }, onGripClicked: { method: "clearSwatch", @@ -998,20 +990,17 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } function doCommand(command, parameter) { - var parameters, - index, + var index, hasColor, value; switch (command) { case "setColorPerSwatch": - parameters = parameter.split("."); - index = optionsOverlaysIDs.indexOf(parameters[0]); + index = optionsOverlaysIDs.indexOf(parameter); hasColor = Overlays.getProperty(optionsOverlays[index], "solid"); if (hasColor) { - // Swatch has a color; set current fill color to swatch color. - value = evaluateParameter(parameter); + value = Overlays.getProperty(optionsOverlays[index], "color"); Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], { color: value }); @@ -1026,8 +1015,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: value, solid: true }); - if (optionsSettings[parameters[0]]) { - Settings.setValue(optionsSettings[parameters[0]].key, value); + if (optionsSettings[parameter]) { + Settings.setValue(optionsSettings[parameter].key, value); } } break; From 8d01c02c29bb48f46e1aaacb9bfdec8d0ca67c84 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 15 Aug 2017 17:54:21 +1200 Subject: [PATCH 193/722] Tidy clearing color swatches --- scripts/vr-edit/modules/toolMenu.js | 48 ++++++++++++----------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 0df93cce3c..9f6ff12382 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -305,9 +305,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { command: { method: "setColorPerSwatch" }, - onGripClicked: { - method: "clearSwatch", - parameter: "colorSwatch1" + clear: { + method: "clearSwatch" } }, { @@ -325,9 +324,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { command: { method: "setColorPerSwatch" }, - onGripClicked: { - method: "clearSwatch", - parameter: "colorSwatch2" + clear: { + method: "clearSwatch" } }, { @@ -345,9 +343,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { command: { method: "setColorPerSwatch" }, - onGripClicked: { - method: "clearSwatch", - parameter: "colorSwatch3" + clear: { + method: "clearSwatch" } }, { @@ -365,9 +362,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { command: { method: "setColorPerSwatch" }, - onGripClicked: { - method: "clearSwatch", - parameter: "colorSwatch4" + clear: { + method: "clearSwatch" } }, { @@ -385,9 +381,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { command: { method: "setColorPerSwatch" }, - onGripClicked: { - method: "clearSwatch", - parameter: "colorSwatch5" + clear: { + method: "clearSwatch" } }, { @@ -405,9 +400,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { command: { method: "setColorPerSwatch" }, - onGripClicked: { - method: "clearSwatch", - parameter: "colorSwatch6" + clear: { + method: "clearSwatch" } }, { @@ -425,9 +419,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { command: { method: "setColorPerSwatch" }, - onGripClicked: { - method: "clearSwatch", - parameter: "colorSwatch7" + clear: { + method: "clearSwatch" } }, { @@ -445,9 +438,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { command: { method: "setColorPerSwatch" }, - onGripClicked: { - method: "clearSwatch", - parameter: "colorSwatch8" + clear: { + method: "clearSwatch" } }, { @@ -1224,12 +1216,10 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Grip click. if (controlHand.gripClicked() !== isGripClicked) { isGripClicked = !isGripClicked; - if (isGripClicked && intersectionItems && intersectedItem && intersectionItems[intersectedItem].onGripClicked) { + if (isGripClicked && intersectionItems && intersectedItem && intersectionItems[intersectedItem].clear) { controlHand.setGripClickedHandled(); - if (intersectionItems[intersectedItem].onGripClicked.parameter) { - parameter = intersectionItems[intersectedItem].onGripClicked.parameter; - } - doGripClicked(intersectionItems[intersectedItem].onGripClicked.method, parameter); + parameter = intersectionItems[intersectedItem].id; + doGripClicked(intersectionItems[intersectedItem].clear.method, parameter); } } From cb53ad43e089024d4aa08aa0807c617d311ed9b6 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 15 Aug 2017 18:16:21 +1200 Subject: [PATCH 194/722] Tidying --- scripts/vr-edit/modules/toolMenu.js | 277 ++++++++++++++-------------- 1 file changed, 142 insertions(+), 135 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 9f6ff12382..9a95425433 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -500,8 +500,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { callback: "setGravityOn" }, command: { - method: "setGravityOn", - parameter: "gravityToggle" + method: "setGravityOn" } }, { @@ -518,8 +517,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { callback: "setGrabOn" }, command: { - method: "setGrabOn", - parameter: "grabToggle" + method: "setGrabOn" } }, { @@ -536,8 +534,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { callback: "setCollideOn" }, command: { - method: "setCollideOn", - parameter: "collideToggle" + method: "setCollideOn" } }, @@ -809,18 +806,10 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { return [menuPanelOverlay].concat(menuOverlays).concat(optionsOverlays); } - function openOptions(toolOptions) { - var properties, - childProperties, - auxiliaryProperties, - parentID, - value, - imageOffset, - IMAGE_OFFSET = 0.0005, - i, + function closeOptions() { + var i, length; - // Close current panel, if any. Overlays.editOverlay(highlightOverlay, { parentID: menuOriginOverlay }); @@ -833,129 +822,147 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsOverlaysAuxiliaries = []; optionsEnabled = []; optionsItems = null; + } - // Open specified panel, if any. - if (toolOptions) { - optionsItems = OPTONS_PANELS[toolOptions]; - parentID = menuPanelOverlay; // Menu panel parents to background panel. - for (i = 0, length = optionsItems.length; i < length; i += 1) { - properties = Object.clone(UI_ELEMENTS[optionsItems[i].type].properties); - properties = Object.merge(properties, optionsItems[i].properties); - properties.parentID = parentID; - if (properties.url) { - properties.url = Script.resolvePath(properties.url); - } - if (optionsItems[i].setting) { - optionsSettings[optionsItems[i].id] = { key: optionsItems[i].setting.key }; - value = Settings.getValue(optionsItems[i].setting.key); - if (value === "" && optionsItems[i].setting.defaultValue !== undefined) { - value = optionsItems[i].setting.defaultValue; - } - if (value !== "") { - properties[optionsItems[i].setting.property] = value; - if (optionsItems[i].type === "swatch") { - // Special case for when swatch color is defined. - properties.solid = true; - } - if (optionsItems[i].type === "toggleButton") { - // Store value in optionsSettings rather than using overlay property. - optionsSettings[optionsItems[i].id].value = value; - properties.color = value - ? UI_ELEMENTS[optionsItems[i].type].onColor - : UI_ELEMENTS[optionsItems[i].type].offColor; - } - if (optionsItems[i].type === "barSlider") { - // Store value in optionsSettings rather than using overlay property. - optionsSettings[optionsItems[i].id].value = value; - } - if (optionsItems[i].setting.callback) { - uiCommandCallback(optionsItems[i].setting.callback, value); - } - } - } - optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[optionsItems[i].type].overlay, properties)); - optionsOverlaysIDs.push(optionsItems[i].id); - if (optionsItems[i].label) { - properties = Object.clone(UI_ELEMENTS.label.properties); - properties.text = optionsItems[i].label; - properties.parentID = optionsOverlays[optionsOverlays.length - 1]; - Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); - } + function openOptions(toolOptions) { + var properties, + childProperties, + auxiliaryProperties, + parentID, + value, + imageOffset, + IMAGE_OFFSET = 0.0005, + i, + length; - if (optionsItems[i].type === "barSlider") { - optionsOverlaysAuxiliaries[i] = {}; - auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderValue.properties); - auxiliaryProperties.localPosition = { x: 0, y: (0.5 - value / 2) * properties.dimensions.y, z: 0 }; - auxiliaryProperties.dimensions = { - x: properties.dimensions.x, - y: Math.max(value * properties.dimensions.y, MIN_BAR_SLIDER_DIMENSION), - z: properties.dimensions.z - }; - auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; - optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.barSliderValue.overlay, - auxiliaryProperties); - auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderRemainder.properties); - auxiliaryProperties.localPosition = { x: 0, y: (-0.5 + (1.0 - value) / 2) * properties.dimensions.y, z: 0 }; - auxiliaryProperties.dimensions = { - x: properties.dimensions.x, - y: Math.max((1.0 - value) * properties.dimensions.y, MIN_BAR_SLIDER_DIMENSION), - z: properties.dimensions.z - }; - auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; - optionsOverlaysAuxiliaries[i].remainder = Overlays.addOverlay(UI_ELEMENTS.barSliderRemainder.overlay, - auxiliaryProperties); - } + // Close current panel, if any. + closeOptions(); - if (optionsItems[i].type === "imageSlider") { - imageOffset = 0.0; + // TODO: Remove once all tools have an options panel. + if (OPTONS_PANELS[toolOptions] === undefined) { + return; + } - // Primary image. - if (optionsItems[i].imageURL) { - childProperties = Object.clone(UI_ELEMENTS.image.properties); - childProperties.url = Script.resolvePath(optionsItems[i].imageURL); - delete childProperties.dimensions; - childProperties.scale = properties.dimensions.y; - imageOffset += IMAGE_OFFSET; - childProperties.emissive = true; - if (optionsItems[i].useBaseColor) { - childProperties.color = properties.color; - } - childProperties.localPosition = { x: 0, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; - childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; - Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); - } - - // Overlay image. - if (optionsItems[i].imageOverlayURL) { - childProperties = Object.clone(UI_ELEMENTS.image.properties); - childProperties.url = Script.resolvePath(optionsItems[i].imageOverlayURL); - childProperties.drawInFront = true; // TODO: Work-around for rendering bug; remove when bug fixed. - delete childProperties.dimensions; - childProperties.scale = properties.dimensions.y; - imageOffset += IMAGE_OFFSET; - childProperties.localPosition = { x: 0, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; - childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; - Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); - } - - // Value pointers. - optionsOverlaysAuxiliaries[i] = {}; - optionsOverlaysAuxiliaries[i].offset = - { x: -properties.dimensions.x / 2, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; - auxiliaryProperties = Object.clone(UI_ELEMENTS.sliderPointer.properties); - auxiliaryProperties.localPosition = optionsOverlaysAuxiliaries[i].offset; - auxiliaryProperties.drawInFront = true; // TODO: Accommodate work-around above; remove when bug fixed. - auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; - optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.sliderPointer.overlay, - auxiliaryProperties); - auxiliaryProperties.localPosition = { x: 0, y: properties.dimensions.x, z: 0 }; - auxiliaryProperties.localRotation = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }); - auxiliaryProperties.parentID = optionsOverlaysAuxiliaries[i].value; - Overlays.addOverlay(UI_ELEMENTS.sliderPointer.overlay, auxiliaryProperties); - } - parentID = optionsOverlays[0]; // Menu buttons parent to menu panel. - optionsEnabled.push(true); + // Open specified panel. + optionsItems = OPTONS_PANELS[toolOptions]; + parentID = menuPanelOverlay; // Menu panel parents to background panel. + for (i = 0, length = optionsItems.length; i < length; i += 1) { + properties = Object.clone(UI_ELEMENTS[optionsItems[i].type].properties); + properties = Object.merge(properties, optionsItems[i].properties); + properties.parentID = parentID; + if (properties.url) { + properties.url = Script.resolvePath(properties.url); } + if (optionsItems[i].setting) { + optionsSettings[optionsItems[i].id] = { key: optionsItems[i].setting.key }; + value = Settings.getValue(optionsItems[i].setting.key); + if (value === "" && optionsItems[i].setting.defaultValue !== undefined) { + value = optionsItems[i].setting.defaultValue; + } + if (value !== "") { + properties[optionsItems[i].setting.property] = value; + if (optionsItems[i].type === "swatch") { + // Special case for when swatch color is defined. + properties.solid = true; + } + if (optionsItems[i].type === "toggleButton") { + // Store value in optionsSettings rather than using overlay property. + optionsSettings[optionsItems[i].id].value = value; + properties.color = value + ? UI_ELEMENTS[optionsItems[i].type].onColor + : UI_ELEMENTS[optionsItems[i].type].offColor; + } + if (optionsItems[i].type === "barSlider") { + // Store value in optionsSettings rather than using overlay property. + optionsSettings[optionsItems[i].id].value = value; + } + if (optionsItems[i].setting.callback) { + uiCommandCallback(optionsItems[i].setting.callback, value); + } + } + } + optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[optionsItems[i].type].overlay, properties)); + optionsOverlaysIDs.push(optionsItems[i].id); + if (optionsItems[i].label) { + properties = Object.clone(UI_ELEMENTS.label.properties); + properties.text = optionsItems[i].label; + properties.parentID = optionsOverlays[optionsOverlays.length - 1]; + Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); + } + + if (optionsItems[i].type === "barSlider") { + optionsOverlaysAuxiliaries[i] = {}; + auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderValue.properties); + auxiliaryProperties.localPosition = { x: 0, y: (0.5 - value / 2) * properties.dimensions.y, z: 0 }; + auxiliaryProperties.dimensions = { + x: properties.dimensions.x, + y: Math.max(value * properties.dimensions.y, MIN_BAR_SLIDER_DIMENSION), + z: properties.dimensions.z + }; + auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; + optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.barSliderValue.overlay, + auxiliaryProperties); + auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderRemainder.properties); + auxiliaryProperties.localPosition = { x: 0, y: (-0.5 + (1.0 - value) / 2) * properties.dimensions.y, z: 0 }; + auxiliaryProperties.dimensions = { + x: properties.dimensions.x, + y: Math.max((1.0 - value) * properties.dimensions.y, MIN_BAR_SLIDER_DIMENSION), + z: properties.dimensions.z + }; + auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; + optionsOverlaysAuxiliaries[i].remainder = Overlays.addOverlay(UI_ELEMENTS.barSliderRemainder.overlay, + auxiliaryProperties); + } + + if (optionsItems[i].type === "imageSlider") { + imageOffset = 0.0; + + // Primary image. + if (optionsItems[i].imageURL) { + childProperties = Object.clone(UI_ELEMENTS.image.properties); + childProperties.url = Script.resolvePath(optionsItems[i].imageURL); + delete childProperties.dimensions; + childProperties.scale = properties.dimensions.y; + imageOffset += IMAGE_OFFSET; + childProperties.emissive = true; + if (optionsItems[i].useBaseColor) { + childProperties.color = properties.color; + } + childProperties.localPosition = { x: 0, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; + childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; + Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); + } + + // Overlay image. + if (optionsItems[i].imageOverlayURL) { + childProperties = Object.clone(UI_ELEMENTS.image.properties); + childProperties.url = Script.resolvePath(optionsItems[i].imageOverlayURL); + childProperties.drawInFront = true; // TODO: Work-around for rendering bug; remove when bug fixed. + delete childProperties.dimensions; + childProperties.scale = properties.dimensions.y; + imageOffset += IMAGE_OFFSET; + childProperties.localPosition = { x: 0, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; + childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; + Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); + } + + // Value pointers. + optionsOverlaysAuxiliaries[i] = {}; + optionsOverlaysAuxiliaries[i].offset = + { x: -properties.dimensions.x / 2, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; + auxiliaryProperties = Object.clone(UI_ELEMENTS.sliderPointer.properties); + auxiliaryProperties.localPosition = optionsOverlaysAuxiliaries[i].offset; + auxiliaryProperties.drawInFront = true; // TODO: Accommodate work-around above; remove when bug fixed. + auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; + optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.sliderPointer.overlay, + auxiliaryProperties); + auxiliaryProperties.localPosition = { x: 0, y: properties.dimensions.x, z: 0 }; + auxiliaryProperties.localRotation = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }); + auxiliaryProperties.parentID = optionsOverlaysAuxiliaries[i].value; + Overlays.addOverlay(UI_ELEMENTS.sliderPointer.overlay, auxiliaryProperties); + } + parentID = optionsOverlays[0]; // Menu buttons parent to menu panel. + optionsEnabled.push(true); } // Special handling for Group options. @@ -966,7 +973,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } function clearTool() { - openOptions(); + closeOptions(); } function evaluateParameter(parameter) { From 757af1e2e51903c41cb8d65c58a80a6fd2021fbb Mon Sep 17 00:00:00 2001 From: vladest Date: Tue, 15 Aug 2017 14:44:01 +0200 Subject: [PATCH 195/722] Added Save, Directory and Assets async methods --- interface/src/Application.cpp | 2 + interface/src/assets/ATPAssetMigrator.cpp | 266 +++++++++++----------- libraries/ui/src/OffscreenUi.cpp | 110 ++++++++- libraries/ui/src/OffscreenUi.h | 8 + 4 files changed, 254 insertions(+), 132 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 966cead203..130ef49dd0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6776,6 +6776,8 @@ void Application::loadDialog() { auto scriptEngines = DependencyManager::get(); auto offscreenUi = DependencyManager::get(); connect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, this, [=] (QString response) { + auto offscreenUi = DependencyManager::get(); + disconnect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, this, nullptr); if (!response.isEmpty() && QFile(response).exists()) { setPreviousScriptLocation(QFileInfo(response).absolutePath()); DependencyManager::get()->loadScript(response, true, false, false, true); // Don't load from cache diff --git a/interface/src/assets/ATPAssetMigrator.cpp b/interface/src/assets/ATPAssetMigrator.cpp index 45459683e8..a86c012a55 100644 --- a/interface/src/assets/ATPAssetMigrator.cpp +++ b/interface/src/assets/ATPAssetMigrator.cpp @@ -42,159 +42,165 @@ static const QString COMPOUND_SHAPE_URL_KEY = "compoundShapeURL"; static const QString MESSAGE_BOX_TITLE = "ATP Asset Migration"; void ATPAssetMigrator::loadEntityServerFile() { - auto filename = OffscreenUi::getOpenFileName(_dialogParent, tr("Select an entity-server content file to migrate"), QString(), tr("Entity-Server Content (*.gz)")); - - if (!filename.isEmpty()) { - qCDebug(asset_migrator) << "Selected filename for ATP asset migration: " << filename; - - auto migrateResources = [=](QUrl migrationURL, QJsonValueRef jsonValue, bool isModelURL) { - auto request = - DependencyManager::get()->createResourceRequest(this, migrationURL); - - if (request) { - qCDebug(asset_migrator) << "Requesting" << migrationURL << "for ATP asset migration"; - - // add this combination of QUrl and QJsonValueRef to our multi hash so we can change the URL - // to an ATP one once ready - _pendingReplacements.insert(migrationURL, { jsonValue, (isModelURL ? 0 : 1)}); - - connect(request, &ResourceRequest::finished, this, [=]() { - if (request->getResult() == ResourceRequest::Success) { - migrateResource(request); - } else { - ++_errorCount; - _pendingReplacements.remove(migrationURL); - qWarning() << "Could not retrieve asset at" << migrationURL.toString(); - - checkIfFinished(); - } - request->deleteLater(); - }); - - request->send(); - } else { - ++_errorCount; - qWarning() << "Count not create request for asset at" << migrationURL.toString(); - } - - }; - static const QString MIGRATION_CONFIRMATION_TEXT { - "The ATP Asset Migration process will scan the selected entity-server file,\nupload discovered resources to the"\ - " current asset-server\nand then save a new entity-server file with the ATP URLs.\n\nAre you ready to"\ - " continue?\n\nMake sure you are connected to the right domain." - }; - + auto offscreenUi = DependencyManager::get(); + connect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, this, [=] (QString filename) { auto offscreenUi = DependencyManager::get(); - QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { + disconnect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, this, nullptr); + if (!filename.isEmpty()) { + qCDebug(asset_migrator) << "Selected filename for ATP asset migration: " << filename; + + auto migrateResources = [=](QUrl migrationURL, QJsonValueRef jsonValue, bool isModelURL) { + auto request = + DependencyManager::get()->createResourceRequest(this, migrationURL); + + if (request) { + qCDebug(asset_migrator) << "Requesting" << migrationURL << "for ATP asset migration"; + + // add this combination of QUrl and QJsonValueRef to our multi hash so we can change the URL + // to an ATP one once ready + _pendingReplacements.insert(migrationURL, { jsonValue, (isModelURL ? 0 : 1)}); + + connect(request, &ResourceRequest::finished, this, [=]() { + if (request->getResult() == ResourceRequest::Success) { + migrateResource(request); + } else { + ++_errorCount; + _pendingReplacements.remove(migrationURL); + qWarning() << "Could not retrieve asset at" << migrationURL.toString(); + + checkIfFinished(); + } + request->deleteLater(); + }); + + request->send(); + } else { + ++_errorCount; + qWarning() << "Count not create request for asset at" << migrationURL.toString(); + } + + }; + static const QString MIGRATION_CONFIRMATION_TEXT { + "The ATP Asset Migration process will scan the selected entity-server file,\nupload discovered resources to the"\ + " current asset-server\nand then save a new entity-server file with the ATP URLs.\n\nAre you ready to"\ + " continue?\n\nMake sure you are connected to the right domain." + }; + auto offscreenUi = DependencyManager::get(); - QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); + QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { + auto offscreenUi = DependencyManager::get(); + QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); - if (QMessageBox::Yes == answer) { - // try to open the file at the given filename - QFile modelsFile { filename }; + if (QMessageBox::Yes == answer) { + // try to open the file at the given filename + QFile modelsFile { filename }; - if (modelsFile.open(QIODevice::ReadOnly)) { - QByteArray compressedJsonData = modelsFile.readAll(); - QByteArray jsonData; + if (modelsFile.open(QIODevice::ReadOnly)) { + QByteArray compressedJsonData = modelsFile.readAll(); + QByteArray jsonData; - if (!gunzip(compressedJsonData, jsonData)) { - OffscreenUi::asyncWarning(_dialogParent, "Error", "The file at" + filename + "was not in gzip format."); - } + if (!gunzip(compressedJsonData, jsonData)) { + OffscreenUi::asyncWarning(_dialogParent, "Error", "The file at" + filename + "was not in gzip format."); + } - QJsonDocument modelsJSON = QJsonDocument::fromJson(jsonData); - _entitiesArray = modelsJSON.object()["Entities"].toArray(); + QJsonDocument modelsJSON = QJsonDocument::fromJson(jsonData); + _entitiesArray = modelsJSON.object()["Entities"].toArray(); - for (auto jsonValue : _entitiesArray) { - QJsonObject entityObject = jsonValue.toObject(); - QString modelURLString = entityObject.value(MODEL_URL_KEY).toString(); - QString compoundURLString = entityObject.value(COMPOUND_SHAPE_URL_KEY).toString(); + for (auto jsonValue : _entitiesArray) { + QJsonObject entityObject = jsonValue.toObject(); + QString modelURLString = entityObject.value(MODEL_URL_KEY).toString(); + QString compoundURLString = entityObject.value(COMPOUND_SHAPE_URL_KEY).toString(); - for (int i = 0; i < 2; ++i) { - bool isModelURL = (i == 0); - quint8 replacementType = i; - auto migrationURLString = (isModelURL) ? modelURLString : compoundURLString; + for (int i = 0; i < 2; ++i) { + bool isModelURL = (i == 0); + quint8 replacementType = i; + auto migrationURLString = (isModelURL) ? modelURLString : compoundURLString; - if (!migrationURLString.isEmpty()) { - QUrl migrationURL = QUrl(migrationURLString); + if (!migrationURLString.isEmpty()) { + QUrl migrationURL = QUrl(migrationURLString); - if (!_ignoredUrls.contains(migrationURL) - && (migrationURL.scheme() == URL_SCHEME_HTTP || migrationURL.scheme() == URL_SCHEME_HTTPS - || migrationURL.scheme() == URL_SCHEME_FILE || migrationURL.scheme() == URL_SCHEME_FTP)) { + if (!_ignoredUrls.contains(migrationURL) + && (migrationURL.scheme() == URL_SCHEME_HTTP || migrationURL.scheme() == URL_SCHEME_HTTPS + || migrationURL.scheme() == URL_SCHEME_FILE || migrationURL.scheme() == URL_SCHEME_FTP)) { - if (_pendingReplacements.contains(migrationURL)) { - // we already have a request out for this asset, just store the QJsonValueRef - // so we can do the hash replacement when the request comes back - _pendingReplacements.insert(migrationURL, { jsonValue, replacementType }); - } else if (_uploadedAssets.contains(migrationURL)) { - // we already have a hash for this asset - // so just do the replacement immediately - if (isModelURL) { - entityObject[MODEL_URL_KEY] = _uploadedAssets.value(migrationURL).toString(); + if (_pendingReplacements.contains(migrationURL)) { + // we already have a request out for this asset, just store the QJsonValueRef + // so we can do the hash replacement when the request comes back + _pendingReplacements.insert(migrationURL, { jsonValue, replacementType }); + } else if (_uploadedAssets.contains(migrationURL)) { + // we already have a hash for this asset + // so just do the replacement immediately + if (isModelURL) { + entityObject[MODEL_URL_KEY] = _uploadedAssets.value(migrationURL).toString(); + } else { + entityObject[COMPOUND_SHAPE_URL_KEY] = _uploadedAssets.value(migrationURL).toString(); + } + + jsonValue = entityObject; } else { - entityObject[COMPOUND_SHAPE_URL_KEY] = _uploadedAssets.value(migrationURL).toString(); - } - jsonValue = entityObject; - } else { + static bool hasAskedForCompleteMigration { false }; + static bool wantsCompleteMigration { false }; - static bool hasAskedForCompleteMigration { false }; - static bool wantsCompleteMigration { false }; - - if (!hasAskedForCompleteMigration) { - // this is the first resource migration - ask the user if they just want to migrate everything - static const QString COMPLETE_MIGRATION_TEXT { "Do you want to migrate all assets found in this entity-server file?\n"\ - "Select \"Yes\" to upload all discovered assets to the current asset-server immediately.\n"\ - "Select \"No\" to be prompted for each discovered asset." - }; - auto offscreenUi = DependencyManager::get(); - QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { + if (!hasAskedForCompleteMigration) { + // this is the first resource migration - ask the user if they just want to migrate everything + static const QString COMPLETE_MIGRATION_TEXT { "Do you want to migrate all assets found in this entity-server file?\n"\ + "Select \"Yes\" to upload all discovered assets to the current asset-server immediately.\n"\ + "Select \"No\" to be prompted for each discovered asset." + }; auto offscreenUi = DependencyManager::get(); - QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); - if (answer == QMessageBox::Yes) { - wantsCompleteMigration = true; - migrateResources(migrationURL, jsonValue, isModelURL); - } else { - QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { - auto offscreenUi = DependencyManager::get(); - QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); - if (answer == QMessageBox::Yes) { - migrateResources(migrationURL, jsonValue, isModelURL); - } else { - _ignoredUrls.insert(migrationURL); - } - }); - OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, - "Would you like to migrate the following resource?\n" + migrationURL.toString(), - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { + auto offscreenUi = DependencyManager::get(); + QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); + if (answer == QMessageBox::Yes) { + wantsCompleteMigration = true; + migrateResources(migrationURL, jsonValue, isModelURL); + } else { + QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { + auto offscreenUi = DependencyManager::get(); + QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); + if (answer == QMessageBox::Yes) { + migrateResources(migrationURL, jsonValue, isModelURL); + } else { + _ignoredUrls.insert(migrationURL); + } + }); + OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, + "Would you like to migrate the following resource?\n" + migrationURL.toString(), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - } - }); - OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, COMPLETE_MIGRATION_TEXT, - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - hasAskedForCompleteMigration = true; - } - if (wantsCompleteMigration) { - migrateResources(migrationURL, jsonValue, isModelURL); + } + }); + OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, COMPLETE_MIGRATION_TEXT, + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + hasAskedForCompleteMigration = true; + } + if (wantsCompleteMigration) { + migrateResources(migrationURL, jsonValue, isModelURL); + } } } } } } + + _doneReading = true; + + checkIfFinished(); + + } else { + OffscreenUi::asyncWarning(_dialogParent, "Error", + "There was a problem loading that entity-server file for ATP asset migration. Please try again"); } - - _doneReading = true; - - checkIfFinished(); - - } else { - OffscreenUi::asyncWarning(_dialogParent, "Error", - "There was a problem loading that entity-server file for ATP asset migration. Please try again"); } - } - }); - OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, MIGRATION_CONFIRMATION_TEXT, - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - } + }); + OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, MIGRATION_CONFIRMATION_TEXT, + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + } + }); + OffscreenUi::getOpenFileNameAsync(_dialogParent, tr("Select an entity-server content file to migrate"), + QString(), tr("Entity-Server Content (*.gz)")); } void ATPAssetMigrator::migrateResource(ResourceRequest* request) { diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 8ea92d2b1e..470603d0d0 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -736,9 +736,7 @@ QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, void OffscreenUi::fileOpenDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { if (QThread::currentThread() != thread()) { - QString result; BLOCKING_INVOKE_METHOD(this, "fileOpenDialogAsync", - Q_RETURN_ARG(QString, result), Q_ARG(QString, caption), Q_ARG(QString, dir), Q_ARG(QString, filter), @@ -780,6 +778,28 @@ QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir, return fileDialog(map); } +void OffscreenUi::fileSaveDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "fileSaveDialogAsync", + Q_ARG(QString, caption), + Q_ARG(QString, dir), + Q_ARG(QString, filter), + Q_ARG(QString*, selectedFilter), + Q_ARG(QFileDialog::Options, options)); + return; + } + + // FIXME support returning the selected filter... somehow? + QVariantMap map; + map.insert("caption", caption); + map.insert("dir", QUrl::fromLocalFile(dir)); + map.insert("filter", filter); + map.insert("options", static_cast(options)); + map.insert("saveDialog", true); + + return fileDialogAsync(map); +} + QString OffscreenUi::existingDirectoryDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { if (QThread::currentThread() != thread()) { QString result; @@ -802,6 +822,26 @@ QString OffscreenUi::existingDirectoryDialog(const QString& caption, const QStri return fileDialog(map); } +void OffscreenUi::existingDirectoryDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "existingDirectoryDialogAsync", + Q_ARG(QString, caption), + Q_ARG(QString, dir), + Q_ARG(QString, filter), + Q_ARG(QString*, selectedFilter), + Q_ARG(QFileDialog::Options, options)); + return; + } + + QVariantMap map; + map.insert("caption", caption); + map.insert("dir", QUrl::fromLocalFile(dir)); + map.insert("filter", filter); + map.insert("options", static_cast(options)); + map.insert("selectDirectory", true); + return fileDialogAsync(map); +} + QString OffscreenUi::getOpenFileName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { return DependencyManager::get()->fileOpenDialog(caption, dir, filter, selectedFilter, options); } @@ -814,10 +854,18 @@ QString OffscreenUi::getSaveFileName(void* ignored, const QString &caption, cons return DependencyManager::get()->fileSaveDialog(caption, dir, filter, selectedFilter, options); } +void OffscreenUi::getSaveFileNameAsync(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { + return DependencyManager::get()->fileSaveDialogAsync(caption, dir, filter, selectedFilter, options); +} + QString OffscreenUi::getExistingDirectory(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { return DependencyManager::get()->existingDirectoryDialog(caption, dir, filter, selectedFilter, options); } +void OffscreenUi::getExistingDirectoryAsync(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { + return DependencyManager::get()->existingDirectoryDialogAsync(caption, dir, filter, selectedFilter, options); +} + class AssetDialogListener : public ModalDialogListener { // ATP equivalent of FileDialogListener. Q_OBJECT @@ -833,6 +881,9 @@ class AssetDialogListener : public ModalDialogListener { private slots: void onSelectedAsset(QVariant asset) { _result = asset; + auto offscreenUi = DependencyManager::get(); + emit offscreenUi->assetDialogResponse(_result.toUrl().toLocalFile()); + offscreenUi->removeModalDialog(qobject_cast(this)); _finished = true; disconnect(_dialog); } @@ -870,6 +921,35 @@ QString OffscreenUi::assetDialog(const QVariantMap& properties) { return result.toUrl().toString(); } +void OffscreenUi::assetDialogAsync(const QVariantMap& properties) { + // ATP equivalent of fileDialog(). + QVariant buildDialogResult; + bool invokeResult; + auto tabletScriptingInterface = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode()) { + invokeResult = QMetaObject::invokeMethod(_desktop, "assetDialog", + Q_RETURN_ARG(QVariant, buildDialogResult), + Q_ARG(QVariant, QVariant::fromValue(properties))); + } else { + QQuickItem* tabletRoot = tablet->getTabletRoot(); + invokeResult = QMetaObject::invokeMethod(tabletRoot, "assetDialog", + Q_RETURN_ARG(QVariant, buildDialogResult), + Q_ARG(QVariant, QVariant::fromValue(properties))); + emit tabletScriptingInterface->tabletNotification(); + } + + if (!invokeResult) { + qWarning() << "Failed to create asset open dialog"; + return; + } + + AssetDialogListener* assetDialogListener = new AssetDialogListener(qvariant_cast(buildDialogResult)); + QObject* assetModalDialog = qobject_cast(assetDialogListener); + _modalDialogListeners.push_back(assetModalDialog); + return; +} + QString OffscreenUi::assetOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { // ATP equivalent of fileOpenDialog(). if (QThread::currentThread() != thread()) { @@ -893,11 +973,37 @@ QString OffscreenUi::assetOpenDialog(const QString& caption, const QString& dir, return assetDialog(map); } +void OffscreenUi::assetOpenDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { + // ATP equivalent of fileOpenDialog(). + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "assetOpenDialogAsync", + Q_ARG(QString, caption), + Q_ARG(QString, dir), + Q_ARG(QString, filter), + Q_ARG(QString*, selectedFilter), + Q_ARG(QFileDialog::Options, options)); + return; + } + + // FIXME support returning the selected filter... somehow? + QVariantMap map; + map.insert("caption", caption); + map.insert("dir", dir); + map.insert("filter", filter); + map.insert("options", static_cast(options)); + return assetDialogAsync(map); +} + QString OffscreenUi::getOpenAssetName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { // ATP equivalent of getOpenFileName(). return DependencyManager::get()->assetOpenDialog(caption, dir, filter, selectedFilter, options); } +void OffscreenUi::getOpenAssetNameAsync(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { + // ATP equivalent of getOpenFileName(). + return DependencyManager::get()->assetOpenDialogAsync(caption, dir, filter, selectedFilter, options); +} + bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { if (!filterEnabled(originalDestination, event)) { return false; diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 335645ce06..a3529da89c 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -151,9 +151,12 @@ public: Q_INVOKABLE QString fileOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE void fileOpenDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QString fileSaveDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE void fileSaveDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QString existingDirectoryDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE void existingDirectoryDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QString assetOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE void assetOpenDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); // Compatibility with QFileDialog::getOpenFileName static QString getOpenFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); @@ -161,10 +164,13 @@ public: // Compatibility with QFileDialog::getSaveFileName static QString getSaveFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + static void getSaveFileNameAsync(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); // Compatibility with QFileDialog::getExistingDirectory static QString getExistingDirectory(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + static void getExistingDirectoryAsync(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); static QString getOpenAssetName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + static void getOpenAssetNameAsync(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QVariant inputDialog(const Icon icon, const QString& title, const QString& label = QString(), const QVariant& current = QVariant()); Q_INVOKABLE QVariant customInputDialog(const Icon icon, const QString& title, const QVariantMap& config); @@ -194,6 +200,7 @@ signals: void showDesktop(); void response(QMessageBox::StandardButton response); void fileDialogResponse(QString response); + void assetDialogResponse(QString response); public slots: void removeModalDialog(QObject* modal); @@ -201,6 +208,7 @@ private: QString fileDialog(const QVariantMap& properties); void fileDialogAsync(const QVariantMap &properties); QString assetDialog(const QVariantMap& properties); + void assetDialogAsync(const QVariantMap& properties); QQuickItem* _desktop { nullptr }; QQuickItem* _toolWindow { nullptr }; From e9ed05c3baf2296fbd0389f9f199867081aecb78 Mon Sep 17 00:00:00 2001 From: vladest Date: Tue, 15 Aug 2017 15:55:36 +0200 Subject: [PATCH 196/722] Reimplement snapshot dir browser for async --- .../scripting/WindowScriptingInterface.cpp | 21 ++++++++++++------ .../src/scripting/WindowScriptingInterface.h | 4 ++-- scripts/system/snapshot.js | 22 +++++++++++-------- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 36381b3626..2e6867c30b 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -174,8 +174,7 @@ void WindowScriptingInterface::ensureReticleVisible() const { /// \param const QString& title title of the window /// \param const QString& directory directory to start the file browser at /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` -/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` -QScriptValue WindowScriptingInterface::browseDir(const QString& title, const QString& directory) { +void WindowScriptingInterface::browseDir(const QString& title, const QString& directory) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { @@ -184,11 +183,19 @@ QScriptValue WindowScriptingInterface::browseDir(const QString& title, const QSt #ifndef Q_OS_WIN path = fixupPathForMac(directory); #endif - QString result = OffscreenUi::getExistingDirectory(nullptr, title, path); - if (!result.isEmpty()) { - setPreviousBrowseLocation(QFileInfo(result).absolutePath()); - } - return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); + auto offscreenUi = DependencyManager::get(); + connect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, + this, [=] (QString result) { + auto offscreenUi = DependencyManager::get(); + disconnect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, + this, nullptr); + if (!result.isEmpty()) { + setPreviousBrowseLocation(QFileInfo(result).absolutePath()); + } + emit browseDirChanged(result); + }); + + OffscreenUi::getExistingDirectoryAsync(nullptr, title, path); } /// Display an open file dialog. If `directory` is an invalid file or directory the browser will start at the current diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index f8ed20f42f..3de5ec43a3 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -53,7 +53,7 @@ public slots: QScriptValue confirm(const QString& message = ""); QScriptValue prompt(const QString& message = "", const QString& defaultText = ""); CustomPromptResult customPrompt(const QVariant& config); - QScriptValue browseDir(const QString& title = "", const QString& directory = ""); + void browseDir(const QString& title = "", const QString& directory = ""); QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); @@ -87,7 +87,7 @@ signals: void announcement(const QString& message); void messageBoxClosed(int id, int button); - + void browseDirChanged(QString browseDir); // triggered when window size or position changes void geometryChanged(QRect geometry); diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 37618253ee..08614c2030 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -120,15 +120,8 @@ function onMessage(message) { openLoginWindow(); break; case 'chooseSnapshotLocation': - var snapshotPath = Window.browseDir("Choose Snapshots Directory", "", ""); - - if (snapshotPath) { // not cancelled - Snapshot.setSnapshotsLocation(snapshotPath); - tablet.emitScriptEvent(JSON.stringify({ - type: "snapshot", - action: "snapshotLocationChosen" - })); - } + Window.browseDirChanged.connect(snapshotDirChanged); + Window.browseDir("Choose Snapshots Directory", "", ""); break; case 'openSettings': if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false)) @@ -579,6 +572,17 @@ function stillSnapshotTaken(pathStillSnapshot, notify) { }); } +function snapshotDirChanged(snapshotPath) { + Window.browseDirChanged.disconnect(snapshotDirChanged); + if (snapshotPath !== "") { // not cancelled + Snapshot.setSnapshotsLocation(snapshotPath); + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "snapshotLocationChosen" + })); + } +} + function processingGifStarted(pathStillSnapshot) { Window.processingGifStarted.disconnect(processingGifStarted); Window.processingGifCompleted.connect(processingGifCompleted); From 51a5c3035390672374c906cd23e89ef259b90b6d Mon Sep 17 00:00:00 2001 From: vladest Date: Tue, 15 Aug 2017 17:05:55 +0200 Subject: [PATCH 197/722] Reimplement asset browsing for async --- .../scripting/WindowScriptingInterface.cpp | 22 +++++++++++++------ .../src/scripting/WindowScriptingInterface.h | 3 ++- .../marketplace/record/record.js | 17 +++++++++----- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 2e6867c30b..ad33c5177c 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -247,8 +247,7 @@ QScriptValue WindowScriptingInterface::save(const QString& title, const QString& /// \param const QString& title title of the window /// \param const QString& directory directory to start the asset browser at /// \param const QString& nameFilter filter to filter asset names by - see `QFileDialog` -/// \return QScriptValue asset path as a string if one was selected, otherwise `QScriptValue::NullValue` -QScriptValue WindowScriptingInterface::browseAssets(const QString& title, const QString& directory, const QString& nameFilter) { +void WindowScriptingInterface::browseAssets(const QString& title, const QString& directory, const QString& nameFilter) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { @@ -260,11 +259,20 @@ QScriptValue WindowScriptingInterface::browseAssets(const QString& title, const if (path.right(1) != "/") { path = path + "/"; } - QString result = OffscreenUi::getOpenAssetName(nullptr, title, path, nameFilter); - if (!result.isEmpty()) { - setPreviousBrowseAssetLocation(QFileInfo(result).absolutePath()); - } - return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); + + auto offscreenUi = DependencyManager::get(); + connect(offscreenUi.data(), &OffscreenUi::assetDialogResponse, + this, [=] (QString result) { + auto offscreenUi = DependencyManager::get(); + disconnect(offscreenUi.data(), &OffscreenUi::assetDialogResponse, + this, nullptr); + if (!result.isEmpty()) { + setPreviousBrowseAssetLocation(QFileInfo(result).absolutePath()); + } + emit assetsDirChanged(result); + }); + + OffscreenUi::getOpenAssetNameAsync(nullptr, title, path, nameFilter); } void WindowScriptingInterface::showAssetServer(const QString& upload) { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 3de5ec43a3..1338743c20 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -56,7 +56,7 @@ public slots: void browseDir(const QString& title = "", const QString& directory = ""); QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); - QScriptValue browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); + void browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f); @@ -88,6 +88,7 @@ signals: void messageBoxClosed(int id, int button); void browseDirChanged(QString browseDir); + void assetsDirChanged(QString assetsDir); // triggered when window size or position changes void geometryChanged(QRect geometry); diff --git a/unpublishedScripts/marketplace/record/record.js b/unpublishedScripts/marketplace/record/record.js index 5439d68c9a..84f631f307 100644 --- a/unpublishedScripts/marketplace/record/record.js +++ b/unpublishedScripts/marketplace/record/record.js @@ -486,6 +486,15 @@ return isFinishOnOpen; } + function onAssetsDirChanged(recording) { + Window.assetsDirChanged.disconnect(onAssetsDirChanged); + if (recording !== "") { + log("Load recording " + recording); + UserActivityLogger.logAction("record_load_recording", logDetails()); + Player.playRecording("atp:" + recording, MyAvatar.position, MyAvatar.orientation); + } + } + function onWebEventReceived(data) { var message, recording; @@ -520,12 +529,8 @@ break; case LOAD_RECORDING_ACTION: // User wants to select an ATP recording to play. - recording = Window.browseAssets("Select Recording to Play", "recordings", "*.hfr"); - if (recording) { - log("Load recording " + recording); - UserActivityLogger.logAction("record_load_recording", logDetails()); - Player.playRecording("atp:" + recording, MyAvatar.position, MyAvatar.orientation); - } + Window.assetsDirChanged.connect(onAssetsDirChanged); + Window.browseAssets("Select Recording to Play", "recordings", "*.hfr"); break; case START_RECORDING_ACTION: // Start making a recording. From 8afdb27c1b39cacc03d3ce872364122542757ed5 Mon Sep 17 00:00:00 2001 From: vladest Date: Tue, 15 Aug 2017 22:55:44 +0200 Subject: [PATCH 198/722] Reworked WindowScriptingInterface save method for async methods --- interface/src/assets/ATPAssetMigrator.cpp | 61 ++++++++++--------- .../scripting/WindowScriptingInterface.cpp | 21 ++++--- .../src/scripting/WindowScriptingInterface.h | 4 +- scripts/system/edit.js | 19 +++--- scripts/system/libraries/entityList.js | 19 +++--- 5 files changed, 73 insertions(+), 51 deletions(-) diff --git a/interface/src/assets/ATPAssetMigrator.cpp b/interface/src/assets/ATPAssetMigrator.cpp index a86c012a55..4f79d32734 100644 --- a/interface/src/assets/ATPAssetMigrator.cpp +++ b/interface/src/assets/ATPAssetMigrator.cpp @@ -296,9 +296,6 @@ void ATPAssetMigrator::checkIfFinished() { // are we out of pending replacements? if so it is time to save the entity-server file if (_doneReading && _pendingReplacements.empty()) { saveEntityServerFile(); - - // reset after the attempted save, success or fail - reset(); } } @@ -336,39 +333,45 @@ bool ATPAssetMigrator::wantsToMigrateResource(const QUrl& url) { void ATPAssetMigrator::saveEntityServerFile() { // show a dialog to ask the user where they want to save the file - QString saveName = OffscreenUi::getSaveFileName(_dialogParent, "Save Migrated Entities File"); - - QFile saveFile { saveName }; - - if (saveFile.open(QIODevice::WriteOnly)) { - QJsonObject rootObject; - rootObject[ENTITIES_OBJECT_KEY] = _entitiesArray; - - QJsonDocument newDocument { rootObject }; - QByteArray jsonDataForFile; - - if (gzip(newDocument.toJson(), jsonDataForFile, -1)) { - - saveFile.write(jsonDataForFile); - saveFile.close(); + auto offscreenUi = DependencyManager::get(); + connect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, this, [=] (QString saveName) { + QFile saveFile { saveName }; - QString infoMessage = QString("Your new entities file has been saved at\n%1.").arg(saveName); + if (saveFile.open(QIODevice::WriteOnly)) { + QJsonObject rootObject; + rootObject[ENTITIES_OBJECT_KEY] = _entitiesArray; - if (_errorCount > 0) { - infoMessage += QString("\nThere were %1 models that could not be migrated.\n").arg(_errorCount); - infoMessage += "Check the warnings in your log for details.\n"; - infoMessage += "You can re-attempt migration on those models\nby restarting this process with the newly saved file."; + QJsonDocument newDocument { rootObject }; + QByteArray jsonDataForFile; + + if (gzip(newDocument.toJson(), jsonDataForFile, -1)) { + + saveFile.write(jsonDataForFile); + saveFile.close(); + + QString infoMessage = QString("Your new entities file has been saved at\n%1.").arg(saveName); + + if (_errorCount > 0) { + infoMessage += QString("\nThere were %1 models that could not be migrated.\n").arg(_errorCount); + infoMessage += "Check the warnings in your log for details.\n"; + infoMessage += "You can re-attempt migration on those models\nby restarting this process with the newly saved file."; + } + + OffscreenUi::asyncInformation(_dialogParent, "Success", infoMessage); + } else { + OffscreenUi::asyncWarning(_dialogParent, "Error", "Could not gzip JSON data for new entities file."); } - OffscreenUi::asyncInformation(_dialogParent, "Success", infoMessage); } else { - OffscreenUi::asyncWarning(_dialogParent, "Error", "Could not gzip JSON data for new entities file."); + OffscreenUi::asyncWarning(_dialogParent, "Error", + QString("Could not open file at %1 to write new entities file to.").arg(saveName)); } + // reset after the attempted save, success or fail + reset(); + }); + + OffscreenUi::getSaveFileNameAsync(_dialogParent, "Save Migrated Entities File"); - } else { - OffscreenUi::asyncWarning(_dialogParent, "Error", - QString("Could not open file at %1 to write new entities file to.").arg(saveName)); - } } void ATPAssetMigrator::reset() { diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index ad33c5177c..dc8031b98b 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -225,8 +225,7 @@ QScriptValue WindowScriptingInterface::browse(const QString& title, const QStrin /// \param const QString& title title of the window /// \param const QString& directory directory to start the file browser at /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` -/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` -QScriptValue WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) { +void WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { @@ -235,11 +234,19 @@ QScriptValue WindowScriptingInterface::save(const QString& title, const QString& #ifndef Q_OS_WIN path = fixupPathForMac(directory); #endif - QString result = OffscreenUi::getSaveFileName(nullptr, title, path, nameFilter); - if (!result.isEmpty()) { - setPreviousBrowseLocation(QFileInfo(result).absolutePath()); - } - return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); + auto offscreenUi = DependencyManager::get(); + connect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, + this, [=] (QString result) { + auto offscreenUi = DependencyManager::get(); + disconnect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, + this, nullptr); + if (!result.isEmpty()) { + setPreviousBrowseLocation(QFileInfo(result).absolutePath()); + } + emit saveFileChanged(result); + }); + + OffscreenUi::getSaveFileNameAsync(nullptr, title, path, nameFilter); } /// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 1338743c20..6b7f480d67 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -55,7 +55,7 @@ public slots: CustomPromptResult customPrompt(const QVariant& config); void browseDir(const QString& title = "", const QString& directory = ""); QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); - QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); + void save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); @@ -89,6 +89,8 @@ signals: void messageBoxClosed(int id, int button); void browseDirChanged(QString browseDir); void assetsDirChanged(QString assetsDir); + void saveFileChanged(QString saveFile); + // triggered when window size or position changes void geometryChanged(QRect geometry); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index c141c7cd52..74a74ddb7f 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1458,6 +1458,16 @@ function toggleSelectedEntitiesVisible() { } } +function onFileSaveChanged(filename) { + Window.saveFileChanged.disconnect(onFileSaveChanged); + if (filename !== "") { + var success = Clipboard.exportEntities(filename, selectionManager.selections); + if (!success) { + Window.notifyEditError("Export failed."); + } + } +} + function handeMenuEvent(menuItem) { if (menuItem === "Allow Selecting of Small Models") { allowSmallModels = Menu.isOptionChecked("Allow Selecting of Small Models"); @@ -1475,13 +1485,8 @@ function handeMenuEvent(menuItem) { if (!selectionManager.hasSelection()) { Window.notifyEditError("No entities have been selected."); } else { - var filename = Window.save("Select Where to Save", "", "*.json"); - if (filename) { - var success = Clipboard.exportEntities(filename, selectionManager.selections); - if (!success) { - Window.notifyEditError("Export failed."); - } - } + Window.saveFileChanged.connect(onFileSaveChanged); + Window.save("Select Where to Save", "", "*.json"); } } else if (menuItem === "Import Entities" || menuItem === "Import Entities from URL") { var importURL = null; diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 64a05fcebf..d2d815f9e3 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -108,6 +108,16 @@ EntityListTool = function(opts) { webView.emitScriptEvent(JSON.stringify(data)); }; + function onFileSaveChanged(filename) { + Window.saveFileChanged.disconnect(onFileSaveChanged); + if (filename !== "") { + var success = Clipboard.exportEntities(filename, selectionManager.selections); + if (!success) { + Window.notifyEditError("Export failed."); + } + } + } + webView.webEventReceived.connect(function(data) { try { data = JSON.parse(data); @@ -139,13 +149,8 @@ EntityListTool = function(opts) { if (!selectionManager.hasSelection()) { Window.notifyEditError("No entities have been selected."); } else { - var filename = Window.save("Select Where to Save", "", "*.json"); - if (filename) { - var success = Clipboard.exportEntities(filename, selectionManager.selections); - if (!success) { - Window.notifyEditError("Export failed."); - } - } + Window.saveFileChanged.connect(onFileSaveChanged); + Window.save("Select Where to Save", "", "*.json"); } } else if (data.type == "pal") { var sessionIds = {}; // Collect the sessionsIds of all selected entitities, w/o duplicates. From 273261ee5705314b7b6f7ae31d201225abf7f99e Mon Sep 17 00:00:00 2001 From: vladest Date: Tue, 15 Aug 2017 23:12:35 +0200 Subject: [PATCH 199/722] Reworked WindowScriptingInterface browse method for async methods --- .../scripting/WindowScriptingInterface.cpp | 21 +++++--- .../src/scripting/WindowScriptingInterface.h | 5 +- scripts/system/edit.js | 48 ++++++++++--------- 3 files changed, 42 insertions(+), 32 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index dc8031b98b..512d79adb2 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -203,8 +203,7 @@ void WindowScriptingInterface::browseDir(const QString& title, const QString& di /// \param const QString& title title of the window /// \param const QString& directory directory to start the file browser at /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` -/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` -QScriptValue WindowScriptingInterface::browse(const QString& title, const QString& directory, const QString& nameFilter) { +void WindowScriptingInterface::browse(const QString& title, const QString& directory, const QString& nameFilter) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { @@ -213,11 +212,19 @@ QScriptValue WindowScriptingInterface::browse(const QString& title, const QStrin #ifndef Q_OS_WIN path = fixupPathForMac(directory); #endif - QString result = OffscreenUi::getOpenFileName(nullptr, title, path, nameFilter); - if (!result.isEmpty()) { - setPreviousBrowseLocation(QFileInfo(result).absolutePath()); - } - return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); + auto offscreenUi = DependencyManager::get(); + connect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, + this, [=] (QString result) { + auto offscreenUi = DependencyManager::get(); + disconnect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, + this, nullptr); + if (!result.isEmpty()) { + setPreviousBrowseLocation(QFileInfo(result).absolutePath()); + } + emit openFileChanged(result); + }); + + OffscreenUi::getOpenFileNameAsync(nullptr, title, path, nameFilter); } /// Display a save file dialog. If `directory` is an invalid file or directory the browser will start at the current diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 6b7f480d67..8ae315d05c 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -54,7 +54,7 @@ public slots: QScriptValue prompt(const QString& message = "", const QString& defaultText = ""); CustomPromptResult customPrompt(const QVariant& config); void browseDir(const QString& title = "", const QString& directory = ""); - QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); + void browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void showAssetServer(const QString& upload = ""); @@ -89,7 +89,8 @@ signals: void messageBoxClosed(int id, int button); void browseDirChanged(QString browseDir); void assetsDirChanged(QString assetsDir); - void saveFileChanged(QString saveFile); + void saveFileChanged(QString filename); + void openFileChanged(QString filename); // triggered when window size or position changes void geometryChanged(QRect geometry); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 74a74ddb7f..8d57c37bf6 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -431,17 +431,8 @@ var toolBar = (function () { }); addButton("importEntitiesButton", "assets-01.svg", function() { - var importURL = null; - var fullPath = Window.browse("Select Model to Import", "", "*.json"); - if (fullPath) { - importURL = "file:///" + fullPath; - } - if (importURL) { - if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { - toolBar.toggle(); - } - importSVO(importURL); - } + Window.openFileChanged.connect(onFileOpenChanged); + Window.browse("Select Model to Import", "", "*.json"); }); addButton("openAssetBrowserButton", "assets-01.svg", function() { @@ -1468,6 +1459,20 @@ function onFileSaveChanged(filename) { } } +function onFileOpenChanged(filename) { + Window.openFileChanged.disconnect(onFileOpenChanged); + var importURL = null; + if (filename !== "") { + importURL = "file:///" + filename; + } + if (importURL) { + if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { + toolBar.toggle(); + } + importSVO(importURL); + } +} + function handeMenuEvent(menuItem) { if (menuItem === "Allow Selecting of Small Models") { allowSmallModels = Menu.isOptionChecked("Allow Selecting of Small Models"); @@ -1489,21 +1494,18 @@ function handeMenuEvent(menuItem) { Window.save("Select Where to Save", "", "*.json"); } } else if (menuItem === "Import Entities" || menuItem === "Import Entities from URL") { - var importURL = null; if (menuItem === "Import Entities") { - var fullPath = Window.browse("Select Model to Import", "", "*.json"); - if (fullPath) { - importURL = "file:///" + fullPath; - } + Window.openFileChanged.connect(onFileOpenChanged); + Window.browse("Select Model to Import", "", "*.json"); } else { - importURL = Window.prompt("URL of SVO to import", ""); - } - - if (importURL) { - if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { - toolBar.toggle(); + var importURL = Window.prompt("URL of SVO to import", ""); + if (importURL) { + if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { + toolBar.toggle(); + } + importSVO(importURL); } - importSVO(importURL); + } } else if (menuItem === "Entity List...") { entityListTool.toggleVisible(); From b1938bafea0190376e9f484ed434e49c5e297752 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 16 Aug 2017 13:40:06 +1200 Subject: [PATCH 200/722] Apply slider values to entities --- scripts/vr-edit/modules/toolMenu.js | 12 +++++----- scripts/vr-edit/vr-edit.js | 35 ++++++++++++++++++----------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 9a95425433..c6b7527056 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -154,7 +154,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, - "barSlider": { + "barSlider": { // Values range between 0.0 and 1.0. overlay: "cube", properties: { dimensions: { x: 0.02, y: 0.1, z: 0.01 }, @@ -192,7 +192,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, - "imageSlider": { + "imageSlider": { // Values range between 0.0 and 1.0. overlay: "cube", properties: { dimensions: { x: 0.01, y: 0.05, z: 0.01 }, @@ -564,7 +564,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, setting: { key: "VREdit.physicsTool.gravity", - defaultValue: 0, // Slider value in range 0.0 .. 1.0. TODO: Default value. + defaultValue: 0.5, callback: "setGravity" }, command: { @@ -589,7 +589,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, setting: { key: "VREdit.physicsTool.bounce", - defaultValue: 0, // Slider value in range 0.0 .. 1.0. TODO: Default value. + defaultValue: 0.5, callback: "setBounce" }, command: { @@ -614,7 +614,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, setting: { key: "VREdit.physicsTool.damping", - defaultValue: 0, // Slider value in range 0.0 .. 1.0. TODO: Default value. + defaultValue: 0.5, callback: "setDamping" }, command: { @@ -639,7 +639,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, setting: { key: "VREdit.physicsTool.density", - defaultValue: 0, // Slider value in range 0.0 .. 1.0. TODO: Default value. + defaultValue: 0.5, callback: "setDensity" }, command: { diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index d0d307c07e..dd93ed420f 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -33,6 +33,8 @@ toolSelected = TOOL_NONE, colorToolColor = { red: 128, green: 128, blue: 128 }, physicsToolPhysics = { userData: { grabbableKey: {} } }, + EARTH_GRAVITY = -9.80665, + physicsToolGravity = EARTH_GRAVITY, // Primary objects App, @@ -1395,7 +1397,7 @@ case "setGravityOn": if (parameter) { - physicsToolPhysics.gravity = { x: 0, y: -9.8, z: 0 }; // Earth gravity. + physicsToolPhysics.gravity = { x: 0, y: physicsToolGravity, z: 0 }; physicsToolPhysics.dynamic = true; } else { physicsToolPhysics.gravity = Vec3.ZERO; @@ -1415,28 +1417,35 @@ } break; - case "setGravityValue": + case "setGravity": if (parameter !== undefined) { - // TODO - print("setGravityValue = " + parameter); + // Power range 0.0, 0.5, 1.0 maps to -50.0, -9.80665, 50.0. + physicsToolGravity = 82.36785162 * Math.pow(2.214065901, parameter) - 132.36785; + if (physicsToolPhysics.dynamic === true) { // Only apply if gravity is turned on. + physicsToolPhysics.gravity = { x: 0, y: physicsToolGravity, z: 0 }; + } } break; - case "setBounceValue": + case "setBounce": if (parameter !== undefined) { - // TODO - print("setBounceValue = " + parameter); + // Linear range from 0.0, 0.5, 1.0 maps to 0.0, 0.5, 1.0; + physicsToolPhysics.restitution = parameter; } break; - case "setDampingValue": + case "setDamping": if (parameter !== undefined) { - // TODO - print("setDampingValue = " + parameter); + // Power range 0.0, 0.5, 1.0 maps to 0, 0.39, 1.0. + physicsToolPhysics.linearDamping = 0.69136364 * Math.pow(2.446416831, parameter) - 0.691364; + // Power range 0.0, 0.5, 1.0 maps to 0, 0.3935, 1.0. + physicsToolPhysics.angularDamping = 0.72695892 * Math.pow(2.375594, parameter) - 0.726959; + // Linear range from 0.0, 0.5, 1.0 maps to 0.0, 0.5, 1.0; + physicsToolPhysics.friction = parameter; } break; - case "setDensityValue": + case "setDensity": if (parameter !== undefined) { - // TODO - print("setDensityValue = " + parameter); + // Power range 0.0, 0.5, 1.0 maps to 100, 1000, 10000. + physicsToolPhysics.density = Math.pow(10, 2 + 2 * parameter); } break; From cca862b6baedcd679793892f2b317acbb675210d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 16 Aug 2017 13:47:14 +1200 Subject: [PATCH 201/722] Apply physics to just the root entity --- scripts/vr-edit/modules/selection.js | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 1682ce0bbc..75e7b7e105 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -415,35 +415,20 @@ Selection = function (side) { } function applyPhysics(physicsProperties) { - // TODO: For development testing, apply physics to the currently intersected entity. + // Apply physics to just the root entity. var properties; properties = Object.clone(physicsProperties); properties.userData = updatePhysicsUserData(selection[intersectedEntityIndex].userData, physicsProperties.userData); - Entities.editEntity(selection[intersectedEntityIndex].id, properties); - - // TODO: Original functionality applied physics to all entities in the selection. - /* - // Applies physics to the current selection (i.e., the selection made when entity was trigger-clicked to apply physics). - var properties, - i, - length; - - properties = Object.clone(physicsProperties); - - for (i = 0, length = selection.length; i < length; i += 1) { - properties.userData = updatePhysicsUserData(selection[i].userData, physicsProperties.userData); - Entities.editEntity(selection[i].id, properties); - } + Entities.editEntity(rootEntityID, properties); if (physicsProperties.dynamic) { - // Give dynamic entities with zero a little kick to set off physics. - properties = Entities.getEntityProperties(selection[0].id, ["velocity"]); + // Give dynamic entities with zero velocity a little kick to set off physics. + properties = Entities.getEntityProperties(rootEntityID, ["velocity"]); if (Vec3.length(properties.velocity) < DYNAMIC_VELOCITY_THRESHOLD) { - Entities.editEntity(selection[0].id, { velocity: DYNAMIC_VELOCITY_KICK }); + Entities.editEntity(rootEntityID, { velocity: DYNAMIC_VELOCITY_KICK }); } } - */ } function clear() { From c705bb7fad1e5d94ffd362a132a7495dff2a95ca Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 16 Aug 2017 14:55:44 +1200 Subject: [PATCH 202/722] Add picklist button --- scripts/vr-edit/modules/toolMenu.js | 78 ++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index c6b7527056..0f0fe62771 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -61,6 +61,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { NO_SWATCH_COLOR = { red: 128, green: 128, blue: 128 }, + UI_BASE_COLOR = { red: 64, green: 64, blue: 64 }, + UI_HIGHLIGHT_COLOR = { red: 100, green: 240, blue: 100 }, + UI_ELEMENTS = { "panel": { overlay: "cube", @@ -95,8 +98,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { ignoreRayIntersection: false, visible: true }, - onColor: { red: 100, green: 240, blue: 100 }, - offColor: { red: 64, green: 64, blue: 64 } + onColor: UI_HIGHLIGHT_COLOR, + offColor: UI_BASE_COLOR }, "swatch": { overlay: "cube", @@ -118,7 +121,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localRotation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 180 }), topMargin: 0, leftMargin: 0, - color: { red: 128, green: 128, blue: 128 }, + color: { red: 240, green: 240, blue: 240 }, alpha: 1.0, lineHeight: 0.007, backgroundAlpha: 0, @@ -172,7 +175,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.02, y: 0.03, z: 0.01 }, localPosition: { x: 0, y: 0.035, z: 0 }, localRotation: Quat.ZERO, - color: { red: 100, green: 240, blue: 100 }, + color: UI_HIGHLIGHT_COLOR, alpha: 1.0, solid: true, ignoreRayIntersection: true, @@ -185,7 +188,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.02, y: 0.07, z: 0.01 }, localPosition: { x: 0, y: -0.015, z: 0 }, localRotation: Quat.ZERO, - color: { red: 64, green: 64, blue: 64 }, + color: UI_BASE_COLOR, alpha: 1.0, solid: true, ignoreRayIntersection: true, @@ -219,6 +222,24 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { ignoreRayIntersection: true, visible: true } + }, + "picklist": { + overlay: "cube", + properties: { + dimensions: { x: 0.10, y: 0.12, z: 0.01 }, + localRotation: Quat.ZERO, + color: UI_BASE_COLOR, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + } + }, + "picklistBackground": { + + }, + "picklistOption": { + } }, @@ -229,6 +250,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { SLIDER_RAISE_DELTA = { x: 0, y: 0, z: 0.004 }, MIN_BAR_SLIDER_DIMENSION = 0.0001, // Avoid visual artifact for 0 slider values. + PICKLIST_UI_ELEMENTS = ["picklist"], + PICKLIST_RAISE_DELTA = { x: 0, y: 0, z: 0.004 }, + OPTONS_PANELS = { groupOptions: [ @@ -549,10 +573,14 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, { id: "presets", - type: "panel", + type: "picklist", properties: { localPosition: { x: 0.016, y: -0.03, z: -0.005 }, dimensions: { x: 0.06, y: 0.02, z: 0.01 } + }, + label: "DEFAULT", + command: { + method: "togglePresets" } }, { @@ -770,9 +798,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightedSource, isHighlightingButton, isHighlightingSlider, + isHighlightingPicklist, pressedItem = null, pressedSource, isButtonPressed, + isPicklistPressed, isGripClicked, isGroupButtonEnabled, @@ -1148,11 +1178,19 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: localPosition }); } + //Lower old picklist. + if (isHighlightingPicklist) { + localPosition = highlightedItems[highlightedItem].properties.localPosition; + Overlays.editOverlay(highlightedSource[highlightedItem], { + localPosition: localPosition + }); + } // Update status variables. highlightedItem = intersectedItem; highlightedItems = intersectionItems; isHighlightingButton = BUTTON_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; isHighlightingSlider = SLIDER_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; + isHighlightingPicklist = PICKLIST_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; // Raise new slider. if (isHighlightingSlider) { localPosition = intersectionItems[highlightedItem].properties.localPosition; @@ -1160,6 +1198,14 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: Vec3.subtract(localPosition, SLIDER_RAISE_DELTA) }); } + // Raise new picklist. + if (isHighlightingPicklist) { + localPosition = intersectionItems[highlightedItem].properties.localPosition; + Overlays.editOverlay(intersectionOverlays[highlightedItem], { + localPosition: Vec3.subtract(localPosition, PICKLIST_RAISE_DELTA), + color: UI_HIGHLIGHT_COLOR + }); + } } else if (highlightedItem !== NONE) { // Un-highlight previous button. Overlays.editOverlay(highlightOverlay, { @@ -1172,10 +1218,19 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: localPosition }); } + // Lower picklist. + if (isHighlightingPicklist) { + localPosition = highlightedItems[highlightedItem].properties.localPosition; + Overlays.editOverlay(highlightedSource[highlightedItem], { + localPosition: localPosition, + color: UI_BASE_COLOR + }); + } // Update status variables. highlightedItem = NONE; isHighlightingButton = false; isHighlightingSlider = false; + isHighlightingPicklist = false; } highlightedSource = intersectionOverlays; } @@ -1285,6 +1340,15 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } + // Picklist update. + if (intersectionItems && intersectionItems[intersectedItem].type === "picklist" && controlHand.triggerClicked() + && !isPicklistPressed) { + isPicklistPressed = true; + if (intersectionItems[intersectedItem].command) { + doCommand(intersectionItems[intersectedItem].command.method); + } + } + // Special handling for Group options. if (optionsItems && optionsItems === OPTONS_PANELS.groupOptions) { enableGroupButton = groupsCount > 1; @@ -1373,9 +1437,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightedSource = null; isHighlightingButton = false; isHighlightingSlider = false; + isHighlightingPicklist = false; pressedItem = null; pressedSource = null; isButtonPressed = false; + isPicklistPressed = false; isGripClicked = false; isGroupButtonEnabled = false; isUngroupButtonEnabled = false; From bfaae7c220d2363f7a7d14de7d44a233172ee247 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 16 Aug 2017 20:22:24 +1200 Subject: [PATCH 203/722] Add picklist options --- scripts/vr-edit/modules/toolMenu.js | 258 +++++++++++++++++++++------- 1 file changed, 200 insertions(+), 58 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 0f0fe62771..b1483ab673 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -24,7 +24,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsOverlays = [], optionsOverlaysIDs = [], // Text ids (names) of options overlays. - optionsOverlaysAuxiliaries = [], + optionsSliderData = [], // Uses same index values as optionsOverlays. + optionsPicklistItemLabelOverlays = [], optionsEnabled = [], optionsSettings = {}, @@ -226,7 +227,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { "picklist": { overlay: "cube", properties: { - dimensions: { x: 0.10, y: 0.12, z: 0.01 }, + dimensions: { x: 0.06, y: 0.02, z: 0.01 }, localRotation: Quat.ZERO, color: UI_BASE_COLOR, alpha: 1.0, @@ -235,11 +236,18 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, - "picklistBackground": { - - }, - "picklistOption": { - + "picklistItem": { + overlay: "cube", + properties: { + dimensions: { x: 0.06, y: 0.02, z: 0.01 }, + localPosition: Vec3.ZERO, + localRotation: Quat.ZERO, + color: { red: 100, green: 100, blue: 100 }, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: false + } } }, @@ -250,7 +258,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { SLIDER_RAISE_DELTA = { x: 0, y: 0, z: 0.004 }, MIN_BAR_SLIDER_DIMENSION = 0.0001, // Avoid visual artifact for 0 slider values. - PICKLIST_UI_ELEMENTS = ["picklist"], + PICKLIST_UI_ELEMENTS = ["picklist", "picklistItem"], PICKLIST_RAISE_DELTA = { x: 0, y: 0, z: 0.004 }, @@ -580,8 +588,73 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, label: "DEFAULT", command: { - method: "togglePresets" - } + method: "togglePhysicsPresets" + }, + items: [ + "presetDefault", + "presetLead", + "presetWood", + "presetIce", + "presetRubber", + "presetCotton", + "presetTumbleWeed", + "presetZeroG", + "presetBallon" + ] + }, + { + id: "presetDefault", + type: "picklistItem", + label: "DEFAULT", + command: { method: "pickPhysicsPreset" } + }, + { + id: "presetLead", + type: "picklistItem", + label: "LEAD", + command: { method: "pickPhysicsPreset" } + }, + { + id: "presetWood", + type: "picklistItem", + label: "WOOD", + command: { method: "pickPhysicsPreset" } + }, + { + id: "presetIce", + type: "picklistItem", + label: "ICE", + command: { method: "pickPhysicsPreset" } + }, + { + id: "presetRubber", + type: "picklistItem", + label: "RUBBER", + command: { method: "pickPhysicsPreset" } + }, + { + id: "presetCotton", + type: "picklistItem", + label: "COTTON", + command: { method: "pickPhysicsPreset" } + }, + { + id: "presetTumbleWeed", + type: "picklistItem", + label: "TUMBLEWEED", + command: { method: "pickPhysicsPreset" } + }, + { + id: "presetZeroG", + type: "picklistItem", + label: "ZERO-G", + command: { method: "pickPhysicsPreset" } + }, + { + id: "presetBallon", + type: "picklistItem", + label: "BALLOON", + command: { method: "pickPhysicsPreset" } }, { id: "gravitySlider", @@ -799,10 +872,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isHighlightingButton, isHighlightingSlider, isHighlightingPicklist, + isPicklistOpen, pressedItem = null, pressedSource, isButtonPressed, isPicklistPressed, + isPicklistItemPressed, isGripClicked, isGroupButtonEnabled, @@ -849,9 +924,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsOverlays = []; optionsOverlaysIDs = []; - optionsOverlaysAuxiliaries = []; + optionsSliderData = []; optionsEnabled = []; optionsItems = null; + + optionsPicklistItemLabelOverlays = []; } function openOptions(toolOptions) { @@ -862,6 +939,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { value, imageOffset, IMAGE_OFFSET = 0.0005, + id, i, length; @@ -878,7 +956,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { parentID = menuPanelOverlay; // Menu panel parents to background panel. for (i = 0, length = optionsItems.length; i < length; i += 1) { properties = Object.clone(UI_ELEMENTS[optionsItems[i].type].properties); - properties = Object.merge(properties, optionsItems[i].properties); + if (optionsItems[i].properties) { + properties = Object.merge(properties, optionsItems[i].properties); + } properties.parentID = parentID; if (properties.url) { properties.url = Script.resolvePath(properties.url); @@ -917,11 +997,14 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.clone(UI_ELEMENTS.label.properties); properties.text = optionsItems[i].label; properties.parentID = optionsOverlays[optionsOverlays.length - 1]; - Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); + id = Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); + if (optionsItems[i].type === "picklistItem") { + optionsPicklistItemLabelOverlays.push(id); + } } if (optionsItems[i].type === "barSlider") { - optionsOverlaysAuxiliaries[i] = {}; + optionsSliderData[i] = {}; auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderValue.properties); auxiliaryProperties.localPosition = { x: 0, y: (0.5 - value / 2) * properties.dimensions.y, z: 0 }; auxiliaryProperties.dimensions = { @@ -930,7 +1013,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { z: properties.dimensions.z }; auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; - optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.barSliderValue.overlay, + optionsSliderData[i].value = Overlays.addOverlay(UI_ELEMENTS.barSliderValue.overlay, auxiliaryProperties); auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderRemainder.properties); auxiliaryProperties.localPosition = { x: 0, y: (-0.5 + (1.0 - value) / 2) * properties.dimensions.y, z: 0 }; @@ -940,7 +1023,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { z: properties.dimensions.z }; auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; - optionsOverlaysAuxiliaries[i].remainder = Overlays.addOverlay(UI_ELEMENTS.barSliderRemainder.overlay, + optionsSliderData[i].remainder = Overlays.addOverlay(UI_ELEMENTS.barSliderRemainder.overlay, auxiliaryProperties); } @@ -977,18 +1060,18 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } // Value pointers. - optionsOverlaysAuxiliaries[i] = {}; - optionsOverlaysAuxiliaries[i].offset = + optionsSliderData[i] = {}; + optionsSliderData[i].offset = { x: -properties.dimensions.x / 2, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; auxiliaryProperties = Object.clone(UI_ELEMENTS.sliderPointer.properties); - auxiliaryProperties.localPosition = optionsOverlaysAuxiliaries[i].offset; + auxiliaryProperties.localPosition = optionsSliderData[i].offset; auxiliaryProperties.drawInFront = true; // TODO: Accommodate work-around above; remove when bug fixed. auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; - optionsOverlaysAuxiliaries[i].value = Overlays.addOverlay(UI_ELEMENTS.sliderPointer.overlay, + optionsSliderData[i].value = Overlays.addOverlay(UI_ELEMENTS.sliderPointer.overlay, auxiliaryProperties); auxiliaryProperties.localPosition = { x: 0, y: properties.dimensions.x, z: 0 }; auxiliaryProperties.localRotation = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }); - auxiliaryProperties.parentID = optionsOverlaysAuxiliaries[i].value; + auxiliaryProperties.parentID = optionsSliderData[i].value; Overlays.addOverlay(UI_ELEMENTS.sliderPointer.overlay, auxiliaryProperties); } parentID = optionsOverlays[0]; // Menu buttons parent to menu panel. @@ -1021,7 +1104,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { function doCommand(command, parameter) { var index, hasColor, - value; + value, + items, + parentID, + i, + length; switch (command) { @@ -1072,6 +1159,73 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { uiCommandCallback(command, value); break; + case "togglePhysicsPresets": + if (isPicklistOpen) { + // Close picklist. + index = optionsOverlaysIDs.indexOf(parameter); + + // Lower picklist. + Overlays.editOverlay(optionsOverlays[index], { + localPosition: optionsItems[index].properties.localPosition + }); + + // Hide options. + items = optionsItems[index].items; + for (i = 0, length = items.length; i < length; i += 1) { + Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf(items[i])], { + localPosition: Vec3.ZERO, + visible: false + }); + } + + // Hide labels. + for (i = 0, length = optionsPicklistItemLabelOverlays.length; i < length; i += 1) { + Overlays.editOverlay(optionsPicklistItemLabelOverlays[i], { + visible: false + }); + } + } + + isPicklistOpen = !isPicklistOpen; + + if (isPicklistOpen) { + // Open picklist. + index = optionsOverlaysIDs.indexOf(parameter); + parentID = optionsOverlays[index]; + + // Raise picklist. + Overlays.editOverlay(parentID, { + localPosition: Vec3.subtract(optionsItems[index].properties.localPosition, PICKLIST_RAISE_DELTA) + }); + + // Show options. + items = optionsItems[index].items; + for (i = 0, length = items.length; i < length; i += 1) { + Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf(items[i])], { + parentID: parentID, + localPosition: { x: 0, y: (i + 1) * -UI_ELEMENTS.picklistItem.properties.dimensions.y, z: 0 }, + visible: true + }); + } + + // Show labels. + for (i = 0, length = optionsPicklistItemLabelOverlays.length; i < length; i += 1) { + Overlays.editOverlay(optionsPicklistItemLabelOverlays[i], { + visible: true + }); + } + } + break; + + case "pickPhysicsPreset": + doCommand("togglePhysicsPresets", "presets"); // Close picklist. + + // TODO: Update picklist label. + // TODO: Set picklist setting to record picklist label. + // TODO: Set physics parameters - update sliders, settings, and to-apply-values. + + break; + case "setGravity": Settings.setValue(optionsSettings.gravitySlider.key, parameter); uiCommandCallback("setGravity", parameter); @@ -1178,13 +1332,6 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: localPosition }); } - //Lower old picklist. - if (isHighlightingPicklist) { - localPosition = highlightedItems[highlightedItem].properties.localPosition; - Overlays.editOverlay(highlightedSource[highlightedItem], { - localPosition: localPosition - }); - } // Update status variables. highlightedItem = intersectedItem; highlightedItems = intersectionItems; @@ -1198,14 +1345,6 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: Vec3.subtract(localPosition, SLIDER_RAISE_DELTA) }); } - // Raise new picklist. - if (isHighlightingPicklist) { - localPosition = intersectionItems[highlightedItem].properties.localPosition; - Overlays.editOverlay(intersectionOverlays[highlightedItem], { - localPosition: Vec3.subtract(localPosition, PICKLIST_RAISE_DELTA), - color: UI_HIGHLIGHT_COLOR - }); - } } else if (highlightedItem !== NONE) { // Un-highlight previous button. Overlays.editOverlay(highlightOverlay, { @@ -1218,14 +1357,6 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: localPosition }); } - // Lower picklist. - if (isHighlightingPicklist) { - localPosition = highlightedItems[highlightedItem].properties.localPosition; - Overlays.editOverlay(highlightedSource[highlightedItem], { - localPosition: localPosition, - color: UI_BASE_COLOR - }); - } // Update status variables. highlightedItem = NONE; isHighlightingButton = false; @@ -1275,6 +1406,24 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } + // Picklist update. + if (intersectionItems && ((intersectionItems[intersectedItem].type === "picklist" + && controlHand.triggerClicked() !== isPicklistPressed) + || (intersectionItems[intersectedItem].type !== "picklist" && isPicklistPressed))) { + isPicklistPressed = isHighlightingPicklist && controlHand.triggerClicked(); + if (isPicklistPressed) { + doCommand(intersectionItems[intersectedItem].command.method, intersectionItems[intersectedItem].id); + } + } + if (intersectionItems && ((intersectionItems[intersectedItem].type === "picklistItem" + && controlHand.triggerClicked() !== isPicklistItemPressed) + || (intersectionItems[intersectedItem].type !== "picklistItem" && isPicklistItemPressed))) { + isPicklistItemPressed = isHighlightingPicklist && controlHand.triggerClicked(); + if (isPicklistItemPressed) { + doCommand(intersectionItems[intersectedItem].command.method, intersectionItems[intersectedItem].id); + } + } + // Grip click. if (controlHand.gripClicked() !== isGripClicked) { isGripClicked = !isGripClicked; @@ -1298,7 +1447,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Vec3.multiplyQbyV(sliderProperties.orientation, Vec3.UNIT_Y)) / overlayDimensions.y; fraction = adjustSliderFraction(fraction); otherFraction = 1.0 - fraction; - Overlays.editOverlay(optionsOverlaysAuxiliaries[intersectedItem].value, { + Overlays.editOverlay(optionsSliderData[intersectedItem].value, { localPosition: { x: 0, y: (0.5 - fraction / 2) * overlayDimensions.y, z: 0 }, dimensions: { x: overlayDimensions.x, @@ -1306,7 +1455,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { z: overlayDimensions.z } }); - Overlays.editOverlay(optionsOverlaysAuxiliaries[intersectedItem].remainder, { + Overlays.editOverlay(optionsSliderData[intersectedItem].remainder, { localPosition: { x: 0, y: (-0.5 + otherFraction / 2) * overlayDimensions.y, z: 0 }, dimensions: { x: overlayDimensions.x, @@ -1331,8 +1480,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { fraction = Vec3.dot(Vec3.subtract(basePoint, intersection.intersection), Vec3.multiplyQbyV(sliderProperties.orientation, Vec3.UNIT_Y)) / overlayDimensions.y; fraction = adjustSliderFraction(fraction); - Overlays.editOverlay(optionsOverlaysAuxiliaries[intersectedItem].value, { - localPosition: Vec3.sum(optionsOverlaysAuxiliaries[intersectedItem].offset, + Overlays.editOverlay(optionsSliderData[intersectedItem].value, { + localPosition: Vec3.sum(optionsSliderData[intersectedItem].offset, { x: 0, y: (0.5 - fraction) * overlayDimensions.y, z: 0 }) }); if (intersectionItems[intersectedItem].callback) { @@ -1340,15 +1489,6 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } - // Picklist update. - if (intersectionItems && intersectionItems[intersectedItem].type === "picklist" && controlHand.triggerClicked() - && !isPicklistPressed) { - isPicklistPressed = true; - if (intersectionItems[intersectedItem].command) { - doCommand(intersectionItems[intersectedItem].command.method); - } - } - // Special handling for Group options. if (optionsItems && optionsItems === OPTONS_PANELS.groupOptions) { enableGroupButton = groupsCount > 1; @@ -1438,10 +1578,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isHighlightingButton = false; isHighlightingSlider = false; isHighlightingPicklist = false; + isPicklistOpen = false; pressedItem = null; pressedSource = null; isButtonPressed = false; isPicklistPressed = false; + isPicklistItemPressed = false; isGripClicked = false; isGroupButtonEnabled = false; isUngroupButtonEnabled = false; From 30e9b8ea458768d53f681a18be989607f9934878 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 16 Aug 2017 20:56:08 +1200 Subject: [PATCH 204/722] Update picklist label when item is picked --- scripts/vr-edit/modules/toolMenu.js | 50 +++++++++++++++++------------ 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index b1483ab673..fbbfecc287 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -24,8 +24,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsOverlays = [], optionsOverlaysIDs = [], // Text ids (names) of options overlays. + optionsOverlaysLabels = [], // Overlay IDs of labels for optionsOverlays. optionsSliderData = [], // Uses same index values as optionsOverlays. - optionsPicklistItemLabelOverlays = [], optionsEnabled = [], optionsSettings = {}, @@ -587,6 +587,10 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.06, y: 0.02, z: 0.01 } }, label: "DEFAULT", + setting: { + key: "VREdit.physicsTool.presetLabel", + command: "XXX" + }, command: { method: "togglePhysicsPresets" }, @@ -924,11 +928,10 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsOverlays = []; optionsOverlaysIDs = []; + optionsOverlaysLabels = []; optionsSliderData = []; optionsEnabled = []; optionsItems = null; - - optionsPicklistItemLabelOverlays = []; } function openOptions(toolOptions) { @@ -986,6 +989,10 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Store value in optionsSettings rather than using overlay property. optionsSettings[optionsItems[i].id].value = value; } + if (optionsItems[i].type === "picklist") { + // Value is picklist label. + optionsItems[i].label = value; + } if (optionsItems[i].setting.callback) { uiCommandCallback(optionsItems[i].setting.callback, value); } @@ -998,9 +1005,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties.text = optionsItems[i].label; properties.parentID = optionsOverlays[optionsOverlays.length - 1]; id = Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); - if (optionsItems[i].type === "picklistItem") { - optionsPicklistItemLabelOverlays.push(id); - } + optionsOverlaysLabels[i] = id; } if (optionsItems[i].type === "barSlider") { @@ -1107,6 +1112,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { value, items, parentID, + label, i, length; @@ -1172,15 +1178,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Hide options. items = optionsItems[index].items; for (i = 0, length = items.length; i < length; i += 1) { - Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf(items[i])], { + index = optionsOverlaysIDs.indexOf(items[i]); + Overlays.editOverlay(optionsOverlays[index], { localPosition: Vec3.ZERO, visible: false }); - } - - // Hide labels. - for (i = 0, length = optionsPicklistItemLabelOverlays.length; i < length; i += 1) { - Overlays.editOverlay(optionsPicklistItemLabelOverlays[i], { + Overlays.editOverlay(optionsOverlaysLabels[index], { visible: false }); } @@ -1201,27 +1204,32 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Show options. items = optionsItems[index].items; for (i = 0, length = items.length; i < length; i += 1) { - Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf(items[i])], { + index = optionsOverlaysIDs.indexOf(items[i]); + Overlays.editOverlay(optionsOverlays[index], { parentID: parentID, localPosition: { x: 0, y: (i + 1) * -UI_ELEMENTS.picklistItem.properties.dimensions.y, z: 0 }, visible: true }); - } - - // Show labels. - for (i = 0, length = optionsPicklistItemLabelOverlays.length; i < length; i += 1) { - Overlays.editOverlay(optionsPicklistItemLabelOverlays[i], { + Overlays.editOverlay(optionsOverlaysLabels[index], { visible: true }); } + } break; case "pickPhysicsPreset": - doCommand("togglePhysicsPresets", "presets"); // Close picklist. + // Close picklist. + doCommand("togglePhysicsPresets", "presets"); + + // Update picklist label. + index = optionsOverlaysIDs.indexOf(parameter); + label = optionsItems[index].label; + Overlays.editOverlay(optionsOverlaysLabels[optionsOverlaysIDs.indexOf("presets")], { + text: label + }); + Settings.setValue(optionsSettings.presets.key, label); - // TODO: Update picklist label. - // TODO: Set picklist setting to record picklist label. // TODO: Set physics parameters - update sliders, settings, and to-apply-values. break; From f9de451b309a0f940ef9d6edc798945c2f4dc675 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 16 Aug 2017 21:11:10 +1200 Subject: [PATCH 205/722] Make picklist label display "CUSTOM" when slider value is changed --- scripts/vr-edit/modules/toolMenu.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index fbbfecc287..3b59715c6f 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -990,7 +990,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsSettings[optionsItems[i].id].value = value; } if (optionsItems[i].type === "picklist") { - // Value is picklist label. + optionsSettings[optionsItems[i].id].value = value; optionsItems[i].label = value; } if (optionsItems[i].setting.callback) { @@ -1094,6 +1094,17 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { closeOptions(); } + function setPresetsLabelToCustom() { + var CUSTOM = "CUSTOM"; + if (optionsSettings.presets.value !== CUSTOM) { + optionsSettings.presets.value = CUSTOM; + Overlays.editOverlay(optionsOverlaysLabels[optionsOverlaysIDs.indexOf("presets")], { + text: CUSTOM + }); + Settings.setValue(optionsSettings.presets.key, CUSTOM); + } + } + function evaluateParameter(parameter) { var parameters, overlayID, @@ -1223,8 +1234,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { doCommand("togglePhysicsPresets", "presets"); // Update picklist label. - index = optionsOverlaysIDs.indexOf(parameter); - label = optionsItems[index].label; + label = optionsItems[optionsOverlaysIDs.indexOf(parameter)].label; + optionsSettings.presets.value = label; Overlays.editOverlay(optionsOverlaysLabels[optionsOverlaysIDs.indexOf("presets")], { text: label }); @@ -1235,18 +1246,22 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { break; case "setGravity": + setPresetsLabelToCustom(); Settings.setValue(optionsSettings.gravitySlider.key, parameter); uiCommandCallback("setGravity", parameter); break; case "setBounce": + setPresetsLabelToCustom(); Settings.setValue(optionsSettings.bounceSlider.key, parameter); uiCommandCallback("setBounce", parameter); break; case "setDamping": + setPresetsLabelToCustom(); Settings.setValue(optionsSettings.dampingSlider.key, parameter); uiCommandCallback("setDamping", parameter); break; case "setDensity": + setPresetsLabelToCustom(); Settings.setValue(optionsSettings.densitySlider.key, parameter); uiCommandCallback("setDensity", parameter); break; From 2c2f866c8204ec02b5a0c9fd39b27da0e42d4bca Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 16 Aug 2017 21:33:34 +1200 Subject: [PATCH 206/722] Close picklist if click elsewhere --- scripts/vr-edit/modules/toolMenu.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 3b59715c6f..07f7a863ca 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -588,8 +588,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, label: "DEFAULT", setting: { - key: "VREdit.physicsTool.presetLabel", - command: "XXX" + key: "VREdit.physicsTool.presetLabel" }, command: { method: "togglePhysicsPresets" @@ -932,6 +931,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsSliderData = []; optionsEnabled = []; optionsItems = null; + + isPicklistOpen = false; } function openOptions(toolOptions) { @@ -1225,7 +1226,6 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true }); } - } break; @@ -1446,6 +1446,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { doCommand(intersectionItems[intersectedItem].command.method, intersectionItems[intersectedItem].id); } } + if (intersectionItems && isPicklistOpen && controlHand.triggerClicked() + && intersectionItems[intersectedItem].type !== "picklist" + && intersectionItems[intersectedItem].type !== "picklistItem") { + doCommand("togglePhysicsPresets", "presets"); // TODO: This is a bit hacky. + } // Grip click. if (controlHand.gripClicked() !== isGripClicked) { From 2287e16ea75e9dd8e51c450926dce222c0645733 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 17 Aug 2017 00:04:14 +1200 Subject: [PATCH 207/722] Set physics values per picklist items --- scripts/vr-edit/modules/toolMenu.js | 88 +++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 07f7a863ca..130aa68e3a 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -261,6 +261,20 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { PICKLIST_UI_ELEMENTS = ["picklist", "picklistItem"], PICKLIST_RAISE_DELTA = { x: 0, y: 0, z: 0.004 }, + PHYSICS_SLIDER_PRESETS = { + // Slider values in the range 0.0 to 1.0. + // Note: Damping values give the desired linear and angular damping values but friction values are a somewhat out, + // especially for the balloon. + presetDefault: { gravity: 0.5, bounce: 0.5, damping: 0.5, density: 0.5 }, + presetLead: { gravity: 0.5, bounce: 0.0, damping: 0.5, density: 1.0 }, + presetWood: { gravity: 0.5, bounce: 0.4, damping: 0.5, density: 0.5 }, + presetIce: { gravity: 0.5, bounce: 0.99, damping: 0.151004, density: 0.349485 }, + presetRubber: { gravity: 0.5, bounce: 0.99, damping: 0.5, density: 0.5 }, + presetCotton: { gravity: 0.587303, bounce: 0.0, damping: 0.931878, density: 0.0 }, + presetTumbleweed: { gravity: 0.595893, bounce: 0.7, damping: 0.5, density: 0.0 }, + presetZeroG: { gravity: 0.596844, bounce: 0.5, damping: 0.5, density: 0.5 }, + presetBalloon: { gravity: 0.606313, bounce: 0.99, damping: 0.151004, density: 0.0 } + }, OPTONS_PANELS = { groupOptions: [ @@ -600,9 +614,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { "presetIce", "presetRubber", "presetCotton", - "presetTumbleWeed", + "presetTumbleweed", "presetZeroG", - "presetBallon" + "presetBalloon" ] }, { @@ -642,7 +656,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { command: { method: "pickPhysicsPreset" } }, { - id: "presetTumbleWeed", + id: "presetTumbleweed", type: "picklistItem", label: "TUMBLEWEED", command: { method: "pickPhysicsPreset" } @@ -654,7 +668,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { command: { method: "pickPhysicsPreset" } }, { - id: "presetBallon", + id: "presetBalloon", type: "picklistItem", label: "BALLOON", command: { method: "pickPhysicsPreset" } @@ -1106,6 +1120,35 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } + function setBarSliderValue(item, fraction) { + var overlayDimensions, + otherFraction; + + overlayDimensions = optionsItems[item].properties.dimensions; + if (overlayDimensions === undefined) { + overlayDimensions = UI_ELEMENTS.barSlider.properties.dimensions; + } + + otherFraction = 1.0 - fraction; + + Overlays.editOverlay(optionsSliderData[item].value, { + localPosition: { x: 0, y: (0.5 - fraction / 2) * overlayDimensions.y, z: 0 }, + dimensions: { + x: overlayDimensions.x, + y: Math.max(fraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION), + z: overlayDimensions.z + } + }); + Overlays.editOverlay(optionsSliderData[item].remainder, { + localPosition: { x: 0, y: (-0.5 + otherFraction / 2) * overlayDimensions.y, z: 0 }, + dimensions: { + x: overlayDimensions.x, + y: Math.max(otherFraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION), + z: overlayDimensions.z + } + }); + } + function evaluateParameter(parameter) { var parameters, overlayID, @@ -1125,6 +1168,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { items, parentID, label, + values, i, length; @@ -1241,7 +1285,20 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }); Settings.setValue(optionsSettings.presets.key, label); - // TODO: Set physics parameters - update sliders, settings, and to-apply-values. + // Update sliders. + values = PHYSICS_SLIDER_PRESETS[parameter]; + setBarSliderValue(optionsOverlaysIDs.indexOf("gravitySlider"), values.gravity); + Settings.setValue(optionsSettings.gravitySlider.key, values.gravity); + uiCommandCallback("setGravity", values.gravity); + setBarSliderValue(optionsOverlaysIDs.indexOf("bounceSlider"), values.bounce); + Settings.setValue(optionsSettings.bounceSlider.key, values.bounce); + uiCommandCallback("setBounce", values.bounce); + setBarSliderValue(optionsOverlaysIDs.indexOf("dampingSlider"), values.damping); + Settings.setValue(optionsSettings.dampingSlider.key, values.damping); + uiCommandCallback("setDamping", values.damping); + setBarSliderValue(optionsOverlaysIDs.indexOf("densitySlider"), values.density); + Settings.setValue(optionsSettings.densitySlider.key, values.density); + uiCommandCallback("setDensity", values.density); break; @@ -1306,8 +1363,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { sliderProperties, overlayDimensions, basePoint, - fraction, - otherFraction; + fraction; // Intersection details. if (intersection.overlayID) { @@ -1474,23 +1530,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { fraction = Vec3.dot(Vec3.subtract(basePoint, intersection.intersection), Vec3.multiplyQbyV(sliderProperties.orientation, Vec3.UNIT_Y)) / overlayDimensions.y; fraction = adjustSliderFraction(fraction); - otherFraction = 1.0 - fraction; - Overlays.editOverlay(optionsSliderData[intersectedItem].value, { - localPosition: { x: 0, y: (0.5 - fraction / 2) * overlayDimensions.y, z: 0 }, - dimensions: { - x: overlayDimensions.x, - y: Math.max(fraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION), - z: overlayDimensions.z - } - }); - Overlays.editOverlay(optionsSliderData[intersectedItem].remainder, { - localPosition: { x: 0, y: (-0.5 + otherFraction / 2) * overlayDimensions.y, z: 0 }, - dimensions: { - x: overlayDimensions.x, - y: Math.max(otherFraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION), - z: overlayDimensions.z - } - }); + setBarSliderValue(intersectedItem, fraction); if (intersectionItems[intersectedItem].command) { doCommand(intersectionItems[intersectedItem].command.method, fraction); } From 90c2036be77e602712e2de1eccb679bffeb6bc64 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 17 Aug 2017 00:08:09 +1200 Subject: [PATCH 208/722] Fix physics options labels being displayed when options pane opens --- scripts/vr-edit/modules/toolMenu.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 130aa68e3a..744b11ae05 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -1019,6 +1019,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.clone(UI_ELEMENTS.label.properties); properties.text = optionsItems[i].label; properties.parentID = optionsOverlays[optionsOverlays.length - 1]; + properties.visible = optionsItems[i].type !== "picklistItem"; id = Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); optionsOverlaysLabels[i] = id; } From a87a93d991c2667dd6613a525c97b7c094d84de9 Mon Sep 17 00:00:00 2001 From: vladest Date: Wed, 16 Aug 2017 15:35:51 +0200 Subject: [PATCH 209/722] Implemented input dialog async functions --- libraries/ui/src/OffscreenUi.cpp | 76 +++++++++++++++++++++++++++++++- libraries/ui/src/OffscreenUi.h | 21 +++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 470603d0d0..3a5edb6844 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -328,16 +328,29 @@ class InputDialogListener : public ModalDialogListener { Q_OBJECT friend class OffscreenUi; - InputDialogListener(QQuickItem* queryBox) : ModalDialogListener(queryBox) { + InputDialogListener(QQuickItem* queryBox, bool custom = false) : ModalDialogListener(queryBox), _custom(custom) { if (_finished) { return; } connect(_dialog, SIGNAL(selected(QVariant)), this, SLOT(onSelected(const QVariant&))); } +private: + bool _custom { false }; private slots: void onSelected(const QVariant& result) { - _result = result; + if (_custom) { + if (result.isValid()) { + // We get a JSON encoded result, so we unpack it into a QVariant wrapping a QVariantMap + _result = QVariant(QJsonDocument::fromJson(result.toString().toUtf8()).object().toVariantMap()); + } + } else { + _result = result; + } + + auto offscreenUi = DependencyManager::get(); + emit offscreenUi->inputDialogResponse(_result); + offscreenUi->removeModalDialog(qobject_cast(this)); _finished = true; disconnect(_dialog); } @@ -391,6 +404,31 @@ QVariant OffscreenUi::getCustomInfo(const Icon icon, const QString& title, const return result; } +void OffscreenUi::getTextAsync(const Icon icon, const QString& title, const QString& label, const QString& text) { + DependencyManager::get()->inputDialogAsync(icon, title, label, text); +} + +void OffscreenUi::getItemAsync(const Icon icon, const QString& title, const QString& label, const QStringList& items, + int current, bool editable) { + + auto offscreenUi = DependencyManager::get(); + auto inputDialog = offscreenUi->createInputDialog(icon, title, label, current); + if (!inputDialog) { + return; + } + inputDialog->setProperty("items", items); + inputDialog->setProperty("editable", editable); + + InputDialogListener* inputDialogListener = new InputDialogListener(inputDialog); + offscreenUi->getModalDialogListeners().push_back(qobject_cast(inputDialogListener)); + + return; +} + +void OffscreenUi::getCustomInfoAsync(const Icon icon, const QString& title, const QVariantMap& config) { + DependencyManager::get()->customInputDialog(icon, title, config); +} + QVariant OffscreenUi::inputDialog(const Icon icon, const QString& title, const QString& label, const QVariant& current) { if (QThread::currentThread() != thread()) { QVariant result; @@ -406,6 +444,21 @@ QVariant OffscreenUi::inputDialog(const Icon icon, const QString& title, const Q return waitForInputDialogResult(createInputDialog(icon, title, label, current)); } +void OffscreenUi::inputDialogAsync(const Icon icon, const QString& title, const QString& label, const QVariant& current) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "inputDialogAsync", + Q_ARG(Icon, icon), + Q_ARG(QString, title), + Q_ARG(QString, label), + Q_ARG(QVariant, current)); + return; + } + + InputDialogListener* inputDialogListener = new InputDialogListener(createInputDialog(icon, title, label, current)); + QObject* inputDialog = qobject_cast(inputDialogListener); + _modalDialogListeners.push_back(inputDialog); +} + QVariant OffscreenUi::customInputDialog(const Icon icon, const QString& title, const QVariantMap& config) { if (QThread::currentThread() != thread()) { QVariant result; @@ -426,6 +479,21 @@ QVariant OffscreenUi::customInputDialog(const Icon icon, const QString& title, c return result; } +void OffscreenUi::customInputDialogAsync(const Icon icon, const QString& title, const QVariantMap& config) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "customInputDialogAsync", + Q_ARG(Icon, icon), + Q_ARG(QString, title), + Q_ARG(QVariantMap, config)); + return; + } + + InputDialogListener* inputDialogListener = new InputDialogListener(createCustomInputDialog(icon, title, config), true); + QObject* inputDialog = qobject_cast(inputDialogListener); + _modalDialogListeners.push_back(inputDialog); + return; +} + void OffscreenUi::togglePinned() { bool invokeResult = QMetaObject::invokeMethod(_desktop, "togglePinned"); if (!invokeResult) { @@ -950,6 +1018,10 @@ void OffscreenUi::assetDialogAsync(const QVariantMap& properties) { return; } +QList &OffscreenUi::getModalDialogListeners() { + return _modalDialogListeners; +} + QString OffscreenUi::assetOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { // ATP equivalent of fileOpenDialog(). if (QThread::currentThread() != thread()) { diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index a3529da89c..716e598291 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -173,7 +173,9 @@ public: static void getOpenAssetNameAsync(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QVariant inputDialog(const Icon icon, const QString& title, const QString& label = QString(), const QVariant& current = QVariant()); + Q_INVOKABLE void inputDialogAsync(const Icon icon, const QString& title, const QString& label = QString(), const QVariant& current = QVariant()); Q_INVOKABLE QVariant customInputDialog(const Icon icon, const QString& title, const QVariantMap& config); + Q_INVOKABLE void customInputDialogAsync(const Icon icon, const QString& title, const QVariantMap& config); QQuickItem* createInputDialog(const Icon icon, const QString& title, const QString& label, const QVariant& current); QQuickItem* createCustomInputDialog(const Icon icon, const QString& title, const QVariantMap& config); QVariant waitForInputDialogResult(QQuickItem* inputDialog); @@ -191,16 +193,35 @@ public: return getItem(OffscreenUi::ICON_NONE, title, label, items, current, editable, ok); } + // Compatibility with QInputDialog::getText + static void getTextAsync(void* ignored, const QString & title, const QString & label, + QLineEdit::EchoMode mode = QLineEdit::Normal, const QString & text = QString(), bool * ok = 0, + Qt::WindowFlags flags = 0, Qt::InputMethodHints inputMethodHints = Qt::ImhNone) { + return getTextAsync(OffscreenUi::ICON_NONE, title, label, text); + } + // Compatibility with QInputDialog::getItem + static void getItemAsync(void *ignored, const QString & title, const QString & label, const QStringList & items, + int current = 0, bool editable = true, bool * ok = 0, Qt::WindowFlags flags = 0, + Qt::InputMethodHints inputMethodHints = Qt::ImhNone) { + return getItemAsync(OffscreenUi::ICON_NONE, title, label, items, current, editable); + } + static QString getText(const Icon icon, const QString & title, const QString & label, const QString & text = QString(), bool * ok = 0); static QString getItem(const Icon icon, const QString & title, const QString & label, const QStringList & items, int current = 0, bool editable = true, bool * ok = 0); static QVariant getCustomInfo(const Icon icon, const QString& title, const QVariantMap& config, bool* ok = 0); + static void getTextAsync(const Icon icon, const QString & title, const QString & label, const QString & text = QString()); + static void getItemAsync(const Icon icon, const QString & title, const QString & label, const QStringList & items, int current = 0, bool editable = true); + static void getCustomInfoAsync(const Icon icon, const QString& title, const QVariantMap& config); unsigned int getMenuUserDataId() const; + QList &getModalDialogListeners(); + signals: void showDesktop(); void response(QMessageBox::StandardButton response); void fileDialogResponse(QString response); void assetDialogResponse(QString response); + void inputDialogResponse(QVariant response); public slots: void removeModalDialog(QObject* modal); From 4e5c650621ad31aad88a410734f9e46769a9b30d Mon Sep 17 00:00:00 2001 From: vladest Date: Wed, 16 Aug 2017 21:41:18 +0200 Subject: [PATCH 210/722] Modified some usecases for async dialogs --- interface/src/Application.cpp | 18 +++++--- interface/src/AvatarBookmarks.cpp | 41 ++++++++++--------- interface/src/LocationBookmarks.cpp | 29 +++++++------ .../scripting/WindowScriptingInterface.cpp | 16 +++++--- .../src/scripting/WindowScriptingInterface.h | 3 +- scripts/system/edit.js | 20 +++++---- 6 files changed, 74 insertions(+), 53 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 936a0bac6b..effbb35ece 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6849,12 +6849,18 @@ void Application::setPreviousScriptLocation(const QString& location) { } void Application::loadScriptURLDialog() const { - QString newScript = OffscreenUi::getText(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); - if (QUrl(newScript).scheme() == "atp") { - OffscreenUi::asyncWarning("Error Loading Script", "Cannot load client script over ATP"); - } else if (!newScript.isEmpty()) { - DependencyManager::get()->loadScript(newScript.trimmed()); - } + auto offscreenUi = DependencyManager::get(); + connect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, this, [=] (QVariant response) { + disconnect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, this, nullptr); + auto offscreenUi = DependencyManager::get(); + const QString& newScript = response.toString(); + if (QUrl(newScript).scheme() == "atp") { + OffscreenUi::asyncWarning("Error Loading Script", "Cannot load client script over ATP"); + } else if (!newScript.isEmpty()) { + DependencyManager::get()->loadScript(newScript.trimmed()); + } + }); + OffscreenUi::getTextAsync(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); } void Application::loadLODToolsDialog() { diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index 73192b0bef..7c42effbc2 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -106,30 +106,31 @@ void AvatarBookmarks::changeToBookmarkedAvatar() { } void AvatarBookmarks::addBookmark() { - bool ok = false; - auto bookmarkName = OffscreenUi::getText(OffscreenUi::ICON_PLACEMARK, "Bookmark Avatar", "Name", QString(), &ok); - if (!ok) { - return; - } + auto offscreenUi = DependencyManager::get(); + connect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, this, [=] (QVariant response) { + disconnect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, this, nullptr); + auto offscreenUi = DependencyManager::get(); + auto bookmarkName = response.toString(); + bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " "); + if (bookmarkName.length() == 0) { + return; + } - bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " "); - if (bookmarkName.length() == 0) { - return; - } + auto myAvatar = DependencyManager::get()->getMyAvatar(); - auto myAvatar = DependencyManager::get()->getMyAvatar(); + const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString(); + const QVariant& avatarScale = myAvatar->getAvatarScale(); - const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString(); - const QVariant& avatarScale = myAvatar->getAvatarScale(); + // If Avatar attachments ever change, this is where to update them, when saving remember to also append to AVATAR_BOOKMARK_VERSION + QVariantMap *bookmark = new QVariantMap; + bookmark->insert(ENTRY_VERSION, AVATAR_BOOKMARK_VERSION); + bookmark->insert(ENTRY_AVATAR_URL, avatarUrl); + bookmark->insert(ENTRY_AVATAR_SCALE, avatarScale); + bookmark->insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant()); - // If Avatar attachments ever change, this is where to update them, when saving remember to also append to AVATAR_BOOKMARK_VERSION - QVariantMap *bookmark = new QVariantMap; - bookmark->insert(ENTRY_VERSION, AVATAR_BOOKMARK_VERSION); - bookmark->insert(ENTRY_AVATAR_URL, avatarUrl); - bookmark->insert(ENTRY_AVATAR_SCALE, avatarScale); - bookmark->insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant()); - - Bookmarks::addBookmarkToFile(bookmarkName, *bookmark); + Bookmarks::addBookmarkToFile(bookmarkName, *bookmark); + }); + OffscreenUi::getTextAsync(OffscreenUi::ICON_PLACEMARK, "Bookmark Avatar", "Name", QString()); } void AvatarBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) { diff --git a/interface/src/LocationBookmarks.cpp b/interface/src/LocationBookmarks.cpp index eee6cdf3c8..ee8e546b16 100644 --- a/interface/src/LocationBookmarks.cpp +++ b/interface/src/LocationBookmarks.cpp @@ -70,20 +70,23 @@ void LocationBookmarks::teleportToBookmark() { } void LocationBookmarks::addBookmark() { - bool ok = false; - auto bookmarkName = OffscreenUi::getText(OffscreenUi::ICON_PLACEMARK, "Bookmark Location", "Name", QString(), &ok); - if (!ok) { - return; - } + auto offscreenUi = DependencyManager::get(); + connect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, this, [=] (QVariant response) { + disconnect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, this, nullptr); + auto offscreenUi = DependencyManager::get(); + auto bookmarkName = response.toString(); - bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " "); - if (bookmarkName.length() == 0) { - return; - } + bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " "); + if (bookmarkName.length() == 0) { + return; + } - auto addressManager = DependencyManager::get(); - QString bookmarkAddress = addressManager->currentAddress().toString(); - Bookmarks::addBookmarkToFile(bookmarkName, bookmarkAddress); + auto addressManager = DependencyManager::get(); + QString bookmarkAddress = addressManager->currentAddress().toString(); + Bookmarks::addBookmarkToFile(bookmarkName, bookmarkAddress); + }); + + OffscreenUi::getTextAsync(OffscreenUi::ICON_PLACEMARK, "Bookmark Location", "Name", QString()); } void LocationBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& address) { @@ -97,4 +100,4 @@ void LocationBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, co menubar->addActionToQMenuAndActionHash(_bookmarksMenu, teleportAction, name, 0, QAction::NoRole); Bookmarks::sortActions(menubar, _bookmarksMenu); } -} \ No newline at end of file +} diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 512d79adb2..d5c645d176 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -116,11 +116,17 @@ QScriptValue WindowScriptingInterface::confirm(const QString& message) { /// Display a prompt with a text box /// \param const QString& message message to display /// \param const QString& defaultText default text in the text box -/// \return QScriptValue string text value in text box if the dialog was accepted, `null` otherwise. -QScriptValue WindowScriptingInterface::prompt(const QString& message, const QString& defaultText) { - bool ok = false; - QString result = OffscreenUi::getText(nullptr, "", message, QLineEdit::Normal, defaultText, &ok); - return ok ? QScriptValue(result) : QScriptValue::NullValue; +void WindowScriptingInterface::prompt(const QString& message, const QString& defaultText) { + auto offscreenUi = DependencyManager::get(); + connect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, + this, [=] (QVariant result) { + auto offscreenUi = DependencyManager::get(); + disconnect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, + this, nullptr); + emit promptTextChanged(result.toString()); + }); + + OffscreenUi::getTextAsync(nullptr, "", message, QLineEdit::Normal, defaultText); } CustomPromptResult WindowScriptingInterface::customPrompt(const QVariant& config) { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 8ae315d05c..401f47bcdd 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -51,7 +51,7 @@ public slots: void raiseMainWindow(); void alert(const QString& message = ""); QScriptValue confirm(const QString& message = ""); - QScriptValue prompt(const QString& message = "", const QString& defaultText = ""); + void prompt(const QString& message = "", const QString& defaultText = ""); CustomPromptResult customPrompt(const QVariant& config); void browseDir(const QString& title = "", const QString& directory = ""); void browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); @@ -91,6 +91,7 @@ signals: void assetsDirChanged(QString assetsDir); void saveFileChanged(QString filename); void openFileChanged(QString filename); + void promptTextChanged(QString text); // triggered when window size or position changes void geometryChanged(QRect geometry); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 8d57c37bf6..1828146182 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1473,6 +1473,16 @@ function onFileOpenChanged(filename) { } } +function onPromptTextChanged(prompt) { + Window.promptTextChanged.disconnect(onPromptTextChanged); + if (prompt !== "") { + if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { + toolBar.toggle(); + } + importSVO(prompt); + } +} + function handeMenuEvent(menuItem) { if (menuItem === "Allow Selecting of Small Models") { allowSmallModels = Menu.isOptionChecked("Allow Selecting of Small Models"); @@ -1498,14 +1508,8 @@ function handeMenuEvent(menuItem) { Window.openFileChanged.connect(onFileOpenChanged); Window.browse("Select Model to Import", "", "*.json"); } else { - var importURL = Window.prompt("URL of SVO to import", ""); - if (importURL) { - if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { - toolBar.toggle(); - } - importSVO(importURL); - } - + Window.promptTextChanged.connect(onFileOpenChanged); + Window.prompt("URL of SVO to import", ""); } } else if (menuItem === "Entity List...") { entityListTool.toggleVisible(); From 15f8bc0141f993ffec8f651bc74f9956938c1eb8 Mon Sep 17 00:00:00 2001 From: vladest Date: Wed, 16 Aug 2017 22:17:34 +0200 Subject: [PATCH 211/722] Keep old function names for compatibility --- .../scripting/WindowScriptingInterface.cpp | 109 +++++++++++++++++- .../src/scripting/WindowScriptingInterface.h | 15 ++- scripts/system/edit.js | 8 +- scripts/system/libraries/entityList.js | 2 +- scripts/system/snapshot.js | 2 +- .../marketplace/record/record.js | 2 +- 6 files changed, 121 insertions(+), 17 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index d5c645d176..698e6ebb29 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -116,7 +116,17 @@ QScriptValue WindowScriptingInterface::confirm(const QString& message) { /// Display a prompt with a text box /// \param const QString& message message to display /// \param const QString& defaultText default text in the text box -void WindowScriptingInterface::prompt(const QString& message, const QString& defaultText) { +/// \return QScriptValue string text value in text box if the dialog was accepted, `null` otherwise. +QScriptValue WindowScriptingInterface::prompt(const QString& message, const QString& defaultText) { + bool ok = false; + QString result = OffscreenUi::getText(nullptr, "", message, QLineEdit::Normal, defaultText, &ok); + return ok ? QScriptValue(result) : QScriptValue::NullValue; +} + +/// Display a prompt with a text box +/// \param const QString& message message to display +/// \param const QString& defaultText default text in the text box +void WindowScriptingInterface::promptAsync(const QString& message, const QString& defaultText) { auto offscreenUi = DependencyManager::get(); connect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, this, [=] (QVariant result) { @@ -180,7 +190,29 @@ void WindowScriptingInterface::ensureReticleVisible() const { /// \param const QString& title title of the window /// \param const QString& directory directory to start the file browser at /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` -void WindowScriptingInterface::browseDir(const QString& title, const QString& directory) { +/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` +QScriptValue WindowScriptingInterface::browseDir(const QString& title, const QString& directory) { + ensureReticleVisible(); + QString path = directory; + if (path.isEmpty()) { + path = getPreviousBrowseLocation(); + } +#ifndef Q_OS_WIN + path = fixupPathForMac(directory); +#endif + QString result = OffscreenUi::getExistingDirectory(nullptr, title, path); + if (!result.isEmpty()) { + setPreviousBrowseLocation(QFileInfo(result).absolutePath()); + } + return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); +} + +/// Display a "browse to directory" dialog. If `directory` is an invalid file or directory the browser will start at the current +/// working directory. +/// \param const QString& title title of the window +/// \param const QString& directory directory to start the file browser at +/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` +void WindowScriptingInterface::browseDirAsync(const QString& title, const QString& directory) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { @@ -204,12 +236,32 @@ void WindowScriptingInterface::browseDir(const QString& title, const QString& di OffscreenUi::getExistingDirectoryAsync(nullptr, title, path); } +/// \param const QString& title title of the window +/// \param const QString& directory directory to start the file browser at +/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` +/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` +QScriptValue WindowScriptingInterface::browse(const QString& title, const QString& directory, const QString& nameFilter) { + ensureReticleVisible(); + QString path = directory; + if (path.isEmpty()) { + path = getPreviousBrowseLocation(); + } +#ifndef Q_OS_WIN + path = fixupPathForMac(directory); +#endif + QString result = OffscreenUi::getOpenFileName(nullptr, title, path, nameFilter); + if (!result.isEmpty()) { + setPreviousBrowseLocation(QFileInfo(result).absolutePath()); + } + return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); +} + /// Display an open file dialog. If `directory` is an invalid file or directory the browser will start at the current /// working directory. /// \param const QString& title title of the window /// \param const QString& directory directory to start the file browser at /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` -void WindowScriptingInterface::browse(const QString& title, const QString& directory, const QString& nameFilter) { +void WindowScriptingInterface::browseAsync(const QString& title, const QString& directory, const QString& nameFilter) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { @@ -238,7 +290,29 @@ void WindowScriptingInterface::browse(const QString& title, const QString& direc /// \param const QString& title title of the window /// \param const QString& directory directory to start the file browser at /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` -void WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) { +/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` +QScriptValue WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) { + ensureReticleVisible(); + QString path = directory; + if (path.isEmpty()) { + path = getPreviousBrowseLocation(); + } +#ifndef Q_OS_WIN + path = fixupPathForMac(directory); +#endif + QString result = OffscreenUi::getSaveFileName(nullptr, title, path, nameFilter); + if (!result.isEmpty()) { + setPreviousBrowseLocation(QFileInfo(result).absolutePath()); + } + return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); +} + +/// Display a save file dialog. If `directory` is an invalid file or directory the browser will start at the current +/// working directory. +/// \param const QString& title title of the window +/// \param const QString& directory directory to start the file browser at +/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` +void WindowScriptingInterface::saveAsync(const QString& title, const QString& directory, const QString& nameFilter) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { @@ -262,12 +336,37 @@ void WindowScriptingInterface::save(const QString& title, const QString& directo OffscreenUi::getSaveFileNameAsync(nullptr, title, path, nameFilter); } +/// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid +/// directory the browser will start at the root directory. +/// \param const QString& title title of the window +/// \param const QString& directory directory to start the asset browser at +/// \param const QString& nameFilter filter to filter asset names by - see `QFileDialog` +/// \return QScriptValue asset path as a string if one was selected, otherwise `QScriptValue::NullValue` +QScriptValue WindowScriptingInterface::browseAssets(const QString& title, const QString& directory, const QString& nameFilter) { + ensureReticleVisible(); + QString path = directory; + if (path.isEmpty()) { + path = getPreviousBrowseAssetLocation(); + } + if (path.left(1) != "/") { + path = "/" + path; + } + if (path.right(1) != "/") { + path = path + "/"; + } + QString result = OffscreenUi::getOpenAssetName(nullptr, title, path, nameFilter); + if (!result.isEmpty()) { + setPreviousBrowseAssetLocation(QFileInfo(result).absolutePath()); + } + return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); +} + /// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid /// directory the browser will start at the root directory. /// \param const QString& title title of the window /// \param const QString& directory directory to start the asset browser at /// \param const QString& nameFilter filter to filter asset names by - see `QFileDialog` -void WindowScriptingInterface::browseAssets(const QString& title, const QString& directory, const QString& nameFilter) { +void WindowScriptingInterface::browseAssetsAsync(const QString& title, const QString& directory, const QString& nameFilter) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 401f47bcdd..3304aed4ee 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -51,12 +51,17 @@ public slots: void raiseMainWindow(); void alert(const QString& message = ""); QScriptValue confirm(const QString& message = ""); - void prompt(const QString& message = "", const QString& defaultText = ""); + QScriptValue prompt(const QString& message, const QString& defaultText); + void promptAsync(const QString& message = "", const QString& defaultText = ""); CustomPromptResult customPrompt(const QVariant& config); - void browseDir(const QString& title = "", const QString& directory = ""); - void browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); - void save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); - void browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); + QScriptValue browseDir(const QString& title = "", const QString& directory = ""); + void browseDirAsync(const QString& title = "", const QString& directory = ""); + QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); + void browseAsync(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); + QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); + void saveAsync(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); + QScriptValue browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); + void browseAssetsAsync(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 1828146182..75510a31b3 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -432,7 +432,7 @@ var toolBar = (function () { addButton("importEntitiesButton", "assets-01.svg", function() { Window.openFileChanged.connect(onFileOpenChanged); - Window.browse("Select Model to Import", "", "*.json"); + Window.browseAsync("Select Model to Import", "", "*.json"); }); addButton("openAssetBrowserButton", "assets-01.svg", function() { @@ -1501,15 +1501,15 @@ function handeMenuEvent(menuItem) { Window.notifyEditError("No entities have been selected."); } else { Window.saveFileChanged.connect(onFileSaveChanged); - Window.save("Select Where to Save", "", "*.json"); + Window.saveAsync("Select Where to Save", "", "*.json"); } } else if (menuItem === "Import Entities" || menuItem === "Import Entities from URL") { if (menuItem === "Import Entities") { Window.openFileChanged.connect(onFileOpenChanged); - Window.browse("Select Model to Import", "", "*.json"); + Window.browseAsync("Select Model to Import", "", "*.json"); } else { Window.promptTextChanged.connect(onFileOpenChanged); - Window.prompt("URL of SVO to import", ""); + Window.promptAsync("URL of SVO to import", ""); } } else if (menuItem === "Entity List...") { entityListTool.toggleVisible(); diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index d2d815f9e3..9d9689000e 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -150,7 +150,7 @@ EntityListTool = function(opts) { Window.notifyEditError("No entities have been selected."); } else { Window.saveFileChanged.connect(onFileSaveChanged); - Window.save("Select Where to Save", "", "*.json"); + Window.saveAsync("Select Where to Save", "", "*.json"); } } else if (data.type == "pal") { var sessionIds = {}; // Collect the sessionsIds of all selected entitities, w/o duplicates. diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 08614c2030..bf0ab69789 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -121,7 +121,7 @@ function onMessage(message) { break; case 'chooseSnapshotLocation': Window.browseDirChanged.connect(snapshotDirChanged); - Window.browseDir("Choose Snapshots Directory", "", ""); + Window.browseDirAsync("Choose Snapshots Directory", "", ""); break; case 'openSettings': if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false)) diff --git a/unpublishedScripts/marketplace/record/record.js b/unpublishedScripts/marketplace/record/record.js index 84f631f307..a3f9d6ca0d 100644 --- a/unpublishedScripts/marketplace/record/record.js +++ b/unpublishedScripts/marketplace/record/record.js @@ -530,7 +530,7 @@ case LOAD_RECORDING_ACTION: // User wants to select an ATP recording to play. Window.assetsDirChanged.connect(onAssetsDirChanged); - Window.browseAssets("Select Recording to Play", "recordings", "*.hfr"); + Window.browseAssetsAsync("Select Recording to Play", "recordings", "*.hfr"); break; case START_RECORDING_ACTION: // Start making a recording. From 35d06c0244da377521b5ccdf80e53fb3e5972a50 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 17 Aug 2017 10:12:28 +1200 Subject: [PATCH 212/722] Make space for color circle --- scripts/vr-edit/modules/toolMenu.js | 92 +++-------------------------- 1 file changed, 8 insertions(+), 84 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 744b11ae05..de57bcd231 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -341,12 +341,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: -0.03, z: -0.005 } + localPosition: { x: -0.035, y: 0.02, z: -0.005 } }, setting: { key: "VREdit.colorTool.swatch1Color", property: "color", - defaultValue: { red: 0, green: 255, blue: 255 } + defaultValue: { red: 0, green: 255, blue: 0 } }, command: { method: "setColorPerSwatch" @@ -360,12 +360,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.01, y: -0.03, z: -0.005 } + localPosition: { x: -0.01, y: 0.02, z: -0.005 } }, setting: { key: "VREdit.colorTool.swatch2Color", property: "color", - defaultValue: { red: 255, green: 0, blue: 255 } + defaultValue: { red: 0, green: 0, blue: 255 } }, command: { method: "setColorPerSwatch" @@ -379,12 +379,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: -0.005, z: -0.005 } + localPosition: { x: -0.035, y: 0.045, z: -0.005 } }, setting: { key: "VREdit.colorTool.swatch3Color", - property: "color", - defaultValue: { red: 255, green: 255, blue: 0 } + property: "color" + // Default to empty swatch. }, command: { method: "setColorPerSwatch" @@ -396,88 +396,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { { id: "colorSwatch4", type: "swatch", - properties: { - dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.01, y: -0.005, z: -0.005 } - }, - setting: { - key: "VREdit.colorTool.swatch4Color", - property: "color", - defaultValue: { red: 255, green: 0, blue: 0 } - }, - command: { - method: "setColorPerSwatch" - }, - clear: { - method: "clearSwatch" - } - }, - { - id: "colorSwatch5", - type: "swatch", - properties: { - dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: 0.02, z: -0.005 } - }, - setting: { - key: "VREdit.colorTool.swatch5Color", - property: "color", - defaultValue: { red: 0, green: 255, blue: 0 } - }, - command: { - method: "setColorPerSwatch" - }, - clear: { - method: "clearSwatch" - } - }, - { - id: "colorSwatch6", - type: "swatch", - properties: { - dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.01, y: 0.02, z: -0.005 } - }, - setting: { - key: "VREdit.colorTool.swatch6Color", - property: "color", - defaultValue: { red: 0, green: 0, blue: 255 } - }, - command: { - method: "setColorPerSwatch" - }, - clear: { - method: "clearSwatch" - } - }, - { - id: "colorSwatch7", - type: "swatch", - properties: { - dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: 0.045, z: -0.005 } - }, - setting: { - key: "VREdit.colorTool.swatch7Color", - property: "color" - // Default to empty swatch. - }, - command: { - method: "setColorPerSwatch" - }, - clear: { - method: "clearSwatch" - } - }, - { - id: "colorSwatch8", - type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, localPosition: { x: -0.01, y: 0.045, z: -0.005 } }, setting: { - key: "VREdit.colorTool.swatch8Color", + key: "VREdit.colorTool.swatch4Color", property: "color" // Default to empty swatch. }, From 069102f68b00e83d835235bbda8982e2f799f119 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 17 Aug 2017 13:59:56 +1200 Subject: [PATCH 213/722] Initial color circle UI elements --- scripts/vr-edit/assets/color-circle-black.png | Bin 0 -> 5270 bytes scripts/vr-edit/assets/color-circle.png | Bin 0 -> 57223 bytes scripts/vr-edit/assets/slider-white-alpha.png | Bin 564 -> 537 bytes scripts/vr-edit/assets/slider-white.png | Bin 187 -> 196 bytes scripts/vr-edit/modules/toolMenu.js | 216 +++++++++++++++--- scripts/vr-edit/vr-edit.js | 7 - 6 files changed, 185 insertions(+), 38 deletions(-) create mode 100644 scripts/vr-edit/assets/color-circle-black.png create mode 100644 scripts/vr-edit/assets/color-circle.png diff --git a/scripts/vr-edit/assets/color-circle-black.png b/scripts/vr-edit/assets/color-circle-black.png new file mode 100644 index 0000000000000000000000000000000000000000..3494b63b708f8ba2945cc6ac445c0c5bc7a13444 GIT binary patch literal 5270 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 Q006+~tofNTJ=Yum2Y0~9`Tzg` literal 0 HcmV?d00001 diff --git a/scripts/vr-edit/assets/color-circle.png b/scripts/vr-edit/assets/color-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..b1ba4fe80fb76afa2e6877c8a850b27729ad90d6 GIT binary patch literal 57223 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

=Syo>!mhk!h%2ZyWNNLNjIjD1KPrsD|0yF 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 RpECde002ovPDHLkV1lt8)G`17 delta 502 zcmV(RA@u(meEE8VHk$@FA|YRBodKG zq?3qr(s3jb-9aR}gGh7-k?0O0(OD!CiF6W?NF*|3+nAl*+3A0I?_i&$v=2Ye(dU`@ zzhOe7(I`qF8kpM=LQxc%9YKYu9YKZPb_5lE*%4IuX-Ck&#EzhWA9e%{e77U$Qr_%; z2r7KFBdGAjj-bM4JAw+I>^#qpqF`uk4($jkJg_6EaNmy5UhL+7PfHeiFA9d%X4j6O!W}z;3b*YD?Zs~Xv}Ccj zqF`ukHth&1+^{34aNUm3UhL)%zu5J9eJu)x)@Id?pu!b9f(n=I2r68%BdBoEj_@BB zyH=|$M8UuxS+_hN1w)TKXGc)stQ|oEXY2?qxs>TB7$M8DDLaA+C+!HKTCGlD*b!7X zZbt}dnvU5KR5)r!(7+Kpf(8!T5j1efju0x9%Ag%Vg#&hkP%fAI?FcIDvm=BgNqX%F sD(tZ%XkfP;K?A$&2pZUF2Pl<(07C&KznWFn9RL6T07*qoM6N<$f@8zjdH?_b diff --git a/scripts/vr-edit/assets/slider-white.png b/scripts/vr-edit/assets/slider-white.png index 2097e1bcdad46d74ddf06744cb85f7cf4208f2d7..5d55f7c71d524d8985c5222f62d0cc41480337ad 100644 GIT binary patch delta 54 zcmV-60LlNm0mK227YcX?1^@s6^1sq7ks&n!S&>&vk&Xx%?(f5y13F0r>^cC7h5!Hn M07*qoM6N<$g74cB2mk;8 delta 50 zcmX@YxSMf;I9Cb>8v_GF+iHO`6BSJv115(1NbKiN=j(Q1IkNKC^ 1; @@ -1570,6 +1723,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightedSource = null; isHighlightingButton = false; isHighlightingSlider = false; + isHighlightingColorCircle = false; isHighlightingPicklist = false; isPicklistOpen = false; pressedItem = null; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index dd93ed420f..eca85994b9 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1457,13 +1457,6 @@ } break; - case "setSliderValue": - if (parameter !== undefined) { - // TODO - print("setSliderValue = " + parameter); - } - break; - default: log("ERROR: Unexpected command in onUICommand(): " + command + ", " + parameter); } From 32113e84e483dbba4099bef5bd45326118fd7846 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 17 Aug 2017 14:45:47 +1200 Subject: [PATCH 214/722] Move color circle crosshairs witih laser --- scripts/vr-edit/modules/toolMenu.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 6d444fe423..45e6ce7869 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -1118,6 +1118,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; auxiliaryProperties.visible = false; optionsColorData[i].value = Overlays.addOverlay(UI_ELEMENTS.sphere.overlay, auxiliaryProperties); + optionsColorData[i].maxRadius = childProperties.scale / 2; auxiliaryProperties = Object.clone(UI_ELEMENTS.circlePointer.properties); auxiliaryProperties.parentID = optionsColorData[i].value; @@ -1413,7 +1414,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { sliderProperties, overlayDimensions, basePoint, - fraction; + fraction, + delta, + radius; // Intersection details. if (intersection.overlayID) { @@ -1476,7 +1479,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: { x: HIGHLIGHT_PROPERTIES.properties.localPosition.x, y: HIGHLIGHT_PROPERTIES.properties.localPosition.z, - z: HIGHLIGHT_PROPERTIES.properties.localPosition.y + z: HIGHLIGHT_PROPERTIES.properties.localPosition.y }, localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }), color: HIGHLIGHT_PROPERTIES.properties.color, @@ -1632,7 +1635,20 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Color circle update. if (intersectionItems && intersectionItems[intersectedItem].type === "colorCircle" && controlHand.triggerClicked()) { - // TODO + sliderProperties = Overlays.getProperties(intersection.overlayID, ["position", "orientation"]); + delta = Vec3.multiplyQbyV(Quat.inverse(sliderProperties.orientation), + Vec3.subtract(intersection.intersection, sliderProperties.position)); + radius = Vec3.length(delta); + if (radius > optionsColorData[intersectedItem].maxRadius) { + delta = Vec3.multiply(optionsColorData[intersectedItem].maxRadius / radius, delta); + } + Overlays.editOverlay(optionsColorData[intersectedItem].value, { + localPosition: Vec3.sum(optionsColorData[intersectedItem].offset, + { x: delta.x, y: 0, z: delta.z }) + }); + if (intersectionItems[intersectedItem].callback) { + uiCommandCallback(intersectionItems[intersectedItem].callback.method, fraction); + } } // Special handling for Group options. From 5dd74d71fb9feb242b559b6ffd93763e8526582f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 18 Aug 2017 09:55:09 +1200 Subject: [PATCH 215/722] Activate the color picker --- scripts/vr-edit/assets/color-circle.png | Bin 57223 -> 76746 bytes scripts/vr-edit/assets/slider-v-alpha.png | Bin 0 -> 911 bytes scripts/vr-edit/assets/slider-white-alpha.png | Bin 537 -> 0 bytes scripts/vr-edit/modules/toolMenu.js | 198 +++++++++++++++--- 4 files changed, 165 insertions(+), 33 deletions(-) create mode 100644 scripts/vr-edit/assets/slider-v-alpha.png delete mode 100644 scripts/vr-edit/assets/slider-white-alpha.png diff --git a/scripts/vr-edit/assets/color-circle.png b/scripts/vr-edit/assets/color-circle.png index b1ba4fe80fb76afa2e6877c8a850b27729ad90d6..408139972ebd4fc7254c19a67dbc9ab20c1e3cc3 100644 GIT binary patch literal 76746 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

=Syo>!mhk!h%2ZyWNNLNjIjD1KPrsD|0yF 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 b==FX9(_r-}=Hs6;00000NkvXXu0mjfHR9i$ diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 45e6ce7869..8bd39f0e35 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -151,7 +151,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { dimensions: { x: 0.1, y: 0.1 }, localPosition: { x: 0, y: 0, z: 0 }, - localRotation: Quat.ZERO, + localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }), color: { red: 255, green: 255, blue: 255 }, alpha: 1.0, ignoreRayIntersection: true, @@ -379,12 +379,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "colorSlider", type: "imageSlider", properties: { - localPosition: { x: 0.035, y: -0.025, z: -0.005 }, - color: { red: 255, green: 0, blue: 0 } + localPosition: { x: 0.035, y: -0.025, z: -0.005 } }, useBaseColor: true, imageURL: "../assets/slider-white.png", - imageOverlayURL: "../assets/slider-white-alpha.png", + imageOverlayURL: "../assets/slider-v-alpha.png", command: { method: "setColorPerSlider" } @@ -474,7 +473,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { setting: { key: "VREdit.colorTool.currentColor", property: "color", - defaultValue: { red: 128, green: 128, blue: 128 } + defaultValue: { red: 128, green: 128, blue: 128 }, + command: "setPickColor" } }, { @@ -880,6 +880,12 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { groupButtonIndex, ungroupButtonIndex, + hsvControl = { + hsv: { h: 0, s: 0, v: 0 }, + circle: {}, + slider: {} + }, + isDisplaying = false, // References. @@ -988,6 +994,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsSettings[optionsItems[i].id].value = value; optionsItems[i].label = value; } + if (optionsItems[i].setting.command) { + doCommand(optionsItems[i].setting.command, value); + } if (optionsItems[i].setting.callback) { uiCommandCallback(optionsItems[i].setting.callback, value); } @@ -1043,8 +1052,10 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { childProperties.color = properties.color; } childProperties.localPosition = { x: 0, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; + hsvControl.slider.localPosition = childProperties.localPosition; childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; - Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); + hsvControl.slider.colorOverlay = Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); + hsvControl.slider.length = properties.dimensions.y; } // Overlay image. @@ -1066,10 +1077,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { { x: -properties.dimensions.x / 2, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; auxiliaryProperties = Object.clone(UI_ELEMENTS.sliderPointer.properties); auxiliaryProperties.localPosition = optionsSliderData[i].offset; + hsvControl.slider.localPosition = auxiliaryProperties.localPosition; auxiliaryProperties.drawInFront = true; // TODO: Accommodate work-around above; remove when bug fixed. auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; - optionsSliderData[i].value = Overlays.addOverlay(UI_ELEMENTS.sliderPointer.overlay, - auxiliaryProperties); + optionsSliderData[i].value = Overlays.addOverlay(UI_ELEMENTS.sliderPointer.overlay, auxiliaryProperties); + hsvControl.slider.pointerOverlay = optionsSliderData[i].value; auxiliaryProperties.localPosition = { x: 0, y: properties.dimensions.x, z: 0 }; auxiliaryProperties.localRotation = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }); auxiliaryProperties.parentID = optionsSliderData[i].value; @@ -1102,10 +1114,10 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { childProperties.scale = 0.95 * properties.dimensions.x; // TODO: Magic number. imageOffset += IMAGE_OFFSET; childProperties.localPosition = { x: 0, y: -properties.dimensions.y / 2 - imageOffset, z: 0 }; - childProperties.localRotation = Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }); + childProperties.localRotation = Quat.fromVec3Degrees({ x: 90, y: 90, z: 0 }); childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; childProperties.alpha = 0.0; - Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); + hsvControl.circle.overlay = Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); } // Value pointers. @@ -1118,7 +1130,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; auxiliaryProperties.visible = false; optionsColorData[i].value = Overlays.addOverlay(UI_ELEMENTS.sphere.overlay, auxiliaryProperties); - optionsColorData[i].maxRadius = childProperties.scale / 2; + hsvControl.circle.radius = childProperties.scale / 2; + hsvControl.circle.localPosition = auxiliaryProperties.localPosition; + hsvControl.circle.cursorOverlay = optionsColorData[i].value; auxiliaryProperties = Object.clone(UI_ELEMENTS.circlePointer.properties); auxiliaryProperties.parentID = optionsColorData[i].value; @@ -1196,8 +1210,103 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }); } - function setColorCircleValue() { - // TODO + function hsvToRGB(hsv) { + // https://en.wikipedia.org/wiki/HSL_and_HSV + var c, h, x, rgb, m; + + c = hsv.v * hsv.s; + h = hsv.h * 6.0; + x = c * (1 - Math.abs(h % 2 - 1)); + if (0 <= h && h <= 1) { + rgb = { red: c, green: x, blue: 0 }; + } else if (1 < h && h <= 2) { + rgb = { red: x, green: c, blue: 0 }; + } else if (2 < h && h <= 3) { + rgb = { red: 0, green: c, blue: x }; + } else if (3 < h && h <= 4) { + rgb = { red: 0, green: x, blue: c }; + } else if (4 < h && h <= 5) { + rgb = { red: x, green: 0, blue: c }; + } else { + rgb = { red: c, green: 0, blue: x }; + } + m = hsv.v - c; + rgb = { + red: Math.round((rgb.red + m) * 255), + green: Math.round((rgb.green + m) * 255), + blue: Math.round((rgb.blue + m) * 255) + }; + return rgb; + } + + function rgbToHSV(rgb) { + // https://en.wikipedia.org/wiki/HSL_and_HSV + var mMax, mMin, c, h, v, s; + + mMax = Math.max(rgb.red, rgb.green, rgb.blue); + mMin = Math.min(rgb.red, rgb.green, rgb.blue); + c = mMax - mMin; + + if (c === 0) { + h = 0; + } else if (mMax === rgb.red) { + h = ((rgb.green - rgb.blue) / c) % 6; + } else if (mMax === rgb.green) { + h = (rgb.blue - rgb.red) / c + 2; + } else { + h = (rgb.red - rgb.green) / c + 4; + } + h = h / 6; + v = mMax / 255; + s = v === 0 ? 0 : c / mMax; + return { h: h, s: s, v: v }; + } + + function updateColorCircle() { + var theta, r, x, y; + + // V overlay alpha per v. + Overlays.editOverlay(hsvControl.circle.overlay, { alpha: 1.0 - hsvControl.hsv.v }); + + // Cursor position per h & s. + theta = 2 * Math.PI * hsvControl.hsv.h; + r = hsvControl.hsv.s * hsvControl.circle.radius; + x = r * Math.cos(theta); + y = r * Math.sin(theta); + Overlays.editOverlay(hsvControl.circle.cursorOverlay, { + localPosition: { x: y, y: hsvControl.circle.localPosition.y, z: -x } + }); + } + + function updateColorSlider() { + // Base color per h & s. + Overlays.editOverlay(hsvControl.slider.colorOverlay, { + color: hsvToRGB({ h: hsvControl.hsv.h, s: hsvControl.hsv.s, v: 1.0 }) + }); + + // Slider position per v. + Overlays.editOverlay(hsvControl.slider.pointerOverlay, { + localPosition: { + x: hsvControl.slider.localPosition.x, + y: (0.5 - hsvControl.hsv.v) * hsvControl.slider.length, + z: hsvControl.slider.localPosition.z + } + }); + } + + function setColorPicker(rgb) { + hsvControl.hsv = rgbToHSV(rgb); + updateColorCircle(); + updateColorSlider(); + } + + function setCurrentColor(rgb) { + Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], { + color: rgb + }); + if (optionsSettings.currentColor) { + Settings.setValue(optionsSettings.currentColor.key, rgb); + } } function evaluateParameter(parameter) { @@ -1225,17 +1334,32 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { switch (command) { + case "setPickColor": + setColorPicker(parameter); + break; + + case "setColorPerCircle": + hsvControl.hsv.h = parameter.h; + hsvControl.hsv.s = parameter.s; + updateColorSlider(); + setCurrentColor(hsvToRGB(hsvControl.hsv)); + uiCommandCallback("setColor", value); + break; + + case "setColorPerSlider": + hsvControl.hsv.v = parameter; + updateColorCircle(); + setCurrentColor(hsvToRGB(hsvControl.hsv)); + uiCommandCallback("setColor", value); + break; + case "setColorPerSwatch": index = optionsOverlaysIDs.indexOf(parameter); hasColor = Overlays.getProperty(optionsOverlays[index], "solid"); if (hasColor) { value = Overlays.getProperty(optionsOverlays[index], "color"); - Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], { - color: value - }); - if (optionsSettings.currentColor) { - Settings.setValue(optionsSettings.currentColor.key, value); - } + setCurrentColor(value); + setColorPicker(value); uiCommandCallback("setColor", value); } else { // Swatch has no color; set swatch color to current fill color. @@ -1251,12 +1375,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { break; case "setColorFromPick": - Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], { - color: parameter - }); - if (optionsSettings.currentColor) { - Settings.setValue(optionsSettings.currentColor.key, parameter); - } + setCurrentColor(parameter); + setColorPicker(parameter); break; case "setGravityOn": @@ -1416,7 +1536,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { basePoint, fraction, delta, - radius; + radius, + x, + y, + s, + h; // Intersection details. if (intersection.overlayID) { @@ -1468,7 +1592,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { ["dimensions", "localPosition"]); if (isHighlightingColorCircle) { // Cylinder used has different coordinate system to other elements. - // TODO: Should be able to remove this special case when UI look is reword. + // TODO: Should be able to remove this special case when UI look is reworked. Overlays.editOverlay(highlightOverlay, { parentID: intersectionOverlays[intersectedItem], dimensions: { @@ -1628,8 +1752,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: Vec3.sum(optionsSliderData[intersectedItem].offset, { x: 0, y: (0.5 - fraction) * overlayDimensions.y, z: 0 }) }); - if (intersectionItems[intersectedItem].callback) { - uiCommandCallback(intersectionItems[intersectedItem].callback.method, fraction); + if (intersectionItems[intersectedItem].command) { + doCommand(intersectionItems[intersectedItem].command.method, fraction); } } @@ -1639,15 +1763,23 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { delta = Vec3.multiplyQbyV(Quat.inverse(sliderProperties.orientation), Vec3.subtract(intersection.intersection, sliderProperties.position)); radius = Vec3.length(delta); - if (radius > optionsColorData[intersectedItem].maxRadius) { - delta = Vec3.multiply(optionsColorData[intersectedItem].maxRadius / radius, delta); + if (radius > hsvControl.circle.radius) { + delta = Vec3.multiply(hsvControl.circle.radius / radius, delta); } Overlays.editOverlay(optionsColorData[intersectedItem].value, { localPosition: Vec3.sum(optionsColorData[intersectedItem].offset, { x: delta.x, y: 0, z: delta.z }) }); - if (intersectionItems[intersectedItem].callback) { - uiCommandCallback(intersectionItems[intersectedItem].callback.method, fraction); + if (intersectionItems[intersectedItem].command) { + // Cartesian planar coordinates. + x = -delta.z; + y = delta.x; + s = Math.sqrt(x * x + y * y) / hsvControl.circle.radius; + h = Math.atan2(y, x) / (2 * Math.PI); + if (h < 0) { + h = h + 1; + } + doCommand(intersectionItems[intersectedItem].command.method, { h: h, s: s }); } } From c3f96b672738002a1edc0ad8f77d45b71b55c4d1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 18 Aug 2017 13:27:38 +1200 Subject: [PATCH 216/722] Fix damping property name --- scripts/vr-edit/vr-edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index eca85994b9..e278ff12a9 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1435,7 +1435,7 @@ case "setDamping": if (parameter !== undefined) { // Power range 0.0, 0.5, 1.0 maps to 0, 0.39, 1.0. - physicsToolPhysics.linearDamping = 0.69136364 * Math.pow(2.446416831, parameter) - 0.691364; + physicsToolPhysics.damping = 0.69136364 * Math.pow(2.446416831, parameter) - 0.691364; // Power range 0.0, 0.5, 1.0 maps to 0, 0.3935, 1.0. physicsToolPhysics.angularDamping = 0.72695892 * Math.pow(2.375594, parameter) - 0.726959; // Linear range from 0.0, 0.5, 1.0 maps to 0.0, 0.5, 1.0; From 9434ef44c3c40f99bea77b389d213143c129db00 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 20 Aug 2017 14:15:34 +1200 Subject: [PATCH 217/722] Delay physics kick in order to avoid some erratic behavior --- scripts/vr-edit/modules/selection.js | 51 +++++++++++++++++----------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 75e7b7e105..b28a95817d 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -25,9 +25,7 @@ Selection = function (side) { scaleRootOffset, scaleRootOrientation, ENTITY_TYPE = "entity", - ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "ParticleEffect"], - DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD - DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.02, z: 0 }; + ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "ParticleEffect"]; if (!this instanceof Selection) { @@ -188,6 +186,30 @@ Selection = function (side) { }; } + function doKick(entityID) { + var properties, + DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD + DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.1, z: 0 }; + + if (entityID === rootEntityID) { + // Don't kick if have started editing entity again. + return; + } + + properties = Entities.getEntityProperties(entityID, ["velocity", "gravity", "parentID"]); + if (Vec3.length(properties.gravity) > 0 && Vec3.length(properties.velocity) < DYNAMIC_VELOCITY_THRESHOLD) { + Entities.editEntity(entityID, { velocity: DYNAMIC_VELOCITY_KICK }); + } + } + + function kickPhysics(entityID) { + // Gives entities a small kick to start off physics, if necessary. + var KICK_DELAY = 500; // ms + + // Give physics a chance to catch up. Avoids some erratic behavior. + Script.setTimeout(function () { doKick(entityID); }, KICK_DELAY); + } + function startEditing() { var count, i; @@ -202,28 +224,20 @@ Selection = function (side) { } function finishEditing() { - var firstDynamicEntityID = null, - properties, - count, + var count, i; // Restore entity set's physics. for (i = 0, count = selection.length; i < count; i += 1) { - if (firstDynamicEntityID === null && selection[i].dynamic) { - firstDynamicEntityID = selection[i].id; - } Entities.editEntity(selection[i].id, { dynamic: selection[i].dynamic, collisionless: selection[i].collisionless }); } - // If dynamic with gravity, and velocity is zero, give the entity set a little kick to set off physics. - if (firstDynamicEntityID) { - properties = Entities.getEntityProperties(firstDynamicEntityID, ["velocity", "gravity"]); - if (Vec3.length(properties.gravity) > 0 && Vec3.length(properties.velocity) < DYNAMIC_VELOCITY_THRESHOLD) { - Entities.editEntity(firstDynamicEntityID, { velocity: DYNAMIC_VELOCITY_KICK }); - } + // Kick off physics if necessary. + if (selection.length > 0 && selection[0].dynamic) { + kickPhysics(selection[0].id); } } @@ -422,12 +436,9 @@ Selection = function (side) { properties.userData = updatePhysicsUserData(selection[intersectedEntityIndex].userData, physicsProperties.userData); Entities.editEntity(rootEntityID, properties); + // Kick off physics if necessary. if (physicsProperties.dynamic) { - // Give dynamic entities with zero velocity a little kick to set off physics. - properties = Entities.getEntityProperties(rootEntityID, ["velocity"]); - if (Vec3.length(properties.velocity) < DYNAMIC_VELOCITY_THRESHOLD) { - Entities.editEntity(rootEntityID, { velocity: DYNAMIC_VELOCITY_KICK }); - } + kickPhysics(rootEntityID); } } From 5b2d5b01743bdeafafa8d359bdd8d384738ab2af Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 20 Aug 2017 20:48:45 +1200 Subject: [PATCH 218/722] Minor merge fix --- .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 8457e27bc3..aef5c73fa3 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -354,7 +354,7 @@ void HmdDisplayPlugin::updateFrameData() { auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)) * modelMat; _overlayRenderer.mvps[eye] = _eyeProjections[eye] * modelView; }); - } +} void HmdDisplayPlugin::OverlayRenderer::build() { vertices = std::make_shared(); From e34e8ff2c09bcea07f028b3ede4eda55dd417136 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 21 Aug 2017 13:24:07 +1200 Subject: [PATCH 219/722] Fix color circle and slider changes not being applied when coloring --- scripts/vr-edit/modules/toolMenu.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 8bd39f0e35..dc58ce1bc9 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -1342,14 +1342,16 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { hsvControl.hsv.h = parameter.h; hsvControl.hsv.s = parameter.s; updateColorSlider(); - setCurrentColor(hsvToRGB(hsvControl.hsv)); + value = hsvToRGB(hsvControl.hsv); + setCurrentColor(value); uiCommandCallback("setColor", value); break; case "setColorPerSlider": hsvControl.hsv.v = parameter; updateColorCircle(); - setCurrentColor(hsvToRGB(hsvControl.hsv)); + value = hsvToRGB(hsvControl.hsv); + setCurrentColor(value); uiCommandCallback("setColor", value); break; From ca3eadb82f72018b949bb27a339edd8ca9266f4a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 21 Aug 2017 16:04:34 +1200 Subject: [PATCH 220/722] Don't display menu buttons when tool options panel open, and vice versa --- scripts/vr-edit/modules/toolMenu.js | 66 ++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index dc58ce1bc9..a16d504370 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -912,13 +912,52 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { return [menuPanelOverlay].concat(menuOverlays).concat(optionsOverlays); } - function closeOptions() { + function openMenu() { + var parentID, + properties, + i, + length; + + parentID = menuPanelOverlay; // Menu panel parents to background panel. + for (i = 0, length = MENU_ITEMS.length; i < length; i += 1) { + properties = Object.clone(UI_ELEMENTS[MENU_ITEMS[i].type].properties); + properties = Object.merge(properties, MENU_ITEMS[i].properties); + properties.parentID = parentID; + menuOverlays.push(Overlays.addOverlay(UI_ELEMENTS[MENU_ITEMS[i].type].overlay, properties)); + if (MENU_ITEMS[i].label) { + properties = Object.clone(UI_ELEMENTS.label.properties); + properties.text = MENU_ITEMS[i].label; + properties.parentID = menuOverlays[menuOverlays.length - 1]; + Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); + } + parentID = menuOverlays[0]; // Menu buttons parent to menu panel. + } + } + + function closeMenu() { var i, length; Overlays.editOverlay(highlightOverlay, { parentID: menuOriginOverlay }); + + for (i = 0, length = menuOverlays.length; i < length; i += 1) { + Overlays.deleteOverlay(menuOverlays[i]); + } + + menuOverlays = []; + } + + function closeOptions() { + var i, + length; + + // Remove options items. + Overlays.editOverlay(highlightOverlay, { + parentID: menuOriginOverlay + }); + for (i = 0, length = optionsOverlays.length; i < length; i += 1) { Overlays.deleteOverlay(optionsOverlays[i]); } @@ -932,6 +971,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsItems = null; isPicklistOpen = false; + + // Display menu items. + openMenu(true); } function openOptions(toolOptions) { @@ -947,15 +989,15 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { i, length; - // Close current panel, if any. - closeOptions(); + // Remove menu items. + closeMenu(); // TODO: Remove once all tools have an options panel. if (OPTONS_PANELS[toolOptions] === undefined) { return; } - // Open specified panel. + // Open specified options panel. optionsItems = OPTONS_PANELS[toolOptions]; parentID = menuPanelOverlay; // Menu panel parents to background panel. for (i = 0, length = optionsItems.length; i < length; i += 1) { @@ -1815,7 +1857,6 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Creates and shows menu entities. var handJointIndex, properties, - parentID, id, i, length; @@ -1845,20 +1886,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { menuPanelOverlay = Overlays.addOverlay("cube", properties); // Menu items. - parentID = menuPanelOverlay; // Menu panel parents to background panel. - for (i = 0, length = MENU_ITEMS.length; i < length; i += 1) { - properties = Object.clone(UI_ELEMENTS[MENU_ITEMS[i].type].properties); - properties = Object.merge(properties, MENU_ITEMS[i].properties); - properties.parentID = parentID; - menuOverlays.push(Overlays.addOverlay(UI_ELEMENTS[MENU_ITEMS[i].type].overlay, properties)); - if (MENU_ITEMS[i].label) { - properties = Object.clone(UI_ELEMENTS.label.properties); - properties.text = MENU_ITEMS[i].label; - properties.parentID = menuOverlays[menuOverlays.length - 1]; - Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); - } - parentID = menuOverlays[0]; // Menu buttons parent to menu panel. - } + openMenu(); // Prepare highlight overlay. properties = Object.clone(HIGHLIGHT_PROPERTIES); From 0064c516108638d934580ac10df3af0f0db4dc6e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 21 Aug 2017 16:24:55 +1200 Subject: [PATCH 221/722] Add "Finish" buttons for scale, clone, and delete tools --- scripts/vr-edit/modules/toolMenu.js | 73 +++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index a16d504370..d720b7adeb 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -318,6 +318,50 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, OPTONS_PANELS = { + scaleOptions: [ + { + id: "scaleOptionsPanel", + type: "panel", + properties: { + localPosition: { x: 0.055, y: 0.0, z: -0.005 } + } + }, + { + id: "scaleFinishButton", + type: "button", + properties: { + dimensions: { x: 0.07, y: 0.03, z: 0.01 }, + localPosition: { x: 0, y: 0, z: -0.005 }, + color: { red: 200, green: 200, blue: 200 } + }, + label: "FINISH", + command: { + method: "closeOptions" + } + } + ], + cloneOptions: [ + { + id: "cloneOptionsPanel", + type: "panel", + properties: { + localPosition: { x: 0.055, y: 0.0, z: -0.005 } + } + }, + { + id: "cloneFinishButton", + type: "button", + properties: { + dimensions: { x: 0.07, y: 0.03, z: 0.01 }, + localPosition: { x: 0, y: 0, z: -0.005 }, + color: { red: 200, green: 200, blue: 200 } + }, + label: "FINISH", + command: { + method: "closeOptions" + } + } + ], groupOptions: [ { id: "groupOptionsPanel", @@ -750,6 +794,28 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: { x: 0.045, y: 0.057, z: -0.0075 } } } + ], + deleteOptions: [ + { + id: "deleteOptionsPanel", + type: "panel", + properties: { + localPosition: { x: 0.055, y: 0.0, z: -0.005 } + } + }, + { + id: "deleteFinishButton", + type: "button", + properties: { + dimensions: { x: 0.07, y: 0.03, z: 0.01 }, + localPosition: { x: 0, y: 0, z: -0.005 }, + color: { red: 200, green: 200, blue: 200 } + }, + label: "FINISH", + command: { + method: "closeOptions" + } + } ] }, @@ -769,6 +835,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: { red: 0, green: 240, blue: 240 } }, label: " SCALE", + toolOptions: "scaleOptions", callback: { method: "scaleTool" } @@ -781,6 +848,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: { red: 240, green: 240, blue: 0 } }, label: " CLONE", + toolOptions: "cloneOptions", callback: { method: "cloneTool" } @@ -833,6 +901,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: { red: 240, green: 60, blue: 60 } }, label: " DELETE", + toolOptions: "deleteOptions", callback: { method: "deleteTool" } @@ -1538,6 +1607,10 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { uiCommandCallback("setDensity", parameter); break; + case "closeOptions": + closeOptions(); + break; + default: App.log(side, "ERROR: ToolMenu: Unexpected command! " + command); } From fcf4831a3b61221ea9cf1e6f5e7f846f6a5fbcf2 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 21 Aug 2017 17:26:03 +1200 Subject: [PATCH 222/722] Center Tools menu and options UI --- scripts/vr-edit/modules/toolMenu.js | 36 ++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index d720b7adeb..d212aee2f4 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -323,7 +323,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "scaleOptionsPanel", type: "panel", properties: { - localPosition: { x: 0.055, y: 0.0, z: -0.005 } + localPosition: { x: 0.0, y: 0.0, z: -0.005 } } }, { @@ -345,7 +345,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "cloneOptionsPanel", type: "panel", properties: { - localPosition: { x: 0.055, y: 0.0, z: -0.005 } + localPosition: { x: 0.0, y: 0.0, z: -0.005 } } }, { @@ -367,7 +367,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "groupOptionsPanel", type: "panel", properties: { - localPosition: { x: 0.055, y: 0.0, z: -0.005 } + localPosition: { x: 0.0, y: 0.0, z: -0.005 } } }, { @@ -404,7 +404,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "colorOptionsPanel", type: "panel", properties: { - localPosition: { x: 0.055, y: 0.0, z: -0.005 } + localPosition: { x: 0.0, y: 0.0, z: -0.005 } } }, { @@ -540,7 +540,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "physicsOptionsPanel", type: "panel", properties: { - localPosition: { x: 0.055, y: 0.0, z: -0.005 } + localPosition: { x: 0.0, y: 0.0, z: -0.005 } } }, @@ -800,7 +800,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "deleteOptionsPanel", type: "panel", properties: { - localPosition: { x: 0.055, y: 0.0, z: -0.005 } + localPosition: { x: 0.0, y: 0.0, z: -0.005 } } }, { @@ -824,7 +824,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "toolsMenuPanel", type: "panel", properties: { - localPosition: { x: -0.055, y: 0.0, z: -0.005 } + localPosition: { x: -0.0, y: 0.0, z: -0.005 } } }, { @@ -942,6 +942,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isButtonPressed, isPicklistPressed, isPicklistItemPressed, + isTriggerClicked, + wasTriggerClicked, isGripClicked, isGroupButtonEnabled, @@ -1016,6 +1018,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } menuOverlays = []; + pressedItem = null; } function closeOptions() { @@ -1041,6 +1044,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isPicklistOpen = false; + pressedItem = null; + // Display menu items. openMenu(true); } @@ -1681,7 +1686,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Highlight clickable item. if (intersectedItem !== highlightedItem || intersectionOverlays !== highlightedSource) { - if (intersectedItem !== NONE && (intersectionItems[intersectedItem].command !== undefined + if (intersectedItem !== NONE && intersectionItems[intersectedItem] && + (intersectionItems[intersectedItem].command !== undefined || intersectionItems[intersectedItem].callback !== undefined)) { // Lower old slider or color circle. if (isHighlightingSlider || isHighlightingColorCircle) { @@ -1763,8 +1769,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } // Press/unpress button. + if (controlHand.triggerClicked() !== isTriggerClicked) { + isTriggerClicked = !isTriggerClicked; + } if ((pressedItem && intersectedItem !== pressedItem.index) || intersectionOverlays !== pressedSource - || controlHand.triggerClicked() !== isButtonPressed) { + || isTriggerClicked !== isButtonPressed) { if (pressedItem) { // Unpress previous button. Overlays.editOverlay(intersectionOverlays[pressedItem.index], { @@ -1772,9 +1781,10 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }); pressedItem = null; } - isButtonPressed = isHighlightingButton && controlHand.triggerClicked(); - if (isButtonPressed && (intersectionEnabled === null || intersectionEnabled[intersectedItem])) { + if (isHighlightingButton && (intersectionEnabled === null || intersectionEnabled[intersectedItem]) + && isTriggerClicked && !wasTriggerClicked) { // Press new button. + isButtonPressed = true; localPosition = intersectionItems[intersectedItem].properties.localPosition; Overlays.editOverlay(intersectionOverlays[intersectedItem], { localPosition: Vec3.sum(localPosition, BUTTON_PRESS_DELTA) @@ -1924,6 +1934,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsEnabled[ungroupButtonIndex] = enableUngroupButton; } } + + wasTriggerClicked = isTriggerClicked; } function display() { @@ -1982,6 +1994,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isButtonPressed = false; isPicklistPressed = false; isPicklistItemPressed = false; + isTriggerClicked = false; + wasTriggerClicked = false; isGripClicked = false; isGroupButtonEnabled = false; isUngroupButtonEnabled = false; From cc0b95c70a3814515201bae566ca9a974f15aa09 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 21 Aug 2017 17:32:40 +1200 Subject: [PATCH 223/722] Lint --- scripts/vr-edit/modules/toolMenu.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index d212aee2f4..c0b418cd1e 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -960,7 +960,10 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isDisplaying = false, // References. - controlHand; + controlHand, + + // Forward declarations. + doCommand; if (!this instanceof ToolMenu) { @@ -1437,7 +1440,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { return Overlays.getProperty(optionsOverlays[optionsOverlaysIDs.indexOf(overlayID)], overlayProperty); } - function doCommand(command, parameter) { + doCommand = function (command, parameter) { var index, hasColor, value, @@ -1619,7 +1622,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { default: App.log(side, "ERROR: ToolMenu: Unexpected command! " + command); } - } + }; function doGripClicked(command, parameter) { var overlayID; From c953df00409a36c172279e3d7dbf6d417aed317f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 22 Aug 2017 14:35:56 +1200 Subject: [PATCH 224/722] Fix "Finish" buttons not clearing tool properly --- scripts/vr-edit/modules/toolMenu.js | 10 +++++++--- scripts/vr-edit/vr-edit.js | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index c0b418cd1e..c9195155af 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -336,7 +336,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, label: "FINISH", command: { - method: "closeOptions" + method: "clearTool" } } ], @@ -358,7 +358,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, label: "FINISH", command: { - method: "closeOptions" + method: "clearTool" } } ], @@ -813,7 +813,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, label: "FINISH", command: { - method: "closeOptions" + method: "clearTool" } } ] @@ -1619,6 +1619,10 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { closeOptions(); break; + case "clearTool": + uiCommandCallback("clearTool"); + break; + default: App.log(side, "ERROR: ToolMenu: Unexpected command! " + command); } diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index e278ff12a9..3bac2fcd90 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -846,6 +846,7 @@ toolSelected = TOOL_NONE; grouping.clear(); ui.clearTool(); + ui.updateUIEntities(); } } @@ -1378,6 +1379,10 @@ ui.setToolIcon(ui.DELETE_TOOL); ui.updateUIEntities(); break; + case "clearTool": + ui.clearTool(); + ui.updateUIEntities(); + break; case "groupButton": grouping.group(); From 79056c385fa1d8c5569d7e561751aa5ea58217c2 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 22 Aug 2017 14:57:17 +1200 Subject: [PATCH 225/722] Fix options buttons not pressing --- scripts/vr-edit/modules/toolMenu.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index c9195155af..6062b0b277 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -939,7 +939,6 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isPicklistOpen, pressedItem = null, pressedSource, - isButtonPressed, isPicklistPressed, isPicklistItemPressed, isTriggerClicked, @@ -1776,11 +1775,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } // Press/unpress button. - if (controlHand.triggerClicked() !== isTriggerClicked) { - isTriggerClicked = !isTriggerClicked; - } + isTriggerClicked = controlHand.triggerClicked(); if ((pressedItem && intersectedItem !== pressedItem.index) || intersectionOverlays !== pressedSource - || isTriggerClicked !== isButtonPressed) { + || isTriggerClicked !== (pressedItem !== null)) { if (pressedItem) { // Unpress previous button. Overlays.editOverlay(intersectionOverlays[pressedItem.index], { @@ -1791,7 +1788,6 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (isHighlightingButton && (intersectionEnabled === null || intersectionEnabled[intersectedItem]) && isTriggerClicked && !wasTriggerClicked) { // Press new button. - isButtonPressed = true; localPosition = intersectionItems[intersectedItem].properties.localPosition; Overlays.editOverlay(intersectionOverlays[intersectedItem], { localPosition: Vec3.sum(localPosition, BUTTON_PRESS_DELTA) @@ -1998,7 +1994,6 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isPicklistOpen = false; pressedItem = null; pressedSource = null; - isButtonPressed = false; isPicklistPressed = false; isPicklistItemPressed = false; isTriggerClicked = false; From bb7e4fa3025628066456df70073725147ce5a9c3 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 22 Aug 2017 16:02:24 +1200 Subject: [PATCH 226/722] Revise coordinate system --- scripts/vr-edit/modules/createPalette.js | 27 ++-- scripts/vr-edit/modules/toolMenu.js | 164 ++++++++++++----------- 2 files changed, 101 insertions(+), 90 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index d1c52d41f9..34dc1162b7 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -24,15 +24,18 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { controlJointName, + /* Coordinate system: UI lies in x-y plane with the front surface being in the +ve z direction. */ CANVAS_SIZE = { x: 0.21, y: 0.13 }, - PALETTE_ROOT_POSITION = { x: -CANVAS_SIZE.x / 2, y: 0.15, z: 0.11 }, - PALETTE_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 180 }), + HAND_JOINT_OFFSET = 0.15, // Distance from hand (wrist) joint to center of panel. + PANELS_SEPARATION = 0.01, // Gap between Tools menu and Create panel. + PALETTE_ORIGIN_POSITION = { x: 0, y: HAND_JOINT_OFFSET - CANVAS_SIZE.y / 2, z: PANELS_SEPARATION + CANVAS_SIZE.x / 2 }, + PALETTE_ORIGIN_ROTATION = Quat.ZERO, lateralOffset, PALETTE_ORIGIN_PROPERTIES = { dimensions: { x: 0.005, y: 0.005, z: 0.005 }, - localPosition: PALETTE_ROOT_POSITION, - localRotation: PALETTE_ROOT_ROTATION, + localPosition: PALETTE_ORIGIN_POSITION, + localRotation: PALETTE_ORIGIN_ROTATION, color: { red: 255, blue: 0, green: 0 }, alpha: 1.0, parentID: Uuid.SELF, @@ -42,7 +45,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { PALETTE_PANEL_PROPERTIES = { dimensions: { x: CANVAS_SIZE.x, y: CANVAS_SIZE.y, z: 0.001 }, - localPosition: { x: CANVAS_SIZE.x / 2, y: CANVAS_SIZE.y / 2, z: 0 }, + localPosition: { x: 0, y: 0, z: 0 }, localRotation: Quat.ZERO, color: { red: 192, green: 192, blue: 192 }, alpha: 0.3, @@ -69,7 +72,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "cube", properties: { dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: 0.02, y: 0.02, z: 0.0 }, + localPosition: { x: -0.04, y: 0.04, z: 0.0 }, localRotation: Quat.ZERO, color: { red: 240, green: 0, blue: 0 }, alpha: 1.0, @@ -90,7 +93,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { shape: "Cylinder", dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: 0.06, y: 0.02, z: 0.0 }, + localPosition: { x: 0.0, y: 0.04, z: 0.0 }, localRotation: Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }), color: { red: 240, green: 0, blue: 0 }, alpha: 1.0, @@ -112,8 +115,8 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { shape: "Cone", dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: 0.10, y: 0.02, z: 0.0 }, - localRotation: Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }), + localPosition: { x: 0.04, y: 0.04, z: 0.0 }, + localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }), color: { red: 240, green: 0, blue: 0 }, alpha: 1.0, solid: true, @@ -133,7 +136,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "sphere", properties: { dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: 0.14, y: 0.02, z: 0.0 }, + localPosition: { x: -0.04, y: 0.0, z: 0.0 }, localRotation: Quat.ZERO, color: { red: 240, green: 0, blue: 0 }, alpha: 1.0, @@ -183,7 +186,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { var itemIndex, isTriggerClicked, properties, - PRESS_DELTA = { x: 0, y: 0, z: 0.01 }, + PRESS_DELTA = { x: 0, y: 0, z: -0.01 }, CREATE_OFFSET = { x: 0, y: 0.05, z: -0.02 }, INVERSE_HAND_BASIS_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }); @@ -257,7 +260,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { // Calculate position to put palette. properties = Object.clone(PALETTE_ORIGIN_PROPERTIES); properties.parentJointIndex = handJointIndex; - properties.localPosition = Vec3.sum(PALETTE_ROOT_POSITION, { x: lateralOffset, y: 0, z: 0 }); + properties.localPosition = Vec3.sum(PALETTE_ORIGIN_POSITION, { x: lateralOffset, y: 0, z: 0 }); paletteOriginOverlay = Overlays.addOverlay("sphere", properties); // Create palette. diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 6062b0b277..6095db7bb3 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -34,28 +34,34 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { LEFT_HAND = 0, - CANVAS_SIZE = { x: 0.22, y: 0.13 }, - PANEL_ORIGIN_POSITION = { x: -0.005 - CANVAS_SIZE.x / 2, y: 0.15, z: -CANVAS_SIZE.x / 2 }, - PANEL_ROOT_ROTATION = Quat.fromVec3Degrees({ x: 0, y: -90, z: 180 }), + /* Coordinate system: UI lies in x-y plane with the front surface being in the +ve z direction. */ + CANVAS_SIZE = { x: 0.21, y: 0.13 }, + HAND_JOINT_OFFSET = 0.15, // Distance from hand (wrist) joint to center of panel. + PANELS_SEPARATION = 0.01, // Gap between Tools menu and Create panel. + PANEL_ORIGIN_POSITION = { x: -PANELS_SEPARATION - CANVAS_SIZE.x / 2, y: HAND_JOINT_OFFSET - CANVAS_SIZE.y / 2, z: 0 }, + PANEL_ORIGIN_ROTATION = Quat.fromVec3Degrees({ x: 0, y: -90, z: 0 }), panelLateralOffset, MENU_ORIGIN_PROPERTIES = { dimensions: { x: 0.005, y: 0.005, z: 0.005 }, localPosition: PANEL_ORIGIN_POSITION, - localRotation: PANEL_ROOT_ROTATION, + localRotation: PANEL_ORIGIN_ROTATION, color: { red: 255, blue: 0, green: 0 }, alpha: 1.0, parentID: Uuid.SELF, ignoreRayIntersection: true, - visible: false + //visible: false + visible: true, + displayInFront: true }, MENU_PANEL_PROPERTIES = { dimensions: { x: CANVAS_SIZE.x, y: CANVAS_SIZE.y, z: 0.01 }, - localPosition: { x: CANVAS_SIZE.x / 2, y: CANVAS_SIZE.y / 2, z: 0.005 }, + localPosition: { x: 0, y: 0, z: 0.0 }, localRotation: Quat.ZERO, color: { red: 164, green: 164, blue: 164 }, - alpha: 1.0, + //alpha: 1.0, + alpha: 0.5, solid: true, ignoreRayIntersection: false, visible: true @@ -73,7 +79,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.10, y: 0.12, z: 0.01 }, localRotation: Quat.ZERO, color: { red: 192, green: 192, blue: 192 }, - alpha: 1.0, + //alpha: 1.0, + alpha: 0.5, solid: true, ignoreRayIntersection: false, visible: true @@ -119,8 +126,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { overlay: "text3d", properties: { dimensions: { x: 0.03, y: 0.0075 }, - localPosition: { x: 0.0, y: 0.0, z: -0.005 }, - localRotation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 180 }), + localPosition: { x: 0, y: 0, z: 0.005 }, + localRotation: Quat.ZERO, topMargin: 0, leftMargin: 0, color: { red: 240, green: 240, blue: 240 }, @@ -137,8 +144,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { overlay: "circle3d", properties: { size: 0.01, - localPosition: { x: 0.0, y: 0.0, z: -0.01 }, - localRotation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 180 }), + localPosition: { x: 0.0, y: 0.0, z: 0.01 }, + localRotation: Quat.ZERO, color: { red: 128, green: 128, blue: 128 }, alpha: 1.0, solid: true, @@ -151,7 +158,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { dimensions: { x: 0.1, y: 0.1 }, localPosition: { x: 0, y: 0, z: 0 }, - localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 }), + localRotation: Quat.ZERO, color: { red: 255, green: 255, blue: 255 }, alpha: 1.0, ignoreRayIntersection: true, @@ -293,7 +300,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, BUTTON_UI_ELEMENTS = ["button", "toggleButton", "swatch"], - BUTTON_PRESS_DELTA = { x: 0, y: 0, z: 0.004 }, + BUTTON_PRESS_DELTA = { x: 0, y: 0, z: -0.004 }, SLIDER_UI_ELEMENTS = ["barSlider", "imageSlider"], COLOR_CIRCLE_UI_ELEMENTS = ["colorCircle"], @@ -323,7 +330,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "scaleOptionsPanel", type: "panel", properties: { - localPosition: { x: 0.0, y: 0.0, z: -0.005 } + localPosition: { x: 0.0, y: 0.0, z: 0.005 } } }, { @@ -331,7 +338,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "button", properties: { dimensions: { x: 0.07, y: 0.03, z: 0.01 }, - localPosition: { x: 0, y: 0, z: -0.005 }, + localPosition: { x: 0, y: 0, z: 0.005 }, color: { red: 200, green: 200, blue: 200 } }, label: "FINISH", @@ -345,7 +352,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "cloneOptionsPanel", type: "panel", properties: { - localPosition: { x: 0.0, y: 0.0, z: -0.005 } + localPosition: { x: 0.0, y: 0.0, z: 0.005 } } }, { @@ -353,7 +360,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "button", properties: { dimensions: { x: 0.07, y: 0.03, z: 0.01 }, - localPosition: { x: 0, y: 0, z: -0.005 }, + localPosition: { x: 0, y: 0, z: 0.005 }, color: { red: 200, green: 200, blue: 200 } }, label: "FINISH", @@ -367,7 +374,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "groupOptionsPanel", type: "panel", properties: { - localPosition: { x: 0.0, y: 0.0, z: -0.005 } + localPosition: { x: 0.0, y: 0.0, z: 0.005 } } }, { @@ -375,7 +382,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "button", properties: { dimensions: { x: 0.07, y: 0.03, z: 0.01 }, - localPosition: { x: 0, y: -0.025, z: -0.005 }, + localPosition: { x: 0, y: 0.025, z: 0.005 }, color: { red: 200, green: 200, blue: 200 } }, label: " GROUP", @@ -389,7 +396,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "button", properties: { dimensions: { x: 0.07, y: 0.03, z: 0.01 }, - localPosition: { x: 0, y: 0.025, z: -0.005 }, + localPosition: { x: 0, y: -0.025, z: 0.005 }, color: { red: 200, green: 200, blue: 200 } }, label: "UNGROUP", @@ -404,14 +411,14 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "colorOptionsPanel", type: "panel", properties: { - localPosition: { x: 0.0, y: 0.0, z: -0.005 } + localPosition: { x: 0.0, y: 0.0, z: 0.005 } } }, { id: "colorCircle", type: "colorCircle", properties: { - localPosition: { x: -0.0125, y: -0.025, z: -0.005 } + localPosition: { x: -0.0125, y: 0.025, z: 0.005 } }, imageURL: "../assets/color-circle.png", imageOverlayURL: "../assets/color-circle-black.png", @@ -423,7 +430,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "colorSlider", type: "imageSlider", properties: { - localPosition: { x: 0.035, y: -0.025, z: -0.005 } + localPosition: { x: 0.035, y: 0.025, z: 0.005 } }, useBaseColor: true, imageURL: "../assets/slider-white.png", @@ -437,7 +444,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: 0.02, z: -0.005 } + localPosition: { x: -0.035, y: -0.02, z: 0.005 } }, setting: { key: "VREdit.colorTool.swatch1Color", @@ -456,7 +463,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.01, y: 0.02, z: -0.005 } + localPosition: { x: -0.01, y: -0.02, z: 0.005 } }, setting: { key: "VREdit.colorTool.swatch2Color", @@ -475,7 +482,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: 0.045, z: -0.005 } + localPosition: { x: -0.035, y: -0.045, z: 0.005 } }, setting: { key: "VREdit.colorTool.swatch3Color", @@ -494,7 +501,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "swatch", properties: { dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.01, y: 0.045, z: -0.005 } + localPosition: { x: -0.01, y: -0.045, z: 0.005 } }, setting: { key: "VREdit.colorTool.swatch4Color", @@ -512,7 +519,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "currentColor", type: "circle", properties: { - localPosition: { x: 0.025, y: 0.02, z: -0.007 } + localPosition: { x: 0.025, y: -0.02, z: 0.007 } }, setting: { key: "VREdit.colorTool.currentColor", @@ -526,7 +533,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "button", properties: { dimensions: { x: 0.04, y: 0.02, z: 0.01 }, - localPosition: { x: 0.025, y: 0.045, z: -0.005 }, + localPosition: { x: 0.025, y: -0.045, z: 0.005 }, color: { red: 255, green: 255, blue: 255 } }, label: " PICK", @@ -540,7 +547,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "physicsOptionsPanel", type: "panel", properties: { - localPosition: { x: 0.0, y: 0.0, z: -0.005 } + localPosition: { x: 0.0, y: 0.0, z: 0.005 } } }, @@ -550,14 +557,14 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { text: "PROPERTIES", lineHeight: 0.0045, - localPosition: { x: -0.031, y: -0.0475, z: -0.0075} + localPosition: { x: -0.031, y: 0.0475, z: 0.0075} } }, { id: "gravityToggle", type: "toggleButton", properties: { - localPosition: { x: -0.0325, y: -0.03, z: -0.005 }, + localPosition: { x: -0.0325, y: 0.03, z: 0.005 }, dimensions: { x: 0.03, y: 0.02, z: 0.01 } }, label: "GRAVITY", @@ -574,7 +581,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "grabToggle", type: "toggleButton", properties: { - localPosition: { x: -0.0325, y: -0.005, z: -0.005 }, + localPosition: { x: -0.0325, y: 0.005, z: 0.005 }, dimensions: { x: 0.03, y: 0.02, z: 0.01 } }, label: " GRAB", @@ -591,7 +598,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "collideToggle", type: "toggleButton", properties: { - localPosition: { x: -0.0325, y: 0.02, z: -0.005 }, + localPosition: { x: -0.0325, y: -0.02, z: 0.005 }, dimensions: { x: 0.03, y: 0.02, z: 0.01 } }, label: "COLLIDE", @@ -611,14 +618,14 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { text: "PRESETS", lineHeight: 0.0045, - localPosition: { x: 0.002, y: -0.0475, z: -0.0075 } + localPosition: { x: 0.002, y: 0.0475, z: 0.0075 } } }, { id: "presets", type: "picklist", properties: { - localPosition: { x: 0.016, y: -0.03, z: -0.005 }, + localPosition: { x: 0.016, y: 0.03, z: 0.005 }, dimensions: { x: 0.06, y: 0.02, z: 0.01 } }, label: "DEFAULT", @@ -698,7 +705,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "gravitySlider", type: "barSlider", properties: { - localPosition: { x: -0.007, y: 0.016, z: -0.005 }, + localPosition: { x: -0.007, y: -0.016, z: 0.005 }, dimensions: { x: 0.014, y: 0.06, z: 0.01 } }, setting: { @@ -716,14 +723,14 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { text: "GRAVITY", lineHeight: 0.0045, - localPosition: { x: -0.003, y: 0.052, z: -0.0075 } + localPosition: { x: -0.003, y: -0.052, z: 0.0075 } } }, { id: "bounceSlider", type: "barSlider", properties: { - localPosition: { x: 0.009, y: 0.016, z: -0.005 }, + localPosition: { x: 0.009, y: -0.016, z: 0.005 }, dimensions: { x: 0.014, y: 0.06, z: 0.01 } }, setting: { @@ -741,14 +748,14 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { text: "BOUNCE", lineHeight: 0.0045, - localPosition: { x: 0.015, y: 0.057, z: -0.0075 } + localPosition: { x: 0.015, y: -0.057, z: 0.0075 } } }, { id: "dampingSlider", type: "barSlider", properties: { - localPosition: { x: 0.024, y: 0.016, z: -0.005 }, + localPosition: { x: 0.024, y: -0.016, z: 0.005 }, dimensions: { x: 0.014, y: 0.06, z: 0.01 } }, setting: { @@ -766,14 +773,14 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { text: "DAMPING", lineHeight: 0.0045, - localPosition: { x: 0.030, y: 0.052, z: -0.0075 } + localPosition: { x: 0.030, y: -0.052, z: 0.0075 } } }, { id: "densitySlider", type: "barSlider", properties: { - localPosition: { x: 0.039, y: 0.016, z: -0.005 }, + localPosition: { x: 0.039, y: -0.016, z: 0.005 }, dimensions: { x: 0.014, y: 0.06, z: 0.01 } }, setting: { @@ -791,7 +798,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { text: "DENSITY", lineHeight: 0.0045, - localPosition: { x: 0.045, y: 0.057, z: -0.0075 } + localPosition: { x: 0.045, y: -0.057, z: 0.0075 } } } ], @@ -800,7 +807,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "deleteOptionsPanel", type: "panel", properties: { - localPosition: { x: 0.0, y: 0.0, z: -0.005 } + localPosition: { x: 0.0, y: 0.0, z: 0.005 } } }, { @@ -808,7 +815,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "button", properties: { dimensions: { x: 0.07, y: 0.03, z: 0.01 }, - localPosition: { x: 0, y: 0, z: -0.005 }, + localPosition: { x: 0, y: 0, z: 0.005 }, color: { red: 200, green: 200, blue: 200 } }, label: "FINISH", @@ -824,14 +831,14 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "toolsMenuPanel", type: "panel", properties: { - localPosition: { x: -0.0, y: 0.0, z: -0.005 } + localPosition: { x: 0, y: 0, z: 0.005 } } }, { id: "scaleButton", type: "button", properties: { - localPosition: { x: -0.022, y: -0.04, z: -0.005 }, + localPosition: { x: -0.022, y: 0.04, z: 0.005 }, color: { red: 0, green: 240, blue: 240 } }, label: " SCALE", @@ -844,7 +851,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "cloneButton", type: "button", properties: { - localPosition: { x: 0.022, y: -0.04, z: -0.005 }, + localPosition: { x: 0.022, y: 0.04, z: 0.005 }, color: { red: 240, green: 240, blue: 0 } }, label: " CLONE", @@ -857,7 +864,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "groupButton", type: "button", properties: { - localPosition: { x: -0.022, y: 0.0, z: -0.005 }, + localPosition: { x: -0.022, y: 0.0, z: 0.005 }, color: { red: 220, green: 60, blue: 220 } }, label: " GROUP", @@ -870,7 +877,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "colorButton", type: "button", properties: { - localPosition: { x: 0.022, y: 0.0, z: -0.005 }, + localPosition: { x: 0.022, y: 0.0, z: 0.005 }, color: { red: 220, green: 220, blue: 220 } }, label: " COLOR", @@ -884,7 +891,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "physicsButton", type: "button", properties: { - localPosition: { x: -0.022, y: 0.04, z: -0.005 }, + localPosition: { x: -0.022, y: -0.04, z: 0.005 }, color: { red: 60, green: 60, blue: 240 } }, label: "PHYSICS", @@ -897,7 +904,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "deleteButton", type: "button", properties: { - localPosition: { x: 0.022, y: 0.04, z: -0.005 }, + localPosition: { x: 0.022, y: -0.04, z: 0.005 }, color: { red: 240, green: 60, blue: 60 } }, label: " DELETE", @@ -913,7 +920,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { yDelta: 0.004, zDimension: 0.001, properties: { - localPosition: { x: 0, y: 0, z: -0.003 }, + localPosition: { x: 0, y: 0, z: 0.003 }, localRotation: Quat.ZERO, color: { red: 255, green: 255, blue: 0 }, alpha: 0.8, @@ -1134,7 +1141,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (optionsItems[i].type === "barSlider") { optionsSliderData[i] = {}; auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderValue.properties); - auxiliaryProperties.localPosition = { x: 0, y: (0.5 - value / 2) * properties.dimensions.y, z: 0 }; + auxiliaryProperties.localPosition = { x: 0, y: (-0.5 + value / 2) * properties.dimensions.y, z: 0 }; auxiliaryProperties.dimensions = { x: properties.dimensions.x, y: Math.max(value * properties.dimensions.y, MIN_BAR_SLIDER_DIMENSION), @@ -1144,7 +1151,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsSliderData[i].value = Overlays.addOverlay(UI_ELEMENTS.barSliderValue.overlay, auxiliaryProperties); auxiliaryProperties = Object.clone(UI_ELEMENTS.barSliderRemainder.properties); - auxiliaryProperties.localPosition = { x: 0, y: (-0.5 + (1.0 - value) / 2) * properties.dimensions.y, z: 0 }; + auxiliaryProperties.localPosition = { x: 0, y: (0.5 - (1.0 - value) / 2) * properties.dimensions.y, z: 0 }; auxiliaryProperties.dimensions = { x: properties.dimensions.x, y: Math.max((1.0 - value) * properties.dimensions.y, MIN_BAR_SLIDER_DIMENSION), @@ -1169,7 +1176,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (optionsItems[i].useBaseColor) { childProperties.color = properties.color; } - childProperties.localPosition = { x: 0, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; + childProperties.localPosition = { x: 0, y: 0, z: properties.dimensions.z / 2 + imageOffset }; hsvControl.slider.localPosition = childProperties.localPosition; childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; hsvControl.slider.colorOverlay = Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); @@ -1184,7 +1191,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { delete childProperties.dimensions; childProperties.scale = properties.dimensions.y; imageOffset += IMAGE_OFFSET; - childProperties.localPosition = { x: 0, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; + childProperties.localPosition = { x: 0, y: 0, z: properties.dimensions.z / 2 + imageOffset }; childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); } @@ -1192,7 +1199,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Value pointers. optionsSliderData[i] = {}; optionsSliderData[i].offset = - { x: -properties.dimensions.x / 2, y: 0, z: -properties.dimensions.z / 2 - imageOffset }; + { x: -properties.dimensions.x / 2, y: 0, z: properties.dimensions.z / 2 + imageOffset }; auxiliaryProperties = Object.clone(UI_ELEMENTS.sliderPointer.properties); auxiliaryProperties.localPosition = optionsSliderData[i].offset; hsvControl.slider.localPosition = auxiliaryProperties.localPosition; @@ -1217,8 +1224,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { childProperties.scale = 0.95 * properties.dimensions.x; // TODO: Magic number. imageOffset += IMAGE_OFFSET; childProperties.emissive = true; - childProperties.localPosition = { x: 0, y: -properties.dimensions.y / 2 - imageOffset, z: 0 }; - childProperties.localRotation = Quat.fromVec3Degrees({ x: 90, y: 90, z: 0 }); + childProperties.localPosition = { x: 0, y: properties.dimensions.y / 2 + imageOffset, z: 0 }; + childProperties.localRotation = Quat.fromVec3Degrees({ x: -90, y: 90, z: 0 }); childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); } @@ -1231,7 +1238,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { delete childProperties.dimensions; childProperties.scale = 0.95 * properties.dimensions.x; // TODO: Magic number. imageOffset += IMAGE_OFFSET; - childProperties.localPosition = { x: 0, y: -properties.dimensions.y / 2 - imageOffset, z: 0 }; + childProperties.localPosition = { x: 0, y: properties.dimensions.y / 2 + imageOffset, z: 0 }; childProperties.localRotation = Quat.fromVec3Degrees({ x: 90, y: 90, z: 0 }); childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; childProperties.alpha = 0.0; @@ -1242,7 +1249,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Invisible sphere at target point with cones as decoration. optionsColorData[i] = {}; optionsColorData[i].offset = - { x: 0, y: -properties.dimensions.y / 2 - imageOffset, z: 0 }; + { x: 0, y: properties.dimensions.y / 2 + imageOffset, z: 0 }; auxiliaryProperties = Object.clone(UI_ELEMENTS.sphere.properties); auxiliaryProperties.localPosition = optionsColorData[i].offset; auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; @@ -1311,7 +1318,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { otherFraction = 1.0 - fraction; Overlays.editOverlay(optionsSliderData[item].value, { - localPosition: { x: 0, y: (0.5 - fraction / 2) * overlayDimensions.y, z: 0 }, + localPosition: { x: 0, y: (-0.5 + fraction / 2) * overlayDimensions.y, z: 0 }, dimensions: { x: overlayDimensions.x, y: Math.max(fraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION), @@ -1319,7 +1326,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }); Overlays.editOverlay(optionsSliderData[item].remainder, { - localPosition: { x: 0, y: (-0.5 + otherFraction / 2) * overlayDimensions.y, z: 0 }, + localPosition: { x: 0, y: (0.5 - otherFraction / 2) * overlayDimensions.y, z: 0 }, dimensions: { x: overlayDimensions.x, y: Math.max(otherFraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION), @@ -1392,7 +1399,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { x = r * Math.cos(theta); y = r * Math.sin(theta); Overlays.editOverlay(hsvControl.circle.cursorOverlay, { - localPosition: { x: y, y: hsvControl.circle.localPosition.y, z: -x } + // Coordinates based on rotate cylinder entity. TODO: Use FBX model instead of cylinder entity. + localPosition: { x: -y, y: hsvControl.circle.localPosition.y, z: -x } }); } @@ -1406,7 +1414,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Overlays.editOverlay(hsvControl.slider.pointerOverlay, { localPosition: { x: hsvControl.slider.localPosition.x, - y: (0.5 - hsvControl.hsv.v) * hsvControl.slider.length, + y: (hsvControl.hsv.v - 0.5) * hsvControl.slider.length, z: hsvControl.slider.localPosition.z } }); @@ -1545,7 +1553,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Raise picklist. Overlays.editOverlay(parentID, { - localPosition: Vec3.subtract(optionsItems[index].properties.localPosition, ITEM_RAISE_DELTA) + localPosition: Vec3.sum(optionsItems[index].properties.localPosition, ITEM_RAISE_DELTA) }); // Show options. @@ -1554,7 +1562,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { index = optionsOverlaysIDs.indexOf(items[i]); Overlays.editOverlay(optionsOverlays[index], { parentID: parentID, - localPosition: { x: 0, y: (i + 1) * -UI_ELEMENTS.picklistItem.properties.dimensions.y, z: 0 }, + localPosition: { x: 0, y: (i + 1) * UI_ELEMENTS.picklistItem.properties.dimensions.y, z: 0 }, visible: true }); Overlays.editOverlay(optionsOverlaysLabels[index], { @@ -1713,7 +1721,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (isHighlightingSlider || isHighlightingColorCircle) { localPosition = intersectionItems[highlightedItem].properties.localPosition; Overlays.editOverlay(intersectionOverlays[highlightedItem], { - localPosition: Vec3.subtract(localPosition, ITEM_RAISE_DELTA) + localPosition: Vec3.sum(localPosition, ITEM_RAISE_DELTA) }); } // Highlight new item. (The existence of a command or callback infers that the item should be highlighted.) @@ -1856,8 +1864,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { overlayDimensions = UI_ELEMENTS.barSlider.properties.dimensions; } basePoint = Vec3.sum(sliderProperties.position, - Vec3.multiplyQbyV(sliderProperties.orientation, { x: 0, y: overlayDimensions.y / 2, z: 0 })); - fraction = Vec3.dot(Vec3.subtract(basePoint, intersection.intersection), + Vec3.multiplyQbyV(sliderProperties.orientation, { x: 0, y: -overlayDimensions.y / 2, z: 0 })); + fraction = Vec3.dot(Vec3.subtract(intersection.intersection, basePoint), Vec3.multiplyQbyV(sliderProperties.orientation, Vec3.UNIT_Y)) / overlayDimensions.y; fraction = adjustSliderFraction(fraction); setBarSliderValue(intersectedItem, fraction); @@ -1874,13 +1882,13 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { overlayDimensions = UI_ELEMENTS.imageSlider.properties.dimensions; } basePoint = Vec3.sum(sliderProperties.position, - Vec3.multiplyQbyV(sliderProperties.orientation, { x: 0, y: overlayDimensions.y / 2, z: 0 })); - fraction = Vec3.dot(Vec3.subtract(basePoint, intersection.intersection), + Vec3.multiplyQbyV(sliderProperties.orientation, { x: 0, y: -overlayDimensions.y / 2, z: 0 })); + fraction = Vec3.dot(Vec3.subtract(intersection.intersection, basePoint), Vec3.multiplyQbyV(sliderProperties.orientation, Vec3.UNIT_Y)) / overlayDimensions.y; fraction = adjustSliderFraction(fraction); Overlays.editOverlay(optionsSliderData[intersectedItem].value, { localPosition: Vec3.sum(optionsSliderData[intersectedItem].offset, - { x: 0, y: (0.5 - fraction) * overlayDimensions.y, z: 0 }) + { x: 0, y: (fraction - 0.5) * overlayDimensions.y, z: 0 }) }); if (intersectionItems[intersectedItem].command) { doCommand(intersectionItems[intersectedItem].command.method, fraction); @@ -1902,8 +1910,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }); if (intersectionItems[intersectedItem].command) { // Cartesian planar coordinates. - x = -delta.z; - y = delta.x; + x = -delta.z; // Coordinates based on rotate cylinder entity. TODO: Use FBX model instead of cylinder entity. + y = -delta.x; // "" s = Math.sqrt(x * x + y * y) / hsvControl.circle.radius; h = Math.atan2(y, x) / (2 * Math.PI); if (h < 0) { From 5a4ebbd54df9042fdf0810af2582b65564234bce Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 22 Aug 2017 21:59:31 +1200 Subject: [PATCH 227/722] Style Create palette header and panel --- scripts/vr-edit/assets/create/create.svg | 12 +++ scripts/vr-edit/modules/createPalette.js | 97 +++++++++++++++++++----- scripts/vr-edit/modules/uit.js | 37 +++++++++ scripts/vr-edit/vr-edit.js | 1 + 4 files changed, 130 insertions(+), 17 deletions(-) create mode 100644 scripts/vr-edit/assets/create/create.svg create mode 100644 scripts/vr-edit/modules/uit.js diff --git a/scripts/vr-edit/assets/create/create.svg b/scripts/vr-edit/assets/create/create.svg new file mode 100644 index 0000000000..fa2a096ede --- /dev/null +++ b/scripts/vr-edit/assets/create/create.svg @@ -0,0 +1,12 @@ + + + + CREATE + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 34dc1162b7..1eb3d4c607 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -16,6 +16,9 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { "use strict"; var paletteOriginOverlay, + paletteHeaderOverlay, + paletteHeaderBarOverlay, + paletteTitleOverlay, palettePanelOverlay, highlightOverlay, paletteItemOverlays = [], @@ -24,11 +27,11 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { controlJointName, - /* Coordinate system: UI lies in x-y plane with the front surface being in the +ve z direction. */ - CANVAS_SIZE = { x: 0.21, y: 0.13 }, - HAND_JOINT_OFFSET = 0.15, // Distance from hand (wrist) joint to center of panel. - PANELS_SEPARATION = 0.01, // Gap between Tools menu and Create panel. - PALETTE_ORIGIN_POSITION = { x: 0, y: HAND_JOINT_OFFSET - CANVAS_SIZE.y / 2, z: PANELS_SEPARATION + CANVAS_SIZE.x / 2 }, + PALETTE_ORIGIN_POSITION = { + x: 0, + y: UIT.dimensions.handOffset, + z: UIT.dimensions.canvasSeparation + UIT.dimensions.canvas.x / 2 + }, PALETTE_ORIGIN_ROTATION = Quat.ZERO, lateralOffset, @@ -43,12 +46,55 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: false }, - PALETTE_PANEL_PROPERTIES = { - dimensions: { x: CANVAS_SIZE.x, y: CANVAS_SIZE.y, z: 0.001 }, - localPosition: { x: 0, y: 0, z: 0 }, + PALETTE_HEADER_PROPERTIES = { + dimensions: UIT.dimensions.header, + localPosition: { + x: 0, + y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.header.y / 2, + z: UIT.dimensions.header.z / 2 + }, localRotation: Quat.ZERO, - color: { red: 192, green: 192, blue: 192 }, - alpha: 0.3, + color: UIT.colors.baseGray, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + }, + + PALETTE_HEADER_BAR_PROPERTIES = { + dimensions: UIT.dimensions.headerBar, + localPosition: { + x: 0, + y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.header.y - UIT.dimensions.headerBar.y / 2, + z: UIT.dimensions.headerBar.z / 2 + }, + localRotation: Quat.ZERO, + color: UIT.colors.blueHighlight, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + }, + + PALETTE_TITLE_PROPERTIES = { + url: "../assets/create/create.svg", + scale: 0.0363, + localPosition: { x: 0, y: 0, z: PALETTE_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOffset }, + localRotation: Quat.ZERO, + color: UIT.colors.white, + alpha: 1.0, + emissive: true, + ignoreRayIntersection: true, + isFacingAvatar: false, + visible: true + }, + + PALETTE_PANEL_PROPERTIES = { + dimensions: UIT.dimensions.panel, + localPosition: { x: 0, y: UIT.dimensions.panel.y / 2 - UIT.dimensions.canvas.y / 2, z: UIT.dimensions.panel.z / 2 }, + localRotation: Quat.ZERO, + color: UIT.colors.baseGray, + alpha: 1.0, solid: true, ignoreRayIntersection: false, visible: true @@ -72,7 +118,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "cube", properties: { dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: -0.04, y: 0.04, z: 0.0 }, + localPosition: { x: -0.04, y: 0.04, z: 0.03 }, localRotation: Quat.ZERO, color: { red: 240, green: 0, blue: 0 }, alpha: 1.0, @@ -93,7 +139,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { shape: "Cylinder", dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: 0.0, y: 0.04, z: 0.0 }, + localPosition: { x: 0.0, y: 0.04, z: 0.03 }, localRotation: Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }), color: { red: 240, green: 0, blue: 0 }, alpha: 1.0, @@ -115,7 +161,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { shape: "Cone", dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: 0.04, y: 0.04, z: 0.0 }, + localPosition: { x: 0.04, y: 0.04, z: 0.03 }, localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }), color: { red: 240, green: 0, blue: 0 }, alpha: 1.0, @@ -136,7 +182,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "sphere", properties: { dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: -0.04, y: 0.0, z: 0.0 }, + localPosition: { x: -0.04, y: 0.0, z: 0.03 }, localRotation: Quat.ZERO, color: { red: 240, green: 0, blue: 0 }, alpha: 1.0, @@ -173,13 +219,13 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { side = hand; controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); controlJointName = side === LEFT_HAND ? "LeftHand" : "RightHand"; - lateralOffset = side === LEFT_HAND ? -0.01 : 0.01; + lateralOffset = side === LEFT_HAND ? -UIT.dimensions.handLateralOffset : UIT.dimensions.handLateralOffset; } setHand(side); function getEntityIDs() { - return [palettePanelOverlay].concat(paletteItemOverlays); + return [palettePanelOverlay, paletteHeaderOverlay, paletteHeaderBarOverlay].concat(paletteItemOverlays); } function update(intersectionOverlayID) { @@ -227,7 +273,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.clone(PALETTE_ITEMS[itemIndex].entity); properties.position = Vec3.sum(controlHand.palmPosition(), Vec3.multiplyQbyV(controlHand.orientation(), - Vec3.sum({ x: 0, y: properties.dimensions.z / 2, z: 0 }, CREATE_OFFSET))); + Vec3.sum({ x: 0, y: properties.dimensions.z + 0.01, z: 0 }, CREATE_OFFSET))); properties.rotation = Quat.multiply(controlHand.orientation(), INVERSE_HAND_BASIS_ROTATION); Entities.addEntity(properties); @@ -264,9 +310,23 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { paletteOriginOverlay = Overlays.addOverlay("sphere", properties); // Create palette. + properties = Object.clone(PALETTE_HEADER_PROPERTIES); + properties.parentID = paletteOriginOverlay; + paletteHeaderOverlay = Overlays.addOverlay("cube", properties); + + properties = Object.clone(PALETTE_HEADER_BAR_PROPERTIES); + properties.parentID = paletteOriginOverlay; + paletteHeaderBarOverlay = Overlays.addOverlay("cube", properties); + + properties = Object.clone(PALETTE_TITLE_PROPERTIES); + properties.parentID = paletteHeaderOverlay; + properties.url = Script.resolvePath(properties.url); + paletteTitleOverlay = Overlays.addOverlay("image3d", properties); + properties = Object.clone(PALETTE_PANEL_PROPERTIES); properties.parentID = paletteOriginOverlay; palettePanelOverlay = Overlays.addOverlay("cube", properties); + for (i = 0, length = PALETTE_ITEMS.length; i < length; i += 1) { properties = Object.clone(PALETTE_ITEMS[i].overlay.properties); properties.parentID = paletteOriginOverlay; @@ -295,6 +355,9 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { Overlays.deleteOverlay(paletteItemOverlays[i]); } Overlays.deleteOverlay(palettePanelOverlay); + Overlays.deleteOverlay(paletteTitleOverlay); + Overlays.deleteOverlay(paletteHeaderBarOverlay); + Overlays.deleteOverlay(paletteHeaderOverlay); Overlays.deleteOverlay(paletteOriginOverlay); isDisplaying = false; diff --git a/scripts/vr-edit/modules/uit.js b/scripts/vr-edit/modules/uit.js new file mode 100644 index 0000000000..4c2362044e --- /dev/null +++ b/scripts/vr-edit/modules/uit.js @@ -0,0 +1,37 @@ +// +// uit.js +// +// Created by David Rowe on 22 Aug 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global UIT */ + +UIT = (function () { + // User Interface Toolkit. Global object. + + return { + colors: { + baseGray: { red: 0x40, green: 0x40, blue: 0x40 }, + blueHighlight: { red: 0x00, green: 0xbf, blue: 0xef }, + white: { red: 0xff, green: 0xff, blue: 0xff } + }, + + // Coordinate system: UI lies in x-y plane with the front surface being +z. + dimensions: { + canvas: { x: 0.24, y: 0.24 }, // Overall UI size. + canvasSeparation: 0.01, // Gap between Tools menu and Create panel. + handOffset: 0.085, // Distance from hand (wrist) joint to center of canvas. + handLateralOffset: 0.01, // Offset of UI in direction of palm normal. + + header: { x: 0.24, y: 0.044, z: 0.012 }, + headerBar: { x: 0.24, y: 0.004, z: 0.012 }, + panel: { x: 0.24, y: 0.18, z: 0.008 }, + + imageOffset: 0.001 // Raise image above surface. + } + }; +}()); diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 3bac2fcd90..7fc2537703 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -81,6 +81,7 @@ Script.include("./modules/selection.js"); Script.include("./modules/toolIcon.js"); Script.include("./modules/toolMenu.js"); + Script.include("./modules/uit.js"); function log(side, message) { From f542c54e6c256137151ab892c05a3e64eefd6c4d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 22 Aug 2017 22:40:38 +1200 Subject: [PATCH 228/722] Lay out Create palette entity items --- scripts/vr-edit/modules/createPalette.js | 70 ++++++++++++++++-------- scripts/vr-edit/modules/uit.js | 5 +- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 1eb3d4c607..c28d8421e0 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -22,6 +22,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { palettePanelOverlay, highlightOverlay, paletteItemOverlays = [], + paletteItemPositions = [], LEFT_HAND = 0, @@ -112,15 +113,19 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: false }, + PALETTE_ENTITY_DIMENSIONS = { x: 0.024, y: 0.024, z: 0.024 }, + PALETTE_ENTITY_COLOR = UIT.colors.faintGray, + ENTITY_CREATION_DIMENSIONS = { x: 0.2, y: 0.2, z: 0.2 }, + ENTITY_CREATION_COLOR = { red: 192, green: 192, blue: 192 }, + PALETTE_ITEMS = [ { overlay: { type: "cube", properties: { - dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: -0.04, y: 0.04, z: 0.03 }, + dimensions: PALETTE_ENTITY_DIMENSIONS, localRotation: Quat.ZERO, - color: { red: 240, green: 0, blue: 0 }, + color: PALETTE_ENTITY_COLOR, alpha: 1.0, solid: true, ignoreRayIntersection: false, @@ -129,8 +134,8 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { }, entity: { type: "Box", - dimensions: { x: 0.2, y: 0.2, z: 0.2 }, - color: { red: 192, green: 192, blue: 192 } + dimensions: ENTITY_CREATION_DIMENSIONS, + color: ENTITY_CREATION_COLOR } }, { @@ -138,10 +143,9 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "shape", properties: { shape: "Cylinder", - dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: 0.0, y: 0.04, z: 0.03 }, + dimensions: PALETTE_ENTITY_DIMENSIONS, localRotation: Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }), - color: { red: 240, green: 0, blue: 0 }, + color: PALETTE_ENTITY_COLOR, alpha: 1.0, solid: true, ignoreRayIntersection: false, @@ -151,8 +155,8 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { entity: { type: "Shape", shape: "Cylinder", - dimensions: { x: 0.2, y: 0.2, z: 0.2 }, - color: { red: 192, green: 192, blue: 192 } + dimensions: ENTITY_CREATION_DIMENSIONS, + color: ENTITY_CREATION_COLOR } }, { @@ -160,10 +164,9 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "shape", properties: { shape: "Cone", - dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: 0.04, y: 0.04, z: 0.03 }, + dimensions: PALETTE_ENTITY_DIMENSIONS, localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }), - color: { red: 240, green: 0, blue: 0 }, + color: PALETTE_ENTITY_COLOR, alpha: 1.0, solid: true, ignoreRayIntersection: false, @@ -173,18 +176,17 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { entity: { type: "Shape", shape: "Cone", - dimensions: { x: 0.2, y: 0.2, z: 0.2 }, - color: { red: 192, green: 192, blue: 192 } + dimensions: ENTITY_CREATION_DIMENSIONS, + color: ENTITY_CREATION_COLOR } }, { overlay: { type: "sphere", properties: { - dimensions: { x: 0.03, y: 0.03, z: 0.03 }, - localPosition: { x: -0.04, y: 0.0, z: 0.03 }, + dimensions: PALETTE_ENTITY_DIMENSIONS, localRotation: Quat.ZERO, - color: { red: 240, green: 0, blue: 0 }, + color: PALETTE_ENTITY_COLOR, alpha: 1.0, solid: true, ignoreRayIntersection: false, @@ -193,8 +195,8 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { }, entity: { type: "Sphere", - dimensions: { x: 0.2, y: 0.2, z: 0.2 }, - color: { red: 192, green: 192, blue: 192 } + dimensions: ENTITY_CREATION_DIMENSIONS, + color: ENTITY_CREATION_COLOR } } ], @@ -257,7 +259,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { // Unpress currently pressed item. if (pressedItem !== NONE && pressedItem !== itemIndex) { Overlays.editOverlay(paletteItemOverlays[pressedItem], { - localPosition: PALETTE_ITEMS[pressedItem].overlay.properties.localPosition + localPosition: paletteItemPositions[pressedItem] }); pressedItem = NONE; } @@ -266,7 +268,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { isTriggerClicked = controlHand.triggerClicked(); if (highlightedItem !== NONE && pressedItem === NONE && isTriggerClicked && !wasTriggerClicked) { Overlays.editOverlay(paletteItemOverlays[itemIndex], { - localPosition: Vec3.sum(PALETTE_ITEMS[itemIndex].overlay.properties.localPosition, PRESS_DELTA) + localPosition: Vec3.sum(paletteItemPositions[itemIndex], PRESS_DELTA) }); pressedItem = itemIndex; @@ -283,6 +285,26 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { wasTriggerClicked = isTriggerClicked; } + function itemPosition(index) { + // Position relative to palette panel. + var ITEMS_PER_ROW = 3, + ROW_ZERO_Y_OFFSET = 0.0580, + ROW_SPACING = 0.0560, + COLUMN_ZERO_OFFSET = -0.08415, + COLUMN_SPACING = 0.0561, + row, + column; + + row = Math.floor(index / ITEMS_PER_ROW); + column = index % ITEMS_PER_ROW; + + return { + x: COLUMN_ZERO_OFFSET + column * COLUMN_SPACING, + y: ROW_ZERO_Y_OFFSET - row * ROW_SPACING, + z: UIT.dimensions.panel.z + PALETTE_ENTITY_DIMENSIONS.z / 2 + }; + } + function display() { // Creates and shows menu entities. var handJointIndex, @@ -329,8 +351,10 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { for (i = 0, length = PALETTE_ITEMS.length; i < length; i += 1) { properties = Object.clone(PALETTE_ITEMS[i].overlay.properties); - properties.parentID = paletteOriginOverlay; + properties.parentID = palettePanelOverlay; + properties.localPosition = itemPosition(i); paletteItemOverlays[i] = Overlays.addOverlay(PALETTE_ITEMS[i].overlay.type, properties); + paletteItemPositions[i] = properties.localPosition; } // Prepare cube highlight overlay. diff --git a/scripts/vr-edit/modules/uit.js b/scripts/vr-edit/modules/uit.js index 4c2362044e..2a5301e0e3 100644 --- a/scripts/vr-edit/modules/uit.js +++ b/scripts/vr-edit/modules/uit.js @@ -15,9 +15,10 @@ UIT = (function () { return { colors: { + white: { red: 0xff, green: 0xff, blue: 0xff }, + faintGray: { red: 0xe3, green: 0xe3, blue: 0xe3 }, baseGray: { red: 0x40, green: 0x40, blue: 0x40 }, - blueHighlight: { red: 0x00, green: 0xbf, blue: 0xef }, - white: { red: 0xff, green: 0xff, blue: 0xff } + blueHighlight: { red: 0x00, green: 0xbf, blue: 0xef } }, // Coordinate system: UI lies in x-y plane with the front surface being +z. From 44778e791f9281521db799511510179c65465612 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 22 Aug 2017 22:55:43 +1200 Subject: [PATCH 229/722] Fix entity creation position --- scripts/vr-edit/modules/createPalette.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index c28d8421e0..366fc51c44 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -267,15 +267,17 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { // Press item and create new entity. isTriggerClicked = controlHand.triggerClicked(); if (highlightedItem !== NONE && pressedItem === NONE && isTriggerClicked && !wasTriggerClicked) { + // Press item. Overlays.editOverlay(paletteItemOverlays[itemIndex], { localPosition: Vec3.sum(paletteItemPositions[itemIndex], PRESS_DELTA) }); pressedItem = itemIndex; + // Create entity. properties = Object.clone(PALETTE_ITEMS[itemIndex].entity); properties.position = Vec3.sum(controlHand.palmPosition(), Vec3.multiplyQbyV(controlHand.orientation(), - Vec3.sum({ x: 0, y: properties.dimensions.z + 0.01, z: 0 }, CREATE_OFFSET))); + Vec3.sum({ x: 0, y: properties.dimensions.z / 2, z: 0 }, CREATE_OFFSET))); properties.rotation = Quat.multiply(controlHand.orientation(), INVERSE_HAND_BASIS_ROTATION); Entities.addEntity(properties); From d6a23abb7c304b3bd279fc689fd03939a6c9903a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 23 Aug 2017 09:30:19 +1200 Subject: [PATCH 230/722] Tools menu header and panel --- .../create/{create.svg => create-heading.svg} | 0 .../vr-edit/assets/tools/tools-heading.svg | 12 +++ scripts/vr-edit/modules/createPalette.js | 14 +-- scripts/vr-edit/modules/toolMenu.js | 90 +++++++++++++++---- 4 files changed, 93 insertions(+), 23 deletions(-) rename scripts/vr-edit/assets/create/{create.svg => create-heading.svg} (100%) create mode 100644 scripts/vr-edit/assets/tools/tools-heading.svg diff --git a/scripts/vr-edit/assets/create/create.svg b/scripts/vr-edit/assets/create/create-heading.svg similarity index 100% rename from scripts/vr-edit/assets/create/create.svg rename to scripts/vr-edit/assets/create/create-heading.svg diff --git a/scripts/vr-edit/assets/tools/tools-heading.svg b/scripts/vr-edit/assets/tools/tools-heading.svg new file mode 100644 index 0000000000..e180ae7251 --- /dev/null +++ b/scripts/vr-edit/assets/tools/tools-heading.svg @@ -0,0 +1,12 @@ + + + + TOOLS + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 366fc51c44..fe8b04cc16 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -34,7 +34,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { z: UIT.dimensions.canvasSeparation + UIT.dimensions.canvas.x / 2 }, PALETTE_ORIGIN_ROTATION = Quat.ZERO, - lateralOffset, + paletteLateralOffset, PALETTE_ORIGIN_PROPERTIES = { dimensions: { x: 0.005, y: 0.005, z: 0.005 }, @@ -78,7 +78,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { }, PALETTE_TITLE_PROPERTIES = { - url: "../assets/create/create.svg", + url: "../assets/create/create-heading.svg", scale: 0.0363, localPosition: { x: 0, y: 0, z: PALETTE_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOffset }, localRotation: Quat.ZERO, @@ -221,7 +221,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { side = hand; controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); controlJointName = side === LEFT_HAND ? "LeftHand" : "RightHand"; - lateralOffset = side === LEFT_HAND ? -UIT.dimensions.handLateralOffset : UIT.dimensions.handLateralOffset; + paletteLateralOffset = side === LEFT_HAND ? -UIT.dimensions.handLateralOffset : UIT.dimensions.handLateralOffset; } setHand(side); @@ -330,27 +330,27 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { // Calculate position to put palette. properties = Object.clone(PALETTE_ORIGIN_PROPERTIES); properties.parentJointIndex = handJointIndex; - properties.localPosition = Vec3.sum(PALETTE_ORIGIN_POSITION, { x: lateralOffset, y: 0, z: 0 }); + properties.localPosition = Vec3.sum(PALETTE_ORIGIN_POSITION, { x: paletteLateralOffset, y: 0, z: 0 }); paletteOriginOverlay = Overlays.addOverlay("sphere", properties); - // Create palette. + // Header. properties = Object.clone(PALETTE_HEADER_PROPERTIES); properties.parentID = paletteOriginOverlay; paletteHeaderOverlay = Overlays.addOverlay("cube", properties); - properties = Object.clone(PALETTE_HEADER_BAR_PROPERTIES); properties.parentID = paletteOriginOverlay; paletteHeaderBarOverlay = Overlays.addOverlay("cube", properties); - properties = Object.clone(PALETTE_TITLE_PROPERTIES); properties.parentID = paletteHeaderOverlay; properties.url = Script.resolvePath(properties.url); paletteTitleOverlay = Overlays.addOverlay("image3d", properties); + // Palette background. properties = Object.clone(PALETTE_PANEL_PROPERTIES); properties.parentID = paletteOriginOverlay; palettePanelOverlay = Overlays.addOverlay("cube", properties); + // Palette items. for (i = 0, length = PALETTE_ITEMS.length; i < length; i += 1) { properties = Object.clone(PALETTE_ITEMS[i].overlay.properties); properties.parentID = palettePanelOverlay; diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolMenu.js index 6095db7bb3..8d30609f43 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolMenu.js @@ -18,6 +18,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { var attachmentJointName, menuOriginOverlay, + menuHeaderOverlay, + menuHeaderBarOverlay, + menuTitleOverlay, menuPanelOverlay, menuOverlays = [], @@ -33,12 +36,11 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightOverlay, LEFT_HAND = 0, - - /* Coordinate system: UI lies in x-y plane with the front surface being in the +ve z direction. */ - CANVAS_SIZE = { x: 0.21, y: 0.13 }, - HAND_JOINT_OFFSET = 0.15, // Distance from hand (wrist) joint to center of panel. - PANELS_SEPARATION = 0.01, // Gap between Tools menu and Create panel. - PANEL_ORIGIN_POSITION = { x: -PANELS_SEPARATION - CANVAS_SIZE.x / 2, y: HAND_JOINT_OFFSET - CANVAS_SIZE.y / 2, z: 0 }, + PANEL_ORIGIN_POSITION = { + x: -UIT.dimensions.canvasSeparation - UIT.dimensions.canvas.x / 2, + y: UIT.dimensions.handOffset, + z: 0 + }, PANEL_ORIGIN_ROTATION = Quat.fromVec3Degrees({ x: 0, y: -90, z: 0 }), panelLateralOffset, @@ -50,18 +52,59 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { alpha: 1.0, parentID: Uuid.SELF, ignoreRayIntersection: true, - //visible: false - visible: true, + visible: false, displayInFront: true }, - MENU_PANEL_PROPERTIES = { - dimensions: { x: CANVAS_SIZE.x, y: CANVAS_SIZE.y, z: 0.01 }, - localPosition: { x: 0, y: 0, z: 0.0 }, + MENU_HEADER_PROPERTIES = { + dimensions: UIT.dimensions.header, + localPosition: { + x: 0, + y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.header.y / 2, + z: UIT.dimensions.header.z / 2 + }, localRotation: Quat.ZERO, - color: { red: 164, green: 164, blue: 164 }, - //alpha: 1.0, - alpha: 0.5, + color: UIT.colors.baseGray, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + }, + + MENU_HEADER_BAR_PROPERTIES = { + dimensions: UIT.dimensions.headerBar, + localPosition: { + x: 0, + y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.header.y - UIT.dimensions.headerBar.y / 2, + z: UIT.dimensions.headerBar.z / 2 + }, + localRotation: Quat.ZERO, + color: UIT.colors.blueHighlight, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + }, + + MENU_TITLE_PROPERTIES = { + url: "../assets/tools/tools-heading.svg", + scale: 0.0363, + localPosition: { x: 0, y: 0, z: MENU_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOffset }, + localRotation: Quat.ZERO, + color: UIT.colors.white, + alpha: 1.0, + emissive: true, + ignoreRayIntersection: true, + isFacingAvatar: false, + visible: true + }, + + MENU_PANEL_PROPERTIES = { + dimensions: UIT.dimensions.panel, + localPosition: { x: 0, y: UIT.dimensions.panel.y / 2 - UIT.dimensions.canvas.y / 2, z: UIT.dimensions.panel.z / 2 }, + localRotation: Quat.ZERO, + color: UIT.colors.baseGray, + alpha: 1.0, solid: true, ignoreRayIntersection: false, visible: true @@ -983,13 +1026,13 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { side = hand; controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); attachmentJointName = side === LEFT_HAND ? "LeftHand" : "RightHand"; - panelLateralOffset = side === LEFT_HAND ? -0.01 : 0.01; + panelLateralOffset = side === LEFT_HAND ? -UIT.dimensions.handLateralOffset : UIT.dimensions.handLateralOffset; } setHand(side); function getEntityIDs() { - return [menuPanelOverlay].concat(menuOverlays).concat(optionsOverlays); + return [menuPanelOverlay, menuHeaderOverlay, menuHeaderBarOverlay].concat(menuOverlays).concat(optionsOverlays); } function openMenu() { @@ -1976,6 +2019,18 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties.localPosition = Vec3.sum(properties.localPosition, { x: panelLateralOffset, y: 0, z: 0 }); menuOriginOverlay = Overlays.addOverlay("sphere", properties); + // Header. + properties = Object.clone(MENU_HEADER_PROPERTIES); + properties.parentID = menuOriginOverlay; + menuHeaderOverlay = Overlays.addOverlay("cube", properties); + properties = Object.clone(MENU_HEADER_BAR_PROPERTIES); + properties.parentID = menuOriginOverlay; + menuHeaderBarOverlay = Overlays.addOverlay("cube", properties); + properties = Object.clone(MENU_TITLE_PROPERTIES); + properties.parentID = menuHeaderOverlay; + properties.url = Script.resolvePath(properties.url); + menuTitleOverlay = Overlays.addOverlay("image3d", properties); + // Panel background. properties = Object.clone(MENU_PANEL_PROPERTIES); properties.parentID = menuOriginOverlay; @@ -2045,6 +2100,9 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } menuOverlays = []; + Overlays.deleteOverlay(menuHeaderOverlay); + Overlays.deleteOverlay(menuHeaderBarOverlay); + Overlays.deleteOverlay(menuTitleOverlay); Overlays.deleteOverlay(menuPanelOverlay); Overlays.deleteOverlay(menuOriginOverlay); From 7051ff8f1f5a713e9dc1f9e2efa4ce5a093ce114 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 23 Aug 2017 09:41:46 +1200 Subject: [PATCH 231/722] Rename "tool menu" to "tools menu" --- .../modules/{toolMenu.js => toolsMenu.js} | 18 ++++++------ scripts/vr-edit/vr-edit.js | 28 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) rename scripts/vr-edit/modules/{toolMenu.js => toolsMenu.js} (99%) diff --git a/scripts/vr-edit/modules/toolMenu.js b/scripts/vr-edit/modules/toolsMenu.js similarity index 99% rename from scripts/vr-edit/modules/toolMenu.js rename to scripts/vr-edit/modules/toolsMenu.js index 8d30609f43..5d47cda292 100644 --- a/scripts/vr-edit/modules/toolMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -1,5 +1,5 @@ // -// toolMenu.js +// toolsMenu.js // // Created by David Rowe on 22 Jul 2017. // Copyright 2017 High Fidelity, Inc. @@ -8,9 +8,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global App, ToolMenu */ +/* global App, ToolsMenu */ -ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { +ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Tool menu displayed on top of forearm. "use strict"; @@ -1015,8 +1015,8 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { doCommand; - if (!this instanceof ToolMenu) { - return new ToolMenu(); + if (!this instanceof ToolsMenu) { + return new ToolsMenu(); } controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); @@ -1674,7 +1674,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { break; default: - App.log(side, "ERROR: ToolMenu: Unexpected command! " + command); + App.log(side, "ERROR: ToolsMenu: Unexpected command! " + command); } }; @@ -1692,7 +1692,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } break; default: - App.log(side, "ERROR: ToolMenu: Unexpected command! " + command); + App.log(side, "ERROR: ToolsMenu: Unexpected command! " + command); } } @@ -2009,7 +2009,7 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (handJointIndex === NONE) { // Don't display if joint isn't available (yet) to attach to. // User can clear this condition by toggling the app off and back on once avatar finishes loading. - App.log(side, "ERROR: ToolMenu: Hand joint index isn't available!"); + App.log(side, "ERROR: ToolsMenu: Hand joint index isn't available!"); return; } @@ -2125,4 +2125,4 @@ ToolMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }; }; -ToolMenu.prototype = {}; +ToolsMenu.prototype = {}; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 7fc2537703..4c4af48635 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -58,7 +58,7 @@ Laser, Selection, ToolIcon, - ToolMenu, + ToolsMenu, // Miscellaneous UPDATE_LOOP_TIMEOUT = 16, @@ -80,7 +80,7 @@ Script.include("./modules/laser.js"); Script.include("./modules/selection.js"); Script.include("./modules/toolIcon.js"); - Script.include("./modules/toolMenu.js"); + Script.include("./modules/toolsMenu.js"); Script.include("./modules/uit.js"); @@ -196,7 +196,7 @@ // Tool menu and Create palette. var // Primary objects. - toolMenu, + toolsMenu, toolIcon, createPalette, @@ -210,7 +210,7 @@ } toolIcon = new ToolIcon(otherHand(side)); - toolMenu = new ToolMenu(side, leftInputs, rightInputs, uiCommandCallback); + toolsMenu = new ToolsMenu(side, leftInputs, rightInputs, uiCommandCallback); createPalette = new CreatePalette(side, leftInputs, rightInputs, uiCommandCallback); getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; @@ -218,7 +218,7 @@ function setHand(side) { toolIcon.setHand(otherHand(side)); - toolMenu.setHand(side); + toolsMenu.setHand(side); createPalette.setHand(side); getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; } @@ -233,17 +233,17 @@ function clearTool() { toolIcon.clear(); - toolMenu.clearTool(); + toolsMenu.clearTool(); } function setUIEntities() { - var uiEntityIDs = [].concat(toolMenu.entityIDs(), createPalette.entityIDs()); + var uiEntityIDs = [].concat(toolsMenu.entityIDs(), createPalette.entityIDs()); leftInputs.setUIEntities(side === RIGHT_HAND ? uiEntityIDs : []); rightInputs.setUIEntities(side === LEFT_HAND ? uiEntityIDs : []); } function display() { - toolMenu.display(); + toolsMenu.display(); createPalette.display(); setUIEntities(); isDisplaying = true; @@ -254,21 +254,21 @@ if (isDisplaying) { intersection = getIntersection(); - toolMenu.update(intersection, grouping.groupsCount(), grouping.entitiesCount()); + toolsMenu.update(intersection, grouping.groupsCount(), grouping.entitiesCount()); createPalette.update(intersection.overlayID); toolIcon.update(); } } function doPickColor(color) { - toolMenu.doCommand("setColorFromPick", color); + toolsMenu.doCommand("setColorFromPick", color); } function clear() { leftInputs.setUIEntities([]); rightInputs.setUIEntities([]); toolIcon.clear(); - toolMenu.clear(); + toolsMenu.clear(); createPalette.clear(); isDisplaying = false; @@ -279,9 +279,9 @@ createPalette.destroy(); createPalette = null; } - if (toolMenu) { - toolMenu.destroy(); - toolMenu = null; + if (toolsMenu) { + toolsMenu.destroy(); + toolsMenu = null; } if (toolIcon) { toolIcon.destroy(); From 0ef03eb52ae32cf26a97d59d240b65c44078e01f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 23 Aug 2017 10:42:24 +1200 Subject: [PATCH 232/722] Remove extraneous panels --- scripts/vr-edit/modules/toolsMenu.js | 74 +--------------------------- 1 file changed, 2 insertions(+), 72 deletions(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 5d47cda292..92aab30b6c 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -116,19 +116,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { UI_HIGHLIGHT_COLOR = { red: 100, green: 240, blue: 100 }, UI_ELEMENTS = { - "panel": { - overlay: "cube", - properties: { - dimensions: { x: 0.10, y: 0.12, z: 0.01 }, - localRotation: Quat.ZERO, - color: { red: 192, green: 192, blue: 192 }, - //alpha: 1.0, - alpha: 0.5, - solid: true, - ignoreRayIntersection: false, - visible: true - } - }, "button": { overlay: "cube", properties: { @@ -369,13 +356,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { OPTONS_PANELS = { scaleOptions: [ - { - id: "scaleOptionsPanel", - type: "panel", - properties: { - localPosition: { x: 0.0, y: 0.0, z: 0.005 } - } - }, { id: "scaleFinishButton", type: "button", @@ -391,13 +371,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } ], cloneOptions: [ - { - id: "cloneOptionsPanel", - type: "panel", - properties: { - localPosition: { x: 0.0, y: 0.0, z: 0.005 } - } - }, { id: "cloneFinishButton", type: "button", @@ -413,13 +386,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } ], groupOptions: [ - { - id: "groupOptionsPanel", - type: "panel", - properties: { - localPosition: { x: 0.0, y: 0.0, z: 0.005 } - } - }, { id: "groupButton", type: "button", @@ -450,13 +416,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } ], colorOptions: [ - { - id: "colorOptionsPanel", - type: "panel", - properties: { - localPosition: { x: 0.0, y: 0.0, z: 0.005 } - } - }, { id: "colorCircle", type: "colorCircle", @@ -586,14 +545,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } ], physicsOptions: [ - { - id: "physicsOptionsPanel", - type: "panel", - properties: { - localPosition: { x: 0.0, y: 0.0, z: 0.005 } - } - }, - { id: "propertiesLabel", type: "label", @@ -846,13 +797,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } ], deleteOptions: [ - { - id: "deleteOptionsPanel", - type: "panel", - properties: { - localPosition: { x: 0.0, y: 0.0, z: 0.005 } - } - }, { id: "deleteFinishButton", type: "button", @@ -870,13 +814,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, MENU_ITEMS = [ - { - id: "toolsMenuPanel", - type: "panel", - properties: { - localPosition: { x: 0, y: 0, z: 0.005 } - } - }, { id: "scaleButton", type: "button", @@ -1041,7 +978,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { i, length; - parentID = menuPanelOverlay; // Menu panel parents to background panel. + parentID = menuPanelOverlay; for (i = 0, length = MENU_ITEMS.length; i < length; i += 1) { properties = Object.clone(UI_ELEMENTS[MENU_ITEMS[i].type].properties); properties = Object.merge(properties, MENU_ITEMS[i].properties); @@ -1053,7 +990,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties.parentID = menuOverlays[menuOverlays.length - 1]; Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); } - parentID = menuOverlays[0]; // Menu buttons parent to menu panel. } } @@ -1118,14 +1054,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Remove menu items. closeMenu(); - // TODO: Remove once all tools have an options panel. - if (OPTONS_PANELS[toolOptions] === undefined) { - return; - } - // Open specified options panel. optionsItems = OPTONS_PANELS[toolOptions]; - parentID = menuPanelOverlay; // Menu panel parents to background panel. + parentID = menuPanelOverlay; for (i = 0, length = optionsItems.length; i < length; i += 1) { properties = Object.clone(UI_ELEMENTS[optionsItems[i].type].properties); if (optionsItems[i].properties) { @@ -1323,7 +1254,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Overlays.addOverlay(UI_ELEMENTS.circlePointer.overlay, auxiliaryProperties); } - parentID = optionsOverlays[0]; // Menu buttons parent to menu panel. optionsEnabled.push(true); } From 1aba95ecb8319700f3295263e0f35ea440b101de Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 23 Aug 2017 18:20:51 +1200 Subject: [PATCH 233/722] Style Tools menu buttons --- scripts/vr-edit/assets/tools/clone-icon.svg | 14 + scripts/vr-edit/assets/tools/clone-label.svg | 12 + scripts/vr-edit/assets/tools/color-icon.svg | 15 + scripts/vr-edit/assets/tools/color-label.svg | 12 + scripts/vr-edit/assets/tools/delete-icon.svg | 14 + scripts/vr-edit/assets/tools/delete-label.svg | 12 + scripts/vr-edit/assets/tools/group-icon.svg | 33 ++ scripts/vr-edit/assets/tools/group-label.svg | 12 + scripts/vr-edit/assets/tools/physics-icon.svg | 15 + .../vr-edit/assets/tools/physics-label.svg | 12 + scripts/vr-edit/assets/tools/stretch-icon.svg | 14 + .../vr-edit/assets/tools/stretch-label.svg | 12 + scripts/vr-edit/assets/tools/tool-label.svg | 12 + scripts/vr-edit/modules/createPalette.js | 2 +- scripts/vr-edit/modules/toolsMenu.js | 381 ++++++++++++++---- scripts/vr-edit/modules/uit.js | 13 +- 16 files changed, 503 insertions(+), 82 deletions(-) create mode 100644 scripts/vr-edit/assets/tools/clone-icon.svg create mode 100644 scripts/vr-edit/assets/tools/clone-label.svg create mode 100644 scripts/vr-edit/assets/tools/color-icon.svg create mode 100644 scripts/vr-edit/assets/tools/color-label.svg create mode 100644 scripts/vr-edit/assets/tools/delete-icon.svg create mode 100644 scripts/vr-edit/assets/tools/delete-label.svg create mode 100644 scripts/vr-edit/assets/tools/group-icon.svg create mode 100644 scripts/vr-edit/assets/tools/group-label.svg create mode 100644 scripts/vr-edit/assets/tools/physics-icon.svg create mode 100644 scripts/vr-edit/assets/tools/physics-label.svg create mode 100644 scripts/vr-edit/assets/tools/stretch-icon.svg create mode 100644 scripts/vr-edit/assets/tools/stretch-label.svg create mode 100644 scripts/vr-edit/assets/tools/tool-label.svg diff --git a/scripts/vr-edit/assets/tools/clone-icon.svg b/scripts/vr-edit/assets/tools/clone-icon.svg new file mode 100644 index 0000000000..324c7d57ba --- /dev/null +++ b/scripts/vr-edit/assets/tools/clone-icon.svg @@ -0,0 +1,14 @@ + + + + clone-icon + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/clone-label.svg b/scripts/vr-edit/assets/tools/clone-label.svg new file mode 100644 index 0000000000..1a141714e8 --- /dev/null +++ b/scripts/vr-edit/assets/tools/clone-label.svg @@ -0,0 +1,12 @@ + + + + CLONE + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/color-icon.svg b/scripts/vr-edit/assets/tools/color-icon.svg new file mode 100644 index 0000000000..9363b7607f --- /dev/null +++ b/scripts/vr-edit/assets/tools/color-icon.svg @@ -0,0 +1,15 @@ + + + + color-icon + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/color-label.svg b/scripts/vr-edit/assets/tools/color-label.svg new file mode 100644 index 0000000000..008b7b963d --- /dev/null +++ b/scripts/vr-edit/assets/tools/color-label.svg @@ -0,0 +1,12 @@ + + + + COLOR + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/delete-icon.svg b/scripts/vr-edit/assets/tools/delete-icon.svg new file mode 100644 index 0000000000..f77d40f1e6 --- /dev/null +++ b/scripts/vr-edit/assets/tools/delete-icon.svg @@ -0,0 +1,14 @@ + + + + delete-icon + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/delete-label.svg b/scripts/vr-edit/assets/tools/delete-label.svg new file mode 100644 index 0000000000..e63000e209 --- /dev/null +++ b/scripts/vr-edit/assets/tools/delete-label.svg @@ -0,0 +1,12 @@ + + + + DELETE + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/group-icon.svg b/scripts/vr-edit/assets/tools/group-icon.svg new file mode 100644 index 0000000000..56abd0a30c --- /dev/null +++ b/scripts/vr-edit/assets/tools/group-icon.svg @@ -0,0 +1,33 @@ + + + + group-icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/group-label.svg b/scripts/vr-edit/assets/tools/group-label.svg new file mode 100644 index 0000000000..001ce5b953 --- /dev/null +++ b/scripts/vr-edit/assets/tools/group-label.svg @@ -0,0 +1,12 @@ + + + + GROUP + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/physics-icon.svg b/scripts/vr-edit/assets/tools/physics-icon.svg new file mode 100644 index 0000000000..ef2635c312 --- /dev/null +++ b/scripts/vr-edit/assets/tools/physics-icon.svg @@ -0,0 +1,15 @@ + + + + physics-icon + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/physics-label.svg b/scripts/vr-edit/assets/tools/physics-label.svg new file mode 100644 index 0000000000..27006f62b8 --- /dev/null +++ b/scripts/vr-edit/assets/tools/physics-label.svg @@ -0,0 +1,12 @@ + + + + PHYSICS + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/stretch-icon.svg b/scripts/vr-edit/assets/tools/stretch-icon.svg new file mode 100644 index 0000000000..dc0547b813 --- /dev/null +++ b/scripts/vr-edit/assets/tools/stretch-icon.svg @@ -0,0 +1,14 @@ + + + + stretch-icon + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/stretch-label.svg b/scripts/vr-edit/assets/tools/stretch-label.svg new file mode 100644 index 0000000000..0aecd01a84 --- /dev/null +++ b/scripts/vr-edit/assets/tools/stretch-label.svg @@ -0,0 +1,12 @@ + + + + STRETCH + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/tool-label.svg b/scripts/vr-edit/assets/tools/tool-label.svg new file mode 100644 index 0000000000..bc8b059bdb --- /dev/null +++ b/scripts/vr-edit/assets/tools/tool-label.svg @@ -0,0 +1,12 @@ + + + + TOOL + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index fe8b04cc16..91bb7f5db8 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -80,7 +80,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { PALETTE_TITLE_PROPERTIES = { url: "../assets/create/create-heading.svg", scale: 0.0363, - localPosition: { x: 0, y: 0, z: PALETTE_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOffset }, + localPosition: { x: 0, y: 0, z: PALETTE_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, localRotation: Quat.ZERO, color: UIT.colors.white, alpha: 1.0, diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 92aab30b6c..61ba482bf1 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -24,6 +24,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { menuPanelOverlay, menuOverlays = [], + menuHoverOverlays = [], optionsOverlays = [], optionsOverlaysIDs = [], // Text ids (names) of options overlays. @@ -89,7 +90,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { MENU_TITLE_PROPERTIES = { url: "../assets/tools/tools-heading.svg", scale: 0.0363, - localPosition: { x: 0, y: 0, z: MENU_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOffset }, + localPosition: { x: 0, y: 0, z: MENU_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, localRotation: Quat.ZERO, color: UIT.colors.white, alpha: 1.0, @@ -127,6 +128,71 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, + "menuButton": { + overlay: "cube", // Invisible cube for hit area. + properties: { + dimensions: UIT.dimensions.itemCollisionZone, + localRotation: Quat.ZERO, + alpha: 0.0, // Invisible. + solid: true, + ignoreRayIntersection: false, + visible: true // So that laser intersects. + }, + hoverButton: { + overlay: "shape", + properties: { + shape: "Cylinder", + dimensions: { + x: UIT.dimensions.menuButton.x, + y: UIT.dimensions.menuButton.z, + z: UIT.dimensions.menuButton.y + }, + localPosition: UIT.dimensions.menuButtonIconOffset, + localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: -90 }), + color: UIT.colors.greenHighlight, + alpha: 1.0, + emissive: true, // TODO: This has no effect. + solid: true, + ignoreRayIntersection: true, + visible: false + } + }, + icon: { + // Relative to hoverButton. + type: "image", + properties: { + localPosition: { x: 0, y: UIT.dimensions.menuButton.z / 2 + UIT.dimensions.imageOverlayOffset, z: 0 }, + localRotation: Quat.fromVec3Degrees({ x: -90, y: 90, z: 0 }), + color: UIT.colors.lightGrayText + } + }, + label: { + // Relative to menuButton. + type: "image", + properties: { + localPosition: { + x: 0, + y: UIT.dimensions.menuButtonLabelYOffset, + z: -UIT.dimensions.itemCollisionZone.z / 2 + UIT.dimensions.imageOverlayOffset + }, + color: UIT.colors.white + } + }, + sublabel: { + // Relative to menuButton. + type: "image", + properties: { + url: "../assets/tools/tool-label.svg", + scale: 0.0152, + localPosition: { + x: 0, + y: UIT.dimensions.menuButtonSublabelYOffset, + z: -UIT.dimensions.itemCollisionZone.z / 2 + UIT.dimensions.imageOverlayOffset + }, + color: UIT.colors.lightGrayText + } + } + }, "toggleButton": { overlay: "cube", properties: { @@ -186,11 +252,11 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { "image": { overlay: "image3d", properties: { - dimensions: { x: 0.1, y: 0.1 }, localPosition: { x: 0, y: 0, z: 0 }, localRotation: Quat.ZERO, color: { red: 255, green: 255, blue: 255 }, alpha: 1.0, + emissive: true, ignoreRayIntersection: true, isFacingAvatar: false, visible: true @@ -329,7 +395,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, - BUTTON_UI_ELEMENTS = ["button", "toggleButton", "swatch"], + BUTTON_UI_ELEMENTS = ["button", "menuButton", "toggleButton", "swatch"], BUTTON_PRESS_DELTA = { x: 0, y: 0, z: -0.004 }, SLIDER_UI_ELEMENTS = ["barSlider", "imageSlider"], @@ -813,54 +879,34 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { ] }, + MENU_ITEM_XS = [-0.08415, -0.02805, 0.02805, 0.08415], + MENU_ITEM_YS = [0.058, 0.002, -0.054], + MENU_ITEMS = [ - { - id: "scaleButton", - type: "button", - properties: { - localPosition: { x: -0.022, y: 0.04, z: 0.005 }, - color: { red: 0, green: 240, blue: 240 } - }, - label: " SCALE", - toolOptions: "scaleOptions", - callback: { - method: "scaleTool" - } - }, - { - id: "cloneButton", - type: "button", - properties: { - localPosition: { x: 0.022, y: 0.04, z: 0.005 }, - color: { red: 240, green: 240, blue: 0 } - }, - label: " CLONE", - toolOptions: "cloneOptions", - callback: { - method: "cloneTool" - } - }, - { - id: "groupButton", - type: "button", - properties: { - localPosition: { x: -0.022, y: 0.0, z: 0.005 }, - color: { red: 220, green: 60, blue: 220 } - }, - label: " GROUP", - toolOptions: "groupOptions", - callback: { - method: "groupTool" - } - }, { id: "colorButton", - type: "button", + type: "menuButton", properties: { - localPosition: { x: 0.022, y: 0.0, z: 0.005 }, - color: { red: 220, green: 220, blue: 220 } + localPosition: { + x: MENU_ITEM_XS[0], + y: MENU_ITEM_YS[0], + z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2 + } + }, + icon: { + type: "image", + properties: { + url: "../assets/tools/color-icon.svg", + dimensions: { x: 0.0165, y: 0.0187 } + } + }, + label: { + type: "image", + properties: { + url: "../assets/tools/color-label.svg", + scale: 0.0241 + } }, - label: " COLOR", toolOptions: "colorOptions", callback: { method: "colorTool", @@ -868,13 +914,116 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, { - id: "physicsButton", - type: "button", + id: "scaleButton", + type: "menuButton", properties: { - localPosition: { x: -0.022, y: -0.04, z: 0.005 }, - color: { red: 60, green: 60, blue: 240 } + localPosition: { + x: MENU_ITEM_XS[1], + y: MENU_ITEM_YS[0], + z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2 + } + }, + icon: { + type: "image", + properties: { + url: "../assets/tools/stretch-icon.svg", + dimensions: { x: 0.0167, y: 0.0167 } + } + }, + label: { + type: "image", + properties: { + url: "../assets/tools/stretch-label.svg", + scale: 0.0311 + } + }, + toolOptions: "scaleOptions", + callback: { + method: "scaleTool" + } + }, + { + id: "cloneButton", + type: "menuButton", + properties: { + localPosition: { + x: MENU_ITEM_XS[2], + y: MENU_ITEM_YS[0], + z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2 + } + }, + icon: { + type: "image", + properties: { + url: "../assets/tools/clone-icon.svg", + dimensions: { x: 0.0154, y: 0.0155 } + } + }, + label: { + type: "image", + properties: { + url: "../assets/tools/clone-label.svg", + scale: 0.0231 + } + }, + toolOptions: "cloneOptions", + callback: { + method: "cloneTool" + } + }, + { + id: "groupButton", + type: "menuButton", + properties: { + localPosition: { + x: MENU_ITEM_XS[3], + y: MENU_ITEM_YS[0], + z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2 + } + }, + icon: { + type: "image", + properties: { + url: "../assets/tools/group-icon.svg", + dimensions: { x: 0.0161, y: 0.0114 } + } + }, + label: { + type: "image", + properties: { + url: "../assets/tools/group-label.svg", + scale: 0.0250 + } + }, + toolOptions: "groupOptions", + callback: { + method: "groupTool" + } + }, + { + id: "physicsButton", + type: "menuButton", + properties: { + localPosition: { + x: MENU_ITEM_XS[0], + y: MENU_ITEM_YS[1], + z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2 + } + }, + icon: { + type: "image", + properties: { + url: "../assets/tools/physics-icon.svg", + dimensions: { x: 0.0180, y: 0.0198 } + } + }, + label: { + type: "image", + properties: { + url: "../assets/tools/physics-label.svg", + scale: 0.0297 + } }, - label: "PHYSICS", toolOptions: "physicsOptions", callback: { method: "physicsTool" @@ -882,12 +1031,28 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, { id: "deleteButton", - type: "button", + type: "menuButton", properties: { - localPosition: { x: 0.022, y: -0.04, z: 0.005 }, - color: { red: 240, green: 60, blue: 60 } + localPosition: { + x: MENU_ITEM_XS[1], + y: MENU_ITEM_YS[1], + z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2 + } + }, + icon: { + type: "image", + properties: { + url: "../assets/tools/delete-icon.svg", + dimensions: { x: 0.0161, y: 0.0161 } + } + }, + label: { + type: "image", + properties: { + url: "../assets/tools/delete-label.svg", + scale: 0.0254 + } }, - label: " DELETE", toolOptions: "deleteOptions", callback: { method: "deleteTool" @@ -920,6 +1085,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightedItems, highlightedSource, isHighlightingButton, + isHighlightingMenuButton, isHighlightingSlider, isHighlightingColorCircle, isHighlightingPicklist, @@ -973,23 +1139,56 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } function openMenu() { - var parentID, - properties, + var properties, + itemID, + buttonID, i, length; - parentID = menuPanelOverlay; for (i = 0, length = MENU_ITEMS.length; i < length; i += 1) { properties = Object.clone(UI_ELEMENTS[MENU_ITEMS[i].type].properties); properties = Object.merge(properties, MENU_ITEMS[i].properties); - properties.parentID = parentID; - menuOverlays.push(Overlays.addOverlay(UI_ELEMENTS[MENU_ITEMS[i].type].overlay, properties)); + properties.parentID = menuPanelOverlay; + itemID = Overlays.addOverlay(UI_ELEMENTS[MENU_ITEMS[i].type].overlay, properties); + menuOverlays[i] = itemID; + if (MENU_ITEMS[i].label) { properties = Object.clone(UI_ELEMENTS.label.properties); properties.text = MENU_ITEMS[i].label; - properties.parentID = menuOverlays[menuOverlays.length - 1]; + properties.parentID = itemID; Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); } + + if (MENU_ITEMS[i].type === "menuButton") { + // Hover button. + properties = Object.clone(UI_ELEMENTS.menuButton.hoverButton.properties); + properties.parentID = itemID; + buttonID = Overlays.addOverlay(UI_ELEMENTS.menuButton.hoverButton.overlay, properties); + menuHoverOverlays[i] = buttonID; + + // Icon. + properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.icon.type].properties); + properties = Object.merge(properties, UI_ELEMENTS.menuButton.icon.properties); + properties = Object.merge(properties, MENU_ITEMS[i].icon.properties); + properties.url = Script.resolvePath(properties.url); + properties.parentID = buttonID; + Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.icon.type].overlay, properties); + + // Label. + properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.label.type].properties); + properties = Object.merge(properties, UI_ELEMENTS.menuButton.label.properties); + properties = Object.merge(properties, MENU_ITEMS[i].label.properties); + properties.url = Script.resolvePath(properties.url); + properties.parentID = itemID; + Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.label.type].overlay, properties); + + // Sublabel. + properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.sublabel.type].properties); + properties = Object.merge(properties, UI_ELEMENTS.menuButton.sublabel.properties); + properties.url = Script.resolvePath(properties.url); + properties.parentID = itemID; + Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.sublabel.type].overlay, properties); + } } } @@ -1006,6 +1205,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } menuOverlays = []; + menuHoverOverlays = []; pressedItem = null; } @@ -1146,7 +1346,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { delete childProperties.dimensions; childProperties.scale = properties.dimensions.y; imageOffset += IMAGE_OFFSET; - childProperties.emissive = true; if (optionsItems[i].useBaseColor) { childProperties.color = properties.color; } @@ -1164,6 +1363,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { childProperties.drawInFront = true; // TODO: Work-around for rendering bug; remove when bug fixed. delete childProperties.dimensions; childProperties.scale = properties.dimensions.y; + childProperties.emissive = false; imageOffset += IMAGE_OFFSET; childProperties.localPosition = { x: 0, y: 0, z: properties.dimensions.z / 2 + imageOffset }; childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; @@ -1197,7 +1397,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { delete childProperties.dimensions; childProperties.scale = 0.95 * properties.dimensions.x; // TODO: Magic number. imageOffset += IMAGE_OFFSET; - childProperties.emissive = true; childProperties.localPosition = { x: 0, y: properties.dimensions.y / 2 + imageOffset, z: 0 }; childProperties.localRotation = Quat.fromVec3Degrees({ x: -90, y: 90, z: 0 }); childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; @@ -1212,6 +1411,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { delete childProperties.dimensions; childProperties.scale = 0.95 * properties.dimensions.x; // TODO: Magic number. imageOffset += IMAGE_OFFSET; + childProperties.emissive = false; childProperties.localPosition = { x: 0, y: properties.dimensions.y / 2 + imageOffset, z: 0 }; childProperties.localRotation = Quat.fromVec3Degrees({ x: 90, y: 90, z: 0 }); childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; @@ -1676,22 +1876,34 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (intersectedItem !== NONE && intersectionItems[intersectedItem] && (intersectionItems[intersectedItem].command !== undefined || intersectionItems[intersectedItem].callback !== undefined)) { - // Lower old slider or color circle. - if (isHighlightingSlider || isHighlightingColorCircle) { - localPosition = highlightedItems[highlightedItem].properties.localPosition; + if (isHighlightingMenuButton) { + // Lower menu button. + Overlays.editOverlay(menuHoverOverlays[highlightedItem], { + localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, + visible: false + }); + } else if (isHighlightingSlider || isHighlightingColorCircle) { + // Lower old slider or color circle. Overlays.editOverlay(highlightedSource[highlightedItem], { - localPosition: localPosition + localPosition: highlightedItems[highlightedItem].properties.localPosition }); } // Update status variables. highlightedItem = intersectedItem; highlightedItems = intersectionItems; isHighlightingButton = BUTTON_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; + isHighlightingMenuButton = intersectionItems[highlightedItem].type === "menuButton"; isHighlightingSlider = SLIDER_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; isHighlightingColorCircle = COLOR_CIRCLE_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; isHighlightingPicklist = PICKLIST_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; - // Raise new slider or color circle. - if (isHighlightingSlider || isHighlightingColorCircle) { + if (isHighlightingMenuButton) { + // Raise menu button. + Overlays.editOverlay(menuHoverOverlays[highlightedItem], { + localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, ITEM_RAISE_DELTA), + visible: true + }); + } else if (isHighlightingSlider || isHighlightingColorCircle) { + // Raise new slider or color circle. localPosition = intersectionItems[highlightedItem].properties.localPosition; Overlays.editOverlay(intersectionOverlays[highlightedItem], { localPosition: Vec3.sum(localPosition, ITEM_RAISE_DELTA) @@ -1719,7 +1931,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: HIGHLIGHT_PROPERTIES.properties.color, visible: true }); - } else { + } else if (!isHighlightingMenuButton) { Overlays.editOverlay(highlightOverlay, { parentID: intersectionOverlays[intersectedItem], dimensions: { @@ -1738,16 +1950,22 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Overlays.editOverlay(highlightOverlay, { visible: false }); - // Lower slider or color circle. - if (isHighlightingSlider || isHighlightingColorCircle) { - localPosition = highlightedItems[highlightedItem].properties.localPosition; + if (isHighlightingMenuButton) { + // Lower menu button. + Overlays.editOverlay(menuHoverOverlays[highlightedItem], { + localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, + visible: false + }); + // Lower slider or color circle. + } else if (isHighlightingSlider || isHighlightingColorCircle) { Overlays.editOverlay(highlightedSource[highlightedItem], { - localPosition: localPosition + localPosition: highlightedItems[highlightedItem].properties.localPosition }); } // Update status variables. highlightedItem = NONE; isHighlightingButton = false; + isHighlightingMenuButton = false; isHighlightingSlider = false; isHighlightingColorCircle = false; isHighlightingPicklist = false; @@ -1770,9 +1988,11 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { && isTriggerClicked && !wasTriggerClicked) { // Press new button. localPosition = intersectionItems[intersectedItem].properties.localPosition; - Overlays.editOverlay(intersectionOverlays[intersectedItem], { - localPosition: Vec3.sum(localPosition, BUTTON_PRESS_DELTA) - }); + if (!isHighlightingMenuButton) { + Overlays.editOverlay(intersectionOverlays[intersectedItem], { + localPosition: Vec3.sum(localPosition, BUTTON_PRESS_DELTA) + }); + } pressedSource = intersectionOverlays; pressedItem = { index: intersectedItem, @@ -1981,6 +2201,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightedItem = NONE; highlightedSource = null; isHighlightingButton = false; + isHighlightingMenuButton = false; isHighlightingSlider = false; isHighlightingColorCircle = false; isHighlightingPicklist = false; @@ -2020,15 +2241,15 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Overlays.deleteOverlay(highlightOverlay); for (i = 0, length = optionsOverlays.length; i < length; i += 1) { - Overlays.deleteOverlay(optionsOverlays[i]); - // Any auxiliary overlays parented to this overlay are automatically deleted. + Overlays.deleteOverlay(optionsOverlays[i]); // Automatically deletes any child overlays. } optionsOverlays = []; for (i = 0, length = menuOverlays.length; i < length; i += 1) { - Overlays.deleteOverlay(menuOverlays[i]); + Overlays.deleteOverlay(menuOverlays[i]); // Automatically deletes any child overlays. } menuOverlays = []; + menuHoverOverlays = []; Overlays.deleteOverlay(menuHeaderOverlay); Overlays.deleteOverlay(menuHeaderBarOverlay); diff --git a/scripts/vr-edit/modules/uit.js b/scripts/vr-edit/modules/uit.js index 2a5301e0e3..e9ba08ca24 100644 --- a/scripts/vr-edit/modules/uit.js +++ b/scripts/vr-edit/modules/uit.js @@ -17,11 +17,15 @@ UIT = (function () { colors: { white: { red: 0xff, green: 0xff, blue: 0xff }, faintGray: { red: 0xe3, green: 0xe3, blue: 0xe3 }, + lightGrayText: { red: 0xaf, green: 0xaf, blue: 0xaf }, baseGray: { red: 0x40, green: 0x40, blue: 0x40 }, + darkGray: { red: 0x12, green: 0x12, blue: 0x12 }, + greenHighlight: { red: 0x1f, green: 0xc6, blue: 0xa6 }, blueHighlight: { red: 0x00, green: 0xbf, blue: 0xef } }, // Coordinate system: UI lies in x-y plane with the front surface being +z. + // Offsets are relative to parents' centers. dimensions: { canvas: { x: 0.24, y: 0.24 }, // Overall UI size. canvasSeparation: 0.01, // Gap between Tools menu and Create panel. @@ -32,7 +36,14 @@ UIT = (function () { headerBar: { x: 0.24, y: 0.004, z: 0.012 }, panel: { x: 0.24, y: 0.18, z: 0.008 }, - imageOffset: 0.001 // Raise image above surface. + itemCollisionZone: { x: 0.0481, y: 0.0480, z: 0.0060 }, // Cursor intersection zone for Tools and Create items. + + menuButton: { x: 0.0267, y: 0.0267, z: 0.0040 }, + menuButtonIconOffset: { x: 0, y: 0.00935, z: -0.0050 }, // Non-hovered position. + menuButtonLabelYOffset: -0.00915, // Relative to itemCollisionZone. + menuButtonSublabelYOffset: -0.01775, // Relative to itemCollisionZone. + + imageOverlayOffset: 0.001 // Raise image above surface. } }; }()); From be45f600d4594fdf098e8a3ae36df6b735645612 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 23 Aug 2017 21:56:59 +1200 Subject: [PATCH 234/722] Style Create palette items --- scripts/vr-edit/assets/create/cone.fbx | Bin 0 -> 19056 bytes scripts/vr-edit/assets/create/cube.fbx | Bin 0 -> 17696 bytes scripts/vr-edit/assets/create/cylinder.fbx | Bin 0 -> 20240 bytes scripts/vr-edit/assets/create/sphere.fbx | Bin 0 -> 29312 bytes scripts/vr-edit/modules/createPalette.js | 181 ++++++++++----------- scripts/vr-edit/modules/toolsMenu.js | 21 ++- scripts/vr-edit/modules/uit.js | 12 +- 7 files changed, 112 insertions(+), 102 deletions(-) create mode 100644 scripts/vr-edit/assets/create/cone.fbx create mode 100644 scripts/vr-edit/assets/create/cube.fbx create mode 100644 scripts/vr-edit/assets/create/cylinder.fbx create mode 100644 scripts/vr-edit/assets/create/sphere.fbx diff --git a/scripts/vr-edit/assets/create/cone.fbx b/scripts/vr-edit/assets/create/cone.fbx new file mode 100644 index 0000000000000000000000000000000000000000..b883b042c56337c43754ddd3fa7614304cdb7c48 GIT binary patch literal 19056 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 CO_Y=X literal 0 HcmV?d00001 diff --git a/scripts/vr-edit/assets/create/cube.fbx b/scripts/vr-edit/assets/create/cube.fbx new file mode 100644 index 0000000000000000000000000000000000000000..530122b16efab5be9127898ba6f25bcf7d28872e GIT binary patch literal 17696 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 literal 0 HcmV?d00001 diff --git a/scripts/vr-edit/assets/create/cylinder.fbx b/scripts/vr-edit/assets/create/cylinder.fbx new file mode 100644 index 0000000000000000000000000000000000000000..250ce66773f3f970ef26c58798ac070956746bad GIT binary patch literal 20240 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 k*{AMK^Mn_F5AMHJE literal 0 HcmV?d00001 diff --git a/scripts/vr-edit/assets/create/sphere.fbx b/scripts/vr-edit/assets/create/sphere.fbx new file mode 100644 index 0000000000000000000000000000000000000000..b7a5a32952823de483a5cf4eaf26263339d95ca1 GIT binary patch literal 29312 zcmc(I30#s{7dNd=n^sn~xJ}DAO^ceD=7Q5|O)8d|mRqQ3h=8(*iSRZpW|_W?m1QY4 zO;NeFC=xC;nzo2esZb)CnvfE1xGV7eE@0tXHofzH-|xG>Uk3NN=YP*R_uO;tz0dO; zcEKY9;W)VQ_RXHgn~@keF4EW-vKaDS9|D>19pqz~yM43g4g@?9f!jtVATW3&7UK

M~~w4gptQMWPXI1UNdZQiEFSBkUvyq($SxiXw=VeMuvtW@v4iXGJP!h>Xs%D2t-RMC>H0o6as;O5pmnP%M|?Y8cGaCV(`W! zm3EESnVD}`Yu2L$flLG}Lm-f8ZUG1kVkagD>oy+(fjEX@{o$eDD`di_D?n&>JOZ~B zPJru8R0%%|0)b2hY9DjeOnns$?nea&0;VT@7ydu@O zqO}E!Mk6q)PR10G)~ByCU2)hj1df12;H}K=Pg0q79Izc%H((lDu>nMY;*JY-17y40 z2sk7r*bS%z)Bz+nUkC(Z3Y3gDg$Kcj0f!Jc(@{da=@u-8fP)7Rj6JY8l<5v+@FC;v z$UsCWk`QTXW?{Suk4F%UU2%voQ#3phZW@L|qLBn734u4ohaE!T5Nm_{$*z-CoF%9@ z(+0ib0!I7z|IFO4lOd4F5C~+siaB6$FdQ-%iGhbIzih#x!>~k5ph{UlW3SZWY*tY$ z)}Wwl3JVKG1}NwLJ_IHZiyNbNrM{JaKsTF+1Z*GzkJ6u_QjeVqZ{aw2E^str95lec zGdvQGR3ka4kd};tr1n2w0Rz*uL=EPw0t1tDR56sTZ~_AOHa|z_y-Ha|D>Os|UdY_s z)Xds+ote3n@j45e^%gdk=4;K&%-~bU7{nY+=PGy%V&s8O4ss-b>XLFtbN^=(E` zt44ECp)DALHfjX_&?>;h_=d9bJ5?M2200a2%g(?(vmp@38s=JI-pWT6_e+Xa-(Icg z@>RD363-kHkbz|an_OdNy~f=9qh;EdnVH>E6K&88Zsy+<+>HH+$k0Ihb!O(4X6EJ_ zX8|{-F50T-=7t~;keFb6ow}1Tu?j2(AR07deEaHYDtlO|Qu|ckzr!|>k$9ymM`8%d zxx5oYKm;RH0ca;!js&a8)~Lv)e+z+hpJ1h)WqC}KN20IagWxCdg1{-FrvM;9X6Up?Gg zRhHK2fL5cZr#mq9k8=F?XvH%i5U_CAq2g&Kh+1fWBmzU&f(^yu)b218i`(P4*$tSp zsv0n(=IBcw=FU_}x=Do!EMzMp2u=(oY=NT@IQTe$X&wXuam8T+i2(?lu`2?H4?_fi zrN%ZjoZ4Z7&AvmFN?zeAvnel;anYO5!Oa&Hg@$G8#TID%4nyOdSHaVtLnI;TCp4{+dv z)hw0H)_od@PXO4kap`Agw_e!~fk2|w^cvQ+g@`9$(Ms0$zzHgo`nq)$-JPY1jX$Wk z01gFk;S9&25Tn-cpUWkwy65#O8W8Na5OFvJhM++ToZlDSm_ngY0QtXMHiIEhmuaI5 zO1zEsr)|=dEm$Ikp!9~8I;zb8SYUlO+Suvf(>r=sX%L9YLVyh`w;$6rkjJ><2ijv) z08GaXOT+~rV5(gKn4{QF8w4e+Oj|X0wkj3qf@!2Q-Ax!I+Km7YKz${UEA1JMLKvHw z0q69o{HUW66Zq!u_F%CD7i=KH>C^S`N|1(V)At~P5I6)T0D&LVe7#0Ap0O1Xgv21h zeiomk?sH7&0^6)ijSFg>15J#mtAWVVRT@%m-9Vrl8x%V}2hiA2t4h-xmBe2{1FBT6 zyOsL1pZ!G>s8Is|Bb=??JVowBpuN3U#3TCUu6KtqQw^|5% z01l}<5*WvQ0iao0`va`JYzk9F*WP5XD7#xpRisJ5s#RP+Ys$c+^|2TiXv9v5unIu@$XgWNgZbt+bjUeD6Yt{A% z216r0y;hR~H}tu40?V-N{$yta{?H4s?NXO97K@tye|4}6)NsJEC%7s(*t{7_c1B{9 z;_O3!MXAMB<#bSeQL5}KD?gnDDhB@(5l;T6h)xT}_(ZVqcEusl;8=Jc5|8vpf|IH* zO%5<;kEw~(GaQWB7C4@OH+DM&55z{`$D=D$(`l>&_aFigND|`jD8I2B1E<*#$QP~1 z8BRdpknm7ry)Qby2sJ6NkTH zway`UAQBS{ChOQUML@6m11wdI+@uBo2fhY=XYX zj>?5T8+D-?A0UfV?5KB|Ks+_vH*o`7G-i$iZnh$Wf{6I9DQA3#9{)KvYPfIWMlhBe z5bHJ*(P95|Hk&V23AbHkFCe_QU~y=8s5AT@W8xN%F=DXh0xW8X_w`MW!xJ@A+f+<} zv!=Z`I0hdE2b-9GWRlaCsCWX$OcPv{Uh5NNK%@q5HOhjq0@ZRXl=;I>A+W8leglnsEOMr#|{B zMXKi}SX+GF!MF1}s*=AcCT1$v3v5X<$OBKH^o;pxC;T4~ezBVFs}UaT;$~@Vzs7iE zsDVmejfK!|A_xc=a1RqhK;Xi#%DoJjiT`o%0fb62Pz}f~)qp5>0qV6FEGP(%ApFyr z+o1;jMFkAJ+9P@hBa`($`6SN{@?gbRpSD!OmJ0&HWlvw#BQyjYJAqJ zV8NtRPHfc@SGDOMFYZ|weKs~4wEx4ZEAH#S|9tUPYNV1@V;wY>W6$wqg&6-0<^J&~ zZT^n(z<88|uT#Rw<8qt%ca-CD`~L4J$K}@edzFSX)~2Hq3Wf_rs8&y3nBjqHI(29P zw~j{X+``}ii0xRM6SzkU!-RfuT@1WLh8jb||9`|FAcKN12t3|Xx$OU{|A#>Oz5!_e zw*WaTRcYgE93A);plCIa`UD27Rl%(VCj{p2sh9s;wNy1l)#u^h9=j_J@liJ*+Wv!G z(ND`%s2aY=6^keQudo5jR60|iQfd17uR9`^njZ}*M|t@lypYcF&vf?@^`E<-?aNg{ zYMkVML`9D8f1J6-<)O7!YiG`03`fV20>7ZT}YkXk* zpc(}Yy?o+y+>jyo8r8V=rEBt)zQ!H7o#Rt|#ky!;qZ*gPoUc)h%i+8qRinLIHQM0B zReczS4A59H{8I-SriNEPl>k;q7(Mn`8<2 zI>tB2az~A^QDv=Q$kpTGM!+F3!GuHKG&Eg*3-e79S+7!QWqfYFNhsPEU{uQeuY@Z7 zTbOSWYS!w{g&NDvHwm>z4P&JmDX_$G#VPM-25!Oxhkn}>O8Og+ZxX2PZ$Q3DoVjaM z_mEknOb7r90?2AVgX4q+p{w0#0c ztVQbP>L&=m!-X!0U^sXJ!xe!G01vAkf`=j8h+$z^9AP|^DQi`os!_n59QCswn+OCP z(jPphj0r@Lzy3tP1~tku)wqLObK7wU#Nl!G*1?|>IKj4V#*)9pC)ffNuT@1Xbw~ks zjGO{OjWvVSm+AoNnB!x}7mn8!nT}ZrgVP{@{V`H~R`E@!Xf>+(>=dlu0P1Lr`p@s5 zVSXt>Wtgf)OMTn~R%se{RsQ4a7ZAZV3!H(ek50ioiBI&#>g6RI6M*>IB_%HS^Z5e- z-&hR7J~h-*J%xaeI;46U1X%xfqj%<nP3^R{)b0*4;`>PdvK!NEtUy7d;0mF z2~IoqYER2|_5Xc^b9M%1W%AX+lbw)1tQ-T@znY!CZPM!@gKg8%=_j`y_-$g;u1%+R zZ(XtQ`fVvaW#gMmw!@t%!?Hp}iUoU^W5vF2a$~Qd?-hp|=T1cT7v^6@_18Yv893Ym zFJ`QA`JiRA!O+T1CcWeLF9u%WPcHl*(UqR7cW6|UP!%0+=jjwi6@d>!0bOX$mo z+?!YS*;>g{2AV}_40%(b;EY!JK=HN*t!vmnRraU&zLgC&Ps+PMh-qvUMHx=$!0>|y z2=B`0$cvs-oe()|(IJd|W&1xQX>};<$qlr>Iydzo`13EEmbI5JC@!PNI%GxHbo!3a zNmM~t-!b2ZKNn3le67_lC%n_i>!&>&d8_-jx;)yVSC7)&yzH<;>dp_tT}HQxJ|tYu zx)((GmyFf2Drn@5ZhmOdxxt>6`-=RlOc}#Ly30GJ(icT$j=aM@mTqa-K*@if0J|$6 za-0c!aXI0LTeJL*jB=9ikS>A-07ZO10`?hPoV>E zN1iFQ^cw#dA6nLH}lIj>`r$Gf7Q9VuS~qQad3}q%8)eI^gyOu zv2uy8&-aQ+_N5N@blPRD&y2!4Xi!wk3SnkZ?9hw4GnK}N9a2MRVagevQ-7D%{D0S=@rB_@9k~rkq~`yNii)9ie7?gmDlemz1{EEd`|wPC#mngT@jV?BfY!-aYC@k-~nmt2gX3+ za+mWuO^VJ^4w2eBiIv-RTE4aGg=}V%p4}-w;ii#a9lK^2J~1WFD@xZ<)eXcQSWpfp^iPBE;>dUSngK7U%9N4id%$J^bq z>2`Pgj-XItk2_^K8Gen?dyD*2xJk)F^YggZ-&fic?O~p3^T;U7@H^tzyQ&cDTI6rL zNj9B=akfv4bfryAXsdi1?Me&E96Z2Fytg*4t&-AkRX)>qxeI^DOnOy5)3ApA1{!_h z=J1>4@}Dc|R=vUb32l|3eoePB9FC%>e>jMT_DjSa$F8tjqum?0^f&Qx%=%th=Pf1a zV-fd?V>Z;!lUcbWw}OIjE*>p95V{-vnC`|B)oPBqm~T49XTmUT=SrBWJhma zuE>#Wl%j0(_T}Il@^lp;S4-sh)+dZQxn4`;7#aiJQuR<*Rw$*ewoxFW!{jrdBC(L7 z>&KBHVNFi@uN|%6t zp(bSNB)h|7{vGXoEM1bWliC2`aiK-m4tw)OR-I5)o1>APBUAaaQX3!%sSO7W?TQ}v z9TizUx-PPMt=+zRNg0oj$2~vq6;XI!u8@W6tufi6T5?U827%0V#fC-(W5Fvd1O!>R zeLyaps=D6u9axeXyvA3*?l*AkTIwFc&qeb|A#9(Vj9fH7cMm`JOeBhz`6jw$qbRkq zz+_MqsVGz|FP>>pZdP16&?Wo#_geTZ84&(|a`Tss}Q@E_8q>IRLjN93i zmYgszu{6ojXyXs3O+%o+I2qD;l!H!l=rxq;i|e2ngNMxBDSMMoksXRzp9O)0fGg7PjB2W_=0-yP5AEXH!{ zNcW#Cv1AH9bRFiPtsL|B;rX0JS=>4j{>ds!rl771$3a`c^7rEToF%4Q%AJd*zPDHs zVS)&SVNYpdCG#|R9!EarMIB4eHnVBrfg?Psh3>(qZn9Rq#A>R__daX4~XGtz3XoXBP*JiOKxp6N-Fzij?NDi9m?!5=kM;qwz>PY;O zZI;Y82g2c!S`0hysxX%it;k|YNGGKs7`8!kT{0iTux1Ez`5P-TSrSt0{SXY>r@1_t zk6~J07v}QqU9wn`zLQlU7`A!W6At> zop1^V&7|pj;4h<>lyNg9+HnCP81~Vo_sRVBx7K%rxx)ea`|*6)%J^F>iCjO1S4WB| zfm<>K*KV;S+NZaKVA#dYm6lAwoGwN(zy0Yz00$khSKk}YH!#qusUww@9I<2y?uP%& zK}UFh@RMHSvn-i{X?OiNXwuIgyx6(6EBkJGbbV8bbvUS-~QX{JHidN=2L4jC2LQwL1EZ00!}CM+fTjD7v{3p zZAfEDGEX_9FzmE|#AJSZ^6NZdE_=giNUcWERhcB3)#;qeol>KDM z6xiRzanKRi^u2h#KyMYdj?`Z^-I6JobeF+E>6ERrWD1tuC2-JI z))(%uB!*{JpfKz*|JY=HdzWXvFgI%Hf;5&SHEtUU!)ExOP3E_^dgcj}N>lE-N^0BR zcxDT8qt-6C#ge2Z45Bb>J^#XFe*2S)Okr-+O5tsm#5bOW!mvI3Gn4u4H5cy-bEDP> zZ?Po432#tP>-v*#LNIKr0B$nBJ>%jHVQ!RdP$o-KAA2bT!;T0jP3E^3T)ZaCjdHk| z#p0yKwubOXMx_rdnF2?1XAZhQ+{p*ekDlvRQ%9ngp0i{Mg3Y&a(EX84zIcB0oWz{-lL$;f7)3(mOtU*_zUHmPBqCP+dn-oP3Fr?;Nq%p2m{M=U?a6kreU3{%HQ^ zlKJhTg)Cw2@QUqsSQ7b?W!ySaQR#Pqv=ZB1@x%ZY9B%Ou@M>BnNHfPT7O!I~y4B>PS^3j+RWpgDyA+ZRJC8#q*tw zI(T&?i;^fyrXa2h!$DiQ<-6he9DOXWjo%aSQb?+W6eH|);e&Cb1S@0-Pv2v71t zFzk)ZHOYKTCi#Xi_p+^TCQBlWO$x!VBb%Qj^D%|wYr@>i4yjoz$%2!j5Da@t^P6No z<{J5?FxSgEHIpS-5X%U`u(vfoOXg$ll7AQGdf6Ffu_PBxmW5#0E1F*>^FwnNdb4xA zYz%L+Bo|_1P#89%`C2j`!y;!3bG@L3w^)*WCx=lO_Wb6;WIpCb9#fd>wXyv+OR_JP zjl!_qn=_O7n4G-(!dx%=_FF6o_aq;MVOuumCi5{Fc{##dFI&uQmV_I78HHgJn{Oub zLvQEZC83$rI#)a&J)6g?BP}TXS{A7M;fi;taX1ZN*!}BHj!Mr*WyJWv5Q;-=R#6dH?uI$G1C58-M9Vw~=X~`6% zhKF#_Ophyj@O+7Z39pVcT(Z-WDJTs0=b)K>S6uOY+4u3l_a`)mVAzwIUMKV0A6efN z=Gy)kpUIMF$6pV@+&5@``p}|Bbdr+iByD6T>%0$vZuXGQVJ9b4*HGSN3a=9^jb7JK zsxyVxiIzqMX@d)WIAtF~p!+|JVn6 zlwCh1qF`G4VV%0(1$knFFy4~4nH`lr4XNxx+lXj<`%rydZw5`-+x@NEybTj;;zhP6Hk>c*=Bos+jSQ_mT@TJ^tOPMzQJa< zB3hBXJxn%n;>3xCfAR)w=-smlpD_nEG_*uf=tiB(sO7MMl@21kk>89B=lo)QcB}3# zyV&K4zt~&~Oxy)MKF{r!Mf3OW-eNFo{macu3-YvT8pRvo6J)3qezqI$&v*nk)yI1~kanJKx zS5GZ&J9IU6LGAmirwxY|9y~dx^y0x&i-InRR-Rsc_s?DX_6H}TNpG6}wamr3WsS(Y z`v?ld5*@i}EIT&WW%F z?U?S)^><4WcdWbpN8-*6taolZ%x|3Ay=lH>^~;}i*F1Q+NzcB={L$=XapsTbS(LA< zbX&0B|9DdRy^0_BAMTY+aJ&-s+^d51ZW6B!S2=0ZlzVw7eefVQqUegI%=-h?3=(GPkX?$v7(+=aPrA|-KcgQ1 zWL5-a`Clp1e>i5g^yJbL)^m3(J#O^dD4TXSul#c7_Kno-$;cZiE)NH1$!4xA|FPL= z9Y=R8n*+80b4dlcY)S`9$>yPHzfSu;?z`VlHrg)#_txpiz<+Pkj-2PZby~to#&01Z zw;5-HsLo?WKellJxfFjP^6{#|gJVUH%FJtwPni7B?eVeBMRT^!*`hyf`Pt2j7X_Z( zVmM{q?mq*b-~J=nZ#Qfi=fNu%6G6|bO5@_Vbrs92?BMZJQo9@hCinHh(+0lxE59$x zZ;XrZ?o3^U$!ktLD4VRidEplzMd#dhTIN(&?XbvrP_@&lpvU5=*>#{zeEI!H9fOm0 zt(Ka|%YQ1gG>{)V1Jt*#c*HSQru$3=+xURPO^S-#1jgs00{M9@wi1z%7x5A75en zPZDrbX)6+k2q5^Wd48=8foQ4t{B-93LR)#}e<12X(zf{qZtHfTSg+1sD*1VH#-U65 z_JuT!R6U>PG-tPKKzGfg`K~{2`}d~DPP)H@?`J)Dxu-lB*_X|gIUfw$G_Tw-=;ox7 z;qRvgU0P!^e8rwz5U#j1%#WZ&(eZ^15y8T8(Ulp)DOJ&N__dk(y}xKR{}p3Y;#nk! zoJsy6ReNFejXb03DfwN+=@kKwq?dFYzO&0POBp$fe>lHLp1#xPSC1xSQ_AThyykL7 zYqicohdvAXiI_BJhl-q+=Bf9Jy1HXN*ud=Qh==lt(rG16_Ru!dn)OPX%MR4_1_@tw z9=Q-~e+611KC>@r{VU1=yFuf$fP%ryOIu2A51zOiv39#p6Ir^lrcGDdqEI~CcbJQE zYUsa1Yu%FiZbrcctUyB^>U{Bg0(O(2Dks?ClzDyU6UM)^ zn!3(U>M0y>);g4bo}40k!gyqQzk)h%5lu&wEIxxwr6_eJo)R`10JPrE=zjTyMwc#CD%)4*=4P#FQBn1dcTfd185tkB7QWGqxh1HaaL<+Mt>e&+B{H9Ia zPeeZw+hT)nU#C**D5SP4I6=qy_=scY2cil@7kUjA9aV6a8ZbI!>pGq6u*ow0&OBlt z{?UudIQpx4(^ZN`4XgL$w|+3(H?h*B+}G{Bbff+(sr9-N^&9XNw?E)rcJZr7!?m5 zKCsz1#I;Y4r*=jReeV}x6kTTUaQaXteejrJ?*v0%OM1D@xwPA+gH0W`TUPXziOPrF zYoiCVTOXc&zol-LowWaCF|8>ftw!{l1()y+zlb`rCl2|>sh|Tsy$d1sHKPb-yiM9Q z3kKs<4=;4oFPcYA2_?_-=|uH+F5Auf*-jCBerTYo+yu71(6(gYI8?r_cvA2El1cUv ztj57$Q(wr)uw|Ng`$b-nCoyB+#ZO&pNW_^z$7 zj#9fzM1Mi|!L;lAz9G+Q%bcN+sG7x{M?1~8H^q-E9eOmQw+dI)pH73@^@cZZd!{$C zPpfjEc!@A^VS)owda}M@6`OaYqa*KeD)YS7+K#89fi~)$#Ex}#hQt_|pX`Idkm85A zmHfW@!+Cv=TH1!)nv+@u;;YRtoA*Ce=d_jy>X{SB^9$o0%&99HqU$Ni)T>kqhw`VS z+9A1X$H-sdm%=e-f~InlJw;B1&E5q?7EiG@;x+99u&RECAiKxiJ>~4*I*vum9N}I{ zU{HVAS7!Y#d(N%20Q=MLWVOLQEq^c}^U1FVHoCmpRqI6c8vsDKM zdZdfJ@p6Zw{tAP_IYX@-{cV)}WzehAbVd22oJZ1-;^Gr6Ig{jVE%WmlT;zFOW9(34mC$X&;&$E= zQ`mj6cdC|7Qd43hz+p z*}{{PY=|wqGE|}Xj9;$c-Zb(&Xz9J!p?9_~wNh`pEr-=KUgZ7Tw3jE2^m!dhHVl4v zE6umehTSgdZB>|t4FSjjve20Oakgkw=h$55C_gOj`GXc6qny3L67JAI|_5jd=h z!-*Lp4!!h44Q@8IKWO7VaFX3;LGM8o3sNrxO9G>XnJ@Ru_~r>N(`))b|4dtYRvq zEv0%R?)icz4QS{MD?0S};Py6?_!h=Z-TnR5l&7!`{}H+cy=QhaEA08Clgmu`9x`!@ z&y4jQiCHznSF|d8vLbaWJ?j5Z6p7<56Xda?g+m>J=`h6eYQy+eq*5V<%eR)Ah^z(P_ zcl;J`jQ%y;tnC}r`=Q9G**w>q*7{RHQTzGhi)r?f67l2y-}z_FuTY!TQ{qJe|2(|c z{Du&ybJ^LBsTX03DV=(={r;c|7wT0i=5;O&|JkJbVI4ZWlw(V*31&cP$dMCA6COUW zo7-DZa?Lv0Rcl~h?3S)I=~da#WRHUe((kokQ()comrMszd#A-!B^p6rrKKjh(xHgE zEj3NC!}&7m4N3ZNyuo4GlV4~}>9>7O7C-A>BoE)in|LPRShVS`C~8Yo+F;LNYNDOr zqg6%ylEGd-E4y4#Td5Zj`crq^ZI>(&F9l6DcIYEhAD2mAoxN*c)a%_H(KT|1nkBM& zCQY-ZH*BS^J7F_YbW}72N~WH#6j}9>sYimL9;ub4a?=S|;>`Y@owra!HyiKrq?cP* z`wXBl_H@4|QT9dqQ~ln;^(sv}@0}Luk)x=~6m*kIup`6JD@E3d3-m>)){1=O><^!8tkh)YFJ@|lCyml7UDT%nm)65a)n|LSZ)`=;xslqcM&j5Pnt zYlq$xY?qG=9;V&xm|XcnJ|kd1eE8ub88$h1pd$LMDAZ`*Gwg7~Jz<8- zfN`gHoc6+<6kMXGL zJjtN1=($MF99%xMubqZJy{qs1Tdj7QMRYcHO8W`V)W+|IT+5z8^=bWeitXJ6AJUHS zuIpSrIm{>2%6TstzZtRoD()4P61Vm77EbEtZR?W_TxngxQq1akTSyy`OAMN_^VsansPS5dwMfHW7-AV27BAlb{a+Letz#gl7Pd~W&6@5bL ziRz7cMX@72rs7pk&Tu7f z7uA<_H49po?sEROaBJWhzt zF!j#NmYWP+d89jBd0h7DN15l_4AU#nz-U((HOMbdJ7w6XuEAL5*?F`lz;@mCKDt{~ zYe014PeTJy#p3f^J(mv-R;2l8g<<}Rjc#Q*RLkMP_t-5B9DhNp z?z4o;4fV9lYDfLWdc&0&(C{a=M45Ng6{911wXE+dO@F)6d@Jk965?A^k?nwftX3-f zt=*2SB&l`_?MVf)JAo1JSZM7r(KnKMPQ)MFWt zr0i)nZDCc+wRL!2B9@e;92GGK-+E8$8dzcH(6rILYFZ+hT0$l@;e2oM?z-G<)wz#P zF^f{{KTKOvflsM3rwf;m>-TQ+>wOBdb2w$k=q|CSyg#ur;is<7i><;NBQk$}f{Eybt)A?oWlM4Nh)q9p0BzBK3ncJd*k;c!kZK_NPklwu*w%Hr)dTa(`k3 zF8&N@A&!h94;j#?OZ# zZA_hpwD$_`@YF}rnT7pkZ5jPm=Nz8aC+^rX^$d4-SIW9)^=VIgkV8aTv)0QFW25d> z4LU3DbINr4jn=24I-=Lh+!PAgtdc6~o&4Yl(L6#$OVoV4lf(Lyg4LCFuPMpc!ixH= zlO|P+)}H35`7txORF|qQ3T+vshH@`o>sRvc`3=9C7c@|QBG#?1(xK3n&C%&F_ic(= zy2g;Up}C_=K9wMAUuLvfP-VzSjla7?F)6alzCQoE1_wpy%|vr9S8vgV)>q#vCas+u zFg;tn*9FNazf3}?l>>@iC=+Wz9mOWeT+`U}@+ zef+qu1p>hwEdfssekxOgK5gK&HFy8+`;6}XohvD*V}r0zi%eDK!!=Uxns2#@}*xb2WE5f>MiC9YCbJ+mwcPF$rp{ftpo zmi2>Qr6?j%2TLzZ$=BCrK0B|Fy=yKmPDo5sz!FUp5{jIBbz&NRXs8f1n9vOk0%i7461mX`C($F*)c^xDChyn~eIA`3gpi5HdZR2X?+rs-waAmv01 zOg1=Ehu%n(E;57`7YCCEX6n%U2ZR>-mwJQA&!Gla?DDf-L{UPrt~Kd;7|<`m%1wm2 zrV?EbDy1o)1LiGj)|JNc`eFx581>4lnS%&g(*fFaQ};5_+kujy#%Q0TZ|HV>Ivm=| z#td78L!Ts}X+QW5_5}~*r9{Xim1gu(FQ)9$0NS>rv5Hh*Olhk$$sBxQlG(e;E}7vf8#EV7>ml@cP-_2Yl!!57Z$@G&Ou+o&!c?9}ayoSRy9XCsr~BkhUGM(v=k^nS%widRm_R zl79+CJ09jO>!y^QH=^fFJq`PS?<`#F(2**-o&w8%0P`MvLmzkzHKz}}s*K3bg;B(^ zSeQgt8k;4flvx%Hl;oV7Cw|x?*Vm_Cgn18k#iFtYN+|yH)BTq8TEJwyl3~AE4etH; zYS9~Bv`5j2K0{ISgt{Jf(C}Fcl-i|Ua-+toF+I zhB`Wj^4xD|*hHwy8)?$z4Y^%NjTnrSQsh&EY5vggmNFCQ17)vC^eYjezM~rTPz;nL z^?`B30s)f1c)FLFKndoRFG%$k9sVNwa?#U>`i|is+m3EOd&hz5qBbCmv}8CLCZYI4 zyB?8!XNn(VHwiDNWBSWwC=L$_ck? zxa7Q|Z6l>;_)tX0Gm&nVM7Lo`t{RZ0WIsyN0oo~Qi%CD2hN$5ZwGpfI-(n&%FdC98 zF^qr}cRlWhbsQ1kIjG@tG!*@!UVhdf1&O6wuM;Qack9xRf_c8m0UADQK2S;9ux=84 zpd_Xb*vcvgFk(YRhqe9e`iC~&%Y_Xxfd$z$M*B!#$g6#!6*6k(-goxTViMC@Z3YJh z2A;j+S5;NLTihXiHeZ3mo$2p=*Dd3`TdZI|t8r*>po{vvf07r+c~przR*IyR7QW32 z8&>HHMMPlsFqu|Pwro*Ym7wftmE<%19q$@VeOFcZ<&`#?+Kn8KDeCMr0pFUH-yCFv zx*}8W<}SU=N|EY%ShOLnadARXXQ!T_yq?x+6iqsf8bql4eS{VSqUFAC?6XB7TEOYA zslh)Gi43+i+UA;YOLFb+qG6$YU4CE52Jvk)bEj>N;1A5wDI!}f^wy|61bxS+p`GU;i>-V>{ zbzTfTP}+F+&F^m>bm!GQS*M*)xuDY8?t8DL&7CEo&*F*`@7-9Z4Xe0NVQsg<>xbsf z>d-fFRZi@d`z_CFGhAbGyzXkFn0pMkm=>FK!B0d_w!1i+lvV?m?8_>)$=DskK%j#3 zxyX#TVw;SVa8CwtowoP^rWBsSePNwmHMA&(SZmyz?Xo0DpNqK;kXzrBA|xB0-3<0* zJ6mX@nEOlNk`0yC>4F7|VsiNFnzKn>2z@R_99L|EP7L>Cpe?k;4}41DDY=!_=~dbQ z(7ZX@#TKE@MT+8zZ6v>idop|gl2<8mgFSy4zaYITmFUSRXv=nq;=xn;ZWr4~ykdy8 zS=!fAbO{iO(pmHSmbZu`Sd_MQk?UwwKtX3w9Dc*@@z85~CU}amgsQ zN!b?@9LNaP=eEYh72Bj-3io8(2mO#r;VBi*tkbJlpfA(rY?3JWN>HuOMITo#Ca4rH zS^GTbLJr%yIh&LesLySG6<1vAb0OT5{Q@wttrRZFEVoXta$gifd}!00?ZOMx=eE|y z72EhEg?lo@7TRLMmQr{M1~hVB6qCc=*u1`~mHXjBcX=HjFc4J=PicQ@onDm!AXYVJ zyF>skQ{#$l1ee1-8N;9vtrVVO2pXlw72DWf}Qwq<8 zdooNx)ha+30tS?KKkGbkXuCmbO0-3DwoAiv>+~w7g|@gS1@K`7s(=mmgQ_6FC}^Mq z7%h$~w&8$*Ot`lZkwU!^?#ZBov0#AifI(AJ!;_KyFFxoYQmF1R#37;qx7CX1$!Orh zQ>Y#>#Gz;dZmSN_li|RHr^xrl5Qp#v+*VVfC!>hlxbY1isM(}B+l2-gXaEc(#1-4f zESj@PiswK#u)dCaBt=GXZGl%z&XR>asl~Oko?kAmou!TUWPcCWMp>;dhf7Wz-geuGDnzAT2`#jMee;=Y;#5%@5wNSYm3)!D2Jz%9^aT=HDDG)Or9;wCg~Pm395D0 zhCRaC@4$%S%j4&i`}*PZmLbwp1FLY(dLq6VIF0r(Qv z7~+Au+TsU$47kV^oAj!mh@K30akfiZ4Ln8dY={T$Yl|Q3GvFd6HtAJ1L{A1$oJ|tf zAa4q$z8OMDHl!EVqW8w+@bk1$Rt4H9=1v1HrphK=5JdE3bHv#suWGm?7c_E@$>BfL zMlqcYxEQ`ox`0IVWJ|=^B-?7Zgp^)fE7{AqL}CZpCgt#RfNL?}V)|{;1%^aVw!Ju; z6jcM4^ko*;O59>{_)KjS)6am5QP`vlCJ;T@lf>C1ts1x_s2VQO&MdAq*b|du`#>8N z;h7RtYjD=Um)ri-CS9FJ=yog z>#N%9K3us+64k&Z6}+k6)qQPL#BKv_dy`GNUW%hF# z;jDV*u^7?ehQ)?w#nE2mz|4M0<8D@c#4$(F;f6(qiQ;Gva%g71pfQ|P-+!!3bhu$j z>c7R&eq{g5ep%yAR=v(K6Vc(y>9A+LR5LqE-te_-YzebFEUV+)+49~qcD3SR@@jna zl7#-DwW&|N8%h!j3S$`Sc+5^hR>#{*<-N1)_~L%rTD+{UvVTb6_td*+@N@ySBWWEE zE5l_q44yCVT~bsl?vGwGY{ZzF|R#KTTBsA0wjz&CIH~%lUknh!G;4SrOqwM05;1x?yz`pjgf;seQ-#kSr zu2Q}g!Jn!>$_KLXCIo+in5n6rAToHPUtJMSY0;~E`zB&t1#HybzaNoCALxo?J zX#8S^QoS+=lAlgd=J|4hq?@VU7X(KPVQ$KwJTZq%P^Cu%LkV6QEZ6Az<1oF4Kp<@) zpE?_p=F1+bdP7tr>(%Jnq2HXIYWbtjGrXNs)%*!|;S)+ciS1*MwZAoIMII`8I|z01_Kz~HpD5JN#KH09f>>d&ws@TAPA z0>h|`7Np?(Zgk2yxvsptK$-O%Ov)fQ5<~C{#=#>Y6GmGoxjU}z3p}jz(QpC$9rXnu z-^|Zaf`+Qnik50nPydwmQ>mxTm`@yzvHZ26shQM}1Hw z>KOAOKU8I}8o4{aFf&8hN{5VekNu+!s8k_0>FNonOi8 z_!#@ir)!VRA@9_Aq)6r5L0tKmf`7ut{P4;-@&G$(eUP!1EE>x(c%l3oa=nlK-otXN zO5Xn}*L&E8xDt@!I!I0Y-{*QC?Vh1V_^)%lkFqd+nH~1YA$`)gGFUtPy#B!_x3n3K zM<}aRDp!6~s(k!XukoV)j4fCU2K+(@7K8Uz_X!pw6SlYkj|zi*4Bj7ed>{Q{vr(o# z+QXeMHXBVH@DcXq7n_ZG-jB`3l;ZGDU#9?*!_Ns>StzVzLo$-MTe_?m=7!lrzx!z{5Ho&Yxp>-w>8?$`uODJ zm)m}08OqO`_3|;_{~om`M!)xms4tO$-(URyRR#{kh1B=Hp?+wc*-T7WIphBTn2X^} literal 0 HcmV?d00001 diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 91bb7f5db8..1caa6de854 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -20,9 +20,9 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { paletteHeaderBarOverlay, paletteTitleOverlay, palettePanelOverlay, - highlightOverlay, paletteItemOverlays = [], paletteItemPositions = [], + paletteItemHoverOverlays = [], LEFT_HAND = 0, @@ -101,35 +101,55 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true }, - HIGHLIGHT_PROPERTIES = { - dimensions: { x: 0.034, y: 0.034, z: 0.034 }, - localPosition: { x: 0.02, y: 0.02, z: 0.0 }, - localRotation: Quat.ZERO, - color: { red: 240, green: 240, blue: 0 }, - alpha: 0.8, - solid: false, - drawInFront: true, - ignoreRayIntersection: true, - visible: false - }, - - PALETTE_ENTITY_DIMENSIONS = { x: 0.024, y: 0.024, z: 0.024 }, - PALETTE_ENTITY_COLOR = UIT.colors.faintGray, ENTITY_CREATION_DIMENSIONS = { x: 0.2, y: 0.2, z: 0.2 }, ENTITY_CREATION_COLOR = { red: 192, green: 192, blue: 192 }, + PALETTE_ITEM = { + overlay: "cube", // Invisible cube for hit area. + properties: { + dimensions: UIT.dimensions.itemCollisionZone, + localRotation: Quat.ZERO, + alpha: 0.0, // Invisible. + solid: true, + ignoreRayIntersection: false, + visible: true // So that laser intersects. + }, + hoverButton: { + // Relative to root overlay. + overlay: "cube", + properties: { + dimensions: UIT.dimensions.paletteItemButtonDimensions, + localPosition: UIT.dimensions.paletteItemButtonOffset, + localRotation: Quat.ZERO, + color: UIT.colors.blueHighlight, + alpha: 1.0, + emissive: true, // TODO: This has no effect. + solid: true, + ignoreRayIntersection: true, + visible: false + } + }, + icon: { + // Relative to hoverButton. + overlay: "model", + properties: { + dimensions: UIT.dimensions.paletteItemIconDimensions, + localPosition: UIT.dimensions.paletteItemIconOffset, + localRotation: Quat.ZERO, + emissive: true, // TODO: This has no effect. + ignoreRayIntersection: true + } + }, + entity: { + dimensions: ENTITY_CREATION_DIMENSIONS + } + }, + PALETTE_ITEMS = [ { - overlay: { - type: "cube", + icon: { properties: { - dimensions: PALETTE_ENTITY_DIMENSIONS, - localRotation: Quat.ZERO, - color: PALETTE_ENTITY_COLOR, - alpha: 1.0, - solid: true, - ignoreRayIntersection: false, - visible: true + url: "../assets/create/cube.fbx" } }, entity: { @@ -139,17 +159,10 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, { - overlay: { - type: "shape", + icon: { properties: { - shape: "Cylinder", - dimensions: PALETTE_ENTITY_DIMENSIONS, - localRotation: Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }), - color: PALETTE_ENTITY_COLOR, - alpha: 1.0, - solid: true, - ignoreRayIntersection: false, - visible: true + url: "../assets/create/cylinder.fbx", + localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) } }, entity: { @@ -160,17 +173,10 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, { - overlay: { - type: "shape", + icon: { properties: { - shape: "Cone", - dimensions: PALETTE_ENTITY_DIMENSIONS, - localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }), - color: PALETTE_ENTITY_COLOR, - alpha: 1.0, - solid: true, - ignoreRayIntersection: false, - visible: true + url: "../assets/create/cone.fbx", + localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) } }, entity: { @@ -181,16 +187,9 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, { - overlay: { - type: "sphere", + icon: { properties: { - dimensions: PALETTE_ENTITY_DIMENSIONS, - localRotation: Quat.ZERO, - color: PALETTE_ENTITY_COLOR, - alpha: 1.0, - solid: true, - ignoreRayIntersection: false, - visible: true + url: "../assets/create/sphere.fbx" } }, entity: { @@ -205,17 +204,16 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { NONE = -1, highlightedItem = NONE, - pressedItem = NONE, wasTriggerClicked = false, // References. controlHand; - if (!this instanceof CreatePalette) { return new CreatePalette(); } + function setHand(hand) { // Assumes UI is not displaying. side = hand; @@ -234,45 +232,32 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { var itemIndex, isTriggerClicked, properties, - PRESS_DELTA = { x: 0, y: 0, z: -0.01 }, CREATE_OFFSET = { x: 0, y: 0.05, z: -0.02 }, INVERSE_HAND_BASIS_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }); - // Highlight cube. itemIndex = paletteItemOverlays.indexOf(intersectionOverlayID); - if (itemIndex !== NONE) { - if (highlightedItem !== itemIndex) { - Overlays.editOverlay(highlightOverlay, { - parentID: intersectionOverlayID, - localPosition: Vec3.ZERO, - visible: true - }); - highlightedItem = itemIndex; - } - } else { - Overlays.editOverlay(highlightOverlay, { + + // Unhighlight and lower old item. + if (highlightedItem !== NONE && (itemIndex === NONE || itemIndex !== highlightedItem)) { + Overlays.editOverlay(paletteItemHoverOverlays[highlightedItem], { + localPosition: UIT.dimensions.paletteItemButtonOffset, visible: false }); highlightedItem = NONE; } - // Unpress currently pressed item. - if (pressedItem !== NONE && pressedItem !== itemIndex) { - Overlays.editOverlay(paletteItemOverlays[pressedItem], { - localPosition: paletteItemPositions[pressedItem] + // Highlight and raise new item. + if (itemIndex !== NONE && highlightedItem !== itemIndex) { + Overlays.editOverlay(paletteItemHoverOverlays[itemIndex], { + localPosition: UIT.dimensions.paletteItemButtonHoveredOffset, + visible: true }); - pressedItem = NONE; + highlightedItem = itemIndex; } // Press item and create new entity. isTriggerClicked = controlHand.triggerClicked(); - if (highlightedItem !== NONE && pressedItem === NONE && isTriggerClicked && !wasTriggerClicked) { - // Press item. - Overlays.editOverlay(paletteItemOverlays[itemIndex], { - localPosition: Vec3.sum(paletteItemPositions[itemIndex], PRESS_DELTA) - }); - pressedItem = itemIndex; - + if (highlightedItem !== NONE && isTriggerClicked && !wasTriggerClicked) { // Create entity. properties = Object.clone(PALETTE_ITEMS[itemIndex].entity); properties.position = Vec3.sum(controlHand.palmPosition(), @@ -281,6 +266,12 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { properties.rotation = Quat.multiply(controlHand.orientation(), INVERSE_HAND_BASIS_ROTATION); Entities.addEntity(properties); + // Lower and unhighlight item. + Overlays.editOverlay(paletteItemHoverOverlays[itemIndex], { + localPosition: UIT.dimensions.paletteItemButtonOffset, + visible: false + }); + uiCommandCallback("autoGrab"); } @@ -303,7 +294,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { return { x: COLUMN_ZERO_OFFSET + column * COLUMN_SPACING, y: ROW_ZERO_Y_OFFSET - row * ROW_SPACING, - z: UIT.dimensions.panel.z + PALETTE_ENTITY_DIMENSIONS.z / 2 + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.itemCollisionZone.z / 2 }; } @@ -352,17 +343,26 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { // Palette items. for (i = 0, length = PALETTE_ITEMS.length; i < length; i += 1) { - properties = Object.clone(PALETTE_ITEMS[i].overlay.properties); + // Collision overlay. + properties = Object.clone(PALETTE_ITEM.properties); properties.parentID = palettePanelOverlay; properties.localPosition = itemPosition(i); - paletteItemOverlays[i] = Overlays.addOverlay(PALETTE_ITEMS[i].overlay.type, properties); - paletteItemPositions[i] = properties.localPosition; - } - // Prepare cube highlight overlay. - properties = Object.clone(HIGHLIGHT_PROPERTIES); - properties.parentID = paletteOriginOverlay; - highlightOverlay = Overlays.addOverlay("cube", properties); + paletteItemOverlays[i] = Overlays.addOverlay(PALETTE_ITEM.overlay, properties); + paletteItemPositions[i] = properties.localPosition; + + // Highlight overlay. + properties = Object.clone(PALETTE_ITEM.hoverButton.properties); + properties.parentID = paletteItemOverlays[i]; + paletteItemHoverOverlays[i] = Overlays.addOverlay(PALETTE_ITEM.hoverButton.overlay, properties); + + // Icon overlay. + properties = Object.clone(PALETTE_ITEM.icon.properties); + properties = Object.merge(properties, PALETTE_ITEMS[i].icon.properties); + properties.parentID = paletteItemHoverOverlays[i]; + properties.url = Script.resolvePath(properties.url); + Overlays.addOverlay(PALETTE_ITEM.icon.overlay, properties); + } isDisplaying = true; } @@ -376,9 +376,8 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { return; } - Overlays.deleteOverlay(highlightOverlay); for (i = 0, length = paletteItemOverlays.length; i < length; i += 1) { - Overlays.deleteOverlay(paletteItemOverlays[i]); + Overlays.deleteOverlay(paletteItemOverlays[i]); // Child overlays are automatically deleted. } Overlays.deleteOverlay(palettePanelOverlay); Overlays.deleteOverlay(paletteTitleOverlay); diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 61ba482bf1..b6f9ae0daf 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -143,9 +143,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { shape: "Cylinder", dimensions: { - x: UIT.dimensions.menuButton.x, - y: UIT.dimensions.menuButton.z, - z: UIT.dimensions.menuButton.y + x: UIT.dimensions.menuButtonDimensions.x, + y: UIT.dimensions.menuButtonDimensions.z, + z: UIT.dimensions.menuButtonDimensions.y }, localPosition: UIT.dimensions.menuButtonIconOffset, localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: -90 }), @@ -161,7 +161,11 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Relative to hoverButton. type: "image", properties: { - localPosition: { x: 0, y: UIT.dimensions.menuButton.z / 2 + UIT.dimensions.imageOverlayOffset, z: 0 }, + localPosition: { + x: 0, + y: UIT.dimensions.menuButtonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset, + z: 0 + }, localRotation: Quat.fromVec3Degrees({ x: -90, y: 90, z: 0 }), color: UIT.colors.lightGrayText } @@ -401,6 +405,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { SLIDER_UI_ELEMENTS = ["barSlider", "imageSlider"], COLOR_CIRCLE_UI_ELEMENTS = ["colorCircle"], PICKLIST_UI_ELEMENTS = ["picklist", "picklistItem"], + MENU_RAISE_DELTA = { x: 0, y: 0, z: 0.006 }, ITEM_RAISE_DELTA = { x: 0, y: 0, z: 0.004 }, MIN_BAR_SLIDER_DIMENSION = 0.0001, // Avoid visual artifact for 0 slider values. @@ -1160,7 +1165,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } if (MENU_ITEMS[i].type === "menuButton") { - // Hover button. + // Collision overlay. properties = Object.clone(UI_ELEMENTS.menuButton.hoverButton.properties); properties.parentID = itemID; buttonID = Overlays.addOverlay(UI_ELEMENTS.menuButton.hoverButton.overlay, properties); @@ -1877,7 +1882,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { (intersectionItems[intersectedItem].command !== undefined || intersectionItems[intersectedItem].callback !== undefined)) { if (isHighlightingMenuButton) { - // Lower menu button. + // Lower old menu button. Overlays.editOverlay(menuHoverOverlays[highlightedItem], { localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, visible: false @@ -1897,9 +1902,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isHighlightingColorCircle = COLOR_CIRCLE_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; isHighlightingPicklist = PICKLIST_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; if (isHighlightingMenuButton) { - // Raise menu button. + // Raise new menu button. Overlays.editOverlay(menuHoverOverlays[highlightedItem], { - localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, ITEM_RAISE_DELTA), + localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, MENU_RAISE_DELTA), visible: true }); } else if (isHighlightingSlider || isHighlightingColorCircle) { diff --git a/scripts/vr-edit/modules/uit.js b/scripts/vr-edit/modules/uit.js index e9ba08ca24..8bc9cf76e4 100644 --- a/scripts/vr-edit/modules/uit.js +++ b/scripts/vr-edit/modules/uit.js @@ -36,13 +36,19 @@ UIT = (function () { headerBar: { x: 0.24, y: 0.004, z: 0.012 }, panel: { x: 0.24, y: 0.18, z: 0.008 }, - itemCollisionZone: { x: 0.0481, y: 0.0480, z: 0.0060 }, // Cursor intersection zone for Tools and Create items. + itemCollisionZone: { x: 0.0481, y: 0.0480, z: 0.0040 }, // Cursor intersection zone for Tools and Create items. - menuButton: { x: 0.0267, y: 0.0267, z: 0.0040 }, - menuButtonIconOffset: { x: 0, y: 0.00935, z: -0.0050 }, // Non-hovered position. + menuButtonDimensions: { x: 0.0267, y: 0.0267, z: 0.0040 }, + menuButtonIconOffset: { x: 0, y: 0.00935, z: -0.0040 }, // Non-hovered position relative to itemCollisionZone. menuButtonLabelYOffset: -0.00915, // Relative to itemCollisionZone. menuButtonSublabelYOffset: -0.01775, // Relative to itemCollisionZone. + paletteItemButtonDimensions: { x: 0.0481, y: 0.0480, z: 0.0020 }, + paletteItemButtonOffset: { x: 0, y: 0, z: -0.0020 }, // Non-hovered position relative to itemCollisionZone. + paletteItemButtonHoveredOffset: { x: 0, y: 0, z: -0.0010 }, + paletteItemIconDimensions: { x: 0.024, y: 0.024, z: 0.024 }, + paletteItemIconOffset: { x: 0, y: 0, z: 0.015 }, // Non-hovered position relative to palette button. + imageOverlayOffset: 0.001 // Raise image above surface. } }; From 2a2f058898d836e4eb32f63492c48cf7bd70af5e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 23 Aug 2017 21:59:55 +1200 Subject: [PATCH 235/722] Fix Tools menu header underline color --- scripts/vr-edit/modules/toolsMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index b6f9ae0daf..0cc021ac10 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -80,7 +80,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { z: UIT.dimensions.headerBar.z / 2 }, localRotation: Quat.ZERO, - color: UIT.colors.blueHighlight, + color: UIT.colors.greenHighlight, alpha: 1.0, solid: true, ignoreRayIntersection: false, From 5a1b2babde0df3f46c9e9eb9699d09b347b9f93d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 24 Aug 2017 09:21:54 +1200 Subject: [PATCH 236/722] Add further items to Create palette --- scripts/vr-edit/assets/create/icosahedron.fbx | Bin 0 -> 17904 bytes scripts/vr-edit/assets/create/octahedron.fbx | Bin 0 -> 17024 bytes scripts/vr-edit/assets/create/prism.fbx | Bin 0 -> 17392 bytes scripts/vr-edit/assets/create/tetrahedron.fbx | Bin 0 -> 16720 bytes scripts/vr-edit/modules/createPalette.js | 83 +++++++++++++++--- 5 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 scripts/vr-edit/assets/create/icosahedron.fbx create mode 100644 scripts/vr-edit/assets/create/octahedron.fbx create mode 100644 scripts/vr-edit/assets/create/prism.fbx create mode 100644 scripts/vr-edit/assets/create/tetrahedron.fbx diff --git a/scripts/vr-edit/assets/create/icosahedron.fbx b/scripts/vr-edit/assets/create/icosahedron.fbx new file mode 100644 index 0000000000000000000000000000000000000000..52f948816ba00e8a1722ef375621f394d1260460 GIT binary patch literal 17904 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 Date: Thu, 24 Aug 2017 11:10:41 +1200 Subject: [PATCH 237/722] Remove setting tool icon color --- scripts/vr-edit/modules/toolIcon.js | 5 ----- scripts/vr-edit/vr-edit.js | 11 ----------- 2 files changed, 16 deletions(-) diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 9b0d81052d..551e141440 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -94,10 +94,6 @@ ToolIcon = function (side) { } } - function setColor(color) { - Overlays.editOverlay(iconOverlay, { color: color }); - } - function clear() { // Deletes current icon. if (iconOverlay) { @@ -121,7 +117,6 @@ ToolIcon = function (side) { setHand: setHand, update: update, display: display, - setColor: setColor, clear: clear, destroy: destroy }; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 4c4af48635..bd8ae74a18 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -227,10 +227,6 @@ toolIcon.display(icon); } - function setToolColor(color) { - toolIcon.setColor(color); - } - function clearTool() { toolIcon.clear(); toolsMenu.clearTool(); @@ -292,7 +288,6 @@ return { setHand: setHand, setToolIcon: setToolIcon, - setToolColor: setToolColor, clearTool: clearTool, SCALE_TOOL: toolIcon.SCALE_TOOL, CLONE_TOOL: toolIcon.CLONE_TOOL, @@ -912,11 +907,9 @@ if (color) { colorToolColor = color; ui.doPickColor(colorToolColor); - ui.setToolColor(colorToolColor); } toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); - ui.setToolColor(colorToolColor); } else if (toolSelected === TOOL_PHYSICS) { setState(EDITOR_HIGHLIGHTING); selection.applyPhysics(physicsToolPhysics); @@ -995,11 +988,9 @@ if (color) { colorToolColor = color; ui.doPickColor(colorToolColor); - ui.setToolColor(colorToolColor); } toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); - ui.setToolColor(colorToolColor); } else if (toolSelected === TOOL_PHYSICS) { selection.applyPhysics(physicsToolPhysics); } else if (toolSelected === TOOL_DELETE) { @@ -1358,7 +1349,6 @@ grouping.clear(); toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); - ui.setToolColor(parameter); colorToolColor = parameter; ui.updateUIEntities(); break; @@ -1397,7 +1387,6 @@ toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); } - ui.setToolColor(parameter); colorToolColor = parameter; break; From a33dbfe9b312a8c63333e4197106f4e29fd9cb3c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 24 Aug 2017 14:30:05 +1200 Subject: [PATCH 238/722] Change Tools menu header content per current tool --- scripts/vr-edit/assets/tools/back-icon.svg | 14 ++ .../assets/tools/clone-tool-heading.svg | 12 ++ .../assets/tools/color-tool-heading.svg | 12 ++ .../assets/tools/delete-tool-heading.svg | 12 ++ .../assets/tools/group-tool-heading.svg | 12 ++ .../assets/tools/physics-tool-heading.svg | 12 ++ .../assets/tools/stretch-tool-heading.svg | 12 ++ scripts/vr-edit/modules/toolsMenu.js | 193 ++++++++++++++---- 8 files changed, 234 insertions(+), 45 deletions(-) create mode 100644 scripts/vr-edit/assets/tools/back-icon.svg create mode 100644 scripts/vr-edit/assets/tools/clone-tool-heading.svg create mode 100644 scripts/vr-edit/assets/tools/color-tool-heading.svg create mode 100644 scripts/vr-edit/assets/tools/delete-tool-heading.svg create mode 100644 scripts/vr-edit/assets/tools/group-tool-heading.svg create mode 100644 scripts/vr-edit/assets/tools/physics-tool-heading.svg create mode 100644 scripts/vr-edit/assets/tools/stretch-tool-heading.svg diff --git a/scripts/vr-edit/assets/tools/back-icon.svg b/scripts/vr-edit/assets/tools/back-icon.svg new file mode 100644 index 0000000000..7de1781804 --- /dev/null +++ b/scripts/vr-edit/assets/tools/back-icon.svg @@ -0,0 +1,14 @@ + + + + back-icon + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/clone-tool-heading.svg b/scripts/vr-edit/assets/tools/clone-tool-heading.svg new file mode 100644 index 0000000000..6ab57cd0e1 --- /dev/null +++ b/scripts/vr-edit/assets/tools/clone-tool-heading.svg @@ -0,0 +1,12 @@ + + + + CLONE TOOL + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/color-tool-heading.svg b/scripts/vr-edit/assets/tools/color-tool-heading.svg new file mode 100644 index 0000000000..5b1979e776 --- /dev/null +++ b/scripts/vr-edit/assets/tools/color-tool-heading.svg @@ -0,0 +1,12 @@ + + + + COLOR TOOL + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/delete-tool-heading.svg b/scripts/vr-edit/assets/tools/delete-tool-heading.svg new file mode 100644 index 0000000000..e92e3c1d00 --- /dev/null +++ b/scripts/vr-edit/assets/tools/delete-tool-heading.svg @@ -0,0 +1,12 @@ + + + + DELETE TOOL + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/group-tool-heading.svg b/scripts/vr-edit/assets/tools/group-tool-heading.svg new file mode 100644 index 0000000000..e1942213e2 --- /dev/null +++ b/scripts/vr-edit/assets/tools/group-tool-heading.svg @@ -0,0 +1,12 @@ + + + + GROUP TOOL + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/physics-tool-heading.svg b/scripts/vr-edit/assets/tools/physics-tool-heading.svg new file mode 100644 index 0000000000..fb5d696111 --- /dev/null +++ b/scripts/vr-edit/assets/tools/physics-tool-heading.svg @@ -0,0 +1,12 @@ + + + + PHYSICS TOOL + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/stretch-tool-heading.svg b/scripts/vr-edit/assets/tools/stretch-tool-heading.svg new file mode 100644 index 0000000000..0d3fde298c --- /dev/null +++ b/scripts/vr-edit/assets/tools/stretch-tool-heading.svg @@ -0,0 +1,12 @@ + + + + STRETCH TOOL + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 0cc021ac10..9b66fc65c3 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -20,7 +20,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { menuOriginOverlay, menuHeaderOverlay, menuHeaderBarOverlay, - menuTitleOverlay, + menuHeaderBackOverlay, + menuHeaderTitleOverlay, + menuHeaderIconOverlay, menuPanelOverlay, menuOverlays = [], @@ -87,9 +89,26 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true }, - MENU_TITLE_PROPERTIES = { + MENU_HEADER_BACK_PROPERTIES = { + url: "../assets/tools/back-icon.svg", + dimensions: { x: 0.0069, y: 0.0107 }, + localPosition: { + x: -MENU_HEADER_PROPERTIES.dimensions.x / 2 + 0.0118 + 0.0069 / 2, + y: 0, + z: MENU_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset + }, + localRotation: Quat.ZERO, + color: UIT.colors.lightGrayText, + alpha: 1.0, + emissive: true, + ignoreRayIntersection: true, + isFacingAvatar: false, + visible: true + }, + + MENU_HEADER_TITLE_PROPERTIES = { url: "../assets/tools/tools-heading.svg", - scale: 0.0363, + scale: 0.0327, localPosition: { x: 0, y: 0, z: MENU_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, localRotation: Quat.ZERO, color: UIT.colors.white, @@ -100,6 +119,26 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true }, + MENU_HEADER_ICON_OFFSET = { + // Default right center position for header tool icons. + x: MENU_HEADER_PROPERTIES.dimensions.x / 2 - 0.0118, + y: 0, + z: MENU_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset + }, + + MENU_HEADER_ICON_PROPERTIES = { + url: "../assets/tools/color-icon.svg", // Initial value so that the overlay is initialized OK. + dimensions: { x: 0.01, y: 0.01 }, // "" + localPosition: Vec3.ZERO, // "" + localRotation: Quat.ZERO, + color: UIT.colors.lightGrayText, + alpha: 1.0, + emissive: true, + ignoreRayIntersection: true, + isFacingAvatar: false, + visible: false + }, + MENU_PANEL_PROPERTIES = { dimensions: UIT.dimensions.panel, localPosition: { x: 0, y: UIT.dimensions.panel.y / 2 - UIT.dimensions.canvas.y / 2, z: UIT.dimensions.panel.z / 2 }, @@ -903,7 +942,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { url: "../assets/tools/color-icon.svg", dimensions: { x: 0.0165, y: 0.0187 } - } + }, + headerOffset: { x: -0.00825, y: 0.0020, z: 0 } }, label: { type: "image", @@ -912,6 +952,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { scale: 0.0241 } }, + title: { + url: "../assets/tools/color-tool-heading.svg", + scale: 0.0631 + }, toolOptions: "colorOptions", callback: { method: "colorTool", @@ -933,7 +977,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { url: "../assets/tools/stretch-icon.svg", dimensions: { x: 0.0167, y: 0.0167 } - } + }, + headerOffset: { x: -0.00835, y: 0, z: 0 } }, label: { type: "image", @@ -942,6 +987,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { scale: 0.0311 } }, + title: { + url: "../assets/tools/stretch-tool-heading.svg", + scale: 0.0737 + }, toolOptions: "scaleOptions", callback: { method: "scaleTool" @@ -962,7 +1011,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { url: "../assets/tools/clone-icon.svg", dimensions: { x: 0.0154, y: 0.0155 } - } + }, + headerOffset: { x: -0.0077, y: 0, z: 0 } }, label: { type: "image", @@ -971,6 +1021,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { scale: 0.0231 } }, + title: { + url: "../assets/tools/clone-tool-heading.svg", + scale: 0.0621 + }, toolOptions: "cloneOptions", callback: { method: "cloneTool" @@ -991,7 +1045,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { url: "../assets/tools/group-icon.svg", dimensions: { x: 0.0161, y: 0.0114 } - } + }, + headerOffset: { x: -0.00805, y: 0, z: 0 } }, label: { type: "image", @@ -1000,6 +1055,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { scale: 0.0250 } }, + title: { + url: "../assets/tools/group-tool-heading.svg", + scale: 0.0647 + }, toolOptions: "groupOptions", callback: { method: "groupTool" @@ -1020,7 +1079,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { url: "../assets/tools/physics-icon.svg", dimensions: { x: 0.0180, y: 0.0198 } - } + }, + headerOffset: { x: -0.009, y: 0, z: 0 } }, label: { type: "image", @@ -1029,6 +1089,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { scale: 0.0297 } }, + title: { + url: "../assets/tools/physics-tool-heading.svg", + scale: 0.0712 + }, toolOptions: "physicsOptions", callback: { method: "physicsTool" @@ -1049,7 +1113,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { url: "../assets/tools/delete-icon.svg", dimensions: { x: 0.0161, y: 0.0161 } - } + }, + headerOffset: { x: -0.00805, y: 0, z: 0 } }, label: { type: "image", @@ -1058,6 +1123,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { scale: 0.0254 } }, + title: { + url: "../assets/tools/delete-tool-heading.svg", + scale: 0.0653 + }, toolOptions: "deleteOptions", callback: { method: "deleteTool" @@ -1150,6 +1219,15 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { i, length; + // Update header. + Overlays.editOverlay(menuHeaderBackOverlay, { visible: false }); + Overlays.editOverlay(menuHeaderTitleOverlay, { + url: Script.resolvePath(MENU_HEADER_TITLE_PROPERTIES.url), + scale: MENU_HEADER_TITLE_PROPERTIES.scale + }); + Overlays.editOverlay(menuHeaderIconOverlay, { visible: false }); + + // Display menu items. for (i = 0, length = MENU_ITEMS.length; i < length; i += 1) { properties = Object.clone(UI_ELEMENTS[MENU_ITEMS[i].type].properties); properties = Object.merge(properties, MENU_ITEMS[i].properties); @@ -1214,36 +1292,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { pressedItem = null; } - function closeOptions() { - var i, - length; - - // Remove options items. - Overlays.editOverlay(highlightOverlay, { - parentID: menuOriginOverlay - }); - - for (i = 0, length = optionsOverlays.length; i < length; i += 1) { - Overlays.deleteOverlay(optionsOverlays[i]); - } - optionsOverlays = []; - - optionsOverlaysIDs = []; - optionsOverlaysLabels = []; - optionsSliderData = []; - optionsColorData = []; - optionsEnabled = []; - optionsItems = null; - - isPicklistOpen = false; - - pressedItem = null; - - // Display menu items. - openMenu(true); - } - - function openOptions(toolOptions) { + function openOptions(menuItem) { var properties, childProperties, auxiliaryProperties, @@ -1259,8 +1308,21 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Remove menu items. closeMenu(); + // Update header. + Overlays.editOverlay(menuHeaderBackOverlay, { visible: true }); + Overlays.editOverlay(menuHeaderTitleOverlay, { + url: Script.resolvePath(menuItem.title.url), + scale: menuItem.title.scale + }); + Overlays.editOverlay(menuHeaderIconOverlay, { + url: Script.resolvePath(menuItem.icon.properties.url), + dimensions: menuItem.icon.properties.dimensions, + localPosition: Vec3.sum(MENU_HEADER_ICON_OFFSET, menuItem.icon.headerOffset), + visible: true + }); + // Open specified options panel. - optionsItems = OPTONS_PANELS[toolOptions]; + optionsItems = OPTONS_PANELS[menuItem.toolOptions]; parentID = menuPanelOverlay; for (i = 0, length = optionsItems.length; i < length; i += 1) { properties = Object.clone(UI_ELEMENTS[optionsItems[i].type].properties); @@ -1463,12 +1525,41 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } // Special handling for Group options. - if (toolOptions === "groupOptions") { + if (menuItem.toolOptions === "groupOptions") { optionsEnabled[groupButtonIndex] = false; optionsEnabled[ungroupButtonIndex] = false; } } + function closeOptions() { + var i, + length; + + // Remove options items. + Overlays.editOverlay(highlightOverlay, { + parentID: menuOriginOverlay + }); + + for (i = 0, length = optionsOverlays.length; i < length; i += 1) { + Overlays.deleteOverlay(optionsOverlays[i]); + } + optionsOverlays = []; + + optionsOverlaysIDs = []; + optionsOverlaysLabels = []; + optionsSliderData = []; + optionsColorData = []; + optionsEnabled = []; + optionsItems = null; + + isPicklistOpen = false; + + pressedItem = null; + + // Display menu items. + openMenu(true); + } + function clearTool() { closeOptions(); } @@ -2006,7 +2097,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Button press actions. if (intersectionOverlays === menuOverlays) { - openOptions(intersectionItems[intersectedItem].toolOptions); + openOptions(intersectionItems[intersectedItem]); } if (intersectionItems[intersectedItem].command) { parameter = intersectionItems[intersectedItem].id; @@ -2181,10 +2272,20 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.clone(MENU_HEADER_BAR_PROPERTIES); properties.parentID = menuOriginOverlay; menuHeaderBarOverlay = Overlays.addOverlay("cube", properties); - properties = Object.clone(MENU_TITLE_PROPERTIES); + + // Header content. + properties = Object.clone(MENU_HEADER_BACK_PROPERTIES); properties.parentID = menuHeaderOverlay; properties.url = Script.resolvePath(properties.url); - menuTitleOverlay = Overlays.addOverlay("image3d", properties); + menuHeaderBackOverlay = Overlays.addOverlay("image3d", properties); + properties = Object.clone(MENU_HEADER_TITLE_PROPERTIES); + properties.parentID = menuHeaderOverlay; + properties.url = Script.resolvePath(properties.url); + menuHeaderTitleOverlay = Overlays.addOverlay("image3d", properties); + properties = Object.clone(MENU_HEADER_ICON_PROPERTIES); + properties.parentID = menuHeaderOverlay; + properties.url = Script.resolvePath(properties.url); + menuHeaderIconOverlay = Overlays.addOverlay("image3d", properties); // Panel background. properties = Object.clone(MENU_PANEL_PROPERTIES); @@ -2258,7 +2359,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Overlays.deleteOverlay(menuHeaderOverlay); Overlays.deleteOverlay(menuHeaderBarOverlay); - Overlays.deleteOverlay(menuTitleOverlay); + Overlays.deleteOverlay(menuHeaderIconOverlay); + Overlays.deleteOverlay(menuHeaderTitleOverlay); + Overlays.deleteOverlay(menuHeaderBackOverlay); Overlays.deleteOverlay(menuPanelOverlay); Overlays.deleteOverlay(menuOriginOverlay); From 24202b7fa540981bacd6d0671801c204f2664efe Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 24 Aug 2017 15:26:26 +1200 Subject: [PATCH 239/722] Simplify deleting overlays --- scripts/vr-edit/modules/createPalette.js | 16 +++------------- scripts/vr-edit/modules/toolsMenu.js | 24 ++---------------------- 2 files changed, 5 insertions(+), 35 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 4fb1e7ec6c..003b973198 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -426,22 +426,12 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { function clear() { // Deletes menu entities. - var i, - length; - if (!isDisplaying) { return; } - - for (i = 0, length = paletteItemOverlays.length; i < length; i += 1) { - Overlays.deleteOverlay(paletteItemOverlays[i]); // Child overlays are automatically deleted. - } - Overlays.deleteOverlay(palettePanelOverlay); - Overlays.deleteOverlay(paletteTitleOverlay); - Overlays.deleteOverlay(paletteHeaderBarOverlay); - Overlays.deleteOverlay(paletteHeaderOverlay); - Overlays.deleteOverlay(paletteOriginOverlay); - + Overlays.deleteOverlay(paletteOriginOverlay); // Automatically deletes all other overlays because they're children. + paletteItemOverlays = [], + paletteItemHoverOverlays = [], isDisplaying = false; } diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 9b66fc65c3..c84faae972 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -2338,33 +2338,13 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { function clear() { // Deletes menu entities. - var i, - length; - if (!isDisplaying) { return; } - - Overlays.deleteOverlay(highlightOverlay); - for (i = 0, length = optionsOverlays.length; i < length; i += 1) { - Overlays.deleteOverlay(optionsOverlays[i]); // Automatically deletes any child overlays. - } - optionsOverlays = []; - - for (i = 0, length = menuOverlays.length; i < length; i += 1) { - Overlays.deleteOverlay(menuOverlays[i]); // Automatically deletes any child overlays. - } + Overlays.deleteOverlay(menuOriginOverlay); // Automatically deletes all other overlays because they're children. menuOverlays = []; menuHoverOverlays = []; - - Overlays.deleteOverlay(menuHeaderOverlay); - Overlays.deleteOverlay(menuHeaderBarOverlay); - Overlays.deleteOverlay(menuHeaderIconOverlay); - Overlays.deleteOverlay(menuHeaderTitleOverlay); - Overlays.deleteOverlay(menuHeaderBackOverlay); - Overlays.deleteOverlay(menuPanelOverlay); - Overlays.deleteOverlay(menuOriginOverlay); - + optionsOverlays = []; isDisplaying = false; } From 9858c05bdbb49122a58cf081bbb8524763bf0265 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 24 Aug 2017 21:22:05 +1200 Subject: [PATCH 240/722] Pressing header in Tools options takes user back to Tools menu --- scripts/vr-edit/assets/tools/back-heading.svg | 12 ++ scripts/vr-edit/modules/createPalette.js | 38 +++-- scripts/vr-edit/modules/toolsMenu.js | 136 ++++++++++++++---- scripts/vr-edit/modules/uit.js | 3 +- 4 files changed, 143 insertions(+), 46 deletions(-) create mode 100644 scripts/vr-edit/assets/tools/back-heading.svg diff --git a/scripts/vr-edit/assets/tools/back-heading.svg b/scripts/vr-edit/assets/tools/back-heading.svg new file mode 100644 index 0000000000..d70f315ea1 --- /dev/null +++ b/scripts/vr-edit/assets/tools/back-heading.svg @@ -0,0 +1,12 @@ + + + + BACK + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 003b973198..1036fdbcf1 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -16,7 +16,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { "use strict"; var paletteOriginOverlay, - paletteHeaderOverlay, + paletteHeaderHeadingOverlay, paletteHeaderBarOverlay, paletteTitleOverlay, palettePanelOverlay, @@ -47,12 +47,12 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: false }, - PALETTE_HEADER_PROPERTIES = { - dimensions: UIT.dimensions.header, + PALETTE_HEADER_HEADING_PROPERTIES = { + dimensions: UIT.dimensions.headerHeading, localPosition: { x: 0, - y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.header.y / 2, - z: UIT.dimensions.header.z / 2 + y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.headerHeading.y / 2, + z: UIT.dimensions.headerHeading.z / 2 }, localRotation: Quat.ZERO, color: UIT.colors.baseGray, @@ -66,7 +66,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: UIT.dimensions.headerBar, localPosition: { x: 0, - y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.header.y - UIT.dimensions.headerBar.y / 2, + y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.headerHeading.y - UIT.dimensions.headerBar.y / 2, z: UIT.dimensions.headerBar.z / 2 }, localRotation: Quat.ZERO, @@ -80,7 +80,11 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { PALETTE_TITLE_PROPERTIES = { url: "../assets/create/create-heading.svg", scale: 0.0363, - localPosition: { x: 0, y: 0, z: PALETTE_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, + localPosition: { + x: 0, + y: 0, + z: PALETTE_HEADER_HEADING_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset + }, localRotation: Quat.ZERO, color: UIT.colors.white, alpha: 1.0, @@ -92,7 +96,11 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { PALETTE_PANEL_PROPERTIES = { dimensions: UIT.dimensions.panel, - localPosition: { x: 0, y: UIT.dimensions.panel.y / 2 - UIT.dimensions.canvas.y / 2, z: UIT.dimensions.panel.z / 2 }, + localPosition: { + x: 0, + y: UIT.dimensions.panel.y / 2 - UIT.dimensions.canvas.y / 2, + z: UIT.dimensions.panel.z / 2 + }, localRotation: Quat.ZERO, color: UIT.colors.baseGray, alpha: 1.0, @@ -216,7 +224,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { url: "../assets/create/prism.fbx", localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) - } + } }, entity: { type: "Shape", @@ -282,7 +290,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { setHand(side); function getEntityIDs() { - return [palettePanelOverlay, paletteHeaderOverlay, paletteHeaderBarOverlay].concat(paletteItemOverlays); + return [palettePanelOverlay, paletteHeaderHeadingOverlay, paletteHeaderBarOverlay].concat(paletteItemOverlays); } function update(intersectionOverlayID) { @@ -382,14 +390,14 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { paletteOriginOverlay = Overlays.addOverlay("sphere", properties); // Header. - properties = Object.clone(PALETTE_HEADER_PROPERTIES); + properties = Object.clone(PALETTE_HEADER_HEADING_PROPERTIES); properties.parentID = paletteOriginOverlay; - paletteHeaderOverlay = Overlays.addOverlay("cube", properties); + paletteHeaderHeadingOverlay = Overlays.addOverlay("cube", properties); properties = Object.clone(PALETTE_HEADER_BAR_PROPERTIES); properties.parentID = paletteOriginOverlay; paletteHeaderBarOverlay = Overlays.addOverlay("cube", properties); properties = Object.clone(PALETTE_TITLE_PROPERTIES); - properties.parentID = paletteHeaderOverlay; + properties.parentID = paletteHeaderHeadingOverlay; properties.url = Script.resolvePath(properties.url); paletteTitleOverlay = Overlays.addOverlay("image3d", properties); @@ -430,8 +438,8 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { return; } Overlays.deleteOverlay(paletteOriginOverlay); // Automatically deletes all other overlays because they're children. - paletteItemOverlays = [], - paletteItemHoverOverlays = [], + paletteItemOverlays = []; + paletteItemHoverOverlays = []; isDisplaying = false; } diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index c84faae972..c0fdc8754c 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -19,6 +19,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { menuOriginOverlay, menuHeaderOverlay, + menuHeaderHeadingOverlay, menuHeaderBarOverlay, menuHeaderBackOverlay, menuHeaderTitleOverlay, @@ -59,33 +60,41 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { displayInFront: true }, + MENU_HEADER_HOVER_OFFSET = { x: 0, y: 0, z: 0.0040 }, + MENU_HEADER_PROPERTIES = { - dimensions: UIT.dimensions.header, + dimensions: Vec3.sum(UIT.dimensions.header, MENU_HEADER_HOVER_OFFSET), // Keep the laser on top when hover. localPosition: { x: 0, y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.header.y / 2, - z: UIT.dimensions.header.z / 2 + z: UIT.dimensions.header.z / 2 + MENU_HEADER_HOVER_OFFSET.z / 2 }, localRotation: Quat.ZERO, + alpha: 0.0, // Invisible + solid: true, + ignoreRayIntersection: false, + visible: true // Catch laser intersections. + }, + + MENU_HEADER_HEADING_PROPERTIES = { + dimensions: UIT.dimensions.headerHeading, + localPosition: { x: 0, y: UIT.dimensions.headerBar.y / 2, z: -MENU_HEADER_HOVER_OFFSET.z / 2 }, + localRotation: Quat.ZERO, color: UIT.colors.baseGray, alpha: 1.0, solid: true, - ignoreRayIntersection: false, + ignoreRayIntersection: true, visible: true }, MENU_HEADER_BAR_PROPERTIES = { dimensions: UIT.dimensions.headerBar, - localPosition: { - x: 0, - y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.header.y - UIT.dimensions.headerBar.y / 2, - z: UIT.dimensions.headerBar.z / 2 - }, + localPosition: { x: 0, y: -UIT.dimensions.headerHeading.y / 2 - UIT.dimensions.headerBar.y / 2, z: 0 }, localRotation: Quat.ZERO, color: UIT.colors.greenHighlight, alpha: 1.0, solid: true, - ignoreRayIntersection: false, + ignoreRayIntersection: true, visible: true }, @@ -93,9 +102,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { url: "../assets/tools/back-icon.svg", dimensions: { x: 0.0069, y: 0.0107 }, localPosition: { - x: -MENU_HEADER_PROPERTIES.dimensions.x / 2 + 0.0118 + 0.0069 / 2, + x: -MENU_HEADER_HEADING_PROPERTIES.dimensions.x / 2 + 0.0118 + 0.0069 / 2, y: 0, - z: MENU_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset + z: MENU_HEADER_HEADING_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, localRotation: Quat.ZERO, color: UIT.colors.lightGrayText, @@ -109,7 +118,11 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { MENU_HEADER_TITLE_PROPERTIES = { url: "../assets/tools/tools-heading.svg", scale: 0.0327, - localPosition: { x: 0, y: 0, z: MENU_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, + localPosition: { + x: 0, + y: 0, + z: MENU_HEADER_HEADING_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset + }, localRotation: Quat.ZERO, color: UIT.colors.white, alpha: 1.0, @@ -119,11 +132,14 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true }, + MENU_HEADER_TITLE_BACK_URL = "../assets/tools/back-heading.svg", + MENU_HEADER_TITLE_BACK_SCALE = 0.0256, + MENU_HEADER_ICON_OFFSET = { // Default right center position for header tool icons. - x: MENU_HEADER_PROPERTIES.dimensions.x / 2 - 0.0118, + x: MENU_HEADER_HEADING_PROPERTIES.dimensions.x / 2 - 0.0118, y: 0, - z: MENU_HEADER_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset + z: MENU_HEADER_HEADING_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, MENU_HEADER_ICON_PROPERTIES = { @@ -1165,7 +1181,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isHighlightingPicklist, isPicklistOpen, pressedItem = null, - pressedSource, + pressedSource = null, isPicklistPressed, isPicklistItemPressed, isTriggerClicked, @@ -1184,6 +1200,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, isDisplaying = false, + isOptionsOpen = false, + isOptionsHeadingRaised = false, + optionsHeadingURL, + optionsHeadingScale, // References. controlHand, @@ -1209,7 +1229,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { setHand(side); function getEntityIDs() { - return [menuPanelOverlay, menuHeaderOverlay, menuHeaderBarOverlay].concat(menuOverlays).concat(optionsOverlays); + return [menuPanelOverlay, menuHeaderOverlay].concat(menuOverlays).concat(optionsOverlays); } function openMenu() { @@ -1309,10 +1329,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { closeMenu(); // Update header. + optionsHeadingURL = Script.resolvePath(menuItem.title.url); + optionsHeadingScale = menuItem.title.scale; Overlays.editOverlay(menuHeaderBackOverlay, { visible: true }); Overlays.editOverlay(menuHeaderTitleOverlay, { - url: Script.resolvePath(menuItem.title.url), - scale: menuItem.title.scale + url: optionsHeadingURL, + scale: optionsHeadingScale }); Overlays.editOverlay(menuHeaderIconOverlay, { url: Script.resolvePath(menuItem.icon.properties.url), @@ -1529,6 +1551,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsEnabled[groupButtonIndex] = false; optionsEnabled[ungroupButtonIndex] = false; } + + isOptionsOpen = true; } function closeOptions() { @@ -1556,8 +1580,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { pressedItem = null; + isOptionsOpen = false; + // Display menu items. - openMenu(true); + openMenu(); } function clearTool() { @@ -1947,13 +1973,62 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { s, h; - // Intersection details. + isTriggerClicked = controlHand.triggerClicked(); + + // Handle heading. + if (isOptionsOpen && intersection.overlayID === menuHeaderOverlay) { + if (isTriggerClicked && !wasTriggerClicked) { + // Lower and unhighlight heading; go back to Tools menu. + Overlays.editOverlay(menuHeaderHeadingOverlay, { + localPosition: MENU_HEADER_HEADING_PROPERTIES.localPosition, + color: UIT.colors.baseGray, + emissive: false + }); + isOptionsHeadingRaised = false; + doCommand("clearTool"); + } else if (!isOptionsHeadingRaised) { + // Hover heading. + Overlays.editOverlay(menuHeaderHeadingOverlay, { + localPosition: Vec3.sum(MENU_HEADER_HEADING_PROPERTIES.localPosition, MENU_HEADER_HOVER_OFFSET), + color: UIT.colors.greenHighlight, + emissive: true // TODO: This has no effect. + }); + Overlays.editOverlay(menuHeaderTitleOverlay, { + url: Script.resolvePath(MENU_HEADER_TITLE_BACK_URL), + scale: MENU_HEADER_TITLE_BACK_SCALE + }); + Overlays.editOverlay(menuHeaderIconOverlay, { + visible: false + }); + isOptionsHeadingRaised = true; + } + } else { + if (isOptionsHeadingRaised) { + // Unhover heading. + Overlays.editOverlay(menuHeaderHeadingOverlay, { + localPosition: MENU_HEADER_HEADING_PROPERTIES.localPosition, + color: UIT.colors.baseGray, + emissive: false + }); + Overlays.editOverlay(menuHeaderTitleOverlay, { + url: optionsHeadingURL, + scale: optionsHeadingScale + }); + Overlays.editOverlay(menuHeaderIconOverlay, { + visible: true + }); + isOptionsHeadingRaised = false; + } + } + + // Intersection details for menus and options. + intersectionOverlays = null; + intersectionEnabled = null; if (intersection.overlayID) { intersectedItem = menuOverlays.indexOf(intersection.overlayID); if (intersectedItem !== NONE) { intersectionItems = MENU_ITEMS; intersectionOverlays = menuOverlays; - intersectionEnabled = null; } else { intersectedItem = optionsOverlays.indexOf(intersection.overlayID); if (intersectedItem !== NONE) { @@ -1963,9 +2038,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } } - if (!intersectionOverlays) { - return; - } // Highlight clickable item. if (intersectedItem !== highlightedItem || intersectionOverlays !== highlightedSource) { @@ -2043,6 +2115,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } else if (highlightedItem !== NONE) { // Un-highlight previous button. + print("$$$$$$$ unhighlight clickable item"); Overlays.editOverlay(highlightOverlay, { visible: false }); @@ -2070,7 +2143,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } // Press/unpress button. - isTriggerClicked = controlHand.triggerClicked(); if ((pressedItem && intersectedItem !== pressedItem.index) || intersectionOverlays !== pressedSource || isTriggerClicked !== (pressedItem !== null)) { if (pressedItem) { @@ -2079,6 +2151,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: pressedItem.localPosition }); pressedItem = null; + pressedSource = null; } if (isHighlightingButton && (intersectionEnabled === null || intersectionEnabled[intersectedItem]) && isTriggerClicked && !wasTriggerClicked) { @@ -2269,21 +2342,24 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.clone(MENU_HEADER_PROPERTIES); properties.parentID = menuOriginOverlay; menuHeaderOverlay = Overlays.addOverlay("cube", properties); + properties = Object.clone(MENU_HEADER_HEADING_PROPERTIES); + properties.parentID = menuHeaderOverlay; + menuHeaderHeadingOverlay = Overlays.addOverlay("cube", properties); properties = Object.clone(MENU_HEADER_BAR_PROPERTIES); - properties.parentID = menuOriginOverlay; + properties.parentID = menuHeaderHeadingOverlay; menuHeaderBarOverlay = Overlays.addOverlay("cube", properties); - // Header content. + // Heading content. properties = Object.clone(MENU_HEADER_BACK_PROPERTIES); - properties.parentID = menuHeaderOverlay; + properties.parentID = menuHeaderHeadingOverlay; properties.url = Script.resolvePath(properties.url); menuHeaderBackOverlay = Overlays.addOverlay("image3d", properties); properties = Object.clone(MENU_HEADER_TITLE_PROPERTIES); - properties.parentID = menuHeaderOverlay; + properties.parentID = menuHeaderHeadingOverlay; properties.url = Script.resolvePath(properties.url); menuHeaderTitleOverlay = Overlays.addOverlay("image3d", properties); properties = Object.clone(MENU_HEADER_ICON_PROPERTIES); - properties.parentID = menuHeaderOverlay; + properties.parentID = menuHeaderHeadingOverlay; properties.url = Script.resolvePath(properties.url); menuHeaderIconOverlay = Overlays.addOverlay("image3d", properties); diff --git a/scripts/vr-edit/modules/uit.js b/scripts/vr-edit/modules/uit.js index 8bc9cf76e4..70ef74f5c0 100644 --- a/scripts/vr-edit/modules/uit.js +++ b/scripts/vr-edit/modules/uit.js @@ -32,7 +32,8 @@ UIT = (function () { handOffset: 0.085, // Distance from hand (wrist) joint to center of canvas. handLateralOffset: 0.01, // Offset of UI in direction of palm normal. - header: { x: 0.24, y: 0.044, z: 0.012 }, + header: { x: 0.24, y: 0.048, z: 0.012 }, + headerHeading: { x: 0.24, y: 0.044, z: 0.012 }, headerBar: { x: 0.24, y: 0.004, z: 0.012 }, panel: { x: 0.24, y: 0.18, z: 0.008 }, From 5fec240ea6fb6b9a608f49ee97c21b0c8e955336 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 25 Aug 2017 09:20:00 +1200 Subject: [PATCH 241/722] Move Tools and Create panels closer together --- scripts/vr-edit/modules/uit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/uit.js b/scripts/vr-edit/modules/uit.js index 70ef74f5c0..57bc001280 100644 --- a/scripts/vr-edit/modules/uit.js +++ b/scripts/vr-edit/modules/uit.js @@ -28,7 +28,7 @@ UIT = (function () { // Offsets are relative to parents' centers. dimensions: { canvas: { x: 0.24, y: 0.24 }, // Overall UI size. - canvasSeparation: 0.01, // Gap between Tools menu and Create panel. + canvasSeparation: 0.004, // Gap between Tools menu and Create panel. handOffset: 0.085, // Distance from hand (wrist) joint to center of canvas. handLateralOffset: 0.01, // Offset of UI in direction of palm normal. From 3b299fc34d4278ffb9fdc6c329f9911b94fa48d7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 25 Aug 2017 12:05:24 +1200 Subject: [PATCH 242/722] Fix Tools menu displaying on right hand --- scripts/vr-edit/modules/toolsMenu.js | 34 +++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index c0fdc8754c..5cfdd5236b 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -17,6 +17,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { var attachmentJointName, + menuOriginLocalPosition, + menuOriginLocalRotation, + menuOriginOverlay, menuHeaderOverlay, menuHeaderHeadingOverlay, @@ -40,18 +43,22 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightOverlay, LEFT_HAND = 0, - PANEL_ORIGIN_POSITION = { + PANEL_ORIGIN_POSITION_LEFT_HAND = { x: -UIT.dimensions.canvasSeparation - UIT.dimensions.canvas.x / 2, y: UIT.dimensions.handOffset, z: 0 }, - PANEL_ORIGIN_ROTATION = Quat.fromVec3Degrees({ x: 0, y: -90, z: 0 }), + PANEL_ORIGIN_POSITION_RIGHT_HAND = { + x: UIT.dimensions.canvasSeparation + UIT.dimensions.canvas.x / 2, + y: UIT.dimensions.handOffset, + z: 0 + }, + PANEL_ORIGIN_ROTATION_LEFT_HAND = Quat.fromVec3Degrees({ x: 0, y: -90, z: 0 }), + PANEL_ORIGIN_ROTATION_RIGHT_HAND = Quat.fromVec3Degrees({ x: 0, y: 90, z: 0 }), panelLateralOffset, MENU_ORIGIN_PROPERTIES = { dimensions: { x: 0.005, y: 0.005, z: 0.005 }, - localPosition: PANEL_ORIGIN_POSITION, - localRotation: PANEL_ORIGIN_ROTATION, color: { red: 255, blue: 0, green: 0 }, alpha: 1.0, parentID: Uuid.SELF, @@ -1221,9 +1228,19 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { function setHand(hand) { // Assumes UI is not displaying. side = hand; - controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); - attachmentJointName = side === LEFT_HAND ? "LeftHand" : "RightHand"; - panelLateralOffset = side === LEFT_HAND ? -UIT.dimensions.handLateralOffset : UIT.dimensions.handLateralOffset; + if (side === LEFT_HAND) { + controlHand = rightInputs.hand(); + attachmentJointName = "LeftHand"; + panelLateralOffset = -UIT.dimensions.handLateralOffset; + menuOriginLocalPosition = PANEL_ORIGIN_POSITION_LEFT_HAND; + menuOriginLocalRotation = PANEL_ORIGIN_ROTATION_LEFT_HAND; + } else { + controlHand = leftInputs.hand(); + attachmentJointName = "RightHand"; + panelLateralOffset = UIT.dimensions.handLateralOffset; + menuOriginLocalPosition = PANEL_ORIGIN_POSITION_RIGHT_HAND; + menuOriginLocalRotation = PANEL_ORIGIN_ROTATION_RIGHT_HAND; + } } setHand(side); @@ -2335,7 +2352,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Menu origin. properties = Object.clone(MENU_ORIGIN_PROPERTIES); properties.parentJointIndex = handJointIndex; - properties.localPosition = Vec3.sum(properties.localPosition, { x: panelLateralOffset, y: 0, z: 0 }); + properties.localPosition = Vec3.sum(menuOriginLocalPosition, { x: panelLateralOffset, y: 0, z: 0 }); + properties.localRotation = menuOriginLocalRotation; menuOriginOverlay = Overlays.addOverlay("sphere", properties); // Header. From 850efec97bd45064fd856a9e941caf2aea4eaa71 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 25 Aug 2017 14:07:41 +1200 Subject: [PATCH 243/722] Display tool icon on dominant hand when tool is active --- scripts/vr-edit/assets/tools/tool-icon.fbx | Bin 0 -> 55292 bytes scripts/vr-edit/modules/toolIcon.js | 139 +++++++++++++++------ 2 files changed, 102 insertions(+), 37 deletions(-) create mode 100644 scripts/vr-edit/assets/tools/tool-icon.fbx diff --git a/scripts/vr-edit/assets/tools/tool-icon.fbx b/scripts/vr-edit/assets/tools/tool-icon.fbx new file mode 100644 index 0000000000000000000000000000000000000000..f407ad7feb4824077f715ede315c691f1a52eedc GIT binary patch literal 55292 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 UsT&vT`YTc2+l3pQ1Lf!c9{_bN)c^nh literal 0 HcmV?d00001 diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 551e141440..861d1fa624 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -23,37 +23,63 @@ ToolIcon = function (side) { PHYSICS_TOOL = 5, DELETE_TOOL = 6, - ICON_COLORS = [ - { red: 0, green: 240, blue: 240 }, - { red: 240, green: 240, blue: 0 }, - { red: 220, green: 60, blue: 220 }, - { red: 220, green: 220, blue: 220 }, - { red: 0, green: 0, blue: 0 }, - { red: 60, green: 60, blue: 240 }, - { red: 240, green: 60, blue: 60 } - ], - LEFT_HAND = 0, - ICON_DIMENSIONS = { x: 0.1, y: 0.01, z: 0.1 }, - ICON_POSITION = { x: 0, y: 0.01, z: 0 }, - ICON_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: 0 }), + MODEL_DIMENSIONS = { x: 0.1944, y: 0.1928, z: 0.1928 }, // Raw FBX dimensions. + MODEL_SCALE = 0.7, // Adjust icon dimensions so that the green bar matches that of the Tools header. + MODEL_POSITION_LEFT_HAND = { x: -0.03, y: 0.035, z: 0 }, // x raises in thumb direction; y moves in fingers direction. + MODEL_POSITION_RIGHT_HAND = { x: 0.03, y: 0.035, z: 0 }, // "" + MODEL_ROTATION_LEFT_HAND = Quat.fromVec3Degrees({ x: 0, y: 0, z: 100 }), + MODEL_ROTATION_RIGHT_HAND = Quat.fromVec3Degrees({ x: 0, y: 180, z: -100 }), - ICON_TYPE = "sphere", - ICON_PROPERTIES = { - dimensions: ICON_DIMENSIONS, - localPosition: ICON_POSITION, - localRotation: ICON_ROTATION, + MODEL_TYPE = "model", + MODEL_PROPERTIES = { + url: "../assets/tools/tool-icon.fbx", + dimensions: Vec3.multiply(MODEL_SCALE, MODEL_DIMENSIONS), solid: true, alpha: 1.0, parentID: Uuid.SELF, - ignoreRayIntersection: false, + ignoreRayIntersection: true, visible: true }, - handJointName, + IMAGE_TYPE = "image3d", + IMAGE_PROPERTIES = { + localRotation: Quat.fromVec3Degrees({ x: -90, y: -90, z: 0 }), + alpha: 1.0, + emissive: true, + ignoreRayIntersection: true, + isFacingAvatar: false, + visible: true + }, - iconOverlay = null; + ICON_PROPERTIES = { + url: "../assets/tools/stretch-icon.svg", + dimensions: { x: 0.0167, y: 0.0167 }, + localPosition: { x: 0.020, y: 0.069, z: 0 }, // Relative to model overlay. + color: UIT.colors.lightGrayText // x is in fingers direction; y is in thumb direction. + }, + LABEL_PROPERTIES = { + url: "../assets/tools/stretch-label.svg", + scale: 0.0311, + localPosition: { x: -0.040, y: 0.067, z: 0 }, + color: UIT.colors.white + }, + SUBLABEL_PROPERTIES = { + url: "../assets/tools/tool-label.svg", + scale: 0.0152, + localPosition: { x: -0.055, y: 0.067, z: 0 }, + color: UIT.colors.lightGrayText + }, + + ICON_SCALE_FACTOR = 3.0, + LABEL_SCALE_FACTOR = 1.8, + + handJointName, + localPosition, + localRotation, + + modelOverlay = null; if (!this instanceof ToolIcon) { return new ToolIcon(); @@ -61,7 +87,15 @@ ToolIcon = function (side) { function setHand(side) { // Assumes UI is not displaying. - handJointName = side === LEFT_HAND ? "LeftHand" : "RightHand"; + if (side === LEFT_HAND) { + handJointName = "LeftHand"; + localPosition = MODEL_POSITION_LEFT_HAND; + localRotation = MODEL_ROTATION_LEFT_HAND; + } else { + handJointName = "RightHand"; + localPosition = MODEL_POSITION_RIGHT_HAND; + localRotation = MODEL_ROTATION_RIGHT_HAND; + } } setHand(side); @@ -71,10 +105,18 @@ ToolIcon = function (side) { // TODO: Clear icon animation. } + function clear() { + // Deletes current tool model. + if (modelOverlay) { + Overlays.deleteOverlay(modelOverlay); // Child overlays are automatically deleted. + modelOverlay = null; + } + } + function display(icon) { // Displays icon on hand. var handJointIndex, - iconProperties; + properties; handJointIndex = MyAvatar.getJointIndex(handJointName); if (handJointIndex === -1) { @@ -84,22 +126,45 @@ ToolIcon = function (side) { return; } - if (iconOverlay === null) { - iconProperties = Object.clone(ICON_PROPERTIES); - iconProperties.parentJointIndex = handJointIndex; - iconProperties.color = ICON_COLORS[icon]; - iconOverlay = Overlays.addOverlay(ICON_TYPE, iconProperties); - } else { - Overlays.editOverlay(iconOverlay, { color: ICON_COLORS[icon] }); + if (modelOverlay !== null) { + // Should never happen because tool needs to be cleared in order for user to return to Tools menu. + clear(); } - } - function clear() { - // Deletes current icon. - if (iconOverlay) { - Overlays.deleteOverlay(iconOverlay); - iconOverlay = null; - } + // Model. + properties = Object.clone(MODEL_PROPERTIES); + properties.url = Script.resolvePath(properties.url); + properties.parentJointIndex = handJointIndex; + properties.localPosition = localPosition; + properties.localRotation = localRotation; + modelOverlay = Overlays.addOverlay(MODEL_TYPE, properties); + + // Icon. + properties = Object.clone(IMAGE_PROPERTIES); + properties = Object.merge(properties, ICON_PROPERTIES); + properties.parentID = modelOverlay; + properties.url = Script.resolvePath(properties.url); + properties.dimensions = { + x: ICON_SCALE_FACTOR * properties.dimensions.x, + y: ICON_SCALE_FACTOR * properties.dimensions.y + }; + Overlays.addOverlay(IMAGE_TYPE, properties); + + // Label. + properties = Object.clone(IMAGE_PROPERTIES); + properties = Object.merge(properties, LABEL_PROPERTIES); + properties.parentID = modelOverlay; + properties.url = Script.resolvePath(properties.url); + properties.scale = LABEL_SCALE_FACTOR * properties.scale; + Overlays.addOverlay(IMAGE_TYPE, properties); + + // Sublabel. + properties = Object.clone(IMAGE_PROPERTIES); + properties = Object.merge(properties, SUBLABEL_PROPERTIES); + properties.parentID = modelOverlay; + properties.url = Script.resolvePath(properties.url); + properties.scale = LABEL_SCALE_FACTOR * properties.scale; + Overlays.addOverlay(IMAGE_TYPE, properties); } function destroy() { From eda513caf0e5df84391b6f921bacaac294645cd2 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 25 Aug 2017 14:58:05 +1200 Subject: [PATCH 244/722] Display appropriate icon and label on tool icon per current tool --- scripts/vr-edit/modules/toolIcon.js | 40 +++++++--------------------- scripts/vr-edit/modules/toolsMenu.js | 23 +++++++++++++++- scripts/vr-edit/vr-edit.js | 17 ++++++------ 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 861d1fa624..d03351b27c 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -15,15 +15,7 @@ ToolIcon = function (side) { "use strict"; - var SCALE_TOOL = 0, - CLONE_TOOL = 1, - GROUP_TOOL = 2, - COLOR_TOOL = 3, - PICK_COLOR_TOOL = 4, - PHYSICS_TOOL = 5, - DELETE_TOOL = 6, - - LEFT_HAND = 0, + var LEFT_HAND = 0, MODEL_DIMENSIONS = { x: 0.1944, y: 0.1928, z: 0.1928 }, // Raw FBX dimensions. MODEL_SCALE = 0.7, // Adjust icon dimensions so that the green bar matches that of the Tools header. @@ -54,20 +46,14 @@ ToolIcon = function (side) { }, ICON_PROPERTIES = { - url: "../assets/tools/stretch-icon.svg", - dimensions: { x: 0.0167, y: 0.0167 }, localPosition: { x: 0.020, y: 0.069, z: 0 }, // Relative to model overlay. color: UIT.colors.lightGrayText // x is in fingers direction; y is in thumb direction. }, LABEL_PROPERTIES = { - url: "../assets/tools/stretch-label.svg", - scale: 0.0311, localPosition: { x: -0.040, y: 0.067, z: 0 }, color: UIT.colors.white }, SUBLABEL_PROPERTIES = { - url: "../assets/tools/tool-label.svg", - scale: 0.0152, localPosition: { x: -0.055, y: 0.067, z: 0 }, color: UIT.colors.lightGrayText }, @@ -113,7 +99,7 @@ ToolIcon = function (side) { } } - function display(icon) { + function display(iconInfo) { // Displays icon on hand. var handJointIndex, properties; @@ -143,27 +129,28 @@ ToolIcon = function (side) { properties = Object.clone(IMAGE_PROPERTIES); properties = Object.merge(properties, ICON_PROPERTIES); properties.parentID = modelOverlay; - properties.url = Script.resolvePath(properties.url); + properties.url = Script.resolvePath(iconInfo.icon.properties.url); properties.dimensions = { - x: ICON_SCALE_FACTOR * properties.dimensions.x, - y: ICON_SCALE_FACTOR * properties.dimensions.y + x: ICON_SCALE_FACTOR * iconInfo.icon.properties.dimensions.x, + y: ICON_SCALE_FACTOR * iconInfo.icon.properties.dimensions.y }; + properties.localPosition.y += ICON_SCALE_FACTOR * iconInfo.icon.headerOffset.y; Overlays.addOverlay(IMAGE_TYPE, properties); // Label. properties = Object.clone(IMAGE_PROPERTIES); properties = Object.merge(properties, LABEL_PROPERTIES); properties.parentID = modelOverlay; - properties.url = Script.resolvePath(properties.url); - properties.scale = LABEL_SCALE_FACTOR * properties.scale; + properties.url = Script.resolvePath(iconInfo.label.properties.url); + properties.scale = LABEL_SCALE_FACTOR * iconInfo.label.properties.scale; Overlays.addOverlay(IMAGE_TYPE, properties); // Sublabel. properties = Object.clone(IMAGE_PROPERTIES); properties = Object.merge(properties, SUBLABEL_PROPERTIES); properties.parentID = modelOverlay; - properties.url = Script.resolvePath(properties.url); - properties.scale = LABEL_SCALE_FACTOR * properties.scale; + properties.url = Script.resolvePath(iconInfo.sublabel.properties.url); + properties.scale = LABEL_SCALE_FACTOR * iconInfo.sublabel.properties.scale; Overlays.addOverlay(IMAGE_TYPE, properties); } @@ -172,13 +159,6 @@ ToolIcon = function (side) { } return { - SCALE_TOOL: SCALE_TOOL, - CLONE_TOOL: CLONE_TOOL, - GROUP_TOOL: GROUP_TOOL, - COLOR_TOOL: COLOR_TOOL, - PICK_COLOR_TOOL: PICK_COLOR_TOOL, - PHYSICS_TOOL: PHYSICS_TOOL, - DELETE_TOOL: DELETE_TOOL, setHand: setHand, update: update, display: display, diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 5cfdd5236b..6cff0a9e18 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -1156,6 +1156,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } ], + COLOR_TOOL = 0, // Indexes of corresponding MENU_ITEMS item. + SCALE_TOOL = 1, + CLONE_TOOL = 2, + GROUP_TOOL = 3, + PHYSICS_TOOL = 4, + DELETE_TOOL = 5, HIGHLIGHT_PROPERTIES = { xDelta: 0.004, @@ -1249,6 +1255,15 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { return [menuPanelOverlay, menuHeaderOverlay].concat(menuOverlays).concat(optionsOverlays); } + function getIconInfo(tool) { + // Provides details of tool icon, label, and sublabel images for the specified tool. + return { + icon: MENU_ITEMS[tool].icon, + label: MENU_ITEMS[tool].label, + sublabel: UI_ELEMENTS.menuButton.sublabel + }; + } + function openMenu() { var properties, itemID, @@ -2132,7 +2147,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } else if (highlightedItem !== NONE) { // Un-highlight previous button. - print("$$$$$$$ unhighlight clickable item"); Overlays.editOverlay(highlightOverlay, { visible: false }); @@ -2447,6 +2461,13 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } return { + COLOR_TOOL: COLOR_TOOL, + SCALE_TOOL: SCALE_TOOL, + CLONE_TOOL: CLONE_TOOL, + GROUP_TOOL: GROUP_TOOL, + PHYSICS_TOOL: PHYSICS_TOOL, + DELETE_TOOL: DELETE_TOOL, + iconInfo: getIconInfo, setHand: setHand, entityIDs: getEntityIDs, clearTool: clearTool, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index bd8ae74a18..67976a1d91 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -224,7 +224,7 @@ } function setToolIcon(icon) { - toolIcon.display(icon); + toolIcon.display(toolsMenu.iconInfo(icon)); } function clearTool() { @@ -289,13 +289,12 @@ setHand: setHand, setToolIcon: setToolIcon, clearTool: clearTool, - SCALE_TOOL: toolIcon.SCALE_TOOL, - CLONE_TOOL: toolIcon.CLONE_TOOL, - GROUP_TOOL: toolIcon.GROUP_TOOL, - COLOR_TOOL: toolIcon.COLOR_TOOL, - PICK_COLOR_TOOL: toolIcon.PICK_COLOR_TOOL, - PHYSICS_TOOL: toolIcon.PHYSICS_TOOL, - DELETE_TOOL: toolIcon.DELETE_TOOL, + COLOR_TOOL: toolsMenu.COLOR_TOOL, + SCALE_TOOL: toolsMenu.SCALE_TOOL, + CLONE_TOOL: toolsMenu.CLONE_TOOL, + GROUP_TOOL: toolsMenu.GROUP_TOOL, + PHYSICS_TOOL: toolsMenu.PHYSICS_TOOL, + DELETE_TOOL: toolsMenu.DELETE_TOOL, display: display, updateUIEntities: setUIEntities, doPickColor: doPickColor, @@ -1355,7 +1354,7 @@ case "pickColorTool": grouping.clear(); toolSelected = TOOL_PICK_COLOR; - ui.setToolIcon(ui.PICK_COLOR_TOOL); + ui.setToolIcon(ui.COLOR_TOOL); ui.updateUIEntities(); break; case "physicsTool": From 71c3a58e88e1c96df0dd09dc7b8eb32ed8a7e05c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 25 Aug 2017 20:50:12 +1200 Subject: [PATCH 245/722] Style Stretch options --- scripts/vr-edit/assets/horizontal-rule.svg | 66 ++++++++ .../assets/tools/common/actions-label.svg | 12 ++ .../assets/tools/common/finish-label.svg | 12 ++ .../vr-edit/assets/tools/common/info-icon.svg | 12 ++ .../assets/tools/stretch/info-text.svg | 12 ++ scripts/vr-edit/modules/toolsMenu.js | 160 +++++++++++++++--- scripts/vr-edit/modules/uit.js | 8 + 7 files changed, 261 insertions(+), 21 deletions(-) create mode 100644 scripts/vr-edit/assets/horizontal-rule.svg create mode 100644 scripts/vr-edit/assets/tools/common/actions-label.svg create mode 100644 scripts/vr-edit/assets/tools/common/finish-label.svg create mode 100644 scripts/vr-edit/assets/tools/common/info-icon.svg create mode 100644 scripts/vr-edit/assets/tools/stretch/info-text.svg diff --git a/scripts/vr-edit/assets/horizontal-rule.svg b/scripts/vr-edit/assets/horizontal-rule.svg new file mode 100644 index 0000000000..9202a01479 --- /dev/null +++ b/scripts/vr-edit/assets/horizontal-rule.svg @@ -0,0 +1,66 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/scripts/vr-edit/assets/tools/common/actions-label.svg b/scripts/vr-edit/assets/tools/common/actions-label.svg new file mode 100644 index 0000000000..d5428ae3f1 --- /dev/null +++ b/scripts/vr-edit/assets/tools/common/actions-label.svg @@ -0,0 +1,12 @@ + + + + ACTIONS + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/common/finish-label.svg b/scripts/vr-edit/assets/tools/common/finish-label.svg new file mode 100644 index 0000000000..58120a337a --- /dev/null +++ b/scripts/vr-edit/assets/tools/common/finish-label.svg @@ -0,0 +1,12 @@ + + + + FINISH + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/common/info-icon.svg b/scripts/vr-edit/assets/tools/common/info-icon.svg new file mode 100644 index 0000000000..ef2495b728 --- /dev/null +++ b/scripts/vr-edit/assets/tools/common/info-icon.svg @@ -0,0 +1,12 @@ + + + + [ + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/stretch/info-text.svg b/scripts/vr-edit/assets/tools/stretch/info-text.svg new file mode 100644 index 0000000000..4bd23f7b7f --- /dev/null +++ b/scripts/vr-edit/assets/tools/stretch/info-text.svg @@ -0,0 +1,12 @@ + + + + Stretch objects by g + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 6cff0a9e18..34d931bde0 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -179,7 +179,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { UI_HIGHLIGHT_COLOR = { red: 100, green: 240, blue: 100 }, UI_ELEMENTS = { - "button": { + "button": { // TODO: Delete. overlay: "cube", properties: { dimensions: { x: 0.03, y: 0.03, z: 0.01 }, @@ -259,6 +259,27 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } }, + "newButton": { // TODO: Rename to "button". + overlay: "cube", + properties: { + dimensions: UIT.dimensions.buttonDimensions, + localRotation: Quat.ZERO, + color: UIT.colors.baseGrayShadow, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + }, + newLabel: { // TODO: Rename to "label". + // Relative to newButton. + localPosition: { + x: 0, + y: 0, + z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset + }, + color: UIT.colors.white + } + }, "toggleButton": { overlay: "cube", properties: { @@ -320,7 +341,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: 0, y: 0, z: 0 }, localRotation: Quat.ZERO, - color: { red: 255, green: 255, blue: 255 }, alpha: 1.0, emissive: true, ignoreRayIntersection: true, @@ -328,6 +348,20 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, + "horizontalRule": { + overlay: "image3d", + properties: { + url: "../assets/horizontal-rule.svg", + dimensions: { x: UIT.dimensions.panel.x - 2 * UIT.dimensions.leftMargin, y: 0.001 }, + localRotation: Quat.ZERO, + color: UIT.colors.baseGrayShadow, + alpha: 1.0, + solid: true, + ignoreRayIntersection: true, + isFacingAvatar: false, + visible: true + } + }, "sphere": { overlay: "sphere", properties: { @@ -461,7 +495,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, - BUTTON_UI_ELEMENTS = ["button", "menuButton", "toggleButton", "swatch"], + BUTTON_UI_ELEMENTS = ["button", "newButton", "menuButton", "toggleButton", "swatch"], BUTTON_PRESS_DELTA = { x: 0, y: 0, z: -0.004 }, SLIDER_UI_ELEMENTS = ["barSlider", "imageSlider"], @@ -488,21 +522,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, OPTONS_PANELS = { - scaleOptions: [ - { - id: "scaleFinishButton", - type: "button", - properties: { - dimensions: { x: 0.07, y: 0.03, z: 0.01 }, - localPosition: { x: 0, y: 0, z: 0.005 }, - color: { red: 200, green: 200, blue: 200 } - }, - label: "FINISH", - command: { - method: "clearTool" - } - } - ], cloneOptions: [ { id: "cloneFinishButton", @@ -677,6 +696,86 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } ], + scaleOptions: [ + { + id: "stretchActionsLabel", + type: "image", + properties: { + color: UIT.colors.white, + url: "../assets/tools/common/actions-label.svg", + scale: 0.0276, + localPosition: { + x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0276 / 2, + y: UIT.dimensions.panel.y / 2 - UIT.dimensions.topMargin - 0.0047 / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } + } + }, + { + id: "stretchRule1", + type: "horizontalRule", + properties: { + localPosition: { + x: 0, + y: UIT.dimensions.panel.y / 2 - 0.0199, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } + } + }, + { + id: "stretchFinishButton", + type: "newButton", + properties: { + localPosition: { x: 0, y: 0.02, z: 0.005 } + }, + newLabel: { + url: "../assets/tools/common/finish-label.svg", + scale: 0.0318 + }, + command: { + method: "clearTool" + } + }, + { + id: "stretchRule2", + type: "horizontalRule", + properties: { + localPosition: { + x: 0, + y: UIT.dimensions.panel.y / 2 - 0.1197, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } + } + }, + { + id: "stretchInfoIcon", + type: "image", + properties: { + url: "../assets/tools/common/info-icon.svg", + dimensions: { x: 0.0321, y: 0.0320 }, + localPosition: { + x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0321 / 2, + y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + }, + color: UIT.colors.white // Icon SVG is already lightGray color. + } + }, + { + id: "stretchInfo", + type: "image", + properties: { + url: "../assets/tools/stretch/info-text.svg", + scale: 0.1340, + localPosition: { + x: -UIT.dimensions.panel.x / 2 + 0.0679 + 0.1340 / 2, + y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2, // Center on info icon. + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + }, + color: UIT.colors.white + } + } + ], physicsOptions: [ { id: "propertiesLabel", @@ -961,7 +1060,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, icon: { - type: "image", + type: "image", // TODO: Can delete this and similar occurrences? properties: { url: "../assets/tools/color-icon.svg", dimensions: { x: 0.0165, y: 0.0187 } @@ -969,7 +1068,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { headerOffset: { x: -0.00825, y: 0.0020, z: 0 } }, label: { - type: "image", + type: "image", // TODO: Can delete this and similar occurrences? properties: { url: "../assets/tools/color-label.svg", scale: 0.0241 @@ -1188,6 +1287,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightedItems, highlightedSource, isHighlightingButton, + isHighlightingNewButton, // TODO: Delete when no longer needed. isHighlightingMenuButton, isHighlightingSlider, isHighlightingColorCircle, @@ -1432,6 +1532,14 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id = Overlays.addOverlay(UI_ELEMENTS.label.overlay, properties); optionsOverlaysLabels[i] = id; } + if (optionsItems[i].newLabel) { + properties = Object.clone(UI_ELEMENTS.image.properties); + properties = Object.merge(properties, UI_ELEMENTS[optionsItems[i].type].newLabel); + properties = Object.merge(properties, optionsItems[i].newLabel); + properties.url = Script.resolvePath(properties.url); + properties.parentID = optionsOverlays[optionsOverlays.length - 1]; + Overlays.addOverlay(UI_ELEMENTS.image.overlay, properties); + } if (optionsItems[i].type === "barSlider") { optionsSliderData[i] = {}; @@ -2092,6 +2200,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightedItem = intersectedItem; highlightedItems = intersectionItems; isHighlightingButton = BUTTON_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; + isHighlightingNewButton = intersectionItems[highlightedItem].type === "newButton"; isHighlightingMenuButton = intersectionItems[highlightedItem].type === "menuButton"; isHighlightingSlider = SLIDER_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; isHighlightingColorCircle = COLOR_CIRCLE_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; @@ -2131,6 +2240,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: HIGHLIGHT_PROPERTIES.properties.color, visible: true }); + } else if (isHighlightingNewButton) { + Overlays.editOverlay(intersectionOverlays[highlightedItem], { + color: UIT.colors.greenHighlight + }); } else if (!isHighlightingMenuButton) { Overlays.editOverlay(highlightOverlay, { parentID: intersectionOverlays[intersectedItem], @@ -2156,8 +2269,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, visible: false }); - // Lower slider or color circle. + } else if (isHighlightingNewButton) { + Overlays.editOverlay(highlightedSource[highlightedItem], { + color: UIT.colors.baseGrayShadow + }); } else if (isHighlightingSlider || isHighlightingColorCircle) { + // Lower slider or color circle. Overlays.editOverlay(highlightedSource[highlightedItem], { localPosition: highlightedItems[highlightedItem].properties.localPosition }); @@ -2165,6 +2282,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Update status variables. highlightedItem = NONE; isHighlightingButton = false; + isHighlightingNewButton = false; isHighlightingMenuButton = false; isHighlightingSlider = false; isHighlightingColorCircle = false; diff --git a/scripts/vr-edit/modules/uit.js b/scripts/vr-edit/modules/uit.js index 57bc001280..ae3c1674f9 100644 --- a/scripts/vr-edit/modules/uit.js +++ b/scripts/vr-edit/modules/uit.js @@ -19,6 +19,7 @@ UIT = (function () { faintGray: { red: 0xe3, green: 0xe3, blue: 0xe3 }, lightGrayText: { red: 0xaf, green: 0xaf, blue: 0xaf }, baseGray: { red: 0x40, green: 0x40, blue: 0x40 }, + baseGrayShadow: { red: 0x25, green: 0x25, blue: 0x25 }, darkGray: { red: 0x12, green: 0x12, blue: 0x12 }, greenHighlight: { red: 0x1f, green: 0xc6, blue: 0xa6 }, blueHighlight: { red: 0x00, green: 0xbf, blue: 0xef } @@ -32,6 +33,9 @@ UIT = (function () { handOffset: 0.085, // Distance from hand (wrist) joint to center of canvas. handLateralOffset: 0.01, // Offset of UI in direction of palm normal. + topMargin: 0.010, + leftMargin: 0.0118, + header: { x: 0.24, y: 0.048, z: 0.012 }, headerHeading: { x: 0.24, y: 0.044, z: 0.012 }, headerBar: { x: 0.24, y: 0.004, z: 0.012 }, @@ -39,6 +43,8 @@ UIT = (function () { itemCollisionZone: { x: 0.0481, y: 0.0480, z: 0.0040 }, // Cursor intersection zone for Tools and Create items. + buttonDimensions: { x: 0.2164, y: 0.0840, z: 0.0040 }, // Default size of large single options button. + menuButtonDimensions: { x: 0.0267, y: 0.0267, z: 0.0040 }, menuButtonIconOffset: { x: 0, y: 0.00935, z: -0.0040 }, // Non-hovered position relative to itemCollisionZone. menuButtonLabelYOffset: -0.00915, // Relative to itemCollisionZone. @@ -50,6 +56,8 @@ UIT = (function () { paletteItemIconDimensions: { x: 0.024, y: 0.024, z: 0.024 }, paletteItemIconOffset: { x: 0, y: 0, z: 0.015 }, // Non-hovered position relative to palette button. + horizontalRuleHeight : 0.0004, + imageOverlayOffset: 0.001 // Raise image above surface. } }; From 7e817652a773fc1c93a95eb2fac942b512f6e8e6 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 26 Aug 2017 09:51:51 +1200 Subject: [PATCH 246/722] Style Clone options --- scripts/vr-edit/modules/toolsMenu.js | 56 ++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 34d931bde0..9e409cc40e 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -522,21 +522,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, OPTONS_PANELS = { - cloneOptions: [ - { - id: "cloneFinishButton", - type: "button", - properties: { - dimensions: { x: 0.07, y: 0.03, z: 0.01 }, - localPosition: { x: 0, y: 0, z: 0.005 }, - color: { red: 200, green: 200, blue: 200 } - }, - label: "FINISH", - command: { - method: "clearTool" - } - } - ], groupOptions: [ { id: "groupButton", @@ -776,6 +761,47 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } ], + cloneOptions: [ + { + id: "cloneActionsLabel", + type: "image", + properties: { + color: UIT.colors.white, + url: "../assets/tools/common/actions-label.svg", + scale: 0.0276, + localPosition: { + x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0276 / 2, + y: UIT.dimensions.panel.y / 2 - UIT.dimensions.topMargin - 0.0047 / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } + } + }, + { + id: "cloneRule1", + type: "horizontalRule", + properties: { + localPosition: { + x: 0, + y: UIT.dimensions.panel.y / 2 - 0.0199, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } + } + }, + { + id: "cloneFinishButton", + type: "newButton", + properties: { + localPosition: { x: 0, y: 0.02, z: 0.005 } + }, + newLabel: { + url: "../assets/tools/common/finish-label.svg", + scale: 0.0318 + }, + command: { + method: "clearTool" + } + } + ], physicsOptions: [ { id: "propertiesLabel", From 73a6f1f27847a5ac063c4ffaa6cdaffb3f1ba407 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 26 Aug 2017 10:47:41 +1200 Subject: [PATCH 247/722] Style Delete options --- .../vr-edit/assets/tools/delete/info-text.svg | 83 +++++++++++++++++++ scripts/vr-edit/modules/toolsMenu.js | 77 +++++++++++++++-- 2 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 scripts/vr-edit/assets/tools/delete/info-text.svg diff --git a/scripts/vr-edit/assets/tools/delete/info-text.svg b/scripts/vr-edit/assets/tools/delete/info-text.svg new file mode 100644 index 0000000000..4148bd8525 --- /dev/null +++ b/scripts/vr-edit/assets/tools/delete/info-text.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 9e409cc40e..13c46603d0 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -1056,17 +1056,82 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { ], deleteOptions: [ { - id: "deleteFinishButton", - type: "button", + id: "deleteActionsLabel", + type: "image", properties: { - dimensions: { x: 0.07, y: 0.03, z: 0.01 }, - localPosition: { x: 0, y: 0, z: 0.005 }, - color: { red: 200, green: 200, blue: 200 } + color: UIT.colors.white, + url: "../assets/tools/common/actions-label.svg", + scale: 0.0276, + localPosition: { + x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0276 / 2, + y: UIT.dimensions.panel.y / 2 - UIT.dimensions.topMargin - 0.0047 / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } + } + }, + { + id: "deleeteRule1", + type: "horizontalRule", + properties: { + localPosition: { + x: 0, + y: UIT.dimensions.panel.y / 2 - 0.0199, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } + } + }, + { + id: "deleteFinishButton", + type: "newButton", + properties: { + localPosition: { x: 0, y: 0.02, z: 0.005 } + }, + newLabel: { + url: "../assets/tools/common/finish-label.svg", + scale: 0.0318 }, - label: "FINISH", command: { method: "clearTool" } + }, + { + id: "deleteRule2", + type: "horizontalRule", + properties: { + localPosition: { + x: 0, + y: UIT.dimensions.panel.y / 2 - 0.1197, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } + } + }, + { + id: "deleteInfoIcon", + type: "image", + properties: { + url: "../assets/tools/common/info-icon.svg", + dimensions: { x: 0.0321, y: 0.0320 }, + localPosition: { + x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0321 / 2, + y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + }, + color: UIT.colors.white // Icon SVG is already lightGray color. + } + }, + { + id: "deleteInfo", + type: "image", + properties: { + url: "../assets/tools/delete/info-text.svg", + scale: 0.1416, + localPosition: { + x: -UIT.dimensions.panel.x / 2 + 0.0679 + 0.1340 / 2, + y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0240 / 2 + 0.0063 / 2, // Off-center w.r.t. info icon. + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + }, + color: UIT.colors.white + } } ] }, From 2899d5183571cfe1e78a5bd64208bc469c140232 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 26 Aug 2017 11:18:25 +1200 Subject: [PATCH 248/722] Fix "Finish" buttons not clearing tool action --- scripts/vr-edit/vr-edit.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 67976a1d91..1ae8bf7517 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1370,6 +1370,8 @@ ui.updateUIEntities(); break; case "clearTool": + grouping.clear(); + toolSelected = TOOL_NONE; ui.clearTool(); ui.updateUIEntities(); break; From c29a900588b320b6408b18fac441522a4e66dfeb Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 26 Aug 2017 12:08:37 +1200 Subject: [PATCH 249/722] Fix button hovering and pressing problems --- scripts/vr-edit/modules/toolsMenu.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 13c46603d0..ce81847ffe 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -496,7 +496,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, BUTTON_UI_ELEMENTS = ["button", "newButton", "menuButton", "toggleButton", "swatch"], - BUTTON_PRESS_DELTA = { x: 0, y: 0, z: -0.004 }, + BUTTON_PRESS_DELTA = { x: 0, y: 0, z: -0.8 * UIT.dimensions.buttonDimensions.z }, SLIDER_UI_ELEMENTS = ["barSlider", "imageSlider"], COLOR_CIRCLE_UI_ELEMENTS = ["colorCircle"], @@ -2281,6 +2281,11 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, visible: false }); + } else if (isHighlightingNewButton) { + // Unhighlight button. + Overlays.editOverlay(highlightedSource[highlightedItem], { + color: UIT.colors.baseGrayShadow + }); } else if (isHighlightingSlider || isHighlightingColorCircle) { // Lower old slider or color circle. Overlays.editOverlay(highlightedSource[highlightedItem], { @@ -2361,6 +2366,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: false }); } else if (isHighlightingNewButton) { + // Unhighlight button. Overlays.editOverlay(highlightedSource[highlightedItem], { color: UIT.colors.baseGrayShadow }); @@ -2387,7 +2393,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { || isTriggerClicked !== (pressedItem !== null)) { if (pressedItem) { // Unpress previous button. - Overlays.editOverlay(intersectionOverlays[pressedItem.index], { + Overlays.editOverlay(pressedSource[pressedItem.index], { localPosition: pressedItem.localPosition }); pressedItem = null; From b0ef570ce0bc7d10883be42b1c3fb6534e4180d5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 26 Aug 2017 14:20:37 +1200 Subject: [PATCH 250/722] Style Group options --- .../assets/tools/group/group-label.svg | 12 + .../assets/tools/group/ungroup-label.svg | 12 + scripts/vr-edit/modules/toolsMenu.js | 207 +++++++++++++----- scripts/vr-edit/modules/uit.js | 3 + 4 files changed, 183 insertions(+), 51 deletions(-) create mode 100644 scripts/vr-edit/assets/tools/group/group-label.svg create mode 100644 scripts/vr-edit/assets/tools/group/ungroup-label.svg diff --git a/scripts/vr-edit/assets/tools/group/group-label.svg b/scripts/vr-edit/assets/tools/group/group-label.svg new file mode 100644 index 0000000000..b2f15b4b22 --- /dev/null +++ b/scripts/vr-edit/assets/tools/group/group-label.svg @@ -0,0 +1,12 @@ + + + + GROUP + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/group/ungroup-label.svg b/scripts/vr-edit/assets/tools/group/ungroup-label.svg new file mode 100644 index 0000000000..ec246359b5 --- /dev/null +++ b/scripts/vr-edit/assets/tools/group/ungroup-label.svg @@ -0,0 +1,12 @@ + + + + UNGROUP + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index ce81847ffe..83adc7582c 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -31,6 +31,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { menuOverlays = [], menuHoverOverlays = [], + menuEnabled = [], optionsOverlays = [], optionsOverlaysIDs = [], // Text ids (names) of options overlays. @@ -522,36 +523,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, OPTONS_PANELS = { - groupOptions: [ - { - id: "groupButton", - type: "button", - properties: { - dimensions: { x: 0.07, y: 0.03, z: 0.01 }, - localPosition: { x: 0, y: 0.025, z: 0.005 }, - color: { red: 200, green: 200, blue: 200 } - }, - label: " GROUP", - enabledColor: { red: 64, green: 240, blue: 64 }, - callback: { - method: "groupButton" - } - }, - { - id: "ungroupButton", - type: "button", - properties: { - dimensions: { x: 0.07, y: 0.03, z: 0.01 }, - localPosition: { x: 0, y: -0.025, z: 0.005 }, - color: { red: 200, green: 200, blue: 200 } - }, - label: "UNGROUP", - enabledColor: { red: 240, green: 64, blue: 64 }, - callback: { - method: "ungroupButton" - } - } - ], colorOptions: [ { id: "colorCircle", @@ -711,7 +682,11 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "stretchFinishButton", type: "newButton", properties: { - localPosition: { x: 0, y: 0.02, z: 0.005 } + localPosition: { + x: 0, + y: UIT.dimensions.panel.y / 2 - 0.0280 - UIT.dimensions.buttonDimensions.y / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 + } }, newLabel: { url: "../assets/tools/common/finish-label.svg", @@ -791,7 +766,11 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "cloneFinishButton", type: "newButton", properties: { - localPosition: { x: 0, y: 0.02, z: 0.005 } + localPosition: { + x: 0, + y: UIT.dimensions.panel.y / 2 - 0.0280 - UIT.dimensions.buttonDimensions.y / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 + } }, newLabel: { url: "../assets/tools/common/finish-label.svg", @@ -802,6 +781,90 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } ], + groupOptions: [ + { + id: "groupActionsLabel", + type: "image", + properties: { + color: UIT.colors.white, + url: "../assets/tools/common/actions-label.svg", + scale: 0.0276, + localPosition: { + x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0276 / 2, + y: UIT.dimensions.panel.y / 2 - UIT.dimensions.topMargin - 0.0047 / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } + } + }, + { + id: "groupRule1", + type: "horizontalRule", + properties: { + localPosition: { + x: 0, + y: UIT.dimensions.panel.y / 2 - 0.0199, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } + } + }, + { + id: "groupButton", + type: "newButton", + properties: { + dimensions: { + x: UIT.dimensions.buttonDimensions.x, + y: 0.0680, + z: UIT.dimensions.buttonDimensions.z + }, + localPosition: { + x: 0, + y: UIT.dimensions.panel.y / 2 - 0.0280 - 0.0680 / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 + }, + color: UIT.colors.baseGrayShadow + }, + enabledColor: UIT.colors.greenShadow, + highlightColor: UIT.colors.greenHighlight, + newLabel: { + url: "../assets/tools/group/group-label.svg", + scale: 0.0351, + color: UIT.colors.baseGray + }, + labelEnabledColor: UIT.colors.white, + callback: { + method: "groupButton" + } + }, + { + id: "ungroupButton", + type: "newButton", + properties: { + dimensions: { + x: UIT.dimensions.buttonDimensions.x, + y: 0.0680, + z: UIT.dimensions.buttonDimensions.z + }, + localPosition: { + x: 0, + y: -UIT.dimensions.panel.y / 2 + 0.0120 + 0.0680 / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 + }, + color: UIT.colors.baseGrayShadow + + }, + enabledColor: UIT.colors.redAccent, + highlightColor: UIT.colors.redHighlight, + newLabel: { + url: "../assets/tools/group/ungroup-label.svg", + scale: 0.0496, + color: UIT.colors.baseGray + }, + labelEnabledColor: UIT.colors.white, + callback: { + method: "ungroupButton" + } + } + ], physicsOptions: [ { id: "propertiesLabel", @@ -1084,7 +1147,11 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "deleteFinishButton", type: "newButton", properties: { - localPosition: { x: 0, y: 0.02, z: 0.005 } + localPosition: { + x: 0, + y: UIT.dimensions.panel.y / 2 - 0.0280 - UIT.dimensions.buttonDimensions.y / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 + } }, newLabel: { url: "../assets/tools/common/finish-label.svg", @@ -1376,7 +1443,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { intersectionEnabled, highlightedItem, highlightedItems, - highlightedSource, + highlightedSourceOverlays, + highlightedSourceItems, isHighlightingButton, isHighlightingNewButton, // TODO: Delete when no longer needed. isHighlightingMenuButton, @@ -1477,6 +1545,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties.parentID = menuPanelOverlay; itemID = Overlays.addOverlay(UI_ELEMENTS[MENU_ITEMS[i].type].overlay, properties); menuOverlays[i] = itemID; + menuEnabled[i] = true; if (MENU_ITEMS[i].label) { properties = Object.clone(UI_ELEMENTS.label.properties); @@ -1629,7 +1698,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.merge(properties, optionsItems[i].newLabel); properties.url = Script.resolvePath(properties.url); properties.parentID = optionsOverlays[optionsOverlays.length - 1]; - Overlays.addOverlay(UI_ELEMENTS.image.overlay, properties); + id = Overlays.addOverlay(UI_ELEMENTS.image.overlay, properties); + optionsOverlaysLabels[i] = id; } if (optionsItems[i].type === "barSlider") { @@ -2187,6 +2257,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { function update(intersection, groupsCount, entitiesCount) { var intersectedItem = NONE, intersectionItems, + color, parentProperties, localPosition, parameter, @@ -2260,6 +2331,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (intersectedItem !== NONE) { intersectionItems = MENU_ITEMS; intersectionOverlays = menuOverlays; + intersectionEnabled = menuEnabled; } else { intersectedItem = optionsOverlays.indexOf(intersection.overlayID); if (intersectedItem !== NONE) { @@ -2271,7 +2343,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } // Highlight clickable item. - if (intersectedItem !== highlightedItem || intersectionOverlays !== highlightedSource) { + if (intersectedItem !== highlightedItem || intersectionOverlays !== highlightedSourceOverlays) { if (intersectedItem !== NONE && intersectionItems[intersectedItem] && (intersectionItems[intersectedItem].command !== undefined || intersectionItems[intersectedItem].callback !== undefined)) { @@ -2283,13 +2355,20 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }); } else if (isHighlightingNewButton) { // Unhighlight button. - Overlays.editOverlay(highlightedSource[highlightedItem], { - color: UIT.colors.baseGrayShadow + if (highlightedSourceItems[highlightedItem].enabledColor !== undefined && optionsEnabled[highlightedItem]) { + color = highlightedSourceItems[highlightedItem].enabledColor; + } else { + color = highlightedSourceItems[highlightedItem].properties.color !== undefined + ? highlightedSourceItems[highlightedItem].properties.color + : UI_ELEMENTS.newButton.properties.color; + } + Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { + color: color }); } else if (isHighlightingSlider || isHighlightingColorCircle) { // Lower old slider or color circle. - Overlays.editOverlay(highlightedSource[highlightedItem], { - localPosition: highlightedItems[highlightedItem].properties.localPosition + Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { + localPosition: highlightedSourceItems[highlightedItem].properties.localPosition }); } // Update status variables. @@ -2337,9 +2416,13 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true }); } else if (isHighlightingNewButton) { - Overlays.editOverlay(intersectionOverlays[highlightedItem], { - color: UIT.colors.greenHighlight - }); + if (intersectionEnabled[highlightedItem]) { + Overlays.editOverlay(intersectionOverlays[highlightedItem], { + color: intersectionItems[highlightedItem].highlightColor !== undefined + ? intersectionItems[highlightedItem].highlightColor + : UIT.colors.greenHighlight + }); + } } else if (!isHighlightingMenuButton) { Overlays.editOverlay(highlightOverlay, { parentID: intersectionOverlays[intersectedItem], @@ -2367,13 +2450,20 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }); } else if (isHighlightingNewButton) { // Unhighlight button. - Overlays.editOverlay(highlightedSource[highlightedItem], { - color: UIT.colors.baseGrayShadow + if (highlightedSourceItems[highlightedItem].enabledColor !== undefined && optionsEnabled[highlightedItem]) { + color = highlightedSourceItems[highlightedItem].enabledColor; + } else { + color = highlightedSourceItems[highlightedItem].properties.color !== undefined + ? highlightedSourceItems[highlightedItem].properties.color + : UI_ELEMENTS.newButton.properties.color; + } + Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { + color: color }); } else if (isHighlightingSlider || isHighlightingColorCircle) { // Lower slider or color circle. - Overlays.editOverlay(highlightedSource[highlightedItem], { - localPosition: highlightedItems[highlightedItem].properties.localPosition + Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { + localPosition: highlightedSourceItems[highlightedItem].properties.localPosition }); } // Update status variables. @@ -2385,7 +2475,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isHighlightingColorCircle = false; isHighlightingPicklist = false; } - highlightedSource = intersectionOverlays; + highlightedSourceOverlays = intersectionOverlays; + highlightedSourceItems = intersectionItems; } // Press/unpress button. @@ -2536,9 +2627,16 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isGroupButtonEnabled = enableGroupButton; Overlays.editOverlay(optionsOverlays[groupButtonIndex], { color: isGroupButtonEnabled - ? OPTONS_PANELS.groupOptions[groupButtonIndex].enabledColor + ? (highlightedItem === groupButtonIndex + ? OPTONS_PANELS.groupOptions[groupButtonIndex].highlightColor + : OPTONS_PANELS.groupOptions[groupButtonIndex].enabledColor) : OPTONS_PANELS.groupOptions[groupButtonIndex].properties.color }); + Overlays.editOverlay(optionsOverlaysLabels[groupButtonIndex], { + color: isGroupButtonEnabled + ? OPTONS_PANELS.groupOptions[groupButtonIndex].labelEnabledColor + : OPTONS_PANELS.groupOptions[groupButtonIndex].newLabel.color + }); optionsEnabled[groupButtonIndex] = enableGroupButton; } @@ -2547,9 +2645,16 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isUngroupButtonEnabled = enableUngroupButton; Overlays.editOverlay(optionsOverlays[ungroupButtonIndex], { color: isUngroupButtonEnabled - ? OPTONS_PANELS.groupOptions[ungroupButtonIndex].enabledColor + ? (highlightedItem === ungroupButtonIndex + ? OPTONS_PANELS.groupOptions[ungroupButtonIndex].highlightColor + : OPTONS_PANELS.groupOptions[ungroupButtonIndex].enabledColor) : OPTONS_PANELS.groupOptions[ungroupButtonIndex].properties.color }); + Overlays.editOverlay(optionsOverlaysLabels[ungroupButtonIndex], { + color: isUngroupButtonEnabled + ? OPTONS_PANELS.groupOptions[ungroupButtonIndex].labelEnabledColor + : OPTONS_PANELS.groupOptions[ungroupButtonIndex].newLabel.color + }); optionsEnabled[ungroupButtonIndex] = enableUngroupButton; } } @@ -2628,7 +2733,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { intersectionOverlays = null; intersectionEnabled = null; highlightedItem = NONE; - highlightedSource = null; + highlightedSourceOverlays = null; isHighlightingButton = false; isHighlightingMenuButton = false; isHighlightingSlider = false; diff --git a/scripts/vr-edit/modules/uit.js b/scripts/vr-edit/modules/uit.js index ae3c1674f9..0ef61c717c 100644 --- a/scripts/vr-edit/modules/uit.js +++ b/scripts/vr-edit/modules/uit.js @@ -21,6 +21,9 @@ UIT = (function () { baseGray: { red: 0x40, green: 0x40, blue: 0x40 }, baseGrayShadow: { red: 0x25, green: 0x25, blue: 0x25 }, darkGray: { red: 0x12, green: 0x12, blue: 0x12 }, + redAccent: { red: 0xc6, green: 0x21, blue: 0x47 }, + redHighlight: { red: 0xea, green: 0x4c, blue: 0x5f }, + greenShadow: { red: 0x35, green: 0x9d, blue: 0x85 }, greenHighlight: { red: 0x1f, green: 0xc6, blue: 0xa6 }, blueHighlight: { red: 0x00, green: 0xbf, blue: 0xef } }, From 64821ccd76dbf8f91b9942b2d07796559b5d7c21 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 26 Aug 2017 17:09:04 +1200 Subject: [PATCH 251/722] Update general Color options layout --- scripts/vr-edit/modules/toolsMenu.js | 170 ++++++++++++++++++++------- 1 file changed, 127 insertions(+), 43 deletions(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 83adc7582c..91fc176e29 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -297,11 +297,11 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { "swatch": { overlay: "cube", properties: { - dimensions: { x: 0.03, y: 0.03, z: 0.01 }, + dimensions: { x: 0.0294, y: 0.0294, z: UIT.dimensions.buttonDimensions.z }, localRotation: Quat.ZERO, color: NO_SWATCH_COLOR, alpha: 1.0, - solid: false, // False indicates "no swatch color assigned" + solid: false, // False indicates "no swatch color assigned" // TODO ignoreRayIntersection: false, visible: true } @@ -327,7 +327,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { "circle": { overlay: "circle3d", properties: { - size: 0.01, + size: 0.0147, localPosition: { x: 0.0, y: 0.0, z: 0.01 }, localRotation: Quat.ZERO, color: { red: 128, green: 128, blue: 128 }, @@ -525,41 +525,41 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { OPTONS_PANELS = { colorOptions: [ { - id: "colorCircle", - type: "colorCircle", + id: "colorActionsLabel", + type: "image", properties: { - localPosition: { x: -0.0125, y: 0.025, z: 0.005 } - }, - imageURL: "../assets/color-circle.png", - imageOverlayURL: "../assets/color-circle-black.png", - command: { - method: "setColorPerCircle" + color: UIT.colors.white, + url: "../assets/tools/common/actions-label.svg", + scale: 0.0276, + localPosition: { + x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0276 / 2, + y: UIT.dimensions.panel.y / 2 - UIT.dimensions.topMargin - 0.0047 / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } } }, { - id: "colorSlider", - type: "imageSlider", + id: "colorRule1", + type: "horizontalRule", properties: { - localPosition: { x: 0.035, y: 0.025, z: 0.005 } - }, - useBaseColor: true, - imageURL: "../assets/slider-white.png", - imageOverlayURL: "../assets/slider-v-alpha.png", - command: { - method: "setColorPerSlider" + dimensions: { x: 0.0668, y: 0.001 }, + localPosition: { + x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0668 / 2, + y: UIT.dimensions.panel.y / 2 - 0.0199, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } } }, { id: "colorSwatch1", type: "swatch", properties: { - dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: -0.02, z: 0.005 } + localPosition: { x: -0.0935, y: 0.0513, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, setting: { key: "VREdit.colorTool.swatch1Color", - property: "color", - defaultValue: { red: 0, green: 255, blue: 0 } + property: "color" + // defaultValue: { red: ?, green: ?, blue: ? } - Default to empty swatch. }, command: { method: "setColorPerSwatch" @@ -572,13 +572,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "colorSwatch2", type: "swatch", properties: { - dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.01, y: -0.02, z: 0.005 } + localPosition: { x: -0.0561, y: 0.0513, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, setting: { key: "VREdit.colorTool.swatch2Color", - property: "color", - defaultValue: { red: 0, green: 0, blue: 255 } + property: "color" + // defaultValue: { red: ?, green: ?, blue: ? } - Default to empty swatch. }, command: { method: "setColorPerSwatch" @@ -591,13 +590,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "colorSwatch3", type: "swatch", properties: { - dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.035, y: -0.045, z: 0.005 } + localPosition: { x: -0.0935, y: 0.0153, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, setting: { key: "VREdit.colorTool.swatch3Color", property: "color" - // Default to empty swatch. + // defaultValue: { red: ?, green: ?, blue: ? } - Default to empty swatch. }, command: { method: "setColorPerSwatch" @@ -610,13 +608,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "colorSwatch4", type: "swatch", properties: { - dimensions: { x: 0.02, y: 0.02, z: 0.01 }, - localPosition: { x: -0.01, y: -0.045, z: 0.005 } + localPosition: { x: -0.0561, y: 0.0153, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, setting: { key: "VREdit.colorTool.swatch4Color", property: "color" - // Default to empty swatch. + // defaultValue: { red: ?, green: ?, blue: ? } - Default to empty swatch. }, command: { method: "setColorPerSwatch" @@ -626,30 +623,117 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, { - id: "currentColor", - type: "circle", + id: "colorSwatch5", + type: "swatch", properties: { - localPosition: { x: 0.025, y: -0.02, z: 0.007 } + localPosition: { x: -0.0935, y: -0.0207, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, setting: { - key: "VREdit.colorTool.currentColor", - property: "color", - defaultValue: { red: 128, green: 128, blue: 128 }, - command: "setPickColor" + key: "VREdit.colorTool.swatch5Color", + property: "color" + // defaultValue: { red: ?, green: ?, blue: ? }, // Default to empty swatch. + }, + command: { + method: "setColorPerSwatch" + }, + clear: { + method: "clearSwatch" + } + }, + { + id: "colorSwatch6", + type: "swatch", + properties: { + localPosition: { x: -0.0561, y: -0.0207, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } + }, + setting: { + key: "VREdit.colorTool.swatch6Color", + property: "color" + // defaultValue: { red: ?, green: ?, blue: ? }, // Default to empty swatch. + }, + command: { + method: "setColorPerSwatch" + }, + clear: { + method: "clearSwatch" + } + }, + { + id: "colorRule2", + type: "horizontalRule", + properties: { + dimensions: { x: 0.0668, y: 0.001 }, + localPosition: { + x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0668 / 2, + y: -UIT.dimensions.panel.y / 2 + 0.0481, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } + } + }, + { + id: "colorCircle", + type: "colorCircle", + properties: { + localPosition: { x: 0.04675, y: 0.01655, z: 0.005 } + }, + imageURL: "../assets/color-circle.png", + imageOverlayURL: "../assets/color-circle-black.png", + command: { + method: "setColorPerCircle" + } + }, + { + id: "colorSlider", + type: "imageSlider", + properties: { + localPosition: { x: 0.04675, y: -0.0620, z: 0.005 }, + localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }) + }, + useBaseColor: true, + imageURL: "../assets/slider-white.png", + imageOverlayURL: "../assets/slider-v-alpha.png", + command: { + method: "setColorPerSlider" + } + }, + { + id: "colorRule3", + type: "horizontalRule", + properties: { + dimensions: { x: 0.1229, y: 0.001 }, + localPosition: { + x: 0.04675, + y: -0.0781, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } } }, { id: "pickColor", type: "button", properties: { - dimensions: { x: 0.04, y: 0.02, z: 0.01 }, - localPosition: { x: 0.025, y: -0.045, z: 0.005 }, + dimensions: { x: 0.0294, y: 0.0280, z: UIT.dimensions.buttonDimensions.z }, + localPosition: { x: -0.0935, y: -0.064, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 }, color: { red: 255, green: 255, blue: 255 } }, label: " PICK", callback: { method: "pickColorTool" } + }, + { + id: "currentColor", + type: "circle", + properties: { + //dimensions: { x: 0.0294, y: 0.0280, z: UIT.dimensions.buttonDimensions.z }, + localPosition: { x: -0.0561, y: -0.064, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } + }, + setting: { + key: "VREdit.colorTool.currentColor", + property: "color", + defaultValue: { red: 128, green: 128, blue: 128 }, + command: "setPickColor" + } } ], scaleOptions: [ From c8381f32bbd754f37b3023df3df686383a8a09b7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 27 Aug 2017 21:15:27 +1200 Subject: [PATCH 252/722] Style current color indicator --- scripts/vr-edit/modules/toolsMenu.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 91fc176e29..42615b836f 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -324,13 +324,11 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, - "circle": { - overlay: "circle3d", + "square": { + overlay: "cube", // Emulate a 2D square with a cube. properties: { - size: 0.0147, - localPosition: { x: 0.0, y: 0.0, z: 0.01 }, localRotation: Quat.ZERO, - color: { red: 128, green: 128, blue: 128 }, + color: UIT.colors.baseGrayShadow, alpha: 1.0, solid: true, ignoreRayIntersection: true, @@ -723,10 +721,14 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, { id: "currentColor", - type: "circle", + type: "square", properties: { - //dimensions: { x: 0.0294, y: 0.0280, z: UIT.dimensions.buttonDimensions.z }, - localPosition: { x: -0.0561, y: -0.064, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } + dimensions: { x: 0.0294, y: 0.0280, z: UIT.dimensions.imageOverlayOffset }, + localPosition: { + x: -0.0561, + y: -0.064, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 + } }, setting: { key: "VREdit.colorTool.currentColor", From 8dd06bc270f006572c63b9747f9daa4d2dfa7213 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 28 Aug 2017 12:06:26 +1200 Subject: [PATCH 253/722] Style color picker button --- .../assets/tools/color/pick-color-label.svg | 14 +++ scripts/vr-edit/modules/toolsMenu.js | 92 +++++++++++++++++-- scripts/vr-edit/vr-edit.js | 13 ++- 3 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 scripts/vr-edit/assets/tools/color/pick-color-label.svg diff --git a/scripts/vr-edit/assets/tools/color/pick-color-label.svg b/scripts/vr-edit/assets/tools/color/pick-color-label.svg new file mode 100644 index 0000000000..6fa2997328 --- /dev/null +++ b/scripts/vr-edit/assets/tools/color/pick-color-label.svg @@ -0,0 +1,14 @@ + + + + noun_792623 + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 42615b836f..91f14c5076 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -40,6 +40,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsColorData = [], // Uses same index values as optionsOverlays. optionsEnabled = [], optionsSettings = {}, + optionsToggles = {}, // For toggle buttons without a setting. highlightOverlay, @@ -281,7 +282,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: UIT.colors.white } }, - "toggleButton": { + "toggleButton": { // TODO: Delete overlay: "cube", properties: { dimensions: { x: 0.03, y: 0.03, z: 0.01 }, @@ -294,6 +295,29 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { onColor: UI_HIGHLIGHT_COLOR, offColor: UI_BASE_COLOR }, + "newToggleButton": { // TODO: Rename to "toggleButton". + overlay: "cube", + properties: { + dimensions: UIT.dimensions.buttonDimensions, + localRotation: Quat.ZERO, + color: UIT.colors.baseGrayShadow, + alpha: 1.0, + solid: true, + ignoreRayIntersection: false, + visible: true + }, + onColor: UIT.colors.greenShadow, + offColor: UIT.colors.baseGrayShadow, + newLabel: { // TODO: Rename to "label". + // Relative to newToggleButton. + localPosition: { + x: 0, + y: 0, + z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset + }, + color: UIT.colors.white + } + }, "swatch": { overlay: "cube", properties: { @@ -494,7 +518,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, - BUTTON_UI_ELEMENTS = ["button", "newButton", "menuButton", "toggleButton", "swatch"], + BUTTON_UI_ELEMENTS = ["button", "newButton", "menuButton", "toggleButton", "newToggleButton", "swatch"], BUTTON_PRESS_DELTA = { x: 0, y: 0, z: -0.8 * UIT.dimensions.buttonDimensions.z }, SLIDER_UI_ELEMENTS = ["barSlider", "imageSlider"], @@ -708,15 +732,17 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, { id: "pickColor", - type: "button", + type: "newToggleButton", properties: { dimensions: { x: 0.0294, y: 0.0280, z: UIT.dimensions.buttonDimensions.z }, localPosition: { x: -0.0935, y: -0.064, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 }, - color: { red: 255, green: 255, blue: 255 } }, - label: " PICK", - callback: { - method: "pickColorTool" + newLabel: { + url: "../assets/tools/color/pick-color-label.svg", + scale: 0.0120 + }, + command: { + method: "togglePickColor" } }, { @@ -1511,7 +1537,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { yDelta: 0.004, zDimension: 0.001, properties: { - localPosition: { x: 0, y: 0, z: 0.003 }, + localPosition: { x: 0, y: 0, z: 0.001 }, localRotation: Quat.ZERO, color: { red: 255, green: 255, blue: 0 }, alpha: 0.8, @@ -1533,6 +1559,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightedSourceItems, isHighlightingButton, isHighlightingNewButton, // TODO: Delete when no longer needed. + isHighlightingNewToggleButton, // TODO: Rename. isHighlightingMenuButton, isHighlightingSlider, isHighlightingColorCircle, @@ -1767,7 +1794,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { uiCommandCallback(optionsItems[i].setting.callback, value); } } + } else if (optionsItems[i].type === "newToggleButton") { + optionsToggles[optionsItems[i].id] = false; // Default to off. } + optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[optionsItems[i].type].overlay, properties)); optionsOverlaysIDs.push(optionsItems[i].id); if (optionsItems[i].label) { @@ -2184,7 +2214,23 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } break; + case "togglePickColor": + optionsToggles.pickColor = !optionsToggles.pickColor; + index = optionsOverlaysIDs.indexOf("pickColor"); + Overlays.editOverlay(optionsOverlays[index], { + color: optionsToggles.pickColor + ? UI_ELEMENTS[optionsItems[index].type].onColor + : UI_ELEMENTS[optionsItems[index].type].offColor + }); + uiCommandCallback("pickColorTool", optionsToggles.pickColor); + break; + case "setColorFromPick": + optionsToggles.pickColor = false; + index = optionsOverlaysIDs.indexOf("pickColor"); + Overlays.editOverlay(optionsOverlays[index], { + color: UI_ELEMENTS[optionsItems[index].type].offColor + }); setCurrentColor(parameter); setColorPicker(parameter); break; @@ -2440,7 +2486,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: false }); } else if (isHighlightingNewButton) { - // Unhighlight button. + // Unhighlight old button. if (highlightedSourceItems[highlightedItem].enabledColor !== undefined && optionsEnabled[highlightedItem]) { color = highlightedSourceItems[highlightedItem].enabledColor; } else { @@ -2451,6 +2497,14 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { color: color }); + } else if (isHighlightingNewToggleButton) { + // Unhighlight old button. + color = optionsToggles[highlightedSourceItems[highlightedItem].id] + ? UI_ELEMENTS.newToggleButton.onColor + : UI_ELEMENTS.newToggleButton.offColor; + Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { + color: color + }); } else if (isHighlightingSlider || isHighlightingColorCircle) { // Lower old slider or color circle. Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { @@ -2462,6 +2516,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightedItems = intersectionItems; isHighlightingButton = BUTTON_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; isHighlightingNewButton = intersectionItems[highlightedItem].type === "newButton"; + isHighlightingNewToggleButton = intersectionItems[highlightedItem].type === "newToggleButton"; isHighlightingMenuButton = intersectionItems[highlightedItem].type === "menuButton"; isHighlightingSlider = SLIDER_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; isHighlightingColorCircle = COLOR_CIRCLE_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; @@ -2501,7 +2556,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: HIGHLIGHT_PROPERTIES.properties.color, visible: true }); - } else if (isHighlightingNewButton) { + } else if (isHighlightingNewButton || isHighlightingNewToggleButton) { if (intersectionEnabled[highlightedItem]) { Overlays.editOverlay(intersectionOverlays[highlightedItem], { color: intersectionItems[highlightedItem].highlightColor !== undefined @@ -2546,6 +2601,14 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { color: color }); + } else if (isHighlightingNewToggleButton) { + // Unhighlight old button. + color = optionsToggles[highlightedSourceItems[highlightedItem].id] + ? UI_ELEMENTS.newToggleButton.onColor + : UI_ELEMENTS.newToggleButton.offColor; + Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { + color: color + }); } else if (isHighlightingSlider || isHighlightingColorCircle) { // Lower slider or color circle. Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { @@ -2556,6 +2619,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightedItem = NONE; isHighlightingButton = false; isHighlightingNewButton = false; + isHighlightingNewToggleButton = false; isHighlightingMenuButton = false; isHighlightingSlider = false; isHighlightingColorCircle = false; @@ -2821,10 +2885,18 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { highlightedItem = NONE; highlightedSourceOverlays = null; isHighlightingButton = false; + isHighlightingNewButton = false; + isHighlightingNewToggleButton = false; isHighlightingMenuButton = false; isHighlightingSlider = false; isHighlightingColorCircle = false; isHighlightingPicklist = false; + for (id in optionsToggles) { + if (optionsToggles.hasOwnProperty(id)) { + optionsToggles[id] = false; + } + } + isPicklistOpen = false; pressedItem = null; pressedSource = null; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 1ae8bf7517..a760412658 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1352,10 +1352,15 @@ ui.updateUIEntities(); break; case "pickColorTool": - grouping.clear(); - toolSelected = TOOL_PICK_COLOR; - ui.setToolIcon(ui.COLOR_TOOL); - ui.updateUIEntities(); + if (parameter) { + grouping.clear(); + toolSelected = TOOL_PICK_COLOR; + ui.updateUIEntities(); + } else { + grouping.clear(); + toolSelected = TOOL_COLOR; + ui.updateUIEntities(); + } break; case "physicsTool": grouping.clear(); From 9abcf9525c0ce6a33a1cf0b0416bda71c01612c0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 28 Aug 2017 15:51:11 +1200 Subject: [PATCH 254/722] Use Settings values to determine whether or not swatches are populated --- scripts/vr-edit/modules/toolsMenu.js | 40 ++++++++++------------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 91f14c5076..931796f810 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -175,7 +175,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true }, - NO_SWATCH_COLOR = { red: 128, green: 128, blue: 128 }, + EMPTY_SWATCH_COLOR = UIT.colors.baseGrayShadow, UI_BASE_COLOR = { red: 64, green: 64, blue: 64 }, UI_HIGHLIGHT_COLOR = { red: 100, green: 240, blue: 100 }, @@ -323,12 +323,15 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { dimensions: { x: 0.0294, y: 0.0294, z: UIT.dimensions.buttonDimensions.z }, localRotation: Quat.ZERO, - color: NO_SWATCH_COLOR, + color: EMPTY_SWATCH_COLOR, alpha: 1.0, - solid: false, // False indicates "no swatch color assigned" // TODO + solid: true, ignoreRayIntersection: false, visible: true } + // Must have a setting property in order to function property. + // Setting property may optionally include a defaultValue. + // A setting value of "" means that the swatch is unpopulated. }, "label": { overlay: "text3d", @@ -581,7 +584,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { setting: { key: "VREdit.colorTool.swatch1Color", property: "color" - // defaultValue: { red: ?, green: ?, blue: ? } - Default to empty swatch. }, command: { method: "setColorPerSwatch" @@ -599,7 +601,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { setting: { key: "VREdit.colorTool.swatch2Color", property: "color" - // defaultValue: { red: ?, green: ?, blue: ? } - Default to empty swatch. }, command: { method: "setColorPerSwatch" @@ -617,7 +618,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { setting: { key: "VREdit.colorTool.swatch3Color", property: "color" - // defaultValue: { red: ?, green: ?, blue: ? } - Default to empty swatch. }, command: { method: "setColorPerSwatch" @@ -635,7 +635,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { setting: { key: "VREdit.colorTool.swatch4Color", property: "color" - // defaultValue: { red: ?, green: ?, blue: ? } - Default to empty swatch. }, command: { method: "setColorPerSwatch" @@ -653,7 +652,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { setting: { key: "VREdit.colorTool.swatch5Color", property: "color" - // defaultValue: { red: ?, green: ?, blue: ? }, // Default to empty swatch. }, command: { method: "setColorPerSwatch" @@ -671,7 +669,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { setting: { key: "VREdit.colorTool.swatch6Color", property: "color" - // defaultValue: { red: ?, green: ?, blue: ? }, // Default to empty swatch. }, command: { method: "setColorPerSwatch" @@ -1768,10 +1765,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } if (value !== "") { properties[optionsItems[i].setting.property] = value; - if (optionsItems[i].type === "swatch") { - // Special case for when swatch color is defined. - properties.solid = true; - } if (optionsItems[i].type === "toggleButton") { // Store value in optionsSettings rather than using overlay property. optionsSettings[optionsItems[i].id].value = value; @@ -2161,7 +2154,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { doCommand = function (command, parameter) { var index, - hasColor, value, items, parentID, @@ -2195,22 +2187,19 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { case "setColorPerSwatch": index = optionsOverlaysIDs.indexOf(parameter); - hasColor = Overlays.getProperty(optionsOverlays[index], "solid"); - if (hasColor) { - value = Overlays.getProperty(optionsOverlays[index], "color"); + value = Settings.getValue(optionsSettings[parameter].key); + if (value !== "") { + // Set current color to swatch color. setCurrentColor(value); setColorPicker(value); uiCommandCallback("setColor", value); } else { - // Swatch has no color; set swatch color to current fill color. + // Swatch has no color; set swatch color to current color. value = Overlays.getProperty(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], "color"); Overlays.editOverlay(optionsOverlays[index], { - color: value, - solid: true + color: value }); - if (optionsSettings[parameter]) { - Settings.setValue(optionsSettings[parameter].key, value); - } + Settings.setValue(optionsSettings[parameter].key, value); } break; @@ -2369,11 +2358,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { case "clearSwatch": overlayID = optionsOverlaysIDs.indexOf(parameter); Overlays.editOverlay(optionsOverlays[overlayID], { - color: NO_SWATCH_COLOR, - solid: false + color: EMPTY_SWATCH_COLOR }); if (optionsSettings[parameter]) { - Settings.setValue(optionsSettings[parameter].key, null); // Deleted settings value. + Settings.setValue(optionsSettings[parameter].key, null); // Delete settings value. } break; default: From 67eb92da8f260e62d1dbc1704dd609ddb9589078 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 29 Aug 2017 10:51:36 +1200 Subject: [PATCH 255/722] Style color swatches --- scripts/vr-edit/modules/toolsMenu.js | 112 +++++++++++++++++++++++---- 1 file changed, 99 insertions(+), 13 deletions(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 931796f810..2f8147abee 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -39,10 +39,18 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsSliderData = [], // Uses same index values as optionsOverlays. optionsColorData = [], // Uses same index values as optionsOverlays. optionsEnabled = [], - optionsSettings = {}, + optionsSettings = { + //

`qzH5SMg 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ⅈ#d@tb@HpUu1R@;ozl_#<|J&0<|y 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ⅇ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 F{|~4DX_x>2 literal 0 HcmV?d00001 diff --git a/scripts/vr-edit/assets/audio/drop.wav b/scripts/vr-edit/assets/audio/drop.wav new file mode 100644 index 0000000000000000000000000000000000000000..32d0e9f65a9ad1e9bafd09a892b251d29262afa3 GIT binary patch literal 48044 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 literal 0 HcmV?d00001 diff --git a/scripts/vr-edit/assets/audio/equip.wav b/scripts/vr-edit/assets/audio/equip.wav new file mode 100644 index 0000000000000000000000000000000000000000..4a2bf5b4db91197b8029459f02c09e8caf1fc0dd GIT binary patch literal 48044 zcmX_{b!-+(xQAzU_j7ltx3oAEcPSKi_k%kWcXv3r+ri!C;2c~^TPV^}sJnjdvb!@k zm)zVunaMlJB$LT6?K|&*15J2%duX;uI_ZjX@%41)77}Knt8W8DGT*@HV_1@5dML2V8^! z(1S=&AJq9XLV*YbaRvU3KjQ!J-9J3T?{O9`#|#z$1E>d@fsP;%B!X_B3up&gfchW; z=zsu_Ab{Q2jH_@NF2m)x8k?~fGgt_eAR06R?Z6-~9LxeqU>R5g)`2ZxGuQ|=gVkU) zSO(^TBrp+-2cy71&>M6GtwB>z7sLW1FaQ~l10mr59}*w|Dxd`_5CV)K0>pq=PzN*s zO+a(d4s-+wpcm)^hJYbp7#InLgE3$P7!8Jl5nw17332=Bz(@oKyh&&RXy6#N$+frsM0xF=4; z9dJ9`0yo2raeZ7LN8{Q!97o`A9Ey!N6l-w^*5MFr__GSfVK@dy;MzF$&+dk}1#W`d z<90X!cgMYOUpyQS!{hKIJOwA=d3Xh0g*V}?csD+RkK=Rr3ciUS;>Y*{eusbH3|xTA za5Z*e4~BpO5}*b~5Dn^rCZHwg1iFEKU@#a1#)Fw42`mAt!3MAk>;Xr>DR2Q?2lv1O z@CLj9UqA}@1%87fPzq{*4Y)x7AV5L^6hS3aLjw$h(XbY*0~^AIuqA8;+raj)J?sR# zz%H;G>;Zeg-moX^4|~D>f93(O59|y3!QQY3>KF?fjk<1(}qvFuuwVHPpP^dS0vP!XIJc;x5#`}p>F(>%b_%01b2$oa%k zYR9%XTW9Nd%X0H})2W)P)laIDE7L3T%ZtnGrNI)aL{%JB)VMIAV0iwly!E+9b8cp* z{jT~Y_|+(DitOi{`ed9ZOqq)>B{s~Uy42t|D2ZAKkZxUlvH2Jv6PM} zmQU9|P5;#L6Yo=1@|Wc2$q$q7CBI7ko@`Cled_sX)2Gx=`jjOpzf*dpzEACzmYp{5 zv+T?3FALIJer3OAe7pbs#E;z>J2LnEJe&38SHW*VPD1YLypQ?n!udt%#qCP3mBm$D zu8gmKSu@7$ww$&lI#}mj_X2M-KM1DLm)I3}G;Bvka6~*eze4z1oGN`Uf2@3{zNx*T zzh=A=dM*5NA!3J9!t&X4BbY%0+E#q2q+LpI_+2Kg183|3g1`;24 zU)(dUcSfHj{j>wF3~V|0`H<$rt`Ap^Ts|suboAIc8wGnUV+KPz|E(%GUp`{wZHE}Q#vZiA#bNjH+dCE1d25=!zUWhMQav@WS*l6UT* zxlQLjo6~yEjoISa6K7qWSvo^Lqv`a7X+5SUOsPLvHmP*Nt?_gIGLCyUX7p(5$eAN* zhV>o#Y%n!w?0|#)zV!+Ait5>^`=G?JT_$!O(XoH~=52MY-7VfVJJ@7+V@<=a_14sB z6!$G=MU*DuerQi)u5Px*tK2B3q`O6Y!7i?l+5`!_ig5=P`fA*BoRzkzmg<_>RW;?S zO8rIq3Uqn5v%CB%$Xxv+{M+X*i_;pVlz+VXe*W7wuT?M2&%Zo<`S|L?8~-koW2`o=%;Ctn^PeGDD>_wbxUtqzJ0nD^!H&D>MG+rCS^ ztIN*Se<$xS>{z$GX4~v-?yWnwHr-mh<>Hp4E&aAM+0t-J!z~G0CT`iWC3%Z*>x8Xu zw#IL}zAb)x^7d&v41cHpeSGJfUHx{q+S6ojvweyC#~;{u@W~(lkrPoJyP72jKAO#Zq1*Yj*suCbtR(VCL`Wu+CG>Yk?MmaDd34z9bAceH<9@B;G= zm4G0j&6Vu1u`%UjPy*u8rcKzB8XfvpF z?^az}Hg6HpT-?moy*OJyEVv43&q=f|5_-E21^RNrF!N+B_K-Q5Le8$bnfP2j`(`*2{>Q zT6AJ?dSJi*jnCt4>Ye9#>gKpdxb8b;&Sj2b`%wEATR+=R>tw6bvd_}Yl4IU!PB7D^ z52hWa@upTLjS1CM)#TUwuF0+`uCdl|P2r|Q(@fJz(^r$o+}C`-Tx@P;*=;dd23nt4 z<80?`O8ZH>#&OZn$obki%w>1&amRbIJnOs-e1*O}{(b=_@HV)T?#vKu3VQ&J!g0V3 z-oQP?Uu0tnQ<v43C)~vpQx=%(j?~F)L!G#|(&R79)!(kA4)rCb~;Bih2|^B}x(XZ{(23 z>WEblf{0z=^6;%;ICNHMc1W9$qeizO!EjuktE;7(tUaUorRJ&IsKzS)R$P*&$O@%i ziAWM6juSNzwi2}AcjC3?w&gUV>XFfe4hjIoHEapuTuK zI^=e*)oCd<=a|0Lq*Z^YdRzIf;$!*evLB@dCDp~aNLJLKuuH-8{4II+bF*{AIUTc? z|Ni$^Fsn`0uAjd%n`G|Du>BbLeSe96fBtgj4yO0Kl+pby%gFf zv?gR*NLa{yV}dc&Fw~HtAEy7ROVmBr*3ll*K+Q~bimH)nyRur*S#d;ek+qlYmljAG zN>+&9iA16i!jpnBem#B??=d&X>CD+erIB)S6!8zN0S&-v{2r;$c=j#>nL+fOAQPAr zc<49ym-+I&iQYRNqi2^JxR<*e&UsF^W0`~F*k_Nn|7ROu^H~pC6RcI1Qyw&N^-A8k$hJMB>bRkSu{dd%Y(I;Lx_HMO4Cs;U(l+aY#F?AqAVu~%bX#J-4q8~ZZ$-`ESW zdtw*F4vwuCi)wwYb*R?RTIyP#Vphh~j!BJ9isnV1iE0@2JhDUNn~1g%|At3|9|-e= zjtPAdq6nE`d}QDl`s(-SGPDA156yD*4b?9tp{%1AAfGGSE4?ClBhD6?gjgUIgz@Y0 zT5vmXx>5be!Ne#y8cf6!(F}GvGl!lRoEKQ`U*_B3-QwBlKI}T}yyAFbe`m|ER#|*z ztvSxryJmd#_NptDKPu?*`sJg`_LruVa7%g=?<~qJj4qs4keVNvzcsHUcTn!LoF+L} zvZJ%F{;vP~zh8ZRm1b?qipl!?bLG!QKdUpZWzNlPl_|{5;U2Nq>do=J@m~w<4=$s}GhNyGNQ~_`1KfcJiCJWCDw^Zx zq;t>m=JVSN_`-DI5z#QQTJl}8UD`qBkR6rxQdBE;D;ucZsYa>I>LnVe-K+(=^*V=c zs{Xsawc)sdG4wTFHQJ3GLN2A-Tq##!kix!+t|!!w>xoJ*7XWi__iFw$Z-Qbke+1 zcTnF^HBuc_%9Se>74m`d$FgwQI%$a{L2^#ah(?R<3njv-f|q%HTK=;nTg(=owUKp{b(8g()og8Sn`OIi3);Hb|FPTby&X3l0_S39p0ls(g{y`8 zhP$EXh9}ZhKvm$0z%-EPNF|}i;n7rtJqgO}wiWWwvMs0{{8C4d! zJF;2i_lVgMWW?U^i171ab;2%$hKBAB!Nxhp?}nC!!+MLZm+p|(tZA%St$w9K%1+AV ziaYXhnO4?KI$v^H{6b_E@`a5A{rIzZTe;^r&!``yiNH_|>w)%o2%5$&V|LT0gAW5K z{yd-CEAU2n+Pa6hlAOC7*X^k`vsG?wWf^Dw+w`cWvf5ZZuxfkd+X||pYx(Z7jMAvm zMI|4KBa7D*%sNx-3+Ub_ug<^s z{4V?5A^Su&n>{t>M^4w=$GOe&?&P(|f0Ey=AhTdP=r9Vp7m9;1@ zD?eW`qEcOzRduL(bWMb*%yi$p($dqav6a~#+P6AJI2*cHSEl=lXN`A|uZ}`K)&xv(pFN)7e<^0X<#v}L-`AGt!@SAX{C`SBIJX0c&UXXT> zrOPJEP4eXmTCq~eDw9-Is$uFk>Ux@;8j~hbdqP{SZLM3Qd#l5`R{DAR6Z%xWNv|-} zGb9>@8m1cN8s-}o7-kzL8ipDY40R1cgH8WNe^x(R-(FAZKk0Vry66bqE$wuzNc&7P zMuXKS)UDO2s>v#ca*a}?+^f(kj>)z112TndiEvijE5j;atI2eh2;u z9*;Mjo5E?r*-yF15#$phj93dxK_a+?C3q#uV|%dom=I6VJ7>0IjU&d9ZeL|@WG}Iuu#K|mZAI40)@9bN)(ETH z^3(Fra@MldvdS{UGSxECGQl#_l4RLr*=xCB`Cuut2(3-6DIB=bopr_kp*S@4c_TKi5A$fC49ijp$GGGzPMl z*aTFAcHm|p8*GFPiA-V>S&z!0ws4woi@7^_?f4b^gMu!?8sRZfg1AO}SkhWrBHbWs zApa+S47;C&~sBO5aH|qE4kani_ zho+h4u-d8WrMjSWD7q^4$bZYiWmBZ*C1v7p@o3Qj;RgZ3ugjmn+rfRoDWud?Gjcky z5k3MxFhWtN8=J)Jrk@6L14N*e#XY*U;XXY)> z3(NbGyCSz)ZdJ~moY^_;b9gy<*$=Z1XD`W~nB6bCb9S5T=GkqtJ7o9I9+$l&`#|=+ z?3`>Wr$x@poXa^yIrVbq=Dy5T`Gbh^0M;X6%8x%D>qfOtS+lQR@2W!nO~ZhTbfz@);qS9_T~=YeBs>T>g(2c z@;n#4bA2uR$e$e88yrDLG3Cr{b|Gqo8T=eq_^*AQZVeVia&AZ1RA)tiU zg?&X<(Qa`a$!p0lX_<7X49m93#fqH@O1VjCSI$sB)Yo9ZX&*XYmcpX<~0rFyg8qo?&gy;onY&(o*sALx(iSLlc78|z8^Pu(fq z99>XyX_ji@G|B44YO(sRs)wpTxk5=Pk1L`TcjV3FFJ$dxZ>1fiFC?uc zcg6L^XGLn!P9Y;m5)|?K^I!8C@c!Wnxl1_})IjPrS)cre;1ergH5dxM;MVvKibBU& zDZ7Kg^jbO?ToFWpwE-%y$FK2U@ip;%^p5u0J-a=1J!$T_Zk79`Yql%ImG0c`?Bpb! zuN^xagB{@xkNu7Pl6{4JqCLUh&~C5`?3CSWqitRru=DMDdu@AyeT035{e=Cs-DEd7 z`a3o_UN{&>Tjv&Mnp5vea{cErxaYYu-SM829^g&#=6U=3-uc@3ANZRG?gm-}p9MS7 zsq|2$lv&7<=n!gzU*XZf4tBx1#Cu{g$&%-(www~qDsCk2HSaGzDL5lY5Ecv9i^9b( z#G@o0$sTC~*-P12d9{4ALZ-Z^Y@~Xr>Z<;x9-#TD8K(WN9iV%y>!`o14>fEym<_{> z_l=5>IU#RCw4swiZ-sh8TZOF%yB}5+78>3=d{Owx@aN&b!=2$ogeoF3A||4CL~MjE zLL1==cZPooe-yqwd`5Vya6$Oju;XFF!?a;vLN|mq4$TYM7!n=w&N#~GGHfs?4M+8n z`g6Jv-D#~zyIJEEy%L8cKio<0!#81VTU`^Wo^d%t+No=)x+u18LXqq$>& z{jtq&O|)*eWSgVSOHH3@LTi>+XH+$*I#lVa7+>+Zylwg2vZ%82rTWsdCACVf7k4gB zE}B_H7oINcR9IPXupqI(m;WF?DZhEXKQATkNZ$OsZh7_cgn6FavfLlJ-*QuOQ*wXg z=H}Mq5_$T(mU(0HHssyNE6S7Q56s`5|21D#FtOlH0jF?u;gdpr(dwegqJhP4i<_6+ zDydU?skCm{&9WBdZ_4{ulvFIK6jfcRN~kWb-c(b|^x3q?Y_z0W7FnZhS+))Kb`F!{ ztaG$W=uUMn_q6kxyf=O0{h@)(z}{eY8q<%M`D`4@MSF2q-~pH62!bO2CC5+{=N4xG z*ULS~YtH}5pDqBxL&933hoTPRRB=B^x@3?vMcP&NR8~iRLJk$P6kik#lz%Jplr2^3 zRNquO^+@$;^>?*O(?PRHb6oRYQ>G!c;o7F!-r51$N!kh8Y1&EJ(b~b<&f3OWotD<* zYo2NLYi4U&X@r`u>J#eGYJ)mkwM*4QL2bb)ekA_@PsQ8JrMOEt4r&UOM-Cy=h>pY)*brU;MsNa) z@h(K7^(fyr9 zyUw}Jy3Pv69Y>NQ!69(u+8@}r+Gp4k?e*1(yYFh3UeHBD-j-ST4CJX()Xn?IBB+4U=cdM=0_XBa|7+L8|wv_UhZ}P|a?Q zTQg4kP8*@ysLRzg)o<7T($_L9Fg!5$46Td{jMt4hMrlaXkVzqHLe7T#7xFEnBE%KK zgm6O1P%<zKJPSAP5sNnd(Re!0ksc*LTg(v9l>E7?k zb~bP>bfnld_Bpm!R=stRCC%K}yvyXM8CmnPx+GFjP$ z(l(`^OXik{N^TdAC?<;^6iqLREXprDRXDYzj#!)8e%sdCqa10D zh0ZY72iIbEs3+aC)Eno^^lk7r2^0i&20PG|^bsb3wX!ErS8T-xK^Is6cM^5TbaDVSiD&XrMS%+(Yu8Bwl(!DweI5707zXFUTpy7{y%$ zr5va{tt?U2Q%zA_R^_M^>dxvU^&$0hb*?&~R%+^KT5Gy#hG+(BMr(#^25Ne0T5D=+ zv>KnfSp73;*4U10?Tj62g`%9ld_Jo9O(+F zN_t(=Rgxi|E%u527O6#-gbjpG1ziQn{K5Q8-WXm!cN({xGmB%VW>bE0AsHan5-i*d z1#kz@fx|coUqy}3E4CZ^iRjNQ>YVDF;+)}J;9TL{ z?mXeVxwJGC2)6kFLOV2d)@6ln>?wW5breabFbVt%lFz>%fH57 z?(ZJB8!!Yn1%1K4=xn+N^NMN7-enu0%P1CK#4+G1s10wy#>5k%4f&qzLH(dca*8=K zxK8d$9>L$imkW*w!i1NEwMDl@jl~bdO(hQ`O{Djv^<{UCmtwoqoq?$6I?{`ym@?OR|<&23bioQPQTwP{Jv(7Z(<1 z6{i-b7AF^{7ym3SEp`<1OQK5JmP{yFU2>}=ql8=9s&roI&C<%!hGp~0o|SRShnL?d zhZUnLo>u58*Ho5N_O5zb6<2+%T2!;8CQ!4=_!2ZtO%Tex_ z?-aUDx>~xE-P1h*&k=7E-$&mxKkGjcXc_z(oJ~{AX{IgvgPns2d;&KCAHY~x1J@G@ z@)X&SdQ5fUeBlh_=5R;y@^~Zo+5CQjG(kJzePNVnw}=)^5`PlclkAXGO1emoO3l)a zvTd?#nL$2Tenws-*D3}n)+rt+@)ZJQV`X3ELgg0aCFOl(s`8t%P+6j^R+cHtl{v~E z%D2k<%72twl(UsRl?|0pS**CN*sd6)h*H$ZZ_Ag+o5?-0Te4ZQFxgM(W@&3_k>r4+ zrKCu_SsW)$6U`9uMHht~h53T{0+HYhzd1jRH-=~DuH!1X=QzzcZ>T|35jl@!i9Ljo zxD8vubTA57@H#BRmr--{m7Tyc%s!?elR{6XN&0NCORyraBhWOE>)+&W>M!yg@Fn^f z?``jVZ=AQG*8jsFAJ`VC40I1(3<~KaI+O0eTw$c_2DX|Vhd!Z>_!f=^M}P=!fPOfes3Jy_ zzsbJT2dXpYUrrP5Wo{_%C{MuO%=hr;2=WEPgl~jxMHfUm@p`dEJW}#nQdhc5>W~hV z-IJ;1GvulA5XDl(dxc&(S$R(xPU)0Cc6V);5a@Bd& zIF(wJqFkn|ul%N1t%y{-l24X1vg5LbviH)V(n`q!2`k+(F!AP7}^$N*HLMK}0Y9FMQ&CfNj3wEr%n!O1eI_UfE(qlL6aDvn zA--K+uV;qmm%FF?KUci#qBGoiz@c*Nw2SR~Y*O1nYl!u{rK#n)xxcy4w9F(iU9IU^ zQ&GLEx>@zFsvT7=s%k1vSB|JuR%TTksu)`lSK%msUw*iJetFOGI^}|LTiNfj_hk>t zu9uxDJ5_e9>}1)wvg>7!%Tmg6%iLw+@`mLD$`_aaQ=V4tEpJ#czT#9xW`&`0WaZh) z>dIzSo2oLZ>Q=9>&Z%x*bEw8&Gs5)26lXqShL-u3V#^3?iZ#*p)Yig&#~$yv>1gD< z<811B{ke5?Gz!2WfFO+YWpO#;hKbOChr^!>~>GEXx z2l+GkUHMu09{F1NM0sC%eYsd}mc5gmku8#SmuX}b(p%Cs()LoW^rK{}q^pFKJQgn! z$BGL?yG5NvHNw-vM4?A;RM1^e#^1+p!vD!z$FXKmnd{l;Iqvz_^TlKLNWHDRqr6+Z|9XqON?)RHh3|pS;%n%i;lJaz z`r8E71U?0{!AZdfL5dzl-==xYIOYkXVdt}%Y(sPiInhY`3`c{lzzRmd7qBj|p9m5& z$aJzDb&Zm7)^jR2L%FZGalFI4Aa6SV3%|ACgn%!cD*P^NC^{r^iH3>qi4~F=k`zgZ zbeS|&8ZJwcy^@LLBjgw4mGZ`ld5VV$i=vTon)0MFQwdc~R1;O3R1Z|&RbG`!-9+6> zJz1Tk-lqOreMWs+eOY})eNKHreMr4ey;412JxtwIT}v%cSE@d!PN^2Dda87)O66_k za%DRuQoK?uQ?yi6%FoIN$dT-(Y?zFZo|h&{%Osm6A(H3fe&Q<8GLcwxQdm#;P|!v2 zi9eM8llK>|kUO4R&Y8$5qyD1u$x&n`F_1`wUExd665Pgh@hKF7_Oc>&6GPKWX^khM?R@0uBpV>sD)= zHPf=h5@-2tUTOI)upPV zRhz3;S1qiXUzJofw`y6{imIJehpMhuJ*)azWvvodH>@5~y|ns5_19{qx_QmSnsYT- zH9FG>(=k(lskV8p`Hfj*`OEUJg=?K;ePY$vmf61BTHBA=v3;iFi=(~sqEqGC;Ig@< zxWBsldLDV2dM|oIeFuGF|HeQ6GHYOZup~H|&Y}k}DNGmk1=|eWMltvlmV>_m4Hm)* zIGFfMG$${U8fpz?p@wpvabmcexz*ghyvsZh|1bUMNlysJeCAs3O;v{ifF)ey8+92vCf}&T# zRl;UMtKghqv_L3$&0oNeg@-6iZ^wsxqedXRy-mBgN-X-2?-oD-hZ)0z)H`J^4O1(0##4Gn| zz2V+iZyRqX?W{Pzs2VY#t9w@c*1eQ+d@(_P;_2o7PS*^7N?6< zlF^crl441?biDM0G*c>-CCFCFuE=s^9C;J@X!&~iCHV(=nVe7<6|EHA6yp^$6sr^) z6}uD%6bBTC6nhmr6&n>R6f+cK72Om~6$S;87s_ADPs*3c`^sy{U9u0dgR)Vwy0R+i zZRrAOJ*i1@Rx(y1mAn@(632+sMJq%xqGaKGp;UNZFi2qK@8L)D-|~j>EZogpCHFd~ z4d)XznyMg|kbvAx=!o;M0el2HfE3&xXQ7FxlAXtTnRN`0*-z`~3&94#M}e+^FaFVf zlW&Dj==;ap#QWYe#^ZGFaMy9ac1?64=Q(E=XSHLGqqU>VzT4i(ZnvGajj~B>Y1Ylw zL@URdYT0KQVTrQ1%rDJH%+t+X%pqpKDbMuGbl$YxwAeJ+G}zSL)Xvn*)WlTZ)X3D# z)Y8<&)W+ zKlV=wBnKJ=_XdN((ezt7j`^FhF+C?!5koKuyawTL70iN7h`)(aq8+)9 zEF;@e+o(LM9%mWn9Y@R^#y!O?;@0I&;oaaB^J?)&^AGV;_(0HDFjcTe@SmVkAQ83{ z4i~Nx9v40qW(rM0N~9Cj6SWuh5{(f3C7LOkD_SU8Bw8q%FPbHqCK@doBI+z^C5jR$ zL`YaJ{33iPJSkrWrD$i2!Wsfo_~ZtmEV|;dGC3Lcq4hyybA7J z?mTWQF3owySe*uEHnW`R#%P!l`Vqa29z{2$3A!NoD7ZH`H`p^+Hz)|! z1kwZl22KVx29g3J1HA&x19bz2fHc4l(Efnm;dl70e!Ji0_xrJ*3Mc})K;uB;z<|Ke zz=FW$z=^=az}J8|zz^05_72Vp?hQT&<^~D65j~vVLf@r}XgSlFS;$;rau@~Mja|z= zWF2fAnu5-tTx7sQ@nQT8tHA)UAAAE!I0PPqnNUlNBu)@HL>M`iJWCdk(bOdB992w3 zamI1Za&kC2?nv%l?iVhX*NL}+cb8Yi3*ispZ{q*QFXx8|dJ2{cE(*R00)iM}Z()+~ zknpMSmoOmIi{eE?MN>puM2AGTMK49)L`5Q-C?MjBg<^wPC5{ps#1Y~!u~w`WbH$X{ zDKd+4L@A=Dq6?zEqGh7LM2VtUkx*1FOckCJt{09HHWBiK`GPxwO@bkU2!WITgujD7 zfN$g%@GkRa@fz_Q+h(nc0)<2$s*jW0o-SOc{Na9!|^X&%uqsu0b~NEU+rjDiH8L z^{?`G_EY{8-+te4UyRS>ed68d9p;VndOaUKCq45#-96DBpZmA_uKSQX$vx8D#2w?t zF1zce>z(Va>%429YpZLyYo2R{Yl3TxYpiRyYn*GOYm#e56d$`BDH@Q!^-?)q1q^FLjzh|-MwC9t@;fe8f_pb9^@#cCZzV^Q5zVp69 zpTghEzuEuT@AcOY%nV!zI{aM(WV7B-pXqb_JG zdV?VDgjeI|IDngiMc^8!22pSYmyy@VUnHMuPEDb9Q_rbV zO3G=%8P8e8xx#tRso`+B^|=Y$Y220E3c*A(Tch;;N=G-)W;6+PL`KB0KiTK( zVRj`uitWHgvmCaX`Of^yTx2#g3z;!YPo@PE&Bz(RIO!7l7o9@CrXSHa=u7k|`UHKL zK0xoK_tAUk{qzC)D1Ds1KwqNo(U0l(^k+JkuAqH1#psyYOk1WOGl5yo>|!o4&zYZ$ zl@YRWYy$fiyM{f^K4-JpARCU_p~+|ix`93+7gFMmcs%|a-^9N#jpINcuoRpGAAt>M zVHY?Z9)wR}1r!jiiE+dh;vSJfK(ZFupIk&9Bj1r_q?Bq%4X2h;r>Xy_Vv68Ia}qg| zI2$P)ki`03wxSPV!N<%wt{)g z>|iD^@r;1Ur~jq5(-Y~Iw4AOEz73uXt_TheHVKM@ra)TYYG8L@c3@zjc|a9F{vv;x z|BnB-f2)6vf1Pz;$_C4`E^xgB_^4;-W_ucf}@!jz~ z@;&jr^}X|D`o8*Oo)8(Bu$Ns5wF5mW=J4b_zzNDZf^ zP*bTz)DmhnwUJs!?WQ(Ud#G*H-_#CjBek7cMXjcmP_wDo)EH_E)tl-@wWjJ*F_egRL;gi}C0mlAq=0k~xx_o-4snQBOH3pB63vMaf*>m4S9l+u zf@|PR*b6p?Du_YR9}nvwSPI60E}%A002j{1@9;%@2rtBwZ~|_E)fnPx^aH&@7tvv~ z63s%xP-hg6!jKRJ*(x@N{lxyqUT4p-``NAR3O0$I&JJgXvOU<&Y>Q7+5PM>_9pv)O=dIMa@Nao zkrvfQ?NEO-87)S;&>8d)eL}^^hr~Dzx52~k9J~>q!4GgcuE7|GgQj2*m<~3B)8H}4 z02aW7F|ZvR4i~_k@Dh9pv!Me@i5Q|2F`SrBY$eVS4~cI?1;G#+GLB3j2a&VMRpdeP zJo$u7BeO{d86@RYDAkB+OZB0KQR;+6b%{Db?W49+E2u@(L~1nEgK9_Br$Q((6(mh$ zCi#iHMV=$KlFP_3WM8ro8BRjdLVPEl5$B1m#9U$s(VU1N08s{0;0?GJE`~#3Td0Eo z=79I$G}sCzg5Dqw2!IuT!uRnmybuq<&9DZ0P&Rss&Z1Rl8cIM7kpMZ_AM6wM6uX|C z&h}xOutt_;DwuD~zsz}N3$utB!*pkwF`t{Abpy?PrstS(#LQxrGY6T=%zsP_PS-`OXxq8gX~C(V{m6Y3eUs)@HzYzXJQxTg9y+8 z3;=V%dhidp4N^fK@Bkr>>^l=ZWjYBjOd2N_-~@hyuby*a#0n6M!U1At@l`q=ZzH3Q|ofNhPTurKFtXlYEjQ zS%M|pgq^4%iiv#U2l177MZ6&H5Z8zk#35oUv6`4qOeRJWy@^glBcc|eBm{&Tn&5Bv z89swI;Yqjyu7=mI*0_M%;AJz9+xpm}IA znt;Zj!DtZbi@Kqnr~^twtx+e`8ns33Q7hCQwL=|H0!l#LQE$`}4Ml^|STqSuLi5l( zv=*&Jd(b}g54wVGp#RV-^cDR^rO1hVNQjj<8aKvmaUVPk&%_Jx7Q7Fi$9M26{0--0 zJ7%#QgoAj{5exzo!6L9890upXWAF}SflA;50;q>|U_00Yj)l|UD!3h@f za1QJRE5S4{6m$d)ffi7}hV$`9{0N`I`|vV61rNdPaeb`A1a_l5^cg+*^GdHp3(y!e z0JTC5kOqm7o2_AgvZ?G-_8NPd-N|lb=d)AUQEXqf6Wfff&4#mTmd66t!&sS8CZEY* zzA_(}*UW#+eda!Mjk(I4XU;OGn17g~%n9ZobAma*oL~+!$Cwk$N#-nbmbt`SV{R}H zn1{>@<}H)Vd}Dqxg-jV^XIu=y@>mrc#>TSE**0unb^tq>oyo3bx3UM=bL=hl8T*ON zVM|ya3y}&%qGqTg8j8lE#b^yWh|Zw9=ncw1MaYXV(%@L!5_iYL@eI5S|Ba90>-Y)& zgmZ8e4qzeBfx4h2=mCa;DPSSk2=;>0;0AaCK7tHT1k4}+C@6=aur_Q4+y3!5$G}N2 z2`+$};Cgrv9)PFdS$GrPfKT9K_!ho~Y48*L0l&jcm<@lye3%0Z{*VugU^Xm-*)SXa zhCg5?{0!6Kd-wsqhEL&Bcmv*rXW&J62p)ku;1;+FE`dpK8XO0Q!M?C7Yz>>kS}+PK zp%?<_23AlAvOyYn2Ofdj;4C->c7gR^DVPPugW;e%=n9&EdLRra0Uxl~i7Rmd{*F`e z3;Y1z#Ha9a{5Rf=SKx(sI-Y=s;Q=@icg8JoBU}$h;83i>BFx7KvB-m5$b_m;2`Wap z=r{U_zM>x}6@5X;C=IcP=gX`jW+zxlfJ@8;W98bg3@KU@AZ^8TVVSEW+!;kP=oPvMiB3y}mf4pf8 z2nF>)bI=*|2E)JvFb6CJo4`(R9GnNYz+>mJ&$c6%l!V+x6(KsHr#@%q=KL+?h zyc}=CyYU%(9zVd(@JF11b8#hh;vnV&IS2u9pfP9*x`KXS1egG3gN0x%*a-IhPgmyw zKUY<4|23o+LJ|lhln{E64$`DYkRnnQ3p^1k_JaQ9J+WXxs)$l-PeExa3W7?n(rf57 zA)$p52np$7*8lU}YmO7&Z$5YC&bjT}v-eti?R{=0I;XIGiLTa-x=mB`h@R0)nyI;3 zq_;JwGrTjZvvOyAXHsX)&L*7=I@@$M@9faozOzeb=guCT$@Oox_v!r$?gKjSt$)+a zC;y&%zs^4OFTQv0Ozym^vukJD&UT$GI-7Sk?QGClr?XaPLT5r}h0f^Cpw6Jq5-rvo z&C*MHR!`|6J)k>ugRa-5x=iQmT%Dtz=_Gw$$LVYOl0KoM^Z^~HeYA&m($?Bc>u8e3 zX|#r_qotnX*ZsVw`!PT0d;Blod;iks_@_R_C;0pRwvY8O{))fg&-hb5 z(jWDQ{UIOfLwu0m>;3&6@8i9^rzd+4Pxh{Uw|DjK-ov|ja{Vj($$pRb_TJv#`}trW z>_h!Qf5;#8QU17(_Gjzd-}15kp?~O8eVTvab9}zf^<}=q*Z3OW?%VyaAM|uj_iLW* zh3#W&Xqq9N%tNg7x+jO?Ce~~}Avqxv2&fcBbO!w|Kja&IvoG~ueXf7&pZHXN*WdD&{W*WU?6t4o z?OnX3xAeMR%PV_D5BDH8?LOU|+MUvUsC!rU*6uCcYr5BTFYjK~y`+0l z_rmV)y61I&+5J`btnQiJGrFgDPw$@6J*9hc_r&fg-5+&N?Ea{GQul|oeoFVG?kU~V zx~JBDXLir(p40tx_cz^hyMO3j)V;X-x9+9gE4x>AZ|vUOy{mg?_u=lt-KV=Vx-+`7 zyRUT@cNcaCdyvO^j92$0Z|F_Dop z^rN2U7yP>4^b#+pAsVffG*N5m9okeoXa~Jp@6iD|R3Fxn`lOE5mvxN(Ti?+S^kbc> z({;9fq2KB}ov%OY5?!Ld>vCPKYjnMC)=j!yx9Tq4rF(R@9?(5{Q1|H}-LHobfvD)-*k!osta_Heyel!E1j*gbgE9(i8@}# z>sTGDujtD_7WrpXUpFj?eYae71k)Q+>LBT=D!z{+_?*<9wXI$LTozP(RWs`l(LWuXK*i(|Ni;f7ZqN zhc4AMy0SR(Hr=edbf@mqeR@z2=@C7mC-kJA(o{XIshXju^n#wzbj{FIJzp(THBHZG znx52DJ*p@5upTW--&6U|y}C)a>Uv$TEAivq!Djw_69_{5l%ERh4$U{8P13kzc5At&L8sLGo4Duik^$nYu8-;C`iwrW&*~U`Nyq9K{g1w_Z|nQ|o_?qw>&H4- zC+jCVO+VF7b%uVfpX*GWS=Kwd-hZK=7e3F@Svo^!)?Po=Pjsq&Qhf0vouuP+g1)66 z=vaMQ-_$qs4Sh*pE&u$qKA}(O2pz7E=mR=L2kE`qx8m_0+C|%IM{TZmYGZ9$cd|xd zdc0Q9N*bnNrM?Ggffsv$-z;>`tltBg=IMUIPx~Q1=KFoW@AVzN)Bp0ne6z3f4Zg-# z`6~azfA%$NJGzSMs%&$-kW`VwE_7UV+IFcg_GSKS9eG*t z>OXv~uk!W2(Kq-u-|V}5oA2{Ip5ll6h@bQme%90coM-w)zwXyO&vU)dOTEMcG(f{O zOk*{wEVi1~(j;x9^|hHc(>B^pJL+B9UAt>;3we4%er2ls>Od>&yC* zzNT;JSp7eJSI6o5HQp!aWSvrc{S%$3GxQ6csbA`B{ifLamxaOK>$m!y&MP+moqk{I z=N9MwM(36;_@#bTN1dZH^>dw}pXhX*s*`k*PS6kZ1ARxw>3{0VU)5LiMSWIB>tp() z4%ZR&3rGj)ecHD&rd_mC{W8>++Eg28eXXfAG`@ZZX_SU&ke1WiUgY_H-Ea6s&-7G3 z>&N`4AM{(C;G?!fxlZ3{hR)8f7QqMi~fQ? z@6T5DbCi$r$9#kj_lNxvALbAG{r-Rt^}&^&z2EQiK|at2_#hu#>j(J&AL;{rNa;BC zJ!+jBhMOT5G#4bUJhuVGqAqqT}A zXf;hNP6XW>Yb$M`?etFVtR1wQ_RwzHTkqC>+E4rI03BFpC!W7w|5dT!L;9GG&{2i{ zkCh6>mp)lo{IrhJr}gpr_HiAhPwBt(Ngb&pb(D_K5%rr{*tY$~)gd~t*qUo`&E2)9 zcGYg$xjc7kZL6)dg*MeD+E5#5U9G3pwU$=Zs#--WX+@3J3K~&Z9$3Fs28S*50>4#= zp5f3xvDfL@?op15AzR}kd;;;8L zzOJ{hc9n1N)xN>k`UYR;8|!tGZz)gX95BZ1zR$P&LEr6%eZL>|Lw>Si@-u$g&v~j} z@^gOGFL{<C91xNHM$T+F;>jnQ^` zmv*QuWH0Te_h@hJtNpc~4${H(J7!q#FdeQB>!W44|0=6}QlHkRDrz2G$orf=r_a~x z3;L2ir!VUB^-E`I{j9#AqxCs`Mn~(@`iwr=voW?sM{#zp@?MB73;mksc&1;e-)WoX zspaR7cuM`|**(71cldhW>T7&$Plx@@fAL@ZM_=so{RjWv=lXvOLBH_XKFdEV#Q)T% z`81#GlYOF3@{fFyzvmN6Wudf)`p5gb{+^Hb_xzp8(Z28R_znoljo2V&3mIX=t3_1Qkxzpg&OIe+nm{+loH<-Xik7VqO>H~Lop%lG%kTPY=eV{R|)t@mi3V%+_;xAxP%wY;~ufOWR* zr}t<-?WOmY8knqowMQ}ZWKGtt+Fd)`E#D( zsh;kqJk3w~89!d}^D#f+N9*-);gB&VpX2>OPpR(@)%R?9)KmPpAN1qJ6i@guKUL?M z=Eps~cw~mB`#Hbh=lzmj_DsL#m;Huc^;>@3WQ_B?(2Kpu-O7@%)&LC^S@m#@uHOV) zLE{S5`2Lz&U6Zs<{W{=!Ww*_>v9{1=g_do#jds-bdY5+6PCdKrrCqhR_Uu_~Z|$#r zw7=e`_v+w^^F(^|5L)=q`Ypi^=wQ9S)_Ldm1`n+2MHu)Hy-$afw%bqdt8bAEn3 zaSu(d>!t_WY6rbj+h}WTSvK8N8)-vrpmnvrR@XY3poyhM#%V>3(ehfMY&lHQ5OvFv z3%#hU$S*K5YADbb{G6v3zR9j0t?{4YDZblNd|TB|ZuiZ;rBu?j^}5nm`yc*?ukhvm zTPdnb{1^YJmOuJWzQBJh4R*dS@*jMGFRa|`5B`JCulP?J*&3f0_kJHQK>al z+ois!swC)Uu7TqFXI=Gr|I;`78sFlZe6w%!t-jND`OcnhL%BU%e)Dv(JwA!Apf6wb ztCc;^_FFwa!Ly}q{URcIZcu3&ykv~VRFs>bm5cpX)g-N^wY66H%KANDd51RE#@bBp zDAl)F#q(_{rf*Sv3j%ps2&T<26PTs*1*Mf39Adm7Hqr(lP6nqFKL`&8Ag7*-V>jtIAlm zD9oeZu^CZwhstKQ*G{F2cBs!?w1alhj>QV^(z}XtchPp*xwv9SZKs{;S4LxLexr0- zZB?GJl{V|Cs!e*{!>^L^+obE1Mc33st*+Izs#ewnjVm73cJI%`Cq=xH&sS{ zvv2bCzQNb~x9_N#lZD7I@W%evM# z`G!(!iM;=+tLFOm_-^0l`+UD2=*0v2`m`VQGnFe7lc)N{;(a1B%nyfg*KZc5E%sYp z>P24auEDef_Yc&t@{y4msxca&F=WA+1P1MGk zq>amG*4KtwU+>U5+Fa{YHU#=Nu9{{0d`DH<*RDPIwOsZGvm2BuSWD|`-Lmw$T0`q; zq9*nt5SanLVT-yZyINT*l`cbJVE;tsROWCuKT8fyugdR$aB2J zv%Sc(y}++~enm*UV|HaIGyO*W@-BFL(XZ9-@V@AmDyBZ?mn-L`el@+8nWct^xX+be z&8+>wEEx~K;TwCr=vjWH(EC~;_%*-j*?!Y={ifgY953`+#W@SS#EZPdZ+nU9xjjJS zO3T$<<^D!!xJGD{MpmXVrmCH&ar8KP3!9DCM6IStRRx`>b+x9})4CN6*Dq^r@Ne5} zQh3{}l>6q|L|bSRZKX|X?f~SIiLo%3uw}KNmC;ThmOVDD`Xt9~QdtrB#loBPd}0Hw zr}c_qn2T6j6N{fwSjiNjKjEjUM`*XFD{egJX@1GmOLe^D7t8zJ@GBL2-tgS=RM3p4EcIKaw$Sz4hVQAN z!uo@X^@kSk57)@jbt5b0QC}LZ6-)oDT%JPwUPI%wrpA{oCRQY0RqIqX3;u|s;GJbH ztzBwxO|9Gegq72pJ=lw5(E1#WdgRxoSL?mt61;O|jn_&Qk@*d3Vlo~KnpdbQ@KCLw zp%tBn6_$tAT@lgvwePUIwA55gU09(G9`&}Mr*o++&+**exKUS~UFe?Um&;RL^&6h) z*Zp$k;^<@)&@1)%O7Z7Q)k@1NwFS?bRSE!ZMJ*ADqYKD`(EhpN{IU{yn_u%@T=kBH zg=(r}*z4`emB=6li&-sZEE27`@1a%S2i=UwXpQcT5x=iJL1Q&RD{6JEq}4T{P(HqD z4`kt}9>$+}f^{`f>y>@h)mnx8wY7dlQ{LCFZ(&~Qsl?a~HAx$4t@>PFYZYSG`M084 zv(Ai#Cu)s~!^GlMG_iVu!osV@)$9a11EodHcBDpWsFp9K#V?!>EkEV)7vfMx13Ti+ z88Iq}b1HI?W0S$mDqFo!h=7S__(jj~j6(MeKjUdtLqSpH1-c&dW4)TnBb8k}?g#ut zRaYMM1BH7sE$S^nCk((E*%xc9lg~#JP_qkXprGLo>RMDmqwu2N!ZpuU-)21)7R1oi(B@NaZsOv7#nw#d7G&8dZsW>rbSdIdm_Jj zwq_(A_cTAztB5>S>#)KzwSZCP9-s8HJ-l)h3o{wyZ(MU z_?6Ocv+9`y{0-K}XYiGUp6mJbLS<4JVg$%V_=E3UxfvePsmwFH&us1}4X!aA)l+tG zCnG+p7afCb@D1KTH}CN6ivNxZs6W2({}&J=#%eXK*sFS=oWX6dJH9l2G$T=%`Eueg zwQF#Uy+QJ*%J+#x=*{7^pgx(W8liz2spT}Ra*4r}MRQ*$@5E{RfM-s+g%YqrR>sIM zK6nLHaq7#_a-fVCsxA?nPp}Ts-z>gtYh*UmwcrBq${Je!wbJzX+3a2AfMw3k zJ2^90p5MbWeo6*SWsLZY|AK2WN$v$6rk2nDZ^42l$^WK7=E;bRtVqZE=*pz9ADSi= z2iTEXa#)#~V%Qn0fb(&sg2`~Ak-*{_y;+g0B?DrHoV`}-EgXy0V{dGaPjGgu%{$iQ ziGguFPr|0~4=;L)${tZw{a_gd33q{K?i|$(v*E|&B+O(it+AU|`UXZ`P;rcChIObn z!mqEFNxsshZ-Q@f0%Xg+1lH z*qwjZTlmB8;lSo0WY;X@*r+TdF)kJ65ocaJ61@{D=L4vCyh-M?| zQS)aB%P;cV^?%i5oWM0qxUga;xSn5?#rd97-U7zqN-)|YZPxe}U6fcsJ+kpZV+Ur& z7kjoBdrnpAz;ZGGScNA)$)JO2I46k4kHbjZ%Rs$d>N`x_DRo6Pa8TJR^`zk%q7n7N zVpI)LTO(^>b{lk$)GGBq#)O&SP}r3D{PFcUwx^59&BNDlcGh_hBctk9>eV(P z7-UGRXp|<@mAE3hf&agU$iU}Nt)w9uQ_s*4mxq*ni7CT1P$RUQhF7#%uE);UGSyDT zV^J+28#}_ySa5DtjL0;bm1cWhsU)H{>NWEm=&DzGc@#+J`IT9Isq&^53+ZIzL45G{ zykDv633cZe>lJh|Ki=B1XKF5~yJWtJDkv2h=H1BLW_orl$q`U@=)gBTrxxM`QG*)K z62DPwg-6aWA0hg{V~I(LQB;fGuIdab+cVwlVTby;>>E z%z`3=1=0=^5bse|EU@^fo*D>>VfSdPmMO=!@g(|2=7e2Aba)B97>1^L8^w(-L8TC> zv0#?uCX8A<9aP6sj64=d?JRYYWKw7wE*HFhS;*sAsXBy{We({?bYuCO4+mMP(+H6|PQ14w}(;Z`89i3oB|67x0?+ z4}RJ3%qNwxC~xZByrSg5G&v^>NUk!dr|HP8S)%L6QyF!Tgw|cwS9oT8NJWAcP5Deb z5AvHY@j0wAbfRw*SN1~zz!SujXaf8(S0YawRDL?5K2Z>8&GZdzMwKuAJ-DYh&>qx@ z@c`!RSjaXOl|o6h#vj7*Z+n5Ixt939CI=6RCXPBLhG&Gqb7C!+1?}LTCqwwo z65NAi+L%4#*tR4CBx?rG@JxTimPSL#AfPZ=&eCDFT$;$Aa=7m&#qO`;M+&LqVye$3!mZMI=a?{>q*7yi9 z4-a9aQFeKudfSrdjW)+VV>C#kYu=$L^yIq@-F>_VyU~RGtQc$)lamjEYo3%sK_&wr zTOQQYWI;7tPX3%8M|I-^LrQ;;eG%F4e_h|#>*BkK((o_r#F9E0 z8ob#JRCE8##>2sk3=t0|%(yMDfu*@d7slaau+_H|Y#N@$UdeZsuMCTrjus=PlXHPv z@CsV_Y}*=dv!&JP_)I+>#?Cd9F{2^ysHiM_EVUf^ojXxIU4jmQtrB&(f5riJCQ_3{ zQEi~|u*mlOTF_gJH7tupLK(rRWILdm*q5pXRgR|1iR9$3vkL3MUFN-+|Df{8lLM_b z%DU@on;%E3BUb&#=V3RfC zCyOj`-&B5ihxrFqUcxA~$_vOw?Ziu{!lb$yAIZ}x(Y(a%L4|OT2mbINT3BakFHqXl zdw)G`9GPdalO1P0SK#@tRI*z&o4&v}%u?dNFgaFdp~olG}rlsFkolbytLvH~7KtOW*ClLcLi>Jl!;)SULVo&Ze zmc(PhbvO@PCssvqF`jwBo8eOIl1wQYD)|xdFuEL!Cw@~Sq5er6PYpCtlqwZVp7F>x zW-)@*wieuG&-PArg)_k$T!po;Npbw}a_KHZTApf{R4IyfYF!owd*d zG`~I>Etpf08u}>6NeyRM978JvP;$-(Q z43USvk^i$KZ%Axvs=Hft5bl}#O~wft$U#ykjO`Nj5&_ZUU>X|?C@aMV;nv2r;bF8< zxVax&iL*_)gI|`kprg@f=r_*Bktl&!nKLF+M&X73v1_ZtVpTLD{0&o6^TVpJbQUTj zRDLp(mpBcxQE^~q01Gl^M65h}fC@?#3Fd6tH8m#Uc4iug+hj?gn;3~gNB^d}4C;vD z=;EmLsCK@C`^+!V2D+(cEhr9POIV^m(?NALPd2r=D6Wgf4#SYC1-^%yKeG++)1v=UiCAu$_^ z4k*1GtTzphU84*8cI7NAFa>7?yG=vpN?Z|iqpacY%suye8%~GO@%!j)5F9MLUC)IM zEKUQ#ctF!_$;pzJk*9z;(1;pmo$b9E3B5sGz2RQZL7L zFexm{8rz#TroAz7bQmfg{1ST`cegyLDL1Um()2$$K(5;2Lh3cCg<#KSTUd#ENR1O~ zwtNG-4G{l2t=W)x9rc}@rSJE#7wpM1Dc#<)8PqfK93_>HMDJ8XsDXelaE3NV52My; z%lt(?c_-s-aTdi6Ua4p?H%?uSZ8=BQa$dNDS`0XDRZ|o;2u@UnYlzJF5nP1UNCb(W zfMs|Kuf#)Ih6eX%ZW{iNf53V82T{6dIo6tQ5CN#cfV36`$lF-Q7U5Z{8lVy@rVdSH zNStLG(E*Ra((Dliz=NpOs!*M{g5M<~bNR51%UxeNB z^bz+9&cO-uc^Q>39vA}Ij2pGIj2+{b>LQ=1Ul9xN8BhvV!96YPk!aj18K4xk6ov?X z;gHyf_!7(T9Tf8|x{~+UiOeBuEvLrMlV3JG!@xXUX7?FySuSQ28Bvyueyqm&vUUj) zu~OnGcy4N$mWE+kK{f21y@TzBW6m4q4m)7)R?~#fTMdjT5wB`BKCEmHE%kwV#2>-* zAT9CWvSoBjEE$cJ%sN$(#*TP>tDGffllOpcGAA-qB4YBTe&v#gp1Ck0Coz=_yg$Pc zH1=zttfeLjQd6O3pX>*gS&yDhr4$B%O~?zV$MPH;Nai!~0yM+|o&w1Y`Pdq-0^#Z* z8LQIL>>D+Zt8hlH+OC`^)L5Jzw(1EvWxES9X!5Q>6`x_TfxX%I_(ODUaLtG?E+8EA zWo`iUMS~~PW}hfK3A_fWU_Dxoy&8U7W*h`_T#m*%;n{Eo*$^=~c@Z@`azt9_U+ZId zx1pN+foR2@areGALY#Y9V@SqH(L=5L!%d)U=<~^FS0-cGz zVe4#7o&-;{CG5c&Gdsa?ct(p1txxn1mS&-sWD=;w*b`5Ly_VL3UEA}3ShdxB6RBIB z6WhdE#JI#cYyg753-h}33Q5#8+AH%BiJ8PsVtD2{S{x5j!D*ZM0LAPH6TE4DKWE9m zXFhUN|2(WM_1t2WWCPI*sk^pJ4c0-~1=a0N5(DCy%*ik8Re0MR{gU!;e$A<$pCq#m zCKBfu(=a61&I{JWQ(}#HNxlVzSd4knA+UKv(F zb*H9{)tlC&Z(tT~Vr_7FJe-b31H;LwlTG6ZC^yg$1O-2N28TEuwuk?h)#Pv^TWE#< zoBqjYH>?KD;5*ewFb|%I7ksvbwRW9Ud5Ftbo0O@$hbP9(Z|vDf8ewvl?Ve zsW5|k)~SBBN)-`)j=P>yoRE4bXl^k#dy^Ljzp0!CziAd&(I~5YH+}0YKM`NZwgw4Y~ymL3O5=_ti!SXOx)HYc) zztxm+05SbqR5VQDCd!-mji$jXS}etH`jH$qjT&m>-*693iN$zTAJ2H#vaAtXaYYv1 z**ldV{B?0X0g0vZ7khDUxj!tEXw9920Y(JdwaQr#-DU(pejh;@*S;DG;$k1{97Tn# zT1*cYM`yKJvt(dl?aV6T2Yd(F?d*KRuH*%@f@aq6HP+}^Y#aZ>c37}k5|88#X%8o3 zQ^qKBv#c|+d?sdxHN&|5yeD|(J#~-FMxcW893fe4<}ETa0bsa*7Zszqav95i$GoR4_lzb02CLeqj}@KAb|dJt^P_xN*W zvbaAmjOWMq`GyU!NHP+bFA+S<42pwmkljWei_o6<3BR`Wv;>{(jisoI5Y@34ZK)@F!_$d7(n%#JXni*vzz~koL~qqXca1WkFjK=!E(c4VmB>~JA>Ec`jupPEVnS*Dr7@YGCiuul-Se3P~c#h>vX~X`Fo3ULi$aZ>#-(Y!q!@_&iHtW35 zTihKB^@AW8>ocC;^h1>>2^NbNId zZkjn5ZWXN-bJ?Gqm@|P_uuU7s=IZcCmT(Du<9mFHzO_0ZDloAEABvad{_q>ts4n0W zctw_s89ovXhyU}97YOIG@jq4o+Fi6MF*p7B=8q6}vAQ(jQ0_|8X+6io9tA^R^6*dogv&XV$ z<5=QAyHXHN|5`ujS*!?457E-9Y)5V2fx&g|6$|w-&PcJqZHs$zlvpE{hy}44F)(u& zAUVpZAtrSW5R_R9qU5ruLk)v9(4ENIkQl_%9^?m=!Enop!DfF6n>6f(J2-RlF<5|o zlj+6N;(>Tlyd!Z2eaT%m9LH1gEo0K+Hke41&O5%4{EV??jDs#v)$D+!SZ|okC)SGf zv2-jNJF_072gSiRUXrNKUO8jz%z1d_I`PTYwyfH08721+l@{dz?zv<90RK;wDO|=V z^;2SFqX^j$@A~C~V30ToIO_&;4u1I?+?j zkKv#69t*}oyvKI=jMcDQVjk>9W`ZS}WumtJbKIMyh{;_KEnW>0o6SIYQ_sO>@SSzg z9wy*>A~12hSvH@1&vw>0f3qXsq7m^w`ik{(ml>IOYB)LA4yO4FC&oq@)n&OBw1knF zdrZw3jKb83h(SAw7Zkz*EHD83HM|9niNfrQs^-W~Y>=&MZIq(azIJkp7 z@Flk5MJ)8S#g4>>_!3bE?}-25NB9ibZupL`#HaBO{61sGJ7XJ7)M7x`GV>*jewa1L z!v<}Q&mbO~1))J@^QVT-hE}e{zF0RF?%Ni&Xh_D!oIO6Uti|bl?je|F)&tE(wn^4Z zv<|Yt33rw!7TB76EZB|8Wdt)~{TNCtZTuha;fRLSj5QVkwNXe!V=80~*PuRFZc#bt zjg453jX*5OO{R}kf?htGjkwF)52)>Hk&FrZvE)7Dmx!J9j5m3A-#*y2>EUHFE0i_W zi>A1+Gp|J1V3V2T@ImYdFB7k$;XpM zo}8En&UVfNQ{>8c3rnIit+{^GMDrK!15b>eOe~0h;bZ)9$9OZIkyrr#MWx1XVE3Sz z&x~c_GwWG`DEL2?i6=1T4ae*e%V9ly3~OTL#PS^37=V2^b2}<1=X>_&N_`EM>w;gd z#yfw}+1v@&!P4>mb~nusXa*#ER`GzUP{;ZL@6J*p>zB$C{uy*#!N^O}MCzj1K&>%c0Z+Ho|uoo;Fjm0|K6Q3K(gIaLORu-ZvIafc% z2e-j`d&(D#qxM;nA!q+wIh>L?W_khJaP}4rn$O^8^pJh<7w$cXj?b_?PkD2<_yPO1 z=#|lnw=iya4~Wh9hBNzP&h~f=wgxrq9lT<#X7|M1W;GC=bFfbzwzj8S-yQO0ik_6ZBevL#3ezw(FWusJvf&ESi77@DJkaK1N$ z6KC^c57yGs;yaiId*GFAQS-EM?r;XJY)xCc9`T^{;Xl`!&Zbu&H=fWU0r$ZDaaY`9 z-Pkg9F-C_r{GFJ`XkvdXoN2kntcT;V8-5%=;9I<#OeQFr(b8GW5d?Z#0epzo<=Zx3}FK(#juHwa<0sDo&noV-o zj0?xom)J4vmoWxMtY!4sgFf(%T{7OW1K(*OiZY+ka2P!nTX2@3AFMX+#&TixAeb%L zD_7}XDSgUS(--a~c4uF<#9PzONU$)%v{ON#&YdxD$d2_A(by6MF#hc+gocFJ1Us`| zB6ire#Zv4SEV54`Iq$(eDC32t6V0(9XK2xx^@i+R0}cs0z#h?W!FP_#^|>bf;y$A= zI5Yi@SGN0a_Z(HicJ|1Eha^5TlC2fbiC^T8k;NkYSb%rY8q71=@w9kEtdp3_k`LX!S}XsueoCo!^nX~ z&JwR^+#YWV-Z>&`{IQn3f=&J!8^HZsD{WkptMENXCGy02Y@;XfT>6T?V)y=W+%@+d zRR7<5jD2FAHa;0${usMHBlazpac67L$NBRuEuc9S6_Cw&8@^-Z*fF*b#{1d}i)X>2 z^p|7kMe~{dCv9nmms)JjF|klA+APL+HQr-Pn-ycJjAN|Cn8W9cJ4^6_)mlqKBG@D{ zV9!MESSlzCT7zFAZCcrb{W%Ap$=c&RiNis5t{kP`KVQ2R`EZmrXTjoJpZz#vr9LM*dIyj&;u`OG;R=d;Y19%8y#vKJ!j6M4` zAK(*CZ5o^X826U{aujWN!?JeZxFDCc<~^|vXJlIS*JwRsOM1(>=qD}QeR{_6v2asf$$DtbXf=yxyko&2x?uzxF~&hT zpP&ls^(!1v!`P&cT5Ji9gShDN;FIHN4LW0UwnwLDOO9$s^Nq996P{4WwbP><&vv+n zk-?`L%3JT-9n!%cZ?L9S;O|C%6Z9ch|{agUjGBxV!t{?l8DRa7#iwowl@hmsj0& z|8rjUemIYN?eF3A>71T^;vN7DY%`$4gg3<5?1!+};n8qgf*4wMHTsu}7=)vpc!h5#gRPMxFfQGcq*Km(u-P#GWqhk8-n zscuv+tG`qThy`K+9B`^n)zj)B^_2QV{jCaWEKmig57YpxfUM@I57mq6QT3#HLw&23 zsvuwnDg$+aG@u?(6|ey?P^M<9&(*u?HT9}`UwxteREyP+3IP_NJWvN{473E=0&RfS zKpId7r~)JcW`G1_l~=v0Q?;u_YKdB^mZ@P?QAxlAR03)NZGdjT0AMsQ7RUfHfT_SV zU=lDH7zK<5`T>1`bf5#!1ZWIY2Py%HfDJGJ1b_pI3Ic*EtAeVivI+qRKm$4;28aa` zfeJt>P!p&L)CU>^je#~m3m_dx2RZ>gfNnq!pa;+u=mvBF(t&nBE1((B7^nl({NGIs zUJ)W? zI!PU&j!}oI!_;Bw0CkAkPaUKVPzR_3|EGtmgVf>baCNjgN}ZriR41xa)j!m~)W6hu z>Oys?x=dZ8{-bVDx2n6;ed;0gxO!SWqh3=ltM}Er>NE9`dQm;0?pODzd(}hgY4w8o zO#Ps`R7Fh$ngAn#6~H+l2SC84;56_kSOmsEL!tdpHdI+NMRP-=fxE#+VK1DH>_+@Z zI(h`<(E-?1Op8y)Gx1u)R>Dj4C$EquY7X^@YC>xM6zKX3YU z@Z39he7W@%|33Q1lb?06j%Qc?eKxmM{{i@H1 ze%}5W14{?>9&%-y=ZEv#8v*pg_^_ymF zoUmcs`i1NEu6_Lv{m+m!_g7b2eRfr|RlinlT$#QyabddP0t8c9Cu|`>Q>!0OohplV9zUhYM8@p|qxq0uFoUIAl=kEBjv*YgjdphjA zCw{#78TmHg$Aw>V_JG_I`9M+6k`tv-X9v$NZ=t_>XjbGV%k%Z5smcN10~CVev4%u< zY7{e3mtmM<8fxhoQ#)24Uy^V&X>Lm0^2MneD%Pz0s7lXjAF6k%d8<}zotbr?*Fzd~ zXgIym-o{tc9yfX3^nSDR&DXV<*s^A;NUP(m`?vA7S=*Lrx2>I~{nYkv+oyCG-{EYB zZyk{Il=Oz_tuVt{%;e_$4H5_k`2z{cPl@H)tWZJ-TM z4pc|8Qd6jD1|No1cmnbsNkdPf25b!$!ZPq&d=T-D=uAE&TT(Zuy7YOvGINZH*Y4J8 zb^qv8-CugQeyHJtp_%cb(QH~`a+o@qFPJgQc*_He!8*oz*(zFF#jJ{X6eGqou#L6t zu-&!g+f-Y8Y@OIPu{~n@#tw?@9os#&No>tneQenF*>=V@-`2%O**?c?jA<9cS&vvd zTiuqGmK4iHb93`!Q@ZJ~v5E1LAvlUH}bo6&Wh}(S-PyDzs~&B{2cV- z-uL+LYrciQj{REjW#E^B&*MLbKmGHm+^5GM$9*(@eEea~hqMpT%x9UKGbd-Z&8(8i zWP+K#_ks7m_w0KjGcGeNb71C@%uAWMnF$|;emL@>>_f|sJ3mH0_W$(wQ;pBZKHI+R z`x5{4xqri#a#!yDMf23~I#)jB$+(mwaFj{OT zF>=0qMOmPB0VwbjxD)EGq2Y({ETlY|iO#}e@tb%bqLi3V;?yR}MDL(!W+fA1MrvPZ ztLm2Na&^`93-yonh@p*Pk>Q--w*fR(F}633G|n_GGp;pmHf}bqF)lGqGY&JhGgdSr z#$3ZS!)ik>L#!cBe?UK6Z`8lhEznigebp||rf45CqZpCiO;@I0P(3LJIh_Q_^@NEy zjF-pHW3{pCXjAk)(h7MBcYt4Nx@$f_1EDN%6zBw|0kXP6HLFLIhRPFpm|P}pkSa+} z#fc&*UKU0OsBoXp;4Aa_+)-{cSDg#7PuN}T6t)vvo~2oDG%xxt`XqYqfBP)@KAIm5 zMrpPN+m)To9%eIHjBCy<(UsL?TW2K?626#aeUnJcp_m-f;MqeB9^ouDUdWI`DJqTlnp7) zls4sdmMbb(tNe`e*UEd!S4i!Xx-9ir>WkFe)MzSNfvI4wV5q=UkW;;>pHgq9Zb_Y- z+9VZ7eOG>E`8MSv<&Ko=TF#xaJ|!vTMskbfmq{Iyo+Y+OJfD!5uq@sg*D>yBY}D4- zwj<`ZwSslD<*50mNoQ(noNCywf2MP2b=qo7H+mekjNDCJ!2iR(qWMS^Rx}1p98?u- z0JK)SD+A^6(ll|2u#Vr$on!AtKSuJxTu2+L9&8tw=wIeL=Y3zsdn$PPx!1YwIVDGZ z$HLMFcG^C$rWC%K0a!R!&j&-t58I z@!5{7n^_yOret-^YLHbS%baD(GG^Jbs%ACH>X$VwYiHKOEPqzT>`~drvP-iY=d8%d z&1v*||8FpNVs1`u_qGY5kx4djf-l6+$JUE8*FZ=24h^$!_KP^6^5Ja6}v}CChp8KBd2^ z16~62z&cP4v`&)-XTvLy^5}DPIL2b@@i^i|G^vhr`!~-JolB|$aY}~_FZ&GbXc@@RF3?NT#jss{1q7#=@@AcsS+t4NrdJ?+#9YkzltyAdkS}iO5z@o zlvYZjv{)A8B?_jlR}+DMfi&Z3t(wa4OL!Clp-0d*SP`}guS&cnW|AiAE;XD6 znUhRstxLO3S4aO!Kh=O4PZ*nb$^O*ZF>9!ZP4zZ78)8a0~CC9Ii zkH!y4xSwE4{44QAqAh8B(wQWGQnlm>$$OLECI^xeQW~WUNckgWUdq~(wJDoY)}$;> znVK>%rFBYtije#+`B3us~+hVXxFyAwwrUAx%h5~&#{czm@?MH@Sn$Q{4Zt^AJ#^dlN*m!g~@-O^S zQv#t-D%chnp)OMP%GacCqDRmRRr#)52D>(THIf+?L-C=`!I^=B{!c!|+t53??6fD} zo$4OvI^&EuT06Fvme`xy50{Um^c`UfaC<-1)hQxzB%3 z|6TESPR_2J5jn{@!R*J``?42h56bS8T_?L*c2aiv>~h(4vzulQ%ASZ4ELCu z$)^gRg~ehm>8rFAUK?e~ znAgm8W()HN(}^)L9{L)+m`u~Ei57(2w z!>0(_1wfo9mWfj&yEI95$7bHDN+MV=gQnMr|=~d$D#FmNSgyRWa6C&~3;%mmgiW?AD5<4w6 zV4Gp{#f*vhY3*RWYB5_Dntz$HHr4RsC6^)GZ5U0>~f<_B%1`%tUMr-To${y(m~ ziR8luxS?hOv<|!l{8mx5w$fi-CY=($2m)V)@69b^FGPPw43XC1`JwYcN1%LQr2nw5 zz+2fnz3iR`_Y8AiaFMQY&S#Elj!mVaJ;RRMEW}cG!BllwN&fK}V6LS0I_R3Aq?U~y%cXaNw+%>t!bD!mw`B>81o^2mf>MvdHFgnjT z+qtq`3*GUaJDyQxsP~L_h)?oe@DB=Tg13UBL)P$<@Z3nn=&$HMY*Vg`+spS5SmBH~ zKmz0oavw!ej;Ngg2e1~b0zHLBY9g9{-~{Afq$&Cc?TEd_I^nPJ*2GPs8o8TJlo>00Y1>-Oqy>wfD*olaj}UsvBj-$ma;-&5aF-%4Lk zUrtZygSv0JYr1W^@w%2eO7}y1LOV%YQ|n^RFe8|F<~6;Tu0emHmQktHdoqJ$$n!)e zq8MLx;$0KSEQ%%^%?V^ECg8Tg`RhXfBIA!p>pavPo0CqKdmkqMjx#`?RPT<<`oB3kCp|C@6 z3O&X1qE%WfIi$hzE4j6DO{t@vRjUAJfg0cyFb#SPb=CaTjDth)3dD?_M$@nl*l3)? zHxkL@J+doRLd~aX<|NZV`$juV=h7|Ji@MdZtIFR_5pCHkK!rX4b3LYBBp_ zbhbIRLR)(5*;q1eeB9kQOZ$*^*32YN9vsYvT39?TLRRHczAzza;EU7?@y8co{!0J~jSb+@En&+^N{6v2SdH zY&kJwVv4Lotlur^mYe4C=CvlDv77Od0XB@(U)5=JeYJ-eJ6(&OPTe5GL_J~}ehRar z7PLRI6uz%Y_`IMg|~CXnej z`A7K9dbzSLWrsYhyO;ZlOYfTF%yo2i+$*hBdfHyjexjse$<^Yt;>@D4MPlKR!e)i; zg6#zz3zYmj`3v$}=i~Xm@-F7B&YO_eIj>G$a-J~{%fs`~JY8OVUgf;@d4uy7<{i#^ zkr&CUls_VWZ+=#OV!@<>%LRDhz``4a`l1;{KZ;rvUnq_**;<0wm)KSNqEgVY#z8yx zJ1e?wxVpM?+%r9N*@?2&-dyi|U%daWe|SI+oCvlHxkKy24I*D6i=)ZxQ+6Up@aOq% zf?rrK)|6gI6J$x=tyEVZs{MdsU?wPo|3HjpyT$@Xlc3(y|eO)L>#g6HB* zh^<5s(U4q9z9wm^H}wzof?}y^^dNdMeUg4k7t#_{G-RU?wNZq5>Q9UUW^^@F6_902~5iyIXM0~|p;Wh9d*dnYv_8gsr!sr>K zEm8n4gw60JO-D@uG#AoCC%~5A7hnni0=w1v>Puyu!pi&PI`T*9Pl=W;ioL{;uuo_! zIQiXtdp^va;f8P~?j5_1?ab2bhvZrrWJl0wT6Ajkc(gE@!j56jvO%^jx0Ng5TJpR22tPo$ zBczDy#h^Gr`X+UjpU91r+e$6@y@A_ZV%a4JO1q-|R3Cw>-4ewQjPq z*4{CfV=&uL+hrSX>lnK>_CqWY*C}p(+@-i*acF#{`0nxJ;{T4{8h0*nDQ|Xtv#&ymdTco zd6qe1nrU(vM;qT6S{nY<>-DpB-?R<2TNo$ZhTcZm$;RXg;sYLskHF5MZloUa7ko_< zg&ILK!E1n5t))&?PRn+wqBKT4DireN__5q^))B29ofWwg)`WY6_66O6=7G)r5?@Q- z9xq?kzwAFxInPG7AodKaj~~WiB7=BER3(>@KgsIU z0_qt>)4k}ebS8~3&6o^kH}jM!W+-h1Z5wT0?L_Tt?cdr}+Ev;$+9lfg+CQ{|wH>u} zv?eXfd}S^&|1g7@T8uzHr#I1kXbb(F+Cuf9G}JwEDw#xnAQlppiI4a^JQ072O~7#M zEZPZmAuExJ$Rl_-9M$aBRMmWfMnMR)4@?7p0@DE_a8d1{`jst810`EtAXk>(N^_;k z(g$&s*g$j&M}V%xJl*(vNob}xI2{l&^`b*?A3p1Z+CxT^f0{CPgeHx*V2KZFY667iE*TUsx9 zrQY&&xxBJU5tQ+2mf9V70yGD&gLR>cPz}vRO-=Y3+z`2qv_W5>J+SZC2;7cOCs<+` zNl@FUc={+^g}KDk*WT4O)jiO)(*LJ#VYq3iXFO{xZ`x_X&2!8p=H8ZDmPG4v>u+nT zm|ZdMn6|dfwrpF~*m1GfVvA#|#tn+w7u zEgRwWd1v_m|8{?T;Bug4@KbPXhz)HFCr0i>(xXMunJmQ}=IZe;`5}T=SR(4BBT{Yo zf!ta7qKs7S>SQ1QOanvURLBjD)#PY;!q4Ga$Wa7Cr=ma5I@nIkgSE%^;ch&QSV_Dk zD6%iPl6*^s$a+*iYAtn^`bhaGicY1|=q_}BdOSUr&Y&mL$-!g=vY0qUj3deszwj-1XPn0_VnZLUOphVl^xO+sfkn~ZW3FGZsCZ~M?i&# z{2aak@8_;?v$)0_$h~Iwu@l(ltbuh#pG8kbmq-7M_KdcQR*xn{W1@JJilR{}YKX>0 zD@PkgyGKVymqqtSA4ZF#B%8)gV)wA0ScFUGmT|8+oFB~Z<3oH4VW;2`+KWd;P#P({ zkZQ==WJR8${7|~8x7B*UaUc=g2I`?T5T;qCQ8i0p8D56S$VwEzR$@553MYs)1VyeV z32GgM(yM5dUdV)*KeVOV0lJU6ruwUTn_;aXU>IP0XiP9IHD#F^nm3vA%#AGzEuSn} z>k#W6>kq3TrenGwpq5-wq3Rpw$rxDwtsDBYzJ(cY|CupY<+As zZCYD#%>9^+F#}`D#}r#nSjSmoteKX@mKv5!^Fp)9eB0E`RBW7UgpHdFafZG63i`de z1l<-brd`B%=t1;LsyelcFCKsv=Ed##*m4MmmE5)Kr zk#9@5G+aC<075_h1ShjS*)vffIy7=4Yza>feG4`S?h9xFll;GYZG3mU6}^Yc;>xyq zte$P|c=ricP1hr5Z>Q6-!cpGwq;yKDq4cqRhP{ftu;g&bn35_bq2i~-TZ?BDrx#Z% zribRU>yJouXOM3|3?}aEbhRzP0d97$&;JIT9>yl;f1cN_F*u+5or-Gy!jd zjiB>TP0fBy9J~yc;Zev}q#=43g|Ok+Bg~3V$DiO9Via+S2oX)l`Q$A!KvtoKQtPPO zR33%WRp}1&7eHrEk#p=|}W`^j-QoeTLpgZ=`3@W9fEu6&j~Ys0Y+;Y68`Q zLaATm0dg`~jr0*0i439|QH1Zud*cXx2b+S$VXx4+XbSoPnS+!^Uc%GiSopqXoQBX` zga$$!cm(VKx`Ay#W1v7?rPfz}Dyx(_%5Qn2+)VaK2c>}$Aw3lriA}_?a8sBqv=Tre zli$Nn;G6OkU&P(x_Hc8#p_@EVW>kl*!eD$UF5vTs5HXE(k`t)k)DZe3-IaOBG}4~Y zChIopH2SIfEPW@#C4CobtCvnHJy@DkTFx=Tamf*Kbad`^x}EJ^hh3s; zl>33Zs%M=?@{BM0UY72?>#gHE?JMWs?T-y?3z&o3gO<>)P-6H{xI*Myq-OL^v5X0gENtBkzs=P=6)VZplP6J%PNboz@8M+Hq(d^Zznhf{@+!)z` za7ZuoJPKk1v41fRYmKkNKjH?WAF+XWO^8GtawNHyyg~jT6*7)$O!c6~Qgf+g)OKnI zb&%Rm9iVnm8>!{gOll0(jcP>2Q3{zw-Y2(^Q^^*jfy^f^5_5?*1Wdfc*W%r90)K%m z!kS?Y^fcNBrO?~R6vXnsgeAi-HGgT6G|!+JkPUhaP6o~3EnpHr0(aCgDyiO9#wt4H zq5P+uD1VfeN@-H5cv>7F#)%(2t0Coa9kKM?gVxO|PEW%abdUNx+Q(P8D^6mJA{0*Mx z8wrbqrvf7m5>Jbq*iAYn@lr4Os!S-;mA6WLb(gBBuERXuRgLrW1Sz zu8AB%Ea+-fLZ@Lx*bw{;-io+FR3LYe2sMu?rux#4=xWR^Mq$Qk-)U>=HtT}AF8cF& zz%bl!!JskrHXboLj15h5Ot(#vsh)X;`M5dDj9D65hFMlwPFP-AaxA=su*O@fSnF6D zS({qxTkBdYTdh{y8nFDdJh1Gu%(L{gq*x;62j;cr9%jn?(X`yu+T=AJF?KhG3F$54D-PUs2@|@)IdlS|z%K&cZd`$S>mZ*v{P1 zDP3PWqO^V~UTU{LxBqM3W?x|c(>}sJ$llZ5&)&;E%s$aR)4tYz$bQTI!_L|hOFNX# zE3M*u&8cHkYb60{9cpz)eaO+9!A9D@5G=MW4Xj9x+ktQ)o$%f~9?lkhWm37$d> zBK{%n6L!KvrjaAbrQ|{KF8Q7;B|{`iF;pxSN2O44l#Mb{C?%3E@;mv2JVS0GGss?K zRT3q0h%3Zuq90L-D8ujLOYm+uhJVJ^W4$m6dyXzgo1j7DUt~C)LgAv|7AlJ4#D7JHSXY`WJ(mpfNcp;~QHCp56-J$; zzECRyD*-3a1-uGcpuZtI)Jt<;QwiP8Pl)>DAyWIl zl=Y%-(<#ha#?1`WUePA#X6t_H>gzY@?fMpmt%f2)W8-4uN2Ad+(zM@HXo@orF>f-z zG)K)Mof^ULvXzhjBB$7UR%A(H}@xiu0dudG7ZbI%-4qUWo7rMtD8cinX@aJ6$0u3yeG&iT%v&gxFHGvLT}JagQ19CsXY zY~!pM9Cci9+;x0$6gd!QytBP?taGdLw$tN`b@g?vb-i?9?hfuv?yv4-&os|H zkFjig*?(nL?`-cEZv)?6AMcysf8$RJ91k$T<-xMxxX|ZNxA5I? zgl)(qybHb_{|{&JYQ!L7J#m)EA$THzY)gm@UHBNh8Xm%K zVT-Vqn1nt+=c0|#0CENyi^L%x;ni?cxJ+|Y(_e#XZbN@SHs~X`0Biu}0Na6fKtw&G z4pX)2b7ihlS1FN?%Kc@F{9f81b(09`y|`KIFUE?c!dYRN&|XLp9Q-qWFF%9t!&l~w zyqEjVJ>pJsd$`ryY;GnulAFK{-_|g1I{wkl% zGeQetx^PCw6H>&1;sG&FOp&HY*Ca*iF7J{3atmdb;#S(IN7Se~5V!^y!CBxJuqm_` z0ySea?==oJ!eSp+M z&cLbgP7SSD3I)LN;18e^a96Fa?pGLPvFwvZN}t7c;tip?aDX@Q>o|#Bz?Ma)MoS{& zB1PeG;gZlFAzyG_5Dab##0M_;Tl>HHruh)xS#KM!yKF;QT3McFyQigxcb{`lc2{z{ zTxVT#TrFJ&SDy2hbCYwbvx~Ey)8d4j0Y|Rmmm|~h*745q!SU6R?^`L^2!!PRwk%l)uzA+00;jBKZ13jEszgN*PPIBnx616I0&~!HY4AVWON*Q3N1xb zu~FDg>?0;)b@3tiI{ZBT3lHFNL~Wu6F^-r+tRr?2$BFaAW#T4rjkr#nBhC_gh;78* z#8hGs(T=D}(1aI%ho8mQ;KT8TIDuzl7qEp`C(MH7peNBk(7I>@xrxj{(vS#z4W0>C zgI$`#nh_d<<|DKcY6(Tb^Wa1<4*UVE1JVIey`@f7tEq*`VP&XdQ+~$MZ3~m5<|N z`AU3MK8^3f_vL5ufAfd=Tl{yP;}eBe!XLtR;h7K+s)z%`9pXz7m)c9qq&E^O_mg+Y z#d0lWk@7)_Q)j7<)I?x9@CK*|E(U*tji5bH2pXWdu8D=`!oT5W$Z-TlC!+7rTG)0h ziVeo^;|auaB9~}S9wq^51a+6P(NpP6I+5&02a95@8y?#$9?^Lpzo%4inp>ix9n)y=(0*>0naVZI?rHFZ4crpa{uQ(=3e8T z?jG#!=5FS$=dS6l=&s_f=C0*#?r!fMfY_X<^Jpz-Lam|ov*K zWmn2VWwpHXy?4Brubc0X&*f|B-|8>%w+ieC1OwfJ=YmuyBlIp*FT5)(gvUi*N9sop zMiF)fo5gnKu5-!!dY6vDO%g{X&nR8E9RULmnW@kah@yyoOi6onZ)mu34gKrV*j5&}^t0R0bXc z$AYooFJKqY8_)so)U|3C6<6OV>y&PaPWdA5l}E~zWv}!=S}qNglBJONT-+ySh#kcQ zQ5L=lcZ40n-@-7Vn@~$g7HEOvLwqh@$iL^m^RM}@d?ug6|K>eB%cDY!P)led3=`%F z+l3p#cR>`&i(SMy;wkZiNJtH(8PW-a)L5eIjsbhGAAt1pSWI$JSy6SaW`CyH^HldV z_oRDSwhsunforYJsc3?_ecpF7yb}X$EQzX`GrW@O1bb z?13vGN8SQJBX10IK`;uZ0#cvZX- zULH@tjW~u2m>c_vJ;yF%yRpBq(O5gI5(Z%f=v{Od`Ulz$jX^!gJ!B&?6seAI@MCy0 zJOECCU7B;6`Ipg}OsJ=o`2L90taLdBAaC98eV~Q!lFX)W#~HK3CQ$gOpgs zE?<%t$em=fTp-<)mP&)ADiS1p7O#q{#c^VLF+oJceBqsNLD(ZK5vB=4g>FI%p}tUA zNEH$Un-C|&2uVVDp{meWXesm-MhbI;mBKOMrtn<|2nMl%I7Iwg{8#)SDq=Nhn6zGc zDn+Fl@?`n2oF^wJLzRO{zLKg=QZK2j+6veJd;`jZ)4&HH35|eGLn_o&b66A9w1M}) zPPjR;6)8X(qifK7v?{g+dy7@Yr{OPf12LMoNQgu_xt07yR-h(OS13Q#h@MT~qJwl@ zW-@b}$zm97TkUl1G3{F|uT9i-(T&$_(w)-1(*4#+I!s?tUsc~m-(KHW-&;RaKR`c7 z-%HXpL92rasehcgh3=G5t^8Kg$fB9Sb_5NJnZQlmp6kl6kMIYvK zdB1t@doOtRdAE31co%u+dFOcNdKY<@c-MP(dQW<YmiUqK2>@9W$JA^I4CSx73 z>KKWYqR-Gn=p3{Unu7~<|^lvc}ltxrxeNehO`O+#AR0xJSV62Mwh()ZW5fmG zX7PgfL@W_iv4Ye=`cv8>-IEF>quflMA@7sF$&k`qnW>ynN|a=En7UuhQe%N(z&@Z5 zC=dP#UI00;8MGRD2iY{EH0Lx?O)Gdk{0&Y){zR@L8ni3A6D>q5W3#cl7>swtx8lF> z1Y$ICiYO*(kmJa6WFc9O8cyw`GARw6MrY86=}cOnt1!KoMa*I58B@w2+RED2+M(LN zwEt-LXfJ7RYF}zIwcoW_+FWgx_P6$n_M`T(_Kx<9c9(XQcCxm=wy`!|%Q9KaMP?&2 zf@#2@%oqANJ%w&S^VCCX3Dt^{$=l>#WEC=>*g%JFdY`Vtp_U-H*0Mi;%TQ zW#lzH8P>ojH7zv1q1liDItz9HbAY*k7Wh|frT$W;D}-`XZYF=0rb?)ETx=%(6y^#B z;X2=g4{$rUdR#WUkWFRZM*oT?M_)zeM=C^qh1Z1JgoB|=p$VbXP(ko$aD1?8&>y%H zSRUvdNC|}eul)!73;ccj_5DVF)R*mh=DXYL?T z=G){u=)2~7=F9i7K8wGBzpsD3f1m%Q-{X%BbPCK4oDKX6n1UUH%Y*lVe6V3?LFjr& z3N;U}48I8*B7-6)Bfd!U=-TM_Xhn7w`<%6K6S!L(&X3?P@EW17a7GY>-r^xKBBo1w zB)8OD-XQ16HI;?RJH@7sSFfm&nhtCNz5|KiIPfIs2CG6D&>6@MRniR7?A5%~XyAJA zM0h9s3=YDvNPA>5vKhIGd_{bS0j-3lqy5pD=wfsadI-IW-a?|I3BDHO28{%J1`2U2?**-b*DN;t)r^S2j#FbU1_OU zl_L4Ryj~tBH<699OL`_9mKI8bq%_GU31W`;L_8&K73Ya##qMG&vAUQf8byt$3L(KS zI0d&*ER+dOAtbN@ATnaCSV?Rwb`gh*e~D|w6XHWLTNK4usf9F3S}mQIzDcT7O&%<- zmT$>UIZ^4ZtX1wP5v8U&Q9Z8asd2yvU^nm^uz{n%eV`pohNeRQLSd+(W|8Kx28FxA z+u$E?0x}jkiIgFA(Anr+R6<)}%djUHjJL&C{}JVwN!rm}$&7 zW&o4UG-MJOf+?fF(O2o+^fbB`U6}^x&(y!vT&g2wqH@X8l;T4`L+I;bA>6F^00EAvoz+rh zol;ZzAkUP|@*QcY1WCul9-?2^BeW4p`R#lwzLeX?wc(=dadr?(u>VEpL~BHyk;9Qu zk@!e{cz<|exI#D_x)oX->JzFE3I;QSdxMLDJ%Y7@x}YoYEpQ`nEU-E-KQK1XKhQ1E zBG5EYFHkE`FHkekAkZw(I?z2ZI4~)&Ft9OjJa9koH4qHwf^~v@f^&k0f=`3~U~H&! zXnyEaC^w`Hr-%Oz-v|f7H6xQF|3;jV%F(IO3(;`2@&8rz9bi&aP1omk&&)=GV-03hK>(;4rs&4O0;q!&R7M3sSS2V5YP*I2ElgV|-ip6&p&n-?CUzvI*wI|g) zcq&*PR161(v%_;?H#^R5vbAKe%#nQQgy*pw>E;^qhWX88dp*6Eyk%a>YvMsl3$D)X~(z)c(|g)PdALsS~N(RBJycngv$|eS@LF=wMPX zFZeAu9C%^9@T%~h@bz$BxFxh<6MKVw-p;ar+YIR{56I`TRWi{Xqp$$SP~Y5cJ}{e1 z1@9W~P48DP-S6hV?EmbW*k!TNv8A!1SeN+Y@rCh%_=Sl_6EhR15{=UCO#3Kpb6WZI z9_i1ff17?Xy>>?bj4>HMWE{?@lzCO=1DPLWF3&ugSs|-U*1)VMvOdgOl(jkQOjaVh zPIlYup4kJkhh{&X{ap5I*%Pzh%bt=wHT%Qt53}FOemncc?5DCH$i6+hS9YiDhS^!! zxmnw@e#n}dH7e`otkzkHti73EXTF-*FSBvxxr`+lV>5bXRLj_w{%QK4^g8K(r%g$_ zDXn7Ks>GN?r^NC2wD^tjczj`OaI9+VSAV2m+uz`g^e*t$no*{nS%Z zGdkz~oPjyLbFRqgoO4l5tDF|Dwa@90b9v6SIsJ2na*ZfEGr|0j>uUv3r!K($o7ohO+!e&VfZ~se4-_{^J(&6?l}ue4yd10!%7r(E?}fWuzCO(!w+&>t zd?6>LA@0Q|*o~^Dk9ouVY*MDNcaQg$x5CTy8v1?wXZ$by^?tEmBX&h>aO|bn%-GV{ z!B{d@Dc&M}dHkmMf8(R#uf#uye-!^J{!M&wd}(||d}Vx9e0luW_)qcgP* zGyZIRWPD)!#`s0?rtz$JQEYc?RqV^y_}GK7KC!m3?ASSfwLjB;(I4b@_RsT=c`Ljr z-Xq>M-uYgxSz|siBTY|J+vH#sreHX(L^Yh0UuCj9AU&jtR1Pi(ItIOiJA-F}w}QFB%HU97!n$F%aB%oiI6GV) z7KOF#)pod@VprIFTSxlJ)AE(Y_`!JPM9;(%iJ6J*iFjJav_WZarG1ljI4zOhKK-Wj z7t%jUU!A@$Ju{?vt4Gl%x;-IGkauS zpV=exn#}H*mt?lfY@S&=vtp)~nUk?EV?)Nb8PhUe%6KrNPez-J3K==+8`9^bznFeU z`bFtw(|4!+koH{Kz_fa4d5NWov55hRnu%lax$%+li{r(y<*_lbt75U(D*t7_yC3s^ z_MY)Nd4*~Ls!L3kkeEVwhM6YNWUnz}31 zz>U6)C~jXIBv&NICvQ$RN}ecMS~RxkwxVW5Qn;ybM&UDs0}5LemMJ_^u)5%jg0Thn z74$A>S5ULSFF2FGGyk{zMfub7Kg}PXKQ8~7{3r7t%O9TqNd5!)BlAb*Kbil0{! z`BU@f<$s(1NB)-loP5czQP8sB+JZX^UMQGcu()7rL2*IN!ix*UTE*JAK2A4v@PWhnJ7!;tkgv>jK(Z%M$9xd1I#mKx>;`uOm(lTcbE5~ zH{Dy|9rAp?j^D-a=a2Bm`XBk<`@i{r`)7UYSBTY)wTg9!T^+k7c5Cd`*uAm)Vh_ab zkBy8y5PKvxGjO4QvPZGZ-0&dga4&J(I4&K=lAt5 z@$37UevY@*TjqV{z2V*O-R!mX>Uc@B-z+!(GvmxK)5|nB70qdE$9#N*$8iU`q8<|X zN7l;MGEqj$?Q)egm-D319<;0M_ja-!XNTL{?X|X}ZD^}n*uwBoxIJ7K{t|v0&JI5f zr-W~Z6T+9nSHfrA>#1-|_;mP6_)<7NoEW|zP77y+--S!U4dK@CSePHC*($cFz0~%# zciGW)yq#qi+gATct=Uqa$v?D13~?I0%?#rl%QUCYnWNr?IB7 zceOXdd)r&&?e>h{$iLQqz<#E8TziOGrCi60W{5<3z{6S)bKmYG&5tyWrtwB~88(%PkUNb8u^ zIjv(_`?Pjxt<##OHAt(KRxYh5b)1YrL6wI&pX6#zgx>y+kZ=JiamhRs8+< zqw!ng?c&wq=VBXU^J1^Y2FJR@D#yg!%1V|`)=D->c1U(l z-kiKMIWqZja&mHZa%pmN@?;XlHHupoUspW1_?hA<#orZgEY=-WG4Gx6#|;{o$?k)_KdlrQRa%TW^l{ zx%Z*>mN(8D?LF)b@dkL;dELBLUVZO8&+~H40khF8HFL~IW}F#i2AjU7i)m)67|)!= zHmt@1OvP9{ib1#@?NJ}uNXjAEBuiwLOqQ4AA-PMglS`zTRFiZmvPbPUyVfqW3+xm- z$-ZPq+u`;edyDOBd)RKaqit=Q+J?5atz|3QDz>aW&z7-eZ8$Hm>;ao+jZ~G!a*6bkyX0|sRX&pivPSkuo|Hj- zT!MZWh8OVxzQr0GMiMnlGjpxE!#rswnOSC~`P<~1^4Irc{Ejo6d1r((lmcgFh1u8MVyHH+1a zm5GUe%HQq(?*HIV^WXNL^6&Gn_b>6!_cQ!F?=SCH?{n`BZ-jS~*V(J>i8*Xmm^tP( z^PuT%+L&@CADi$s-oqp4j}EAaLfInU%OrVR`b%f2EJ?fF{$!`vr|s?b3R~A^*nh%x z;ezljr#-F+8;9k?++c6;YcMPLAb2_$7Tgqc30eeIf<$mSbtttiwJbG1^=aze)Jv(S zQun6@r~0S*q^?YLNp(oINVQ1SOVvx&Ow~wLO;t%%OjS-*O;t_RPSr})Pc=z3Pqj~V zOm$E7NcBtImKu^8k$N^YHuY}mf2p~tWvTV4J*l&)FjXO_8*~VI1OtMHf-%9QU{0_k z*b*EMOjs>!5%vfNgpY;e!fD|T;im9d=-X|Fb+-DC4?8L2N_q@Vm( zUX;o5wXBf6a#k`?6CH5{24Of}!X(VWB5cBLrOfHg^(okwjWho;G zffU(%d(xh?2kk+-+wQho?RLAx{$aP;^>&N>({8mJ?Jm2`?y-CAA$#1Ou=%#Yij7Md zsVp_+0%<81%Vl!C^pm^fL3vzWmhm!GX3ID7oBSaM^X{%!sM ze~^Exf18^d;NRr;_51o)``7rF`WO2h{3d==zq()D&+ub@k$1{FL3%jvPV|SH}av3lM!;8TrF*+rX(cK?zHReeEUE9ntjIJYx~>FZA*K;&9cSe z@o;;%CR`ZK4nGLThtGr~!#l!T!ye&f;YDGCuwGa$%nogk49*6}g1y1t!KPqSusT>B z{2Kfe{1Pk-76;!2-vvJeUk5)1-vx_<#lbJZ@?d4KHrNns4YmjSf`5XOK|x@Hcvvp1 z7S;`0h8Kn1!=B+S;oadw;pp(y@V)S}aDMo6xGvlg9uEt`bX(Cjv2E>D_GWvReayaK zC);Uuq5aivwMT4$O-NN~A|0it{73GWQ8HHEmzlCiR>?N`NAe_&@~DTF=!zb=1$X0N zJd1Idj88Eai?AH)u?_oh40$L*!ep9CriMA+G&L>FMW(aqYA!cDOi$Cx^ffn`e&#>s z7Bj%yZf-V%On)=b{KwpE`kOxH26MgJuB+*0I+}}28`H!zGPO;0Q_hqzz6p_!lQ@EX z*o+NWiNy|C)9^kf;AK3AhcOIyqaXUXft=F(CwkPD=NoG*2yrqq>ca=vRd zq_)(RT5^HZmB!Lsnn*jh$7Rw*u90h{m)s(^$~|(wJSfk|Q}VjJAs@)6GDE(WA7!zcXuN_+_!zVCJ$}V{?8IT5L5PH@VrrTerj5D6 z^f0%WTg`CuhtLfGB8hQ=A zCSD7#x!2ZfOfTJoS8VdkX>-Wz zHGi2u&2qEE%rmph$L3x0x_R0>VTPDHOfPed>10}(x~8Iun-q>>AJ$_f7GMS@;WdoH zgBXau=z_MWi;D1(Cx>Ob`*zG3GDTjKXXGKdLvE1n(oUL4RmqlOd(Q5)TkR_Qvz=qV zuu;p}jFI47JD&IxCRbHjPzSK-&;H{qgiakwO09a z6$3E@58+Wfk1==~6Yw!U!c5G@H&}=zSc>2AJO02HY{p*f#C{yaVf=$*IF1uI zrpIv-$8Z!!Z~%v~8~d>pJFy9WVm*Gt3M|87EW!fJ#Vkz2r+5$VVm!v;1&qcM?t4b> zz#Zt1KIn-na4FiOHJYL>YM~;^APYbsIdV>p$YI$f+vE?~AS)cVeJ9_@Oqn5{$tUum zOp2%1iQ`JR?ubD0x(#kcZ?^c~C~k1MZtXA8@Y+-D`vlm&as;jFKnhDS1Yo zl^5kDc|%^4cjO)UKt7UbGE-*BH}bvwEX(D0*&thGryP>Qk|Vjo?}sgqvN#`g(H!m2 z0axK_+=KzR2Sf2FM&TvAhKcwHpJE>7;}Jn&%>|~pY2)JB zmF9ZW+YB%R%-!Z5Gt4|>9x_iltwB7#V#b>{%mnj}nP?`N_ssidviZ=wXQrC>%oOvU z`M^vv@0fSZTV{fJ-EBX{ykJI~$IavB0rOuo#N2KMnZD)*bG5m`bT%DK3)95ZGBr%L z$uJg0IE};Di_KVz{oWaU1)!@%k6LWPrJ$Pwtw3b_Jl34f%PRr zDoGV-AWfu|bdbyBYUv|4%k6Tn^O9%e6?t9Wmk(s7%#?-lgRGL@WsB^PLvmDdrBHwb z%Azu=qduCVH7-I|bamfwdn0baK-`PLcnHHW3Zw8WUcjpuhw+$zcQ6_6V+y9?Gkk(C z@j0er24*-kPRCc6j=7lWmcGO+{10E^3w-9bUN zWVbv?-jgXZNj{bj=NtlH3jR2+yN#x=zPU9#J;{bMHCpKaue#c5I#gABsuP_r|y55_B z@pu)_;3+(e|KdIj#4YHJp12$rqaB)~A!?&C${-ygNjWD+<*;m%t@699k|px3ERgB) zxlES#WSoqZXaA)-21q~YEmz2u(n&hVh0;PANj<48Rh;I}l7z&>T5Ah!p*?HQ*<<#E zJz@{p!*-9|Z}-?ec9-30ci3HayWMHGx%VA*r+e+SyX}6L2^_M=>`{Bhp0v3(#}>Ps zK&&s>k|F0wC8;cRrM5JY3#6^ImW$U_xF5qX+9(JB| zKZfH048#2xf_rffwC% zP1xm8(Oqu>K0E{xNP*dWy$xhiOf5~R~Q#Qz2St~2$cZZ2(@{=r+ zALU0`EDL3!EOKgZft!*8zjf_9`A)uT4pBdqpz2roO;*bq$DNyHvuu~` zvRC#vCOamlNE+fOhq9=I>ZpP9(FhlywbPO9aaqYXc=tvh^mBH&8-p>_ z#kI#V3ZpRwWAG|o!vsulv1ux%;&V)M@obJm;n!G*MOci*Sc;z+m%Q{ct0$Ll0bzu4s=AXpZJ+fI6sw3MhvR!~i5E&-Kj#*)Q8=tNh`5 zgMF|_7RX$gDPPFPGSz7gYVa52Wf|>s#Bg~?hRRU6OYW9i7fWa9EFGnzbdZZ&YbWict#ov)opf}_y+k@n7r8_(ldf{P+rOt=Exn|- z^pP9nX6f&~=ll-Gvky2G_^3QCqvdINL0*z^@`k+WV(}+3)y0ar@|E+RMY7C!#X9*@ z{*tXO+8>moa$L?yjuc2r_`U84q$AsTZxvKW9h{GbXyknRLbP#evIDxH3ob)?k%ozsGAt&Oymo4&#Y;kEd6Y+0R7FjvB1ML2X4WwxXH!CYta)|;qsF2 z^QY2ifu^{?MZy}W;xLwlGIKb_n|VRWSjeCnA>ZFJS-2(WAeB>E>Fpm@|-*^W8^uPTf8pgWP*%$Il}w$zI-Gf z%NO#Q%#!J{K<3HUvQWO0U)-++ESKM$r_eVx${yJ&2OW=*$xg``$(4L5l$3-nmxv=B znaD<2lyjDn{1XHvc>7Z&9Y0j$`08nJ7l-3Z?ML(NA}49_u{Wu(4I#f zkDZY`Ip^@sE23q^DRUp`$Ur8_Biq@wDr%q_>Y@(nqX8PanAQ@l(F*O+)>-IMbaB6) zL2kbW*SMH;18zVc+=4#1%|$b6fx9pWcjGP$#$cx}hTuM@7>LUM;(pxk(D|U#62mY8 zL-8OU#C>=O_u@f^-up__#XYzScj0#2fm?CA!#LH;4d~~7DWjLG<6MdE=;r#BJ5lO%kQ#6mbt3Q z5~noyTN~fVJegCX_gPJzCe!3&`BbLJ$4>RXD<8^xGRa|bl1!AhkvC*)pLwm+nu*Ms-Ze6qADt&BFdv2${`EoTu$jD z(;+PmD?S1d2_$qqn(rcLk(`zSIVI=hjGUHJa?)jRC*+v?Bgf>h%Nh>J5jh}7WWW3) z`{am=Li^;99CYsoV`Jb0sN-QtU7m z3ScD$4+$5=GLVfj$VMe(qdLl?hGVyysExW#Lo`4mG{FUEidJaq;!iuYMMt#5r7mw~ z_UfZn*t=~1HdCiF!=H+N&ndmr@0jc&_c z=#6V#9!!;To%=1CtI-WtyI;z=%tcDZ$PVa)Hm(juY_)LNMPt-+*;pNiF#bMGMVyDS z$VL`25JwyyXoJ96B~NlC$Jy+p9Fh~x#`|QK>~%_OqimNyoN8DnYg{h(n?v0%vP^z< zwd5a5(D#jeC-Yr~JYVL^T$wBLWR}d8StYL-GD~K-8W`(f(;WxRmgzEEX2@)Z*|{>u z?M2=9mCTm~^0j>9Dq7#WD#j1;lPs2>-MN{6uXI_*YFY2p4dcRQ`AfFAjDeblEd96q zBL|#s9hI}L`k_olE+eO>L?k5%1d2>Tg+{22`VL8q<+UA}h$#O0P-WCYCDcR()Id2@ zLpfKkqS`9s(8XUEDvOwlk}>dFH9L!;xa2pD7&VKeND8Dt@}$tk6V@V$G&0<2IWA|N zrsOXv9hTE_SWYBFLDY6^P9JXU z^0}ri9yLd+l4#b}y{HGM3s@oQ;&9jzmpTQ}1(!HQcR4OWcU+2Y=!UND_na=nWvsX?BJYNMrNt>!KY@S5u50@vq^!8IK_S3os)HyR09pPHD2nutHYJ za#!X0RhG$8St`HCQu#@Kl^6%@#PyQ}Zhx?t^kfq6Dtbmc3WKEYq+s^ZYW-|(WnndP$*$Q+pO z#2|AKe#WKqQPa)qwl$q{s^$8q3TikUS3?CG>$pTeb)yfF7-I09)@2-HtS8cVUlmZ+ z-79r5(N-BXQN?*LEyi5AKF)XAvL2eaM#gLIkf(Zrzl=tPp;BN@OGV$t<<)H+A5z`1 zwZlb@k1j+9w08QGxwmf3eVJpmL33P)rtY`qTA~S>qY;{-0UA55t>^I1N*N<$b<}i< zrxGfoimO@kZg^iA?v-%WVEPsRibbTrRji5}?{h?n7N%!Xbsv>~t$+@64>%$Qn0gy^3>0&szE2Rfd0;bx!xO zCcWP6zfsmZJa3YXve9uBGl#!qhs!54XV~T90JHsrvezkJjmfMoGixBq&dO7y*aJs z8u3`k%~x@Hf@`^^vWwvKD~;tm67j_9HzPdj^=w&A#YVlROu(_exI>t6Y{pngT>^ye zK2pN!BQZu^Q)Xc;k|^MPD>ian7I(G;Gpr73_2RVT$Z0v}Y^70&ij(VD+c@iLNAwTY zkjP$nl2?M@JYh{u6(i#|brNgZ;%bh(N~&`*Frx!sabzOlqFJ`HZ&^3ic3ug?sZy?i zimpmo1$9bd8U34HUf<=-RPxji%CTQrYv#d)r`M(!AkxE}N+SpZL_)CXG zEwd|T#l-OiXyi0V-4bQO8gCt^3#o5eGo_YiJgDk={5<#K)zeEdT{NWUu$QPB*-t6) z!Rl+usi!=btFbpuOMZ!Oo|H50SFaRrN1O-mmm{**Rhh_idtBQoyIm!j`LgmJ$5J+x zu-{fUwn9a>Nw&%+SGC$If65m5Q?|;+lBSA}h}|K7%5M2X_Q)Ty*Htk7lD}Pji|@?j z*?4y5U58u-anOy_9&=ZEN{+}Gmpw2(pL5wmzMOTeO-)4)q_Uwm@K+?s2DFGl48~Q7 z6lttOGJ7YFFiz2u%#Zja2K5|~!A6X5Jd3C!c8ERBk&`Mr-KkT~>nGfY+c96JocpgV*IpV+?0JF!) zwnQ0gB1Bq-^G9+U{Zs8)##ut67&B7lI~uL3IhG^Skw;WLR7V3;b1Iz3tBHoFi3X@w zlI2pLbBWr$p+h4vSvsemDppxxrdq9CqUjaSs!(d64l29fF(rR#w#=Byp4L9(XgvFl z5t;tNo?<*?6@>ThxnC;{9MY&Yn7`5s^QFKkEXFQoxK#EhUHivr`qOgIDf%NNzni+h zgzZ>^`djwPZdZ{a23dpHDSKtRo3hHa!(nB&L(NWyPrmPT^@<&`N4Cj6*)ID_@U&a@ zyW^0p_*-|(&$U*~{9UWbyb9*!N9DA;K4uQ8gs6;EC(&m#M`!&*vohXs%IO=j0e|0? z`i8#CT*7y%EZvQ>D&EpcG9+qBvJG_)v-)x+_{egjT11T2E2#R;bE=b>9=2UQv)2giq*9g(Zm z_Zd}q-yA!p#!EVk&=wKea-_(OEfZ_B6nUT6D_vXYo?7H5y0_wuN2LbO73(wt&&k-$ zrl_ZFmC>}~#3)aNp5YJ|bG(*SlC{xOi8)%etcyP!c_Z8NMGc@YRvfB0UQzwd+Hei0 z#hE+TEa9HoZb}}i^{;s@QQK3?tA;0Pm8*Ch=35*;A#3qf&8d>}T>e!RWt~Q>qu`Qx}u1`2<0ls{--fIg&_KU*yzd==y2R3k*ojMQPB(CMRm_CjoMx_=qT>7W_3>Zd{$w}$jF%zwa!`-Gi7E!tnRXY zMW1Kgm*^t~_+$c~Zes127~wn7$X^a8B3UJ7J(>JOjOte0j(Z)DW3HCWIGV|)S?!jE0-lbf|s7oJYHlu&^RC+Dz(DVmp67*}u z8X1LgihfW3r|O|!=!;oB^$%k)`H9S=s!y?(?if!wi~hr9Zq5BTWo1AS#%dhT5WRCu zPqD67!de2KvCv#cL~HsSYbVOsIM3fx=e@CM#-`k@N|~r&T{6O(;)fU`ve;NVQPh?4 zpCT?*!d-j4yS%g;aRn=#@zS5{nCex<=#P}ypZK&6&_o<>+-A$X5+4xis zGaT0cnHjT|%Ni7YoV8+N?WB9=hFnL6AtnyHae|X_&?(JRu2Rf#167^G4H-&bIx<5< zMUCTK)Pv+rHm!r6c6Di1X->PR52$JNdN|cAotXxouTw1)IK9I=Q7)sG(fjBRMK1Pg zCc%1w>M)H!)Ye)jV@|~QM33i_G;FjhGkN76jb$7^CTB;SpJ z>6)m9NZrBwITJpzTqTCQ#W6B!ecx%uC@W*lhTKb*pzo52wQkCB;eT@)8JoPu9;S-a zF>U2@@|jlPH3BhfAc8{2Sh=nuOJuQPM62=n8KN=bFWQmVjgGF7OEJ#MCv!8c(NH=0 zuD%~St)Nj%6(rR#M=5yE%Hp&spQa#BP#Z+Ku_|>wN5B^=QskBLj${<3Zy&}t1tTi^X z->8gqRD*F`6|{~cF$(dC8P)X~;Z(<~RxfgOVOE@!IXI=JWJQlPD)QV3H%g;V|3sSZ zxYL@%AnTZ9BhGPNM`V-}Ij^4*xg4kA6GW;AHS1z!hUd^an2rtcYB;CSfbV>IvsiLU zqBFfnYafikjPa%VhUj9{C7Ur{rRS0@baaZmrWsKJrlg9*yn=XOwyPHAh#L_|FCmVy z9Xo0Lf*DqnZ*h*wGn%q@sGF!s^#~f-i7Q4%E-~6`M$LKpB=a)rFuhXtUkp@e?8T_| zNTtS}ELAG3cJa=M3B^W)7WOs~rwC&-Qhga|OjVjxne+_WPWPaX5V>KAaukO%;+b=N zYE(T-t;=gu{iKxv>T_jga&&Z;)W&KJ+J<)3Q2@mnwK_Ee^J1!XzE>=H%7i+OPZ$s# zW!y-3C70Xr`IAgkbv&q*;<64`#1FB_`c+iXVogh*592%W%>3*B#}+x3FDe=K9%BMu z%*eFQRCA=ee$rT|&k<-eSCv64u{Y?AQT)%5lv9w*j;X?!A@d0~tqyYxl;bh1QR=M~vB zlV>H39LBLD*4aezT*aIB$3_ofVDZUZtR&A4s!;+^+njI8z@ea2V8)0)DEE_9Lq) zYPpVB<9UcNW`{g8M`t4?%PS<#R4Hh+n-+$P`5A6XcB5vXLT41?RBMvd1;m>^KM-L} zvty!-FY=q>f;fwsj+3ySOVm_Crb8g>R9fX#ZsgX)Dc9+~T(23lK7XTGKzSrk4sn+i zka3w)YNX_~Qs+c5gIvZaqA^<)hw3FpANB<;rlU$4xfE~I4IJesv$3MUiX`h&9H(Ss zHpKBy*7tH;E_cE`^TYX4wj)-kHHk(=BvD8_sR|`VxkMeP49PJYj;3;|{K+%v_|3Tz zyc1hGF2*b3)fJVDH1yWF6)gs``jA&Ji_S$7kH4HRLSK@S^&LvLd&O_={L0qN${`!pKZU$71xU z3cV`kIqW~JlBOihyE7BFk0x{X{N@^lNL~m(6;3J2yM!4 zS~1~hfL4yQ_N&z>)|QA5)=+eGU#ls)hS(vJv_h=gM0~|1NJB%#6XKE$>QDczPXeYHEbpq8Ep97Dc=ONPA=*{Fe zPRSn;!?Au&4aaIa8AcTyQ4;YTnUBcRcTTy6W2Ho#YEXWrOh_dMU2VN!fG0pa=f#^-@Da_00Z@l9un_wP6JJ1^#g@`uVC(_sS2WmB1kl7M( zr(U6Wpl1**svh`W8snH_u{XGmyrzxYaE=~E9Pu+|fZUc97|jlOKIURN5<~2>DVr(x zlF_Ie*^fk66gP=9F0*gRF3d$4XBbWY&1=ePjIKnMW^hCmmxw#o>uE811-Dg%@ty3Y zsO5M9kIb|2tgPbFwh^b%wu}g>6f~byPS#A5Ua5A~7)EcUJ^5aWHjP;15%qOGiAIf1 z%xM*hicWE-`Z5|X;X2|-8%GYbDJFHChS<^y7ROk493GQgsu)w0k-zwgA}MheRjTM0 zyneojGWt#$LdW8aiS#i>8SN8gHbtA-mUhl_sH2wDiZ4fA_zXZa0z+SCoTelS)<|kc@_Hns#c<-+E}5> zImpPWbNH@V1fwuxFMW{y$*Mm83ysl+UO;c=^N!>R=CO>xjJ{L?w4_>;2xIofHOfci zH*!>jCSs8&VyvT6X+Fs7aAGaSE`E0 zYf%(eR$~unMVN7mmgeX_=d~`PW7(_(v--r*DApQu#F8w>u}xx3pYu^X5NSk`>O7)_ z^}w?ZJ*;7}k(u;K9v$)G*3pwa+#_NwBAHF~AlphwUfR4ou8L|~N3sRAN;l=-wL z6qmY2^`UYYkHJVtgmI1b8A`@rs1oUu%x#p-$ZO==V%Mlo)T7y>>`jdynx`>#Mj3c% z#b31xd53)+)g_50E-A89+t35ZacVuPPA+p#qK&aiSxnhdH3E^%_^nZ#N+D7U|K_zw zokxsK`>{o=PHWMAnz^b*qnB$PU8_g*ea)0um(mJFgpa6Vp(8MSr~Xr}(rsADV0~Db zQO8D=wTLjSC`Sy&Sjl`l3&zcBMyA)u>t?TM%;5d7Ld7`9D9C%(9@6|-wG?ef8>&WN z^itiWRp6*X!rX^g(`Rt>2{+CYd(@Pw=k!SoqL0rOL^`vS_xQP1;mLpeT$_%45k)#j z&SK_8WGR1XCdTteEXyZZMRHt(W?rG=v-B0-iL#lZOud6X$2(RIpl8x|weG0T3lV3u z7d@OP(0A$rwJ8x1HF|^U9>tRShpIxY8b(~j?R1{+s_VH%5yl?V-qL8#b86Lq9?0qp z`AjS2)QhZyXbwP6*70ga4*EFprqQI7)3`pWk`N)Z2yqzoIj0fwl*5Rs2$iM$rCOBx zDza1~(4IUi&r5xvSE8JzBPWa$nzv}3Ac|s~Y937f<{WLJmZPes8X$*|;fS~>N8%F% z^na}~=|~Ha!kUv-D|DoQxFNQv3dx40e8~TqB`-2>CRVv8KPA323sap!b)qcB^D`6B z{?;lPl?kt!{T0=#=r@WnWpi4DwxO+PCH4a&T%;7p5L$QQ9P>3Y1}l-wg2->o;*?uh zD^WBsn^Sa<>$pxiPoI*D7?9Iagpmui%A)IuSuz&Yp*Gd4`h*zIMZO}Rv2mX7%;%$O z8LwNpjQ2o~p&!w+)B}0f>W`6+(l%rb`md_U2y2?B={vo@lzlj_u~>bC??jMD2CQV3 zRI%Al+=`#-c8V$X47cT{QA8xdRkO0!*kkn1(g;ax(O>De{9}ZOyqcb`5rlK>UB*%J zI2njqkoT|3PqQo9h)kszqdH`4BD3ie!L*(FgYKtlQIEwlFj^6_isuMv%4rdo=@s%m zILANQnE#WMKS+OEd^J1-5>KLciE4W74iRdAQ zh{R|O=M`0@qb$t9h+vL1Xl_k~NTlhR83h<4+3Q4?_9J5jGYs~p_7?k1b2Q?O=+X)i zW4t0wa~V|zTA^Zei+F;ya6OtW;=Jduy+lR;XE(|AkYwea^M)F=l_n5i>BJ ziA#DOMH(^4>xiyMual~TSIYV#{g#oFcfuZJuEu*~Tu}|FxMM!XPjyLgq7jI8Dpi@( zgF2e8xTH@+`;qgM7g=*5tI=;XdPQRlRN>LJL}=qxtCFHsHGa@HBi`0HqbiJuqb{cg zr_I<@kJHObD@&|P5CdAD;5>Ox*D78(MyZ(7DzM^+7$VA|sxWhD?#E@%p^VEso}w&z zUO^QL`&n_up44$^M#QLk#$IC&s`lVLGs5tXK1bWq=GqsuCauT_r&V8KPDj|HXGe93 z+LHW6d~uE{E_99mGok!LRmsnYPQ@KLjtEi}N>1dK+Vm5Cs?Q}5Z~Tmy)|$O$jL}mI z%DK@E zDCMIFdqkolPdQAHrikM)w8BEqVjpqjMUh1GMLZUHB(IWZBp0xD!)%h*$NOOKX6EHepTyDHRdj5koT%azD8p*bpw%(<2VnQp%%F`eCf5{w2)4pd6qA#=O zIptc~jeel{EiI`Cqh(pI(2+^Lit0?ND(U??(#ZUn z2vKAai$oWf$Y}Zm0Fk0=i6l1Nj&o!@?#&~zLaZZVieb&YiR)67MUgY=RrVSCP9sGW zA4|1;R12XE*vS5jNva-oG(?f6c@0NlSc%jTOsz7|%T-g-?}-LhAy|Jr=p{4RE*yFr-)ZS)j!p`e2;usrxBkhXVc%6V|e#mN7N8Mz8kUUbN0$#v>aa% z+B7eVtVS!T{kU(0Gp?m2weC^+Gzia0%Tl@0wyG2~MnqT4^~!0iI&oetOlxpHYP24G zn%=DVQ(Zv3k#op0Y`Rp+cEpupk$gtZD`i3M%bZWgIJu{Os>kAzRzA50^}I3|_ag53 zQa!>xj3TdIJ>v-bigB5}rqPghPTr^O7=!p1X>a{c2h|Rx6$NHJS|`^LPGX8pbEBwc zqIr|%Z^Vi664xq{_)HWziHyYBimoNDxD6{UT%#OWntk!;WGg+Va#@5ky&Ar=@h<+2 zGu|2h4~F+dZ=iqhPjRMr(@|gAiQ19AQ(AE-gnEqyT+3_a6|#?sCwdKgkshJ3Q2mSk7THCuL%VABtg1o1l!&AK`HJ!% z>NI`cjjTd8BL=i)Lbl-?xrh9uTt{?iQzWVOBX+nok;W}m3r2MfWk}XKB19{PQN`+* z4?m$-D4&($Ox2J2P;^(SY50l8VJ@@RBdaj#sC}r`hyabZ>iLQYwJI&lW%UY1Hf1Ya zruB(IVuc>WCG{A^Ap3}FDf+aux44$)RQ94zavP#Q>bFuq7{h(ETJrCn<~nZ4PnDgN|MtGC8woqr-&l96g9-8W_6ll5kuNIA|}Fb zQYZHK;=X!RvXwqHpywg0@!Z5xbdB0e8YzgZsIRyt;xqnfcB;Oo@rv)X6D_PNf>ZJX zxs2L@C}aMkqb&4wt&|X1WIOslJ)USF%DAMBI3beAOI)WKP@f2ovaKi!RE~^}p-%zv zOcAnpMbWq_&#EI5ye7R;RzafcRu3Wmct=rg#y|QBy;tjXkyr4KYZZH%5%I6ol2QIn zABdjQqGu>R=oQLgd{;lwT&@&XjC)$4iWDk8Q^r!iqbKTF*<%sLcxBv|+eE&q)f=v1 ze=1}1Q}y>!WR)%}MzpW_x%RoDhFDS7ig3q2dI9@heMM2Gdn$G#i961i6rF} zMqc^`m(@RLeOj37ITx+hd0L!(!5GcCXiIusbkx!*Kjjth%A%N0R4Ll1!PHxM-P|rx zj`Zh$_iA*Xs@XKE(C?WyDPys}i5k9W9qs>8p5i`gJRq+~>S-G`p8TOF&H<3omN4=$yMSDZ-K$|GuX!(Cf zDve_5v*ZL?nLbQD<1*)n4P_AJIU*uz#3Xehxr>}riYnrR+@$~Y#yym|II^sKsG5{} zs5;~}%ALfzqMAo$i^d=nSE?Hsd&yjiFW!SjXWpNBi26q4E&uiedM*DLr}!Q*3++U- psh@D3FSWQvVf7sP0@vy~jdbi`Rqpf=_85<GvP@Y={x zH{D6@v3`yGiv`$#h@kqxzl4N_P76B~zOv}D@Y6*SgF6IJ_;Je4_?zFse{>dc@u4^CYB zA$4Q$i`6z)n)C^1j4#}0%(>VUY&b8~BZ`pLYsMECA(U1YoEl&;o8v5+{x)rkh8r=845pp~4j=kGI zqX%1pZ-+*PEcCy`?z|5Qqy3ME#0T!s(d>{1(aA*~mM!?oZ;i`c7@7ND-UZk8(3N37 zyRT%%CYMXi?l1UC!o3d<@e9q zQLxYEJ52)=80vT3G&TdBhR$`m*unOor&VDEPox>>_j^EWz)AON<#}eKBuA10Kblb3O*6EA}}i8h00c!Heg@itAW{mDNZMI z+?;o&yS{bj`{npw_h0Nc#$#x>A2&-IaewBKI8cz3qb)V!`dBDGaDoFVQh{-*=R1>Oxv@jL2T z?o4%#IOALk+`awE`9-^>@4D&Q>>BHu<=Wz!?E21$GVxrnGi^I=E5S*19d)@~ zHJnkVxM^;FGQmzu=Ur#9Q{MHPYm2MC>kX%)*{-IlqGkHYJalqgmE3Jz3FaNm;xGCs zQ~k^)bHzL{1?IZ>%AC;)iYG|R^@WLX?m3fOf4eHV$~jfdJPlK<{!pNqY0jF9=8#!y zI+NSz->GIMdr4*K4{&Jh`mVWaBvH9BbB_ie`!`E1y)J(Nr$M)x?@Urm8{n6s;(AR|C!D1x``Pq)s})Xg=ov4HT=s3e-9l z@GA$oO_*NSY9*@alBbl_t7<4eJ*2og ztDUN=p62SJ;x$~&R6RGB2rg< zr>)wpWEqpA3tFcyR7tl;z(YedQhW8(SL&wcmBLZ3afIV!5TWv_qduCb?=?hk=w-Fg z0`1pw{h%-Ox!zC}HBeKP)iGxACug`vF}GaF+wJgsdH8k!`h(3xuf z=CmD2Lv!2d?Rwd{#5K<FVLZVn? zJK3H#+n!*w(sW0$%EnL4)mJ-J#-uAsgQm03*@WwL}pbmS!>h-M0(shO#3YRlRw z_O=~O0tZ>kC^izIW@;+S29|JzHu^-T2`0{_*>7p1F^bS6>eHQ#+~GH-*-t$Ah5vXu z@x8)LzSgS0swk5!Odx_38$bvr>>V4+t2(M_isO{6Xx(<0^`{%Jk!`DSmSu9AmGbZ{ zMObYYS{E~TNPup0o&Ok00~*qaxs0Vc4{SI~eRwa9R;;wOYz^Dj&arE3x_yUjPHxo3yl9(Ib|XZPCfHp0GXUnHAA9by#qC`&6Avw^l;wP$S*(Y($=im8R- ziMNHGgPx}LuIW9+{+lHsgmve!D=Id+0QU_a#vC3V&} zudDVC`;RTh7=};>KSFtt*Lj!siROm=+lH~8B)t0n4O97>WRmI1IvZ;@*ev_drq~Qy zi!QXsaMs4zt@fOa=5s!!8F@CvHenLud5=$6%0d2MFdb<`5w6*M+nDL><}AawW{=ty z{KGE#QHo@{*Y35Mwl(ilon(8`x~V}eUZM#-8OdjKr3%^hmc43A@il8$z);%Jg64eB zW1@7HUOctO>>2B(Jwxb8H0SMX+t*I9Nwx()ahgY5VI^az%>_Hw&ahc_204W27B{%c z9=>HRPwA#E%Hx$7~deWK?nanZ{aE}z?7)=LW#9(bDdhiV!xl1vX_1Vh(?BP6*a4DD5Y~V9G(whDZ zW)LI!fdvevF>P2ygnH<0-Ju`Blwm0G9AP_4SwaF)dQEPv=NC>9t?nABjw)m|UlU6w zDw1VS+V5>oJKW~k1zhGCMHEjXhA}}U%wo+T&epaMJ$>v1nrMWQ)ZH{QD^$oL#&d(& zD$rTQs1KX$2ezyI$i8QL+r2i10d%7?^~tj*Y@V&nYkbbXOy?h);<@HIWFytW{AXfJ zd#zyaARHS%nRt->!f)@YT|APlAG#0<9b6c4*uNCuy& zn-&qkK0C+Gv!N`dkYh9^$=S)j(C&TH26f0~o}1ZgPc7WFb8z znS2g0o!-Q7%}%mE*%yc>S|c?{#q}lC=*Z81|7y z3Rn1#`ShVHJ$Z`{_?FcypbI5=Z2fqPH6)S9Dn`wG{gvl+!C zcJP=WC9;T~)Z{r{VH`Iot8zYT-JJmpV=Bv8$1k+Qjp)n-zGXOFD9vN*Vk(hpt1|kP zkNAw0TqOg^Q6BNkXC|M~k5T-m&BFm+?9&GB6GMA)JBjD3#h ziL>==foF*A!DKZy_043ZlE4E}IKpCnL z2<9yg(^?bsv7+VS1Y20gEZSr3Uv{b;XE$0mEt$)8qSQlUHA1y@pH)m`FtJQw5Iu=y z6PL-NhQ{lr!c4fipw2qVI6h(?d%4I?dSTdaT?}F~-xEeZ8*D4s!}e>=vXYXlv+Hde zUR7n&%Uo46g{i&XV!9qGHRp-3REH&c|epJsDf^AgVOp)qt#cf6{ZK=B8Lb?DxLLw%!f>1E6F^? zQa~ndMXH)&G+2M?qOR*74Oct0(uew7W7S4?Sk7;p#G8$}$}X-Dt&Xastu&`9-I>M$ zCh!HVX~$q@@)udu)=9nLY zC3%xY+~rLjRDmAqH&xeVma~#8R9AQPR1>AKi80I|foM(Fuj;Qt)-#fK=*9w0bB_?k zsIgkAzcwgQo77o>+QT{ylZi{I>>+__S}e!xP!8>IXv-;DsE&T(vR!DK@wQy1r1?{! z`h&j+)oeY|O%0dVa^5FMaE(jc1|k(8f8Ay$)A@p7{J=hpH$u~5wa{_KFo@pF<^eTT zL4L|1R9!SfOVme&>?4KFTB9wRrx6;g5gMou^}gOzluqz7Q;B5~bJ)QlwsM>jYN|K& zw%Vz*4)Yf)*v2UeXs5-xpfmbiT~%C#lvO=-(Q7KLrxXybk}9X?)l9F-Xen<~g%12l z3AIsUl~g1{l*mE`(}kW)=TFXane!at zAcxsa9JzGTBJI*a9n%3V(EIY&0p_xSB%V`QJ>wKRIlxu!aG%HIkdGrs=LmnXo_*xg zKpj^Vj0I=*0Y51yu)j> zVK~b;%40G}U@5D4#4Bo{MyjQXGD^pbo{~7uUUspT)vRD4vlvbf`tcLRHr5wXiSy^^7kuO=rQ|_^hA&lZ5BGgmwtB6+6i?000IixHO`tlCv2~cZw z(yOYWZ0>TE6CCF#+gZ+1mb02!{75|cKng4PiZ7VU8h&R2UFgJMzGg5DC`u%sai6|g zsY$A)wG8JQe&jo*vW#`?Ae)bsp%9az&6=;78m-nkMm_f11lyKhS;EJ3W*X14L3Cp%%UQ@MCbNy_^r61dbahlB-SFd?&9@J&$5x^#?dZ!oN~oWv=o5wOJL*xF zeyrdMPkG2m{$M6^nau=x^9J3S%YL@8jq&v16)Mw^x9Gz_hB1*@{J{6jU@Suz#!zPS z3qLZLHZ-IueVNaH+~W`n=*25EU<9WqqbhPKflVAHnOpo#PhzObhb-hD{$vibnZ{%W z(Zv@fJWn)bDdl_fAm+2qrcjkh zFrV=ZVis`(tD33_&aj$!%wjD4Xh$R7W+an{r7tb1PcvH6n{Qdc36i+TA@-9@w9lJ$ zQf;OBXdWa)^;JjyI?rbQU=zDJ%z8#KguzVY6PgiC9lElVdju+zb&TS3=5m5;Qu&u9 zY$1^_h3g@=xy*eZS~|-nR`%|TqTVs zq;ijJT)M+?NNNd3eZua;c)X zG**-KsVZtK@8HHw2iB8CxbCos_j!krtl}tldBP*&S;+==ae-W(VF{GCvQcKoHPqXB zRS`N%9En8fJ&o23&C+81p>c}Qb+(a&zgnqm^0XD2sfuxJ{tys)d@X zi@woD9nfYU0+!Or1j?v{0wiCqHG^kF=q1(md7$fTYJPIEL(-BniiILuLQQAE+IqDm^IFnM@F4msrcYMxo# zPwQe{*}t@MhF_OOKc>>`;^)lzG9S8si+xAd~YW${xG4`HC0s=lUuhr#$I1M1J5baCw1qzNm12PCv{Ub`RNAN@laYd^@_?WmutjvireIo z!($$iOcD=$64XR>^^%GzRACBJQ3c4GA9{?RDyzQlt<@BwG;VR%7YC>FkSjhkdVn+B z;R%nq#Z``To!dO%9?9IrrAlg{HhNt(RZb!Qr#WR6EU!L$tLYxHN%PsjLLPC9Wz1rk zPcoxb-B-marZNhbqbGO>R!y~4D^-%83Mj7XYM|PxpkM_Hf|ZLm3cJBQJh*+j>6P9> zLR4H)ic~TA%cV?maVbPq)m$yrR&^DoG!i+_EpC#;8P0N*$Aqbb%Bhsx$|sHc+~5kS zWd0wQFL8lXa(ul3Iph*3ZxzAIH80>x)Ki~?JSCGn^2jBd zCq7>jpkRe4KyDS_^@g{(#S=1oN4gJlWspQFH}Lpybt#oms61qFjhkF2og6GK1*w=S ztBz`^ii#^xUfH@$5+^y#Db919dtBt4kH>j zkp`-_TC1MVIu&r613p~-D?5B1I82okqnA}t!Sd#XwzG#5q>$%ZWq<!;df}Dy~Qc%TX>F z+~c8d?ioJG&ZhvEB!7WRh2-;$Y@U(h>y7b~pU+zSuR|uw*B|2{AM5L%0gF_Cqa2L# zk^a|F Date: Fri, 1 Sep 2017 14:36:44 +1200 Subject: [PATCH 276/722] Fix action after color picker failure --- scripts/vr-edit/vr-edit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 4ea36412d2..a85c56d0a2 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1002,11 +1002,11 @@ if (color) { colorToolColor = color; ui.doPickColor(colorToolColor); + toolSelected = TOOL_COLOR; + ui.setToolIcon(ui.COLOR_TOOL); } else { Feedback.play(side, Feedback.APPLY_ERROR); } - toolSelected = TOOL_COLOR; - ui.setToolIcon(ui.COLOR_TOOL); } else if (toolSelected === TOOL_PHYSICS) { selection.applyPhysics(physicsToolPhysics); } else if (toolSelected === TOOL_DELETE) { From 296d270098f42a9a6a294b91ef4f55860b540a12 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 2 Sep 2017 19:06:43 +1200 Subject: [PATCH 277/722] Enable entities inside an entity to be manipulated --- scripts/vr-edit/modules/hand.js | 41 ++++++++------ scripts/vr-edit/modules/handles.js | 5 ++ scripts/vr-edit/modules/laser.js | 86 ++++++++++++++---------------- scripts/vr-edit/vr-edit.js | 46 +++++++++++++--- 4 files changed, 109 insertions(+), 69 deletions(-) diff --git a/scripts/vr-edit/modules/hand.js b/scripts/vr-edit/modules/hand.js index 35073173ff..16bd033a74 100644 --- a/scripts/vr-edit/modules/hand.js +++ b/scripts/vr-edit/modules/hand.js @@ -42,6 +42,7 @@ Hand = function (side) { handOrientation, palmPosition, + handleOverlayIDs = [], intersection = {}; if (!this instanceof Hand) { @@ -60,6 +61,10 @@ Hand = function (side) { controllerGrip = Controller.Standard.RightGrip; } + function setHandleOverlays(overlayIDs) { + handleOverlayIDs = overlayIDs; + } + function valid() { return handPose.valid; } @@ -143,25 +148,30 @@ Hand = function (side) { isGripClickedHandled = false; } - // Hand-overlay intersection, if any. + // Hand-overlay intersection, if any handle overlays. overlayID = null; palmPosition = side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); - overlayIDs = Overlays.findOverlays(palmPosition, NEAR_HOVER_RADIUS); - if (overlayIDs.length > 0) { - // Typically, there will be only one overlay; optimize for that case. - overlayID = overlayIDs[0]; - if (overlayIDs.length > 1) { - // Find closest overlay. - overlayDistance = Vec3.length(Vec3.subtract(Overlays.getProperty(overlayID, "position"), palmPosition)); - for (i = 1, length = overlayIDs.length; i < length; i += 1) { - distance = - Vec3.length(Vec3.subtract(Overlays.getProperty(overlayIDs[i], "position"), palmPosition)); - if (distance > overlayDistance) { - overlayID = overlayIDs[i]; - overlayDistance = distance; + if (handleOverlayIDs.length > 0) { + overlayIDs = Overlays.findOverlays(palmPosition, NEAR_HOVER_RADIUS); + if (overlayIDs.length > 0) { + // Typically, there will be only one overlay; optimize for that case. + overlayID = overlayIDs[0]; + if (overlayIDs.length > 1) { + // Find closest overlay. + overlayDistance = Vec3.length(Vec3.subtract(Overlays.getProperty(overlayID, "position"), palmPosition)); + for (i = 1, length = overlayIDs.length; i < length; i += 1) { + distance = + Vec3.length(Vec3.subtract(Overlays.getProperty(overlayIDs[i], "position"), palmPosition)); + if (distance > overlayDistance) { + overlayID = overlayIDs[i]; + overlayDistance = distance; + } } } } + if (handleOverlayIDs.indexOf(overlayID) === -1) { + overlayID = null; + } } // Hand-entity intersection, if any editable, if overlay not intersected. @@ -194,7 +204,7 @@ Hand = function (side) { intersects: overlayID !== null || entityID !== null, overlayID: overlayID, entityID: entityID, - handIntersected: true, + handIntersected: overlayID !== null || entityID !== null, editableEntity: entityID !== null }; } @@ -208,6 +218,7 @@ Hand = function (side) { } return { + setHandleOverlays: setHandleOverlays, valid: valid, position: position, orientation: orientation, diff --git a/scripts/vr-edit/modules/handles.js b/scripts/vr-edit/modules/handles.js index e32918160a..a0279ef9a1 100644 --- a/scripts/vr-edit/modules/handles.js +++ b/scripts/vr-edit/modules/handles.js @@ -109,6 +109,10 @@ Handles = function (side) { return isAxisHandle(overlayID) || isCornerHandle(overlayID); } + function getOverlays() { + return [].concat(cornerHandleOverlays, faceHandleOverlays); + } + function scalingAxis(overlayID) { var axesIndex; if (isCornerHandle(overlayID)) { @@ -340,6 +344,7 @@ Handles = function (side) { return { display: display, + overlays: getOverlays, isHandle: isHandle, scalingAxis: scalingAxis, scalingDirections: scalingDirections, diff --git a/scripts/vr-edit/modules/laser.js b/scripts/vr-edit/modules/laser.js index 991c9173b6..362f2e915d 100644 --- a/scripts/vr-edit/modules/laser.js +++ b/scripts/vr-edit/modules/laser.js @@ -151,64 +151,58 @@ Laser = function (side) { pickRay; if (!isLaserEnabled) { + intersection = {}; return; } - if (!hand.intersection().intersects) { - handPosition = hand.position(); - handOrientation = hand.orientation(); - deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); - pickRay = { - origin: Vec3.sum(handPosition, deltaOrigin), - direction: Quat.getUp(handOrientation), - length: PICK_MAX_DISTANCE - }; + handPosition = hand.position(); + handOrientation = hand.orientation(); + deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); + pickRay = { + origin: Vec3.sum(handPosition, deltaOrigin), + direction: Quat.getUp(handOrientation), + length: PICK_MAX_DISTANCE + }; - if (hand.triggerPressed()) { + if (hand.triggerPressed()) { - // Normal laser operation with trigger. - intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + // Normal laser operation with trigger. + intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + if (!intersection.intersects) { + intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, VISIBLE_ONLY); - if (!intersection.intersects) { - intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, - VISIBLE_ONLY); - intersection.editableEntity = intersection.intersects && Entities.hasEditableRoot(intersection.entityID); - } + intersection.editableEntity = intersection.intersects && Entities.hasEditableRoot(intersection.entityID); + intersection.overlayID = null; + } + intersection.laserIntersected = intersection.intersects; + laserLength = (specifiedLaserLength !== null) + ? specifiedLaserLength + : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); + isLaserOn = true; + display(pickRay.origin, pickRay.direction, laserLength, true, hand.triggerClicked()); + + } else if (uiEntityIDs.length > 0) { + + // Special UI cursor. + intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, uiEntityIDs, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + if (intersection.intersects) { intersection.laserIntersected = true; laserLength = (specifiedLaserLength !== null) ? specifiedLaserLength : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); + if (!isLaserOn) { + // Start laser dot at UI distance. + searchDistance = laserLength; + } isLaserOn = true; - display(pickRay.origin, pickRay.direction, laserLength, true, hand.triggerClicked()); - - } else if (uiEntityIDs.length > 0) { - - // Special UI cursor. - intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, uiEntityIDs, NO_EXCLUDE_IDS, - VISIBLE_ONLY); - if (intersection.intersects) { - intersection.laserIntersected = true; - laserLength = (specifiedLaserLength !== null) - ? specifiedLaserLength - : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); - if (!isLaserOn) { - // Start laser dot at UI distance. - searchDistance = laserLength; - } - isLaserOn = true; - display(pickRay.origin, pickRay.direction, laserLength, false, false); - } else if (isLaserOn) { - isLaserOn = false; - hide(); - } - - } else { - intersection = { intersects: false }; - if (isLaserOn) { - isLaserOn = false; - hide(); - } + display(pickRay.origin, pickRay.direction, laserLength, false, false); + } else if (isLaserOn) { + isLaserOn = false; + hide(); } + } else { intersection = { intersects: false }; if (isLaserOn) { diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index a85c56d0a2..d8773a96c4 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -151,17 +151,33 @@ } function update() { - // Hand update. - hand.update(); - intersection = hand.intersection(); + var laserIntersection, + handIntersection; - // Laser update. - // Displays laser if hand has no intersection and trigger is pressed. + hand.update(); if (hand.valid()) { laser.update(hand); - if (!intersection.intersects) { - intersection = laser.intersection(); + // Use intersections in order to achieve entity manipulation while inside an entity: + // - Use laser overlay intersection if there is one (for UI). + // - Otherwise use hand overlay if there is one (for UI). + // - Otherwise use laser entity intersection if there is one (for entity manipulation). + // Except if hand intersection is for same entity. + // - Otherwise use hand entity intersection if there is one (for entity manipulation). + laserIntersection = laser.intersection(); + if (laserIntersection.intersects && laserIntersection.overlayID !== null) { + intersection = laserIntersection; + } else { + handIntersection = hand.intersection(); + if (handIntersection.intersects && handIntersection.overlayID !== null) { + intersection = handIntersection; + } else if (laserIntersection.intersects && laserIntersection.entityID !== handIntersection.entityID) { + intersection = laserIntersection; + } else { + intersection = handIntersection; + } } + } else { + intersection = {}; } } @@ -407,6 +423,10 @@ return handles.isHandle(overlayID); } + function setHandleOverlays(overlayIDs) { + hand.setHandleOverlays(overlayIDs); + } + function isEditing(aRootEntityID) { // aRootEntityID is an optional parameter. return editorState > EDITOR_HIGHLIGHTING @@ -656,6 +676,9 @@ function enterEditorHighlighting() { selection.select(intersectedEntityID); + if (!intersection.laserIntersected && !isUIVisible) { + laser.disable(); + } if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(rootEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), toolSelected === TOOL_COLOR || toolSelected === TOOL_PICK_COLOR ? selection.intersectedEntityIndex() : null, @@ -676,12 +699,13 @@ } else { highlights.clear(); } - isOtherEditorEditingEntityID = !isOtherEditorEditingEntityID; + isOtherEditorEditingEntityID = otherEditor.isEditing(rootEntityID); } function exitEditorHighlighting() { highlights.clear(); isOtherEditorEditingEntityID = false; + laser.enable(); } function enterEditorGrabbing() { @@ -693,6 +717,7 @@ } if (toolSelected === TOOL_SCALE) { handles.display(rootEntityID, selection.boundingBox(), selection.count() > 1); + otherEditor.setHandleOverlays(handles.overlays()); } startEditing(); wasScaleTool = toolSelected === TOOL_SCALE; @@ -702,14 +727,17 @@ selection.select(intersectedEntityID); if (toolSelected === TOOL_SCALE) { handles.display(rootEntityID, selection.boundingBox(), selection.count() > 1); + otherEditor.setHandleOverlays(handles.overlays()); } else { handles.clear(); + otherEditor.setHandleOverlays([]); } } function exitEditorGrabbing() { finishEditing(); handles.clear(); + otherEditor.setHandleOverlays([]); laser.clearLength(); laser.enable(); } @@ -1170,6 +1198,7 @@ selection.clear(); highlights.clear(); handles.clear(); + otherEditor.setHandleOverlays([]); } function destroy() { @@ -1192,6 +1221,7 @@ hoverHandle: hoverHandle, enableAutoGrab: enableAutoGrab, isHandle: isHandle, + setHandleOverlays: setHandleOverlays, isEditing: isEditing, isScaling: isScaling, intersectedEntityID: getIntersectedEntityID, From 9f9a0b0c58c60c62357a909af106d35a764abe2c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 2 Sep 2017 19:09:36 +1200 Subject: [PATCH 278/722] Hide UI if hand is inside entity that camera is outside of --- scripts/vr-edit/modules/createPalette.js | 28 +++++- scripts/vr-edit/modules/toolsMenu.js | 112 +++++++++++++++++++---- scripts/vr-edit/vr-edit.js | 44 ++++++++- 3 files changed, 163 insertions(+), 21 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index ad76659d04..362551edac 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -23,6 +23,8 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { paletteItemOverlays = [], paletteItemPositions = [], paletteItemHoverOverlays = [], + iconOverlays = [], + staticOverlays = [], LEFT_HAND = 0, @@ -297,6 +299,21 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { return [palettePanelOverlay, paletteHeaderHeadingOverlay, paletteHeaderBarOverlay].concat(paletteItemOverlays); } + function setVisible(visible) { + var i, + length; + + for (i = 0, length = staticOverlays.length; i < length; i += 1) { + Overlays.editOverlay(staticOverlays[i], { visible: visible }); + } + + if (!visible) { + for (i = 0, length = paletteItemHoverOverlays.length; i < length; i += 1) { + Overlays.editOverlay(paletteItemHoverOverlays[i], { visible: false }); + } + } + } + function update(intersectionOverlayID) { var itemIndex, isTriggerClicked, @@ -432,9 +449,13 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.merge(properties, PALETTE_ITEMS[i].icon.properties); properties.parentID = paletteItemHoverOverlays[i]; properties.url = Script.resolvePath(properties.url); - Overlays.addOverlay(PALETTE_ITEM.icon.overlay, properties); + iconOverlays[i] = Overlays.addOverlay(PALETTE_ITEM.icon.overlay, properties); } + // Always-visible overlays. + staticOverlays = [].concat(paletteHeaderHeadingOverlay, paletteHeaderBarOverlay, paletteTitleOverlay, + palettePanelOverlay, paletteItemOverlays, iconOverlays); + isDisplaying = true; } @@ -446,6 +467,8 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { Overlays.deleteOverlay(paletteOriginOverlay); // Automatically deletes all other overlays because they're children. paletteItemOverlays = []; paletteItemHoverOverlays = []; + iconOverlays = []; + staticOverlays = []; isDisplaying = false; } @@ -455,7 +478,8 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { return { setHand: setHand, - entityIDs: getEntityIDs, + entityIDs: getEntityIDs, // TODO: Rename to overlayIDs. + setVisible: setVisible, update: update, display: display, clear: clear, diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 0d208ed09f..e1ce5bbba1 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -31,6 +31,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { menuOverlays = [], menuHoverOverlays = [], + menuLabelOverlays = [], menuEnabled = [], optionsOverlays = [], @@ -39,6 +40,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsOverlaysSublabels = [], // Overlay IDs of sublabels for optionsOverlays. optionsSliderData = [], // Uses same index values as optionsOverlays. optionsColorData = [], // Uses same index values as optionsOverlays. + optionsExtraOverlays = [], optionsEnabled = [], optionsSettings = { // jT^bqCwf9WH1{%AEJcphGKl~ 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

  • `MT{tAR_v7oqpy)krHkhHb+)5*Lv1)X%iF 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

    |^X{9B7EoMgJ`NpmmgXN61 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 z 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

    w zWOT~*=wigjCp^bPT*d|L#vb>CnBv&t?r84ZvPF>tDUsOqQNH3!Ugvopv~Dg>v27<2lasN)V;ddVLsdRFWupA z@z50wDWHzHbktLNPH(tu+b8-=-#K-0B!+W(;xx>}JS@txtj-2($xiIU-#LbVZ~@nH zGxzg2ukmlbX6TQ>7?0_gkLB)qhuG!QxQ3f}h-c0r@XaM2 zgp;TeMdC^vNhnDqk))IqlFFTHAx0yaq;qFm2aeC0{3wb!?=?3IgJyXVyz3? zu!(cemuCSMVtQs~VkTo$#v~l7_Cw$4M}4ZVokr}Y-qY)PL$B!-w|QLGi|+H#S3=oI z_w*nAM_=hHx6^)Cs>dh}(MiU%%*s42Ww5H#jF(H#RY789`;i?IdUaoA-`gs$@kd20rNOh^^68DQs4#_KNB(-xqM|9ZwGrYhx z_sj_C$QEN3#$zCQpe5>|{D1RsYy|L)uX&djd4yY>HgF+?O>u-{j{sL z(I(nJD?1-~Q7xc3G`GWb(`)AcSF8WPMTBblZIm)rm{(-@8LXoYZ15em~ALvcUgW^LsRs^7%aeMn}WSC=2mh`kf

    @mx$7 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 ad*;HrAP%|t@o{r=-&}j+u9wWwSN{jF`jAKf literal 192044 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ⅇ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 literal 48044 zcmX_{b!-+(xQAzU_j7ltx3oAEcPSKi_k%kWcXv3r+ri!C;2c~^TPV^}sJnjdvb!@k zm)zVunaMlJB$LT6?K|&*15J2%duX;uI_ZjX@%41)77}Knt8W8DGT*@HV_1@5dML2V8^! z(1S=&AJq9XLV*YbaRvU3KjQ!J-9J3T?{O9`#|#z$1E>d@fsP;%B!X_B3up&gfchW; z=zsu_Ab{Q2jH_@NF2m)x8k?~fGgt_eAR06R?Z6-~9LxeqU>R5g)`2ZxGuQ|=gVkU) zSO(^TBrp+-2cy71&>M6GtwB>z7sLW1FaQ~l10mr59}*w|Dxd`_5CV)K0>pq=PzN*s zO+a(d4s-+wpcm)^hJYbp7#InLgE3$P7!8Jl5nw17332=Bz(@oKyh&&RXy6#N$+frsM0xF=4; z9dJ9`0yo2raeZ7LN8{Q!97o`A9Ey!N6l-w^*5MFr__GSfVK@dy;MzF$&+dk}1#W`d z<90X!cgMYOUpyQS!{hKIJOwA=d3Xh0g*V}?csD+RkK=Rr3ciUS;>Y*{eusbH3|xTA za5Z*e4~BpO5}*b~5Dn^rCZHwg1iFEKU@#a1#)Fw42`mAt!3MAk>;Xr>DR2Q?2lv1O z@CLj9UqA}@1%87fPzq{*4Y)x7AV5L^6hS3aLjw$h(XbY*0~^AIuqA8;+raj)J?sR# zz%H;G>;Zeg-moX^4|~D>f93(O59|y3!QQY3>KF?fjk<1(}qvFuuwVHPpP^dS0vP!XIJc;x5#`}p>F(>%b_%01b2$oa%k zYR9%XTW9Nd%X0H})2W)P)laIDE7L3T%ZtnGrNI)aL{%JB)VMIAV0iwly!E+9b8cp* z{jT~Y_|+(DitOi{`ed9ZOqq)>B{s~Uy42t|D2ZAKkZxUlvH2Jv6PM} zmQU9|P5;#L6Yo=1@|Wc2$q$q7CBI7ko@`Cled_sX)2Gx=`jjOpzf*dpzEACzmYp{5 zv+T?3FALIJer3OAe7pbs#E;z>J2LnEJe&38SHW*VPD1YLypQ?n!udt%#qCP3mBm$D zu8gmKSu@7$ww$&lI#}mj_X2M-KM1DLm)I3}G;Bvka6~*eze4z1oGN`Uf2@3{zNx*T zzh=A=dM*5NA!3J9!t&X4BbY%0+E#q2q+LpI_+2Kg183|3g1`;24 zU)(dUcSfHj{j>wF3~V|0`H<$rt`Ap^Ts|suboAIc8wGnUV+KPz|E(%GUp`{wZHE}Q#vZiA#bNjH+dCE1d25=!zUWhMQav@WS*l6UT* zxlQLjo6~yEjoISa6K7qWSvo^Lqv`a7X+5SUOsPLvHmP*Nt?_gIGLCyUX7p(5$eAN* zhV>o#Y%n!w?0|#)zV!+Ait5>^`=G?JT_$!O(XoH~=52MY-7VfVJJ@7+V@<=a_14sB z6!$G=MU*DuerQi)u5Px*tK2B3q`O6Y!7i?l+5`!_ig5=P`fA*BoRzkzmg<_>RW;?S zO8rIq3Uqn5v%CB%$Xxv+{M+X*i_;pVlz+VXe*W7wuT?M2&%Zo<`S|L?8~-koW2`o=%;Ctn^PeGDD>_wbxUtqzJ0nD^!H&D>MG+rCS^ ztIN*Se<$xS>{z$GX4~v-?yWnwHr-mh<>Hp4E&aAM+0t-J!z~G0CT`iWC3%Z*>x8Xu zw#IL}zAb)x^7d&v41cHpeSGJfUHx{q+S6ojvweyC#~;{u@W~(lkrPoJyP72jKAO#Zq1*Yj*suCbtR(VCL`Wu+CG>Yk?MmaDd34z9bAceH<9@B;G= zm4G0j&6Vu1u`%UjPy*u8rcKzB8XfvpF z?^az}Hg6HpT-?moy*OJyEVv43&q=f|5_-E21^RNrF!N+B_K-Q5Le8$bnfP2j`(`*2{>Q zT6AJ?dSJi*jnCt4>Ye9#>gKpdxb8b;&Sj2b`%wEATR+=R>tw6bvd_}Yl4IU!PB7D^ z52hWa@upTLjS1CM)#TUwuF0+`uCdl|P2r|Q(@fJz(^r$o+}C`-Tx@P;*=;dd23nt4 z<80?`O8ZH>#&OZn$obki%w>1&amRbIJnOs-e1*O}{(b=_@HV)T?#vKu3VQ&J!g0V3 z-oQP?Uu0tnQ<v43C)~vpQx=%(j?~F)L!G#|(&R79)!(kA4)rCb~;Bih2|^B}x(XZ{(23 z>WEblf{0z=^6;%;ICNHMc1W9$qeizO!EjuktE;7(tUaUorRJ&IsKzS)R$P*&$O@%i ziAWM6juSNzwi2}AcjC3?w&gUV>XFfe4hjIoHEapuTuK zI^=e*)oCd<=a|0Lq*Z^YdRzIf;$!*evLB@dCDp~aNLJLKuuH-8{4II+bF*{AIUTc? z|Ni$^Fsn`0uAjd%n`G|Du>BbLeSe96fBtgj4yO0Kl+pby%gFf zv?gR*NLa{yV}dc&Fw~HtAEy7ROVmBr*3ll*K+Q~bimH)nyRur*S#d;ek+qlYmljAG zN>+&9iA16i!jpnBem#B??=d&X>CD+erIB)S6!8zN0S&-v{2r;$c=j#>nL+fOAQPAr zc<49ym-+I&iQYRNqi2^JxR<*e&UsF^W0`~F*k_Nn|7ROu^H~pC6RcI1Qyw&N^-A8k$hJMB>bRkSu{dd%Y(I;Lx_HMO4Cs;U(l+aY#F?AqAVu~%bX#J-4q8~ZZ$-`ESW zdtw*F4vwuCi)wwYb*R?RTIyP#Vphh~j!BJ9isnV1iE0@2JhDUNn~1g%|At3|9|-e= zjtPAdq6nE`d}QDl`s(-SGPDA156yD*4b?9tp{%1AAfGGSE4?ClBhD6?gjgUIgz@Y0 zT5vmXx>5be!Ne#y8cf6!(F}GvGl!lRoEKQ`U*_B3-QwBlKI}T}yyAFbe`m|ER#|*z ztvSxryJmd#_NptDKPu?*`sJg`_LruVa7%g=?<~qJj4qs4keVNvzcsHUcTn!LoF+L} zvZJ%F{;vP~zh8ZRm1b?qipl!?bLG!QKdUpZWzNlPl_|{5;U2Nq>do=J@m~w<4=$s}GhNyGNQ~_`1KfcJiCJWCDw^Zx zq;t>m=JVSN_`-DI5z#QQTJl}8UD`qBkR6rxQdBE;D;ucZsYa>I>LnVe-K+(=^*V=c zs{Xsawc)sdG4wTFHQJ3GLN2A-Tq##!kix!+t|!!w>xoJ*7XWi__iFw$Z-Qbke+1 zcTnF^HBuc_%9Se>74m`d$FgwQI%$a{L2^#ah(?R<3njv-f|q%HTK=;nTg(=owUKp{b(8g()og8Sn`OIi3);Hb|FPTby&X3l0_S39p0ls(g{y`8 zhP$EXh9}ZhKvm$0z%-EPNF|}i;n7rtJqgO}wiWWwvMs0{{8C4d! zJF;2i_lVgMWW?U^i171ab;2%$hKBAB!Nxhp?}nC!!+MLZm+p|(tZA%St$w9K%1+AV ziaYXhnO4?KI$v^H{6b_E@`a5A{rIzZTe;^r&!``yiNH_|>w)%o2%5$&V|LT0gAW5K z{yd-CEAU2n+Pa6hlAOC7*X^k`vsG?wWf^Dw+w`cWvf5ZZuxfkd+X||pYx(Z7jMAvm zMI|4KBa7D*%sNx-3+Ub_ug<^s z{4V?5A^Su&n>{t>M^4w=$GOe&?&P(|f0Ey=AhTdP=r9Vp7m9;1@ zD?eW`qEcOzRduL(bWMb*%yi$p($dqav6a~#+P6AJI2*cHSEl=lXN`A|uZ}`K)&xv(pFN)7e<^0X<#v}L-`AGt!@SAX{C`SBIJX0c&UXXT> zrOPJEP4eXmTCq~eDw9-Is$uFk>Ux@;8j~hbdqP{SZLM3Qd#l5`R{DAR6Z%xWNv|-} zGb9>@8m1cN8s-}o7-kzL8ipDY40R1cgH8WNe^x(R-(FAZKk0Vry66bqE$wuzNc&7P zMuXKS)UDO2s>v#ca*a}?+^f(kj>)z112TndiEvijE5j;atI2eh2;u z9*;Mjo5E?r*-yF15#$phj93dxK_a+?C3q#uV|%dom=I6VJ7>0IjU&d9ZeL|@WG}Iuu#K|mZAI40)@9bN)(ETH z^3(Fra@MldvdS{UGSxECGQl#_l4RLr*=xCB`Cuut2(3-6DIB=bopr_kp*S@4c_TKi5A$fC49ijp$GGGzPMl z*aTFAcHm|p8*GFPiA-V>S&z!0ws4woi@7^_?f4b^gMu!?8sRZfg1AO}SkhWrBHbWs zApa+S47;C&~sBO5aH|qE4kani_ zho+h4u-d8WrMjSWD7q^4$bZYiWmBZ*C1v7p@o3Qj;RgZ3ugjmn+rfRoDWud?Gjcky z5k3MxFhWtN8=J)Jrk@6L14N*e#XY*U;XXY)> z3(NbGyCSz)ZdJ~moY^_;b9gy<*$=Z1XD`W~nB6bCb9S5T=GkqtJ7o9I9+$l&`#|=+ z?3`>Wr$x@poXa^yIrVbq=Dy5T`Gbh^0M;X6%8x%D>qfOtS+lQR@2W!nO~ZhTbfz@);qS9_T~=YeBs>T>g(2c z@;n#4bA2uR$e$e88yrDLG3Cr{b|Gqo8T=eq_^*AQZVeVia&AZ1RA)tiU zg?&X<(Qa`a$!p0lX_<7X49m93#fqH@O1VjCSI$sB)Yo9ZX&*XYmcpX<~0rFyg8qo?&gy;onY&(o*sALx(iSLlc78|z8^Pu(fq z99>XyX_ji@G|B44YO(sRs)wpTxk5=Pk1L`TcjV3FFJ$dxZ>1fiFC?uc zcg6L^XGLn!P9Y;m5)|?K^I!8C@c!Wnxl1_})IjPrS)cre;1ergH5dxM;MVvKibBU& zDZ7Kg^jbO?ToFWpwE-%y$FK2U@ip;%^p5u0J-a=1J!$T_Zk79`Yql%ImG0c`?Bpb! zuN^xagB{@xkNu7Pl6{4JqCLUh&~C5`?3CSWqitRru=DMDdu@AyeT035{e=Cs-DEd7 z`a3o_UN{&>Tjv&Mnp5vea{cErxaYYu-SM829^g&#=6U=3-uc@3ANZRG?gm-}p9MS7 zsq|2$lv&7<=n!gzU*XZf4tBx1#Cu{g$&%-(www~qDsCk2HSaGzDL5lY5Ecv9i^9b( z#G@o0$sTC~*-P12d9{4ALZ-Z^Y@~Xr>Z<;x9-#TD8K(WN9iV%y>!`o14>fEym<_{> z_l=5>IU#RCw4swiZ-sh8TZOF%yB}5+78>3=d{Owx@aN&b!=2$ogeoF3A||4CL~MjE zLL1==cZPooe-yqwd`5Vya6$Oju;XFF!?a;vLN|mq4$TYM7!n=w&N#~GGHfs?4M+8n z`g6Jv-D#~zyIJEEy%L8cKio<0!#81VTU`^Wo^d%t+No=)x+u18LXqq$>& z{jtq&O|)*eWSgVSOHH3@LTi>+XH+$*I#lVa7+>+Zylwg2vZ%82rTWsdCACVf7k4gB zE}B_H7oINcR9IPXupqI(m;WF?DZhEXKQATkNZ$OsZh7_cgn6FavfLlJ-*QuOQ*wXg z=H}Mq5_$T(mU(0HHssyNE6S7Q56s`5|21D#FtOlH0jF?u;gdpr(dwegqJhP4i<_6+ zDydU?skCm{&9WBdZ_4{ulvFIK6jfcRN~kWb-c(b|^x3q?Y_z0W7FnZhS+))Kb`F!{ ztaG$W=uUMn_q6kxyf=O0{h@)(z}{eY8q<%M`D`4@MSF2q-~pH62!bO2CC5+{=N4xG z*ULS~YtH}5pDqBxL&933hoTPRRB=B^x@3?vMcP&NR8~iRLJk$P6kik#lz%Jplr2^3 zRNquO^+@$;^>?*O(?PRHb6oRYQ>G!c;o7F!-r51$N!kh8Y1&EJ(b~b<&f3OWotD<* zYo2NLYi4U&X@r`u>J#eGYJ)mkwM*4QL2bb)ekA_@PsQ8JrMOEt4r&UOM-Cy=h>pY)*brU;MsNa) z@h(K7^(fyr9 zyUw}Jy3Pv69Y>NQ!69(u+8@}r+Gp4k?e*1(yYFh3UeHBD-j-ST4CJX()Xn?IBB+4U=cdM=0_XBa|7+L8|wv_UhZ}P|a?Q zTQg4kP8*@ysLRzg)o<7T($_L9Fg!5$46Td{jMt4hMrlaXkVzqHLe7T#7xFEnBE%KK zgm6O1P%<zKJPSAP5sNnd(Re!0ksc*LTg(v9l>E7?k zb~bP>bfnld_Bpm!R=stRCC%K}yvyXM8CmnPx+GFjP$ z(l(`^OXik{N^TdAC?<;^6iqLREXprDRXDYzj#!)8e%sdCqa10D zh0ZY72iIbEs3+aC)Eno^^lk7r2^0i&20PG|^bsb3wX!ErS8T-xK^Is6cM^5TbaDVSiD&XrMS%+(Yu8Bwl(!DweI5707zXFUTpy7{y%$ zr5va{tt?U2Q%zA_R^_M^>dxvU^&$0hb*?&~R%+^KT5Gy#hG+(BMr(#^25Ne0T5D=+ zv>KnfSp73;*4U10?Tj62g`%9ld_Jo9O(+F zN_t(=Rgxi|E%u527O6#-gbjpG1ziQn{K5Q8-WXm!cN({xGmB%VW>bE0AsHan5-i*d z1#kz@fx|coUqy}3E4CZ^iRjNQ>YVDF;+)}J;9TL{ z?mXeVxwJGC2)6kFLOV2d)@6ln>?wW5breabFbVt%lFz>%fH57 z?(ZJB8!!Yn1%1K4=xn+N^NMN7-enu0%P1CK#4+G1s10wy#>5k%4f&qzLH(dca*8=K zxK8d$9>L$imkW*w!i1NEwMDl@jl~bdO(hQ`O{Djv^<{UCmtwoqoq?$6I?{`ym@?OR|<&23bioQPQTwP{Jv(7Z(<1 z6{i-b7AF^{7ym3SEp`<1OQK5JmP{yFU2>}=ql8=9s&roI&C<%!hGp~0o|SRShnL?d zhZUnLo>u58*Ho5N_O5zb6<2+%T2!;8CQ!4=_!2ZtO%Tex_ z?-aUDx>~xE-P1h*&k=7E-$&mxKkGjcXc_z(oJ~{AX{IgvgPns2d;&KCAHY~x1J@G@ z@)X&SdQ5fUeBlh_=5R;y@^~Zo+5CQjG(kJzePNVnw}=)^5`PlclkAXGO1emoO3l)a zvTd?#nL$2Tenws-*D3}n)+rt+@)ZJQV`X3ELgg0aCFOl(s`8t%P+6j^R+cHtl{v~E z%D2k<%72twl(UsRl?|0pS**CN*sd6)h*H$ZZ_Ag+o5?-0Te4ZQFxgM(W@&3_k>r4+ zrKCu_SsW)$6U`9uMHht~h53T{0+HYhzd1jRH-=~DuH!1X=QzzcZ>T|35jl@!i9Ljo zxD8vubTA57@H#BRmr--{m7Tyc%s!?elR{6XN&0NCORyraBhWOE>)+&W>M!yg@Fn^f z?``jVZ=AQG*8jsFAJ`VC40I1(3<~KaI+O0eTw$c_2DX|Vhd!Z>_!f=^M}P=!fPOfes3Jy_ zzsbJT2dXpYUrrP5Wo{_%C{MuO%=hr;2=WEPgl~jxMHfUm@p`dEJW}#nQdhc5>W~hV z-IJ;1GvulA5XDl(dxc&(S$R(xPU)0Cc6V);5a@Bd& zIF(wJqFkn|ul%N1t%y{-l24X1vg5LbviH)V(n`q!2`k+(F!AP7}^$N*HLMK}0Y9FMQ&CfNj3wEr%n!O1eI_UfE(qlL6aDvn zA--K+uV;qmm%FF?KUci#qBGoiz@c*Nw2SR~Y*O1nYl!u{rK#n)xxcy4w9F(iU9IU^ zQ&GLEx>@zFsvT7=s%k1vSB|JuR%TTksu)`lSK%msUw*iJetFOGI^}|LTiNfj_hk>t zu9uxDJ5_e9>}1)wvg>7!%Tmg6%iLw+@`mLD$`_aaQ=V4tEpJ#czT#9xW`&`0WaZh) z>dIzSo2oLZ>Q=9>&Z%x*bEw8&Gs5)26lXqShL-u3V#^3?iZ#*p)Yig&#~$yv>1gD< z<811B{ke5?Gz!2WfFO+YWpO#;hKbOChr^!>~>GEXx z2l+GkUHMu09{F1NM0sC%eYsd}mc5gmku8#SmuX}b(p%Cs()LoW^rK{}q^pFKJQgn! z$BGL?yG5NvHNw-vM4?A;RM1^e#^1+p!vD!z$FXKmnd{l;Iqvz_^TlKLNWHDRqr6+Z|9XqON?)RHh3|pS;%n%i;lJaz z`r8E71U?0{!AZdfL5dzl-==xYIOYkXVdt}%Y(sPiInhY`3`c{lzzRmd7qBj|p9m5& z$aJzDb&Zm7)^jR2L%FZGalFI4Aa6SV3%|ACgn%!cD*P^NC^{r^iH3>qi4~F=k`zgZ zbeS|&8ZJwcy^@LLBjgw4mGZ`ld5VV$i=vTon)0MFQwdc~R1;O3R1Z|&RbG`!-9+6> zJz1Tk-lqOreMWs+eOY})eNKHreMr4ey;412JxtwIT}v%cSE@d!PN^2Dda87)O66_k za%DRuQoK?uQ?yi6%FoIN$dT-(Y?zFZo|h&{%Osm6A(H3fe&Q<8GLcwxQdm#;P|!v2 zi9eM8llK>|kUO4R&Y8$5qyD1u$x&n`F_1`wUExd665Pgh@hKF7_Oc>&6GPKWX^khM?R@0uBpV>sD)= zHPf=h5@-2tUTOI)upPV zRhz3;S1qiXUzJofw`y6{imIJehpMhuJ*)azWvvodH>@5~y|ns5_19{qx_QmSnsYT- zH9FG>(=k(lskV8p`Hfj*`OEUJg=?K;ePY$vmf61BTHBA=v3;iFi=(~sqEqGC;Ig@< zxWBsldLDV2dM|oIeFuGF|HeQ6GHYOZup~H|&Y}k}DNGmk1=|eWMltvlmV>_m4Hm)* zIGFfMG$${U8fpz?p@wpvabmcexz*ghyvsZh|1bUMNlysJeCAs3O;v{ifF)ey8+92vCf}&T# zRl;UMtKghqv_L3$&0oNeg@-6iZ^wsxqedXRy-mBgN-X-2?-oD-hZ)0z)H`J^4O1(0##4Gn| zz2V+iZyRqX?W{Pzs2VY#t9w@c*1eQ+d@(_P;_2o7PS*^7N?6< zlF^crl441?biDM0G*c>-CCFCFuE=s^9C;J@X!&~iCHV(=nVe7<6|EHA6yp^$6sr^) z6}uD%6bBTC6nhmr6&n>R6f+cK72Om~6$S;87s_ADPs*3c`^sy{U9u0dgR)Vwy0R+i zZRrAOJ*i1@Rx(y1mAn@(632+sMJq%xqGaKGp;UNZFi2qK@8L)D-|~j>EZogpCHFd~ z4d)XznyMg|kbvAx=!o;M0el2HfE3&xXQ7FxlAXtTnRN`0*-z`~3&94#M}e+^FaFVf zlW&Dj==;ap#QWYe#^ZGFaMy9ac1?64=Q(E=XSHLGqqU>VzT4i(ZnvGajj~B>Y1Ylw zL@URdYT0KQVTrQ1%rDJH%+t+X%pqpKDbMuGbl$YxwAeJ+G}zSL)Xvn*)WlTZ)X3D# z)Y8<&)W+ zKlV=wBnKJ=_XdN((ezt7j`^FhF+C?!5koKuyawTL70iN7h`)(aq8+)9 zEF;@e+o(LM9%mWn9Y@R^#y!O?;@0I&;oaaB^J?)&^AGV;_(0HDFjcTe@SmVkAQ83{ z4i~Nx9v40qW(rM0N~9Cj6SWuh5{(f3C7LOkD_SU8Bw8q%FPbHqCK@doBI+z^C5jR$ zL`YaJ{33iPJSkrWrD$i2!Wsfo_~ZtmEV|;dGC3Lcq4hyybA7J z?mTWQF3owySe*uEHnW`R#%P!l`Vqa29z{2$3A!NoD7ZH`H`p^+Hz)|! z1kwZl22KVx29g3J1HA&x19bz2fHc4l(Efnm;dl70e!Ji0_xrJ*3Mc})K;uB;z<|Ke zz=FW$z=^=az}J8|zz^05_72Vp?hQT&<^~D65j~vVLf@r}XgSlFS;$;rau@~Mja|z= zWF2fAnu5-tTx7sQ@nQT8tHA)UAAAE!I0PPqnNUlNBu)@HL>M`iJWCdk(bOdB992w3 zamI1Za&kC2?nv%l?iVhX*NL}+cb8Yi3*ispZ{q*QFXx8|dJ2{cE(*R00)iM}Z()+~ zknpMSmoOmIi{eE?MN>puM2AGTMK49)L`5Q-C?MjBg<^wPC5{ps#1Y~!u~w`WbH$X{ zDKd+4L@A=Dq6?zEqGh7LM2VtUkx*1FOckCJt{09HHWBiK`GPxwO@bkU2!WITgujD7 zfN$g%@GkRa@fz_Q+h(nc0)<2$s*jW0o-SOc{Na9!|^X&%uqsu0b~NEU+rjDiH8L z^{?`G_EY{8-+te4UyRS>ed68d9p;VndOaUKCq45#-96DBpZmA_uKSQX$vx8D#2w?t zF1zce>z(Va>%429YpZLyYo2R{Yl3TxYpiRyYn*GOYm#e56d$`BDH@Q!^-?)q1q^FLjzh|-MwC9t@;fe8f_pb9^@#cCZzV^Q5zVp69 zpTghEzuEuT@AcOY%nV!zI{aM(WV7B-pXqb_JG zdV?VDgjeI|IDngiMc^8!22pSYmyy@VUnHMuPEDb9Q_rbV zO3G=%8P8e8xx#tRso`+B^|=Y$Y220E3c*A(Tch;;N=G-)W;6+PL`KB0KiTK( zVRj`uitWHgvmCaX`Of^yTx2#g3z;!YPo@PE&Bz(RIO!7l7o9@CrXSHa=u7k|`UHKL zK0xoK_tAUk{qzC)D1Ds1KwqNo(U0l(^k+JkuAqH1#psyYOk1WOGl5yo>|!o4&zYZ$ zl@YRWYy$fiyM{f^K4-JpARCU_p~+|ix`93+7gFMmcs%|a-^9N#jpINcuoRpGAAt>M zVHY?Z9)wR}1r!jiiE+dh;vSJfK(ZFupIk&9Bj1r_q?Bq%4X2h;r>Xy_Vv68Ia}qg| zI2$P)ki`03wxSPV!N<%wt{)g z>|iD^@r;1Ur~jq5(-Y~Iw4AOEz73uXt_TheHVKM@ra)TYYG8L@c3@zjc|a9F{vv;x z|BnB-f2)6vf1Pz;$_C4`E^xgB_^4;-W_ucf}@!jz~ z@;&jr^}X|D`o8*Oo)8(Bu$Ns5wF5mW=J4b_zzNDZf^ zP*bTz)DmhnwUJs!?WQ(Ud#G*H-_#CjBek7cMXjcmP_wDo)EH_E)tl-@wWjJ*F_egRL;gi}C0mlAq=0k~xx_o-4snQBOH3pB63vMaf*>m4S9l+u zf@|PR*b6p?Du_YR9}nvwSPI60E}%A002j{1@9;%@2rtBwZ~|_E)fnPx^aH&@7tvv~ z63s%xP-hg6!jKRJ*(x@N{lxyqUT4p-``NAR3O0$I&JJgXvOU<&Y>Q7+5PM>_9pv)O=dIMa@Nao zkrvfQ?NEO-87)S;&>8d)eL}^^hr~Dzx52~k9J~>q!4GgcuE7|GgQj2*m<~3B)8H}4 z02aW7F|ZvR4i~_k@Dh9pv!Me@i5Q|2F`SrBY$eVS4~cI?1;G#+GLB3j2a&VMRpdeP zJo$u7BeO{d86@RYDAkB+OZB0KQR;+6b%{Db?W49+E2u@(L~1nEgK9_Br$Q((6(mh$ zCi#iHMV=$KlFP_3WM8ro8BRjdLVPEl5$B1m#9U$s(VU1N08s{0;0?GJE`~#3Td0Eo z=79I$G}sCzg5Dqw2!IuT!uRnmybuq<&9DZ0P&Rss&Z1Rl8cIM7kpMZ_AM6wM6uX|C z&h}xOutt_;DwuD~zsz}N3$utB!*pkwF`t{Abpy?PrstS(#LQxrGY6T=%zsP_PS-`OXxq8gX~C(V{m6Y3eUs)@HzYzXJQxTg9y+8 z3;=V%dhidp4N^fK@Bkr>>^l=ZWjYBjOd2N_-~@hyuby*a#0n6M!U1At@l`q=ZzH3Q|ofNhPTurKFtXlYEjQ zS%M|pgq^4%iiv#U2l177MZ6&H5Z8zk#35oUv6`4qOeRJWy@^glBcc|eBm{&Tn&5Bv z89swI;Yqjyu7=mI*0_M%;AJz9+xpm}IA znt;Zj!DtZbi@Kqnr~^twtx+e`8ns33Q7hCQwL=|H0!l#LQE$`}4Ml^|STqSuLi5l( zv=*&Jd(b}g54wVGp#RV-^cDR^rO1hVNQjj<8aKvmaUVPk&%_Jx7Q7Fi$9M26{0--0 zJ7%#QgoAj{5exzo!6L9890upXWAF}SflA;50;q>|U_00Yj)l|UD!3h@f za1QJRE5S4{6m$d)ffi7}hV$`9{0N`I`|vV61rNdPaeb`A1a_l5^cg+*^GdHp3(y!e z0JTC5kOqm7o2_AgvZ?G-_8NPd-N|lb=d)AUQEXqf6Wfff&4#mTmd66t!&sS8CZEY* zzA_(}*UW#+eda!Mjk(I4XU;OGn17g~%n9ZobAma*oL~+!$Cwk$N#-nbmbt`SV{R}H zn1{>@<}H)Vd}Dqxg-jV^XIu=y@>mrc#>TSE**0unb^tq>oyo3bx3UM=bL=hl8T*ON zVM|ya3y}&%qGqTg8j8lE#b^yWh|Zw9=ncw1MaYXV(%@L!5_iYL@eI5S|Ba90>-Y)& zgmZ8e4qzeBfx4h2=mCa;DPSSk2=;>0;0AaCK7tHT1k4}+C@6=aur_Q4+y3!5$G}N2 z2`+$};Cgrv9)PFdS$GrPfKT9K_!ho~Y48*L0l&jcm<@lye3%0Z{*VugU^Xm-*)SXa zhCg5?{0!6Kd-wsqhEL&Bcmv*rXW&J62p)ku;1;+FE`dpK8XO0Q!M?C7Yz>>kS}+PK zp%?<_23AlAvOyYn2Ofdj;4C->c7gR^DVPPugW;e%=n9&EdLRra0Uxl~i7Rmd{*F`e z3;Y1z#Ha9a{5Rf=SKx(sI-Y=s;Q=@icg8JoBU}$h;83i>BFx7KvB-m5$b_m;2`Wap z=r{U_zM>x}6@5X;C=IcP=gX`jW+zxlfJ@8;W98bg3@KU@AZ^8TVVSEW+!;kP=oPvMiB3y}mf4pf8 z2nF>)bI=*|2E)JvFb6CJo4`(R9GnNYz+>mJ&$c6%l!V+x6(KsHr#@%q=KL+?h zyc}=CyYU%(9zVd(@JF11b8#hh;vnV&IS2u9pfP9*x`KXS1egG3gN0x%*a-IhPgmyw zKUY<4|23o+LJ|lhln{E64$`DYkRnnQ3p^1k_JaQ9J+WXxs)$l-PeExa3W7?n(rf57 zA)$p52np$7*8lU}YmO7&Z$5YC&bjT}v-eti?R{=0I;XIGiLTa-x=mB`h@R0)nyI;3 zq_;JwGrTjZvvOyAXHsX)&L*7=I@@$M@9faozOzeb=guCT$@Oox_v!r$?gKjSt$)+a zC;y&%zs^4OFTQv0Ozym^vukJD&UT$GI-7Sk?QGClr?XaPLT5r}h0f^Cpw6Jq5-rvo z&C*MHR!`|6J)k>ugRa-5x=iQmT%Dtz=_Gw$$LVYOl0KoM^Z^~HeYA&m($?Bc>u8e3 zX|#r_qotnX*ZsVw`!PT0d;Blod;iks_@_R_C;0pRwvY8O{))fg&-hb5 z(jWDQ{UIOfLwu0m>;3&6@8i9^rzd+4Pxh{Uw|DjK-ov|ja{Vj($$pRb_TJv#`}trW z>_h!Qf5;#8QU17(_Gjzd-}15kp?~O8eVTvab9}zf^<}=q*Z3OW?%VyaAM|uj_iLW* zh3#W&Xqq9N%tNg7x+jO?Ce~~}Avqxv2&fcBbO!w|Kja&IvoG~ueXf7&pZHXN*WdD&{W*WU?6t4o z?OnX3xAeMR%PV_D5BDH8?LOU|+MUvUsC!rU*6uCcYr5BTFYjK~y`+0l z_rmV)y61I&+5J`btnQiJGrFgDPw$@6J*9hc_r&fg-5+&N?Ea{GQul|oeoFVG?kU~V zx~JBDXLir(p40tx_cz^hyMO3j)V;X-x9+9gE4x>AZ|vUOy{mg?_u=lt-KV=Vx-+`7 zyRUT@cNcaCdyvO^j92$0Z|F_Dop z^rN2U7yP>4^b#+pAsVffG*N5m9okeoXa~Jp@6iD|R3Fxn`lOE5mvxN(Ti?+S^kbc> z({;9fq2KB}ov%OY5?!Ld>vCPKYjnMC)=j!yx9Tq4rF(R@9?(5{Q1|H}-LHobfvD)-*k!osta_Heyel!E1j*gbgE9(i8@}# z>sTGDujtD_7WrpXUpFj?eYae71k)Q+>LBT=D!z{+_?*<9wXI$LTozP(RWs`l(LWuXK*i(|Ni;f7ZqN zhc4AMy0SR(Hr=edbf@mqeR@z2=@C7mC-kJA(o{XIshXju^n#wzbj{FIJzp(THBHZG znx52DJ*p@5upTW--&6U|y}C)a>Uv$TEAivq!Djw_69_{5l%ERh4$U{8P13kzc5At&L8sLGo4Duik^$nYu8-;C`iwrW&*~U`Nyq9K{g1w_Z|nQ|o_?qw>&H4- zC+jCVO+VF7b%uVfpX*GWS=Kwd-hZK=7e3F@Svo^!)?Po=Pjsq&Qhf0vouuP+g1)66 z=vaMQ-_$qs4Sh*pE&u$qKA}(O2pz7E=mR=L2kE`qx8m_0+C|%IM{TZmYGZ9$cd|xd zdc0Q9N*bnNrM?Ggffsv$-z;>`tltBg=IMUIPx~Q1=KFoW@AVzN)Bp0ne6z3f4Zg-# z`6~azfA%$NJGzSMs%&$-kW`VwE_7UV+IFcg_GSKS9eG*t z>OXv~uk!W2(Kq-u-|V}5oA2{Ip5ll6h@bQme%90coM-w)zwXyO&vU)dOTEMcG(f{O zOk*{wEVi1~(j;x9^|hHc(>B^pJL+B9UAt>;3we4%er2ls>Od>&yC* zzNT;JSp7eJSI6o5HQp!aWSvrc{S%$3GxQ6csbA`B{ifLamxaOK>$m!y&MP+moqk{I z=N9MwM(36;_@#bTN1dZH^>dw}pXhX*s*`k*PS6kZ1ARxw>3{0VU)5LiMSWIB>tp() z4%ZR&3rGj)ecHD&rd_mC{W8>++Eg28eXXfAG`@ZZX_SU&ke1WiUgY_H-Ea6s&-7G3 z>&N`4AM{(C;G?!fxlZ3{hR)8f7QqMi~fQ? z@6T5DbCi$r$9#kj_lNxvALbAG{r-Rt^}&^&z2EQiK|at2_#hu#>j(J&AL;{rNa;BC zJ!+jBhMOT5G#4bUJhuVGqAqqT}A zXf;hNP6XW>Yb$M`?etFVtR1wQ_RwzHTkqC>+E4rI03BFpC!W7w|5dT!L;9GG&{2i{ zkCh6>mp)lo{IrhJr}gpr_HiAhPwBt(Ngb&pb(D_K5%rr{*tY$~)gd~t*qUo`&E2)9 zcGYg$xjc7kZL6)dg*MeD+E5#5U9G3pwU$=Zs#--WX+@3J3K~&Z9$3Fs28S*50>4#= zp5f3xvDfL@?op15AzR}kd;;;8L zzOJ{hc9n1N)xN>k`UYR;8|!tGZz)gX95BZ1zR$P&LEr6%eZL>|Lw>Si@-u$g&v~j} z@^gOGFL{<C91xNHM$T+F;>jnQ^` zmv*QuWH0Te_h@hJtNpc~4${H(J7!q#FdeQB>!W44|0=6}QlHkRDrz2G$orf=r_a~x z3;L2ir!VUB^-E`I{j9#AqxCs`Mn~(@`iwr=voW?sM{#zp@?MB73;mksc&1;e-)WoX zspaR7cuM`|**(71cldhW>T7&$Plx@@fAL@ZM_=so{RjWv=lXvOLBH_XKFdEV#Q)T% z`81#GlYOF3@{fFyzvmN6Wudf)`p5gb{+^Hb_xzp8(Z28R_znoljo2V&3mIX=t3_1Qkxzpg&OIe+nm{+loH<-Xik7VqO>H~Lop%lG%kTPY=eV{R|)t@mi3V%+_;xAxP%wY;~ufOWR* zr}t<-?WOmY8knqowMQ}ZWKGtt+Fd)`E#D( zsh;kqJk3w~89!d}^D#f+N9*-);gB&VpX2>OPpR(@)%R?9)KmPpAN1qJ6i@guKUL?M z=Eps~cw~mB`#Hbh=lzmj_DsL#m;Huc^;>@3WQ_B?(2Kpu-O7@%)&LC^S@m#@uHOV) zLE{S5`2Lz&U6Zs<{W{=!Ww*_>v9{1=g_do#jds-bdY5+6PCdKrrCqhR_Uu_~Z|$#r zw7=e`_v+w^^F(^|5L)=q`Ypi^=wQ9S)_Ldm1`n+2MHu)Hy-$afw%bqdt8bAEn3 zaSu(d>!t_WY6rbj+h}WTSvK8N8)-vrpmnvrR@XY3poyhM#%V>3(ehfMY&lHQ5OvFv z3%#hU$S*K5YADbb{G6v3zR9j0t?{4YDZblNd|TB|ZuiZ;rBu?j^}5nm`yc*?ukhvm zTPdnb{1^YJmOuJWzQBJh4R*dS@*jMGFRa|`5B`JCulP?J*&3f0_kJHQK>al z+ois!swC)Uu7TqFXI=Gr|I;`78sFlZe6w%!t-jND`OcnhL%BU%e)Dv(JwA!Apf6wb ztCc;^_FFwa!Ly}q{URcIZcu3&ykv~VRFs>bm5cpX)g-N^wY66H%KANDd51RE#@bBp zDAl)F#q(_{rf*Sv3j%ps2&T<26PTs*1*Mf39Adm7Hqr(lP6nqFKL`&8Ag7*-V>jtIAlm zD9oeZu^CZwhstKQ*G{F2cBs!?w1alhj>QV^(z}XtchPp*xwv9SZKs{;S4LxLexr0- zZB?GJl{V|Cs!e*{!>^L^+obE1Mc33st*+Izs#ewnjVm73cJI%`Cq=xH&sS{ zvv2bCzQNb~x9_N#lZD7I@W%evM# z`G!(!iM;=+tLFOm_-^0l`+UD2=*0v2`m`VQGnFe7lc)N{;(a1B%nyfg*KZc5E%sYp z>P24auEDef_Yc&t@{y4msxca&F=WA+1P1MGk zq>amG*4KtwU+>U5+Fa{YHU#=Nu9{{0d`DH<*RDPIwOsZGvm2BuSWD|`-Lmw$T0`q; zq9*nt5SanLVT-yZyINT*l`cbJVE;tsROWCuKT8fyugdR$aB2J zv%Sc(y}++~enm*UV|HaIGyO*W@-BFL(XZ9-@V@AmDyBZ?mn-L`el@+8nWct^xX+be z&8+>wEEx~K;TwCr=vjWH(EC~;_%*-j*?!Y={ifgY953`+#W@SS#EZPdZ+nU9xjjJS zO3T$<<^D!!xJGD{MpmXVrmCH&ar8KP3!9DCM6IStRRx`>b+x9})4CN6*Dq^r@Ne5} zQh3{}l>6q|L|bSRZKX|X?f~SIiLo%3uw}KNmC;ThmOVDD`Xt9~QdtrB#loBPd}0Hw zr}c_qn2T6j6N{fwSjiNjKjEjUM`*XFD{egJX@1GmOLe^D7t8zJ@GBL2-tgS=RM3p4EcIKaw$Sz4hVQAN z!uo@X^@kSk57)@jbt5b0QC}LZ6-)oDT%JPwUPI%wrpA{oCRQY0RqIqX3;u|s;GJbH ztzBwxO|9Gegq72pJ=lw5(E1#WdgRxoSL?mt61;O|jn_&Qk@*d3Vlo~KnpdbQ@KCLw zp%tBn6_$tAT@lgvwePUIwA55gU09(G9`&}Mr*o++&+**exKUS~UFe?Um&;RL^&6h) z*Zp$k;^<@)&@1)%O7Z7Q)k@1NwFS?bRSE!ZMJ*ADqYKD`(EhpN{IU{yn_u%@T=kBH zg=(r}*z4`emB=6li&-sZEE27`@1a%S2i=UwXpQcT5x=iJL1Q&RD{6JEq}4T{P(HqD z4`kt}9>$+}f^{`f>y>@h)mnx8wY7dlQ{LCFZ(&~Qsl?a~HAx$4t@>PFYZYSG`M084 zv(Ai#Cu)s~!^GlMG_iVu!osV@)$9a11EodHcBDpWsFp9K#V?!>EkEV)7vfMx13Ti+ z88Iq}b1HI?W0S$mDqFo!h=7S__(jj~j6(MeKjUdtLqSpH1-c&dW4)TnBb8k}?g#ut zRaYMM1BH7sE$S^nCk((E*%xc9lg~#JP_qkXprGLo>RMDmqwu2N!ZpuU-)21)7R1oi(B@NaZsOv7#nw#d7G&8dZsW>rbSdIdm_Jj zwq_(A_cTAztB5>S>#)KzwSZCP9-s8HJ-l)h3o{wyZ(MU z_?6Ocv+9`y{0-K}XYiGUp6mJbLS<4JVg$%V_=E3UxfvePsmwFH&us1}4X!aA)l+tG zCnG+p7afCb@D1KTH}CN6ivNxZs6W2({}&J=#%eXK*sFS=oWX6dJH9l2G$T=%`Eueg zwQF#Uy+QJ*%J+#x=*{7^pgx(W8liz2spT}Ra*4r}MRQ*$@5E{RfM-s+g%YqrR>sIM zK6nLHaq7#_a-fVCsxA?nPp}Ts-z>gtYh*UmwcrBq${Je!wbJzX+3a2AfMw3k zJ2^90p5MbWeo6*SWsLZY|AK2WN$v$6rk2nDZ^42l$^WK7=E;bRtVqZE=*pz9ADSi= z2iTEXa#)#~V%Qn0fb(&sg2`~Ak-*{_y;+g0B?DrHoV`}-EgXy0V{dGaPjGgu%{$iQ ziGguFPr|0~4=;L)${tZw{a_gd33q{K?i|$(v*E|&B+O(it+AU|`UXZ`P;rcChIObn z!mqEFNxsshZ-Q@f0%Xg+1lH z*qwjZTlmB8;lSo0WY;X@*r+TdF)kJ65ocaJ61@{D=L4vCyh-M?| zQS)aB%P;cV^?%i5oWM0qxUga;xSn5?#rd97-U7zqN-)|YZPxe}U6fcsJ+kpZV+Ur& z7kjoBdrnpAz;ZGGScNA)$)JO2I46k4kHbjZ%Rs$d>N`x_DRo6Pa8TJR^`zk%q7n7N zVpI)LTO(^>b{lk$)GGBq#)O&SP}r3D{PFcUwx^59&BNDlcGh_hBctk9>eV(P z7-UGRXp|<@mAE3hf&agU$iU}Nt)w9uQ_s*4mxq*ni7CT1P$RUQhF7#%uE);UGSyDT zV^J+28#}_ySa5DtjL0;bm1cWhsU)H{>NWEm=&DzGc@#+J`IT9Isq&^53+ZIzL45G{ zykDv633cZe>lJh|Ki=B1XKF5~yJWtJDkv2h=H1BLW_orl$q`U@=)gBTrxxM`QG*)K z62DPwg-6aWA0hg{V~I(LQB;fGuIdab+cVwlVTby;>>E z%z`3=1=0=^5bse|EU@^fo*D>>VfSdPmMO=!@g(|2=7e2Aba)B97>1^L8^w(-L8TC> zv0#?uCX8A<9aP6sj64=d?JRYYWKw7wE*HFhS;*sAsXBy{We({?bYuCO4+mMP(+H6|PQ14w}(;Z`89i3oB|67x0?+ z4}RJ3%qNwxC~xZByrSg5G&v^>NUk!dr|HP8S)%L6QyF!Tgw|cwS9oT8NJWAcP5Deb z5AvHY@j0wAbfRw*SN1~zz!SujXaf8(S0YawRDL?5K2Z>8&GZdzMwKuAJ-DYh&>qx@ z@c`!RSjaXOl|o6h#vj7*Z+n5Ixt939CI=6RCXPBLhG&Gqb7C!+1?}LTCqwwo z65NAi+L%4#*tR4CBx?rG@JxTimPSL#AfPZ=&eCDFT$;$Aa=7m&#qO`;M+&LqVye$3!mZMI=a?{>q*7yi9 z4-a9aQFeKudfSrdjW)+VV>C#kYu=$L^yIq@-F>_VyU~RGtQc$)lamjEYo3%sK_&wr zTOQQYWI;7tPX3%8M|I-^LrQ;;eG%F4e_h|#>*BkK((o_r#F9E0 z8ob#JRCE8##>2sk3=t0|%(yMDfu*@d7slaau+_H|Y#N@$UdeZsuMCTrjus=PlXHPv z@CsV_Y}*=dv!&JP_)I+>#?Cd9F{2^ysHiM_EVUf^ojXxIU4jmQtrB&(f5riJCQ_3{ zQEi~|u*mlOTF_gJH7tupLK(rRWILdm*q5pXRgR|1iR9$3vkL3MUFN-+|Df{8lLM_b z%DU@on;%E3BUb&#=V3RfC zCyOj`-&B5ihxrFqUcxA~$_vOw?Ziu{!lb$yAIZ}x(Y(a%L4|OT2mbINT3BakFHqXl zdw)G`9GPdalO1P0SK#@tRI*z&o4&v}%u?dNFgaFdp~olG}rlsFkolbytLvH~7KtOW*ClLcLi>Jl!;)SULVo&Ze zmc(PhbvO@PCssvqF`jwBo8eOIl1wQYD)|xdFuEL!Cw@~Sq5er6PYpCtlqwZVp7F>x zW-)@*wieuG&-PArg)_k$T!po;Npbw}a_KHZTApf{R4IyfYF!owd*d zG`~I>Etpf08u}>6NeyRM978JvP;$-(Q z43USvk^i$KZ%Axvs=Hft5bl}#O~wft$U#ykjO`Nj5&_ZUU>X|?C@aMV;nv2r;bF8< zxVax&iL*_)gI|`kprg@f=r_*Bktl&!nKLF+M&X73v1_ZtVpTLD{0&o6^TVpJbQUTj zRDLp(mpBcxQE^~q01Gl^M65h}fC@?#3Fd6tH8m#Uc4iug+hj?gn;3~gNB^d}4C;vD z=;EmLsCK@C`^+!V2D+(cEhr9POIV^m(?NALPd2r=D6Wgf4#SYC1-^%yKeG++)1v=UiCAu$_^ z4k*1GtTzphU84*8cI7NAFa>7?yG=vpN?Z|iqpacY%suye8%~GO@%!j)5F9MLUC)IM zEKUQ#ctF!_$;pzJk*9z;(1;pmo$b9E3B5sGz2RQZL7L zFexm{8rz#TroAz7bQmfg{1ST`cegyLDL1Um()2$$K(5;2Lh3cCg<#KSTUd#ENR1O~ zwtNG-4G{l2t=W)x9rc}@rSJE#7wpM1Dc#<)8PqfK93_>HMDJ8XsDXelaE3NV52My; z%lt(?c_-s-aTdi6Ua4p?H%?uSZ8=BQa$dNDS`0XDRZ|o;2u@UnYlzJF5nP1UNCb(W zfMs|Kuf#)Ih6eX%ZW{iNf53V82T{6dIo6tQ5CN#cfV36`$lF-Q7U5Z{8lVy@rVdSH zNStLG(E*Ra((Dliz=NpOs!*M{g5M<~bNR51%UxeNB z^bz+9&cO-uc^Q>39vA}Ij2pGIj2+{b>LQ=1Ul9xN8BhvV!96YPk!aj18K4xk6ov?X z;gHyf_!7(T9Tf8|x{~+UiOeBuEvLrMlV3JG!@xXUX7?FySuSQ28Bvyueyqm&vUUj) zu~OnGcy4N$mWE+kK{f21y@TzBW6m4q4m)7)R?~#fTMdjT5wB`BKCEmHE%kwV#2>-* zAT9CWvSoBjEE$cJ%sN$(#*TP>tDGffllOpcGAA-qB4YBTe&v#gp1Ck0Coz=_yg$Pc zH1=zttfeLjQd6O3pX>*gS&yDhr4$B%O~?zV$MPH;Nai!~0yM+|o&w1Y`Pdq-0^#Z* z8LQIL>>D+Zt8hlH+OC`^)L5Jzw(1EvWxES9X!5Q>6`x_TfxX%I_(ODUaLtG?E+8EA zWo`iUMS~~PW}hfK3A_fWU_Dxoy&8U7W*h`_T#m*%;n{Eo*$^=~c@Z@`azt9_U+ZId zx1pN+foR2@areGALY#Y9V@SqH(L=5L!%d)U=<~^FS0-cGz zVe4#7o&-;{CG5c&Gdsa?ct(p1txxn1mS&-sWD=;w*b`5Ly_VL3UEA}3ShdxB6RBIB z6WhdE#JI#cYyg753-h}33Q5#8+AH%BiJ8PsVtD2{S{x5j!D*ZM0LAPH6TE4DKWE9m zXFhUN|2(WM_1t2WWCPI*sk^pJ4c0-~1=a0N5(DCy%*ik8Re0MR{gU!;e$A<$pCq#m zCKBfu(=a61&I{JWQ(}#HNxlVzSd4knA+UKv(F zb*H9{)tlC&Z(tT~Vr_7FJe-b31H;LwlTG6ZC^yg$1O-2N28TEuwuk?h)#Pv^TWE#< zoBqjYH>?KD;5*ewFb|%I7ksvbwRW9Ud5Ftbo0O@$hbP9(Z|vDf8ewvl?Ve zsW5|k)~SBBN)-`)j=P>yoRE4bXl^k#dy^Ljzp0!CziAd&(I~5YH+}0YKM`NZwgw4Y~ymL3O5=_ti!SXOx)HYc) zztxm+05SbqR5VQDCd!-mji$jXS}etH`jH$qjT&m>-*693iN$zTAJ2H#vaAtXaYYv1 z**ldV{B?0X0g0vZ7khDUxj!tEXw9920Y(JdwaQr#-DU(pejh;@*S;DG;$k1{97Tn# zT1*cYM`yKJvt(dl?aV6T2Yd(F?d*KRuH*%@f@aq6HP+}^Y#aZ>c37}k5|88#X%8o3 zQ^qKBv#c|+d?sdxHN&|5yeD|(J#~-FMxcW893fe4<}ETa0bsa*7Zszqav95i$GoR4_lzb02CLeqj}@KAb|dJt^P_xN*W zvbaAmjOWMq`GyU!NHP+bFA+S<42pwmkljWei_o6<3BR`Wv;>{(jisoI5Y@34ZK)@F!_$d7(n%#JXni*vzz~koL~qqXca1WkFjK=!E(c4VmB>~JA>Ec`jupPEVnS*Dr7@YGCiuul-Se3P~c#h>vX~X`Fo3ULi$aZ>#-(Y!q!@_&iHtW35 zTihKB^@AW8>ocC;^h1>>2^NbNId zZkjn5ZWXN-bJ?Gqm@|P_uuU7s=IZcCmT(Du<9mFHzO_0ZDloAEABvad{_q>ts4n0W zctw_s89ovXhyU}97YOIG@jq4o+Fi6MF*p7B=8q6}vAQ(jQ0_|8X+6io9tA^R^6*dogv&XV$ z<5=QAyHXHN|5`ujS*!?457E-9Y)5V2fx&g|6$|w-&PcJqZHs$zlvpE{hy}44F)(u& zAUVpZAtrSW5R_R9qU5ruLk)v9(4ENIkQl_%9^?m=!Enop!DfF6n>6f(J2-RlF<5|o zlj+6N;(>Tlyd!Z2eaT%m9LH1gEo0K+Hke41&O5%4{EV??jDs#v)$D+!SZ|okC)SGf zv2-jNJF_072gSiRUXrNKUO8jz%z1d_I`PTYwyfH08721+l@{dz?zv<90RK;wDO|=V z^;2SFqX^j$@A~C~V30ToIO_&;4u1I?+?j zkKv#69t*}oyvKI=jMcDQVjk>9W`ZS}WumtJbKIMyh{;_KEnW>0o6SIYQ_sO>@SSzg z9wy*>A~12hSvH@1&vw>0f3qXsq7m^w`ik{(ml>IOYB)LA4yO4FC&oq@)n&OBw1knF zdrZw3jKb83h(SAw7Zkz*EHD83HM|9niNfrQs^-W~Y>=&MZIq(azIJkp7 z@Flk5MJ)8S#g4>>_!3bE?}-25NB9ibZupL`#HaBO{61sGJ7XJ7)M7x`GV>*jewa1L z!v<}Q&mbO~1))J@^QVT-hE}e{zF0RF?%Ni&Xh_D!oIO6Uti|bl?je|F)&tE(wn^4Z zv<|Yt33rw!7TB76EZB|8Wdt)~{TNCtZTuha;fRLSj5QVkwNXe!V=80~*PuRFZc#bt zjg453jX*5OO{R}kf?htGjkwF)52)>Hk&FrZvE)7Dmx!J9j5m3A-#*y2>EUHFE0i_W zi>A1+Gp|J1V3V2T@ImYdFB7k$;XpM zo}8En&UVfNQ{>8c3rnIit+{^GMDrK!15b>eOe~0h;bZ)9$9OZIkyrr#MWx1XVE3Sz z&x~c_GwWG`DEL2?i6=1T4ae*e%V9ly3~OTL#PS^37=V2^b2}<1=X>_&N_`EM>w;gd z#yfw}+1v@&!P4>mb~nusXa*#ER`GzUP{;ZL@6J*p>zB$C{uy*#!N^O}MCzj1K&>%c0Z+Ho|uoo;Fjm0|K6Q3K(gIaLORu-ZvIafc% z2e-j`d&(D#qxM;nA!q+wIh>L?W_khJaP}4rn$O^8^pJh<7w$cXj?b_?PkD2<_yPO1 z=#|lnw=iya4~Wh9hBNzP&h~f=wgxrq9lT<#X7|M1W;GC=bFfbzwzj8S-yQO0ik_6ZBevL#3ezw(FWusJvf&ESi77@DJkaK1N$ z6KC^c57yGs;yaiId*GFAQS-EM?r;XJY)xCc9`T^{;Xl`!&Zbu&H=fWU0r$ZDaaY`9 z-Pkg9F-C_r{GFJ`XkvdXoN2kntcT;V8-5%=;9I<#OeQFr(b8GW5d?Z#0epzo<=Zx3}FK(#juHwa<0sDo&noV-o zj0?xom)J4vmoWxMtY!4sgFf(%T{7OW1K(*OiZY+ka2P!nTX2@3AFMX+#&TixAeb%L zD_7}XDSgUS(--a~c4uF<#9PzONU$)%v{ON#&YdxD$d2_A(by6MF#hc+gocFJ1Us`| zB6ire#Zv4SEV54`Iq$(eDC32t6V0(9XK2xx^@i+R0}cs0z#h?W!FP_#^|>bf;y$A= zI5Yi@SGN0a_Z(HicJ|1Eha^5TlC2fbiC^T8k;NkYSb%rY8q71=@w9kEtdp3_k`LX!S}XsueoCo!^nX~ z&JwR^+#YWV-Z>&`{IQn3f=&J!8^HZsD{WkptMENXCGy02Y@;XfT>6T?V)y=W+%@+d zRR7<5jD2FAHa;0${usMHBlazpac67L$NBRuEuc9S6_Cw&8@^-Z*fF*b#{1d}i)X>2 z^p|7kMe~{dCv9nmms)JjF|klA+APL+HQr-Pn-ycJjAN|Cn8W9cJ4^6_)mlqKBG@D{ zV9!MESSlzCT7zFAZCcrb{W%Ap$=c&RiNis5t{kP`KVQ2R`EZmrXTjoJpZz#vr9LM*dIyj&;u`OG;R=d;Y19%8y#vKJ!j6M4` zAK(*CZ5o^X826U{aujWN!?JeZxFDCc<~^|vXJlIS*JwRsOM1(>=qD}QeR{_6v2asf$$DtbXf=yxyko&2x?uzxF~&hT zpP&ls^(!1v!`P&cT5Ji9gShDN;FIHN4LW0UwnwLDOO9$s^Nq996P{4WwbP><&vv+n zk-?`L%3JT-9n Date: Sat, 2 Sep 2017 21:23:13 +1200 Subject: [PATCH 284/722] Move hover haptic pulses to dominant hand --- scripts/vr-edit/modules/createPalette.js | 10 ++++------ scripts/vr-edit/modules/toolsMenu.js | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index d682ad335a..8e46783979 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -272,6 +272,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { NONE = -1, highlightedItem = NONE, wasTriggerClicked = false, + otherSide, // References. controlHand; @@ -284,6 +285,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { function setHand(hand) { // Assumes UI is not displaying. side = hand; + otherSide = (side + 1) % 2; controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); controlJointName = side === LEFT_HAND ? "LeftHand" : "RightHand"; paletteLateralOffset = side === LEFT_HAND ? -UIT.dimensions.handLateralOffset : UIT.dimensions.handLateralOffset; @@ -291,10 +293,6 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { setHand(side); - function otherHand(side) { - return (side + 1) % 2; - } - function getOverlayIDs() { return [palettePanelOverlay, paletteHeaderHeadingOverlay, paletteHeaderBarOverlay].concat(paletteItemOverlays); } @@ -334,7 +332,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { // Highlight and raise new item. if (itemIndex !== NONE && highlightedItem !== itemIndex) { - Feedback.play(side, Feedback.HOVER_BUTTON); + Feedback.play(otherSide, Feedback.HOVER_BUTTON); Overlays.editOverlay(paletteItemHoverOverlays[itemIndex], { localPosition: UIT.dimensions.paletteItemButtonHoveredOffset, visible: true @@ -346,7 +344,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { isTriggerClicked = controlHand.triggerClicked(); if (highlightedItem !== NONE && isTriggerClicked && !wasTriggerClicked) { // Create entity. - Feedback.play(otherHand(side), Feedback.CREATE_ENTITY); + Feedback.play(otherSide, Feedback.CREATE_ENTITY); properties = Object.clone(PALETTE_ITEMS[itemIndex].entity); properties.position = Vec3.sum(controlHand.palmPosition(), Vec3.multiplyQbyV(controlHand.orientation(), diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 20bfcefeec..ab5a5f74c1 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -1876,6 +1876,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsHeadingURL, optionsHeadingScale, + otherSide, + // References. controlHand, @@ -1905,6 +1907,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { menuOriginLocalPosition = PANEL_ORIGIN_POSITION_RIGHT_HAND; menuOriginLocalRotation = PANEL_ORIGIN_ROTATION_RIGHT_HAND; } + otherSide = (side + 1) % 2; } setHand(side); @@ -2803,7 +2806,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { doCommand("clearTool"); } else if (!isOptionsHeadingRaised) { // Hover heading. - Feedback.play(side, Feedback.HOVER_BUTTON); + Feedback.play(otherSide, Feedback.HOVER_BUTTON); Overlays.editOverlay(menuHeaderHeadingOverlay, { localPosition: Vec3.sum(MENU_HEADER_HEADING_PROPERTIES.localPosition, MENU_HEADER_HOVER_OFFSET), color: UIT.colors.greenHighlight, @@ -2959,7 +2962,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Hover new item. switch (highlightedElementType) { case "menuButton": - Feedback.play(side, Feedback.HOVER_MENU_ITEM); + Feedback.play(otherSide, Feedback.HOVER_MENU_ITEM); Overlays.editOverlay(menuHoverOverlays[highlightedItem], { localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, MENU_HOVER_DELTA), visible: true @@ -2970,7 +2973,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { break; case "button": if (intersectionEnabled[highlightedItem]) { - Feedback.play(side, Feedback.HOVER_BUTTON); + Feedback.play(otherSide, Feedback.HOVER_BUTTON); localPosition = intersectionItems[highlightedItem].properties.localPosition; Overlays.editOverlay(intersectionOverlays[highlightedItem], { color: intersectionItems[highlightedItem].highlightColor !== undefined @@ -2982,7 +2985,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { break; case "toggleButton": if (intersectionEnabled[highlightedItem]) { - Feedback.play(side, Feedback.HOVER_BUTTON); + Feedback.play(otherSide, Feedback.HOVER_BUTTON); localPosition = intersectionItems[highlightedItem].properties.localPosition; Overlays.editOverlay(intersectionOverlays[highlightedItem], { color: optionsToggles[intersectionItems[highlightedItem].id] @@ -2993,7 +2996,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } break; case "swatch": - Feedback.play(side, Feedback.HOVER_BUTTON); + Feedback.play(otherSide, Feedback.HOVER_BUTTON); localPosition = intersectionItems[highlightedItem].properties.localPosition; if (optionsSettings[intersectionItems[highlightedItem].id].value === "") { // Swatch is empty; highlight it with current color. @@ -3018,14 +3021,14 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { case "barSlider": case "imageSlider": case "colorCircle": - Feedback.play(side, Feedback.HOVER_BUTTON); + Feedback.play(otherSide, Feedback.HOVER_BUTTON); localPosition = intersectionItems[highlightedItem].properties.localPosition; Overlays.editOverlay(intersectionOverlays[highlightedItem], { localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) }); break; case "picklist": - Feedback.play(side, Feedback.HOVER_BUTTON); + Feedback.play(otherSide, Feedback.HOVER_BUTTON); if (!isPicklistOpen) { localPosition = intersectionItems[highlightedItem].properties.localPosition; Overlays.editOverlay(intersectionOverlays[highlightedItem], { @@ -3039,7 +3042,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } break; case "picklistItem": - Feedback.play(side, Feedback.HOVER_BUTTON); + Feedback.play(otherSide, Feedback.HOVER_BUTTON); Overlays.editOverlay(intersectionOverlays[highlightedItem], { color: UIT.colors.greenHighlight }); From 1cbc807016ae1f9645545a2632689fb5b65adace Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 2 Sep 2017 21:23:30 +1200 Subject: [PATCH 285/722] Further haptic and audio tweaks --- scripts/vr-edit/modules/feedback.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/vr-edit/modules/feedback.js b/scripts/vr-edit/modules/feedback.js index 3825efba57..231619f9c3 100644 --- a/scripts/vr-edit/modules/feedback.js +++ b/scripts/vr-edit/modules/feedback.js @@ -25,14 +25,14 @@ Feedback = (function () { ERROR_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/error.wav")), FEEDBACK_PARAMETERS = { - DROP_TOOL: { sound: DROP_SOUND, volume: 0.5, haptic: 0.75 }, + DROP_TOOL: { sound: DROP_SOUND, volume: 0.3, haptic: 0.75 }, DELETE_ENTITY: { sound: DELETE_SOUND, volume: 0.5, haptic: 0.2 }, SELECT_ENTITY: { sound: SELECT_SOUND, volume: 0.2, haptic: 0.1 }, // E.g., Group tool. CLONE_ENTITY: { sound: CLONE_SOUND, volume: 0.2, haptic: 0.1 }, - CREATE_ENTITY: { sound: CREATE_SOUND, volume: 0.5, haptic: 0.2 }, - HOVER_MENU_ITEM: { sound: null, volume: 0, haptic: 0.09 }, // Tools menu. - HOVER_BUTTON: { sound: null, volume: 0, haptic: 0.06 }, // Tools options and Create palette items. - EQUIP_TOOL: { sound: EQUIP_SOUND, volume: 0.5, haptic: 0.6 }, + CREATE_ENTITY: { sound: CREATE_SOUND, volume: 0.4, haptic: 0.2 }, + HOVER_MENU_ITEM: { sound: null, volume: 0, haptic: 0.1 }, // Tools menu. + HOVER_BUTTON: { sound: null, volume: 0, haptic: 0.075 }, // Tools options and Create palette items. + EQUIP_TOOL: { sound: EQUIP_SOUND, volume: 0.3, haptic: 0.6 }, APPLY_PROPERTY: { sound: null, volume: 0, haptic: 0.3 }, APPLY_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 } }, From 2cd6f7fd540cc01c401a883f8884e3ddf97766df Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 4 Sep 2017 12:59:05 +1200 Subject: [PATCH 286/722] Fix cursor not working when change dominant hand --- scripts/vr-edit/vr-edit.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index c5e08e663e..af8fd94bcd 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -233,7 +233,8 @@ getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; - function setHand(side) { + function setHand(newSide) { + side = newSide; toolIcon.setHand(otherHand(side)); toolsMenu.setHand(side); createPalette.setHand(side); From 310750fc0f2534196755a81f3034c146f7143919 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 4 Sep 2017 13:30:51 +1200 Subject: [PATCH 287/722] Close the app when the user changes avatars --- scripts/vr-edit/vr-edit.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index af8fd94bcd..fff9e9b576 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1614,6 +1614,17 @@ } } + function onSkeletonChanged() { + if (isAppActive) { + // Close the app because the new avatar may have different joint numbers meaning that the UI would be attached + // incorrectly. Let the user reopen the app because it can take some time for the new avatar to load. + isAppActive = false; + updateHandControllerGrab(); + button.editProperties({ isActive: false }); + stopApp(); + } + } + function setUp() { updateHandControllerGrab(); @@ -1653,8 +1664,9 @@ // Grouping object. grouping = new Grouping(); - // Settings changes. + // Changes. MyAvatar.dominantHandChanged.connect(onDominantHandChanged); + MyAvatar.skeletonChanged.connect(onSkeletonChanged); // Start main update loop. if (isAppActive) { @@ -1663,16 +1675,20 @@ } function tearDown() { + if (!tablet) { + return; + } + if (updateTimer) { Script.clearTimeout(updateTimer); } + MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged); + MyAvatar.skeletonChanged.disconnect(onSkeletonChanged); + isAppActive = false; updateHandControllerGrab(); - if (!tablet) { - return; - } if (button) { button.clicked.disconnect(onAppButtonClicked); From 61fc32714abdc2e99ecf8f29d74243d4e84199ef Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 6 Sep 2017 12:23:36 +1200 Subject: [PATCH 288/722] Handle domain and permission changes; disable app icon appropriately --- scripts/vr-edit/modules/feedback.js | 20 ++++---- scripts/vr-edit/vr-edit.js | 79 ++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/scripts/vr-edit/modules/feedback.js b/scripts/vr-edit/modules/feedback.js index 231619f9c3..843eebe07b 100644 --- a/scripts/vr-edit/modules/feedback.js +++ b/scripts/vr-edit/modules/feedback.js @@ -25,16 +25,17 @@ Feedback = (function () { ERROR_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/error.wav")), FEEDBACK_PARAMETERS = { - DROP_TOOL: { sound: DROP_SOUND, volume: 0.3, haptic: 0.75 }, - DELETE_ENTITY: { sound: DELETE_SOUND, volume: 0.5, haptic: 0.2 }, - SELECT_ENTITY: { sound: SELECT_SOUND, volume: 0.2, haptic: 0.1 }, // E.g., Group tool. - CLONE_ENTITY: { sound: CLONE_SOUND, volume: 0.2, haptic: 0.1 }, - CREATE_ENTITY: { sound: CREATE_SOUND, volume: 0.4, haptic: 0.2 }, - HOVER_MENU_ITEM: { sound: null, volume: 0, haptic: 0.1 }, // Tools menu. + DROP_TOOL: { sound: DROP_SOUND, volume: 0.3, haptic: 0.75 }, + DELETE_ENTITY: { sound: DELETE_SOUND, volume: 0.5, haptic: 0.2 }, + SELECT_ENTITY: { sound: SELECT_SOUND, volume: 0.2, haptic: 0.1 }, // E.g., Group tool. + CLONE_ENTITY: { sound: CLONE_SOUND, volume: 0.2, haptic: 0.1 }, + CREATE_ENTITY: { sound: CREATE_SOUND, volume: 0.4, haptic: 0.2 }, + HOVER_MENU_ITEM: { sound: null, volume: 0, haptic: 0.1 } , // Tools menu. HOVER_BUTTON: { sound: null, volume: 0, haptic: 0.075 }, // Tools options and Create palette items. - EQUIP_TOOL: { sound: EQUIP_SOUND, volume: 0.3, haptic: 0.6 }, - APPLY_PROPERTY: { sound: null, volume: 0, haptic: 0.3 }, - APPLY_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 } + EQUIP_TOOL: { sound: EQUIP_SOUND, volume: 0.3, haptic: 0.6 }, + APPLY_PROPERTY: { sound: null, volume: 0, haptic: 0.3 }, + APPLY_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 }, + GENERAL_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 } }, VOLUME_MULTPLIER = 0.5, // Resulting volume range should be within 0.0 - 1.0. @@ -67,6 +68,7 @@ Feedback = (function () { EQUIP_TOOL: "EQUIP_TOOL", APPLY_PROPERTY: "APPLY_PROPERTY", APPLY_ERROR: "APPLY_ERROR", + GENERAL_ERROR: "GENERAL_ERROR", play: play }; }()); diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index fff9e9b576..31920d668b 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -15,10 +15,13 @@ var APP_NAME = "VR EDIT", // TODO: App name. APP_ICON_INACTIVE = "icons/tablet-icons/edit-i.svg", // TODO: App icons. APP_ICON_ACTIVE = "icons/tablet-icons/edit-a.svg", + APP_ICON_DISABLED = "icons/tablet-icons/edit-disabled.svg", + ENABLED_CAPTION_COLOR_OVERRIDE = "", + DISABLED_CAPTION_COLOR_OVERRIDE = "#888888", VR_EDIT_SETTING = "io.highfidelity.isVREditing", // Note: This constant is duplicated in utils.js. // Application state - isAppActive = false, + isAppActive, dominantHand, // Tool state @@ -65,6 +68,7 @@ updateTimer = null, tablet, button, + DOMAIN_CHANGED_MESSAGE = "Toolbar-DomainChanged", DEBUG = true; // TODO: Set false. @@ -1569,7 +1573,7 @@ function startApp() { ui.display(); - update(); + update(); // Start main update loop. } function stopApp() { @@ -1584,8 +1588,14 @@ toolSelected = TOOL_NONE; } + function onAppButtonClicked() { // Application tablet/toolbar button clicked. + if (!isAppActive && !(Entities.canRez() || Entities.canRezTmp())) { + Feedback.play(dominantHand, Feedback.GENERAL_ERROR); + return; + } + isAppActive = !isAppActive; updateHandControllerGrab(); button.editProperties({ isActive: isAppActive }); @@ -1597,6 +1607,49 @@ } } + function onDomainChanged() { + // Fires when domain starts or domain changes; does not fire when domain stops. + var hasRezPermissions = Entities.canRez() || Entities.canRezTmp(); + if (isAppActive && !hasRezPermissions) { + isAppActive = false; + updateHandControllerGrab(); + stopApp(); + } + button.editProperties({ + icon: hasRezPermissions ? APP_ICON_INACTIVE : APP_ICON_DISABLED, + captionColorOverride: hasRezPermissions ? ENABLED_CAPTION_COLOR_OVERRIDE : DISABLED_CAPTION_COLOR_OVERRIDE, + isActive: isAppActive + }); + } + + function onCanRezChanged() { + // canRez or canRezTmp changed. + var hasRezPermissions = Entities.canRez() || Entities.canRezTmp(); + if (isAppActive && !hasRezPermissions) { + isAppActive = false; + updateHandControllerGrab(); + stopApp(); + } + button.editProperties({ + icon: hasRezPermissions ? APP_ICON_INACTIVE : APP_ICON_DISABLED, + captionColorOverride: hasRezPermissions ? ENABLED_CAPTION_COLOR_OVERRIDE : DISABLED_CAPTION_COLOR_OVERRIDE, + isActive: isAppActive + }); + } + + function onMessageReceived(channel) { + // Hacky but currently the only way of detecting server stopping or restarting. Also occurs if changing domains. + // TODO: Remove this when Window.domainChanged or other signal is emitted when you disconnect from a domain. + if (channel === DOMAIN_CHANGED_MESSAGE) { + // Happens a little while after server goes away. + if (isAppActive && !location.isConnected) { + // Interface deletes all overlays when domain connection is lost; restart app to work around this. + stopApp(); + startApp(); + } + } + } + function onDominantHandChanged(hand) { dominantHand = hand === "left" ? LEFT_HAND : RIGHT_HAND; @@ -1627,19 +1680,24 @@ function setUp() { - updateHandControllerGrab(); + var hasRezPermissions; tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); if (!tablet) { + App.log("ERROR: Tablet not found! App not started."); return; } - // Settings values. + // Application state. + isAppActive = false; + updateHandControllerGrab(); dominantHand = MyAvatar.getDominantHand() === "left" ? LEFT_HAND : RIGHT_HAND; // Tablet/toolbar button. + hasRezPermissions = Entities.canRez() || Entities.canRezTmp(); button = tablet.addButton({ - icon: APP_ICON_INACTIVE, + icon: hasRezPermissions ? APP_ICON_INACTIVE : APP_ICON_DISABLED, + captionColorOverride: hasRezPermissions ? ENABLED_CAPTION_COLOR_OVERRIDE : DISABLED_CAPTION_COLOR_OVERRIDE, activeIcon: APP_ICON_ACTIVE, text: APP_NAME, isActive: isAppActive @@ -1665,6 +1723,11 @@ grouping = new Grouping(); // Changes. + Window.domainChanged.connect(onDomainChanged); + Entities.canRezChanged.connect(onCanRezChanged); + Entities.canRezTmpChanged.connect(onCanRezChanged); + Messages.subscribe(DOMAIN_CHANGED_MESSAGE); + Messages.messageReceived.connect(onMessageReceived); MyAvatar.dominantHandChanged.connect(onDominantHandChanged); MyAvatar.skeletonChanged.connect(onSkeletonChanged); @@ -1683,13 +1746,17 @@ Script.clearTimeout(updateTimer); } + Window.domainChanged.disconnect(onDomainChanged); + Entities.canRezChanged.disconnect(onCanRezChanged); + Entities.canRezTmpChanged.disconnect(onCanRezChanged); + Messages.messageReceived.disconnect(onMessageReceived); + Messages.unsubscribe(DOMAIN_CHANGED_MESSAGE); MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged); MyAvatar.skeletonChanged.disconnect(onSkeletonChanged); isAppActive = false; updateHandControllerGrab(); - if (button) { button.clicked.disconnect(onAppButtonClicked); tablet.removeButton(button); From 6838461b07d6a121c001add4221f6756acee5744 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 6 Sep 2017 12:44:57 +1200 Subject: [PATCH 289/722] Display notification message when don't have rez permissions --- scripts/system/notifications.js | 13 +++++++++++++ scripts/vr-edit/vr-edit.js | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index b34630f3f8..7b178f53a2 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -85,6 +85,7 @@ var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; var lodTextID = false; + var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications" var NotificationType = { UNKNOWN: 0, @@ -531,6 +532,13 @@ createNotification(wordWrap(msg), NotificationType.UNKNOWN); // Needs a generic notification system for user feedback, thus using this } + function onMessageReceived(channel, message) { + if (channel === NOTIFICATIONS_MESSAGE_CHANNEL) { + message = JSON.parse(message); + createNotification(wordWrap(message.message), message.notificationType); + } + } + function onSnapshotTaken(pathStillSnapshot, notify) { if (notify) { var imageProperties = { @@ -623,6 +631,7 @@ Overlays.deleteOverlay(buttons[notificationIndex]); } Menu.removeMenu(MENU_NAME); + Messages.unsubscribe(NOTIFICATIONS_MESSAGE_CHANNEL); } function menuItemEvent(menuItem) { @@ -665,6 +674,10 @@ Window.notifyEditError = onEditError; Window.notify = onNotify; Tablet.tabletNotification.connect(tabletNotification); + + Messages.subscribe(NOTIFICATIONS_MESSAGE_CHANNEL); + Messages.messageReceived.connect(onMessageReceived); + setup(); }()); // END LOCAL_SCOPE diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 31920d668b..4bb70134b8 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1590,9 +1590,18 @@ function onAppButtonClicked() { + var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications", + EDIT_ERROR = 4, // Per notifications.js. + INSUFFICIENT_PERMISSIONS_ERROR_MSG = + "You do not have the necessary permissions to edit on this domain."; // Same as edit.js. + // Application tablet/toolbar button clicked. if (!isAppActive && !(Entities.canRez() || Entities.canRezTmp())) { Feedback.play(dominantHand, Feedback.GENERAL_ERROR); + Messages.sendLocalMessage(NOTIFICATIONS_MESSAGE_CHANNEL, JSON.stringify({ + message: INSUFFICIENT_PERMISSIONS_ERROR_MSG, + notificationType: EDIT_ERROR + })); return; } From 7b19b39bcef9867977822e014feb86872be3c841 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 6 Sep 2017 18:36:06 +1200 Subject: [PATCH 290/722] Make physics work with multiple entities per HiFi's current capabilities --- scripts/vr-edit/modules/groups.js | 39 +++++++++++++++++++++++----- scripts/vr-edit/modules/selection.js | 26 +++++++++++++++---- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/scripts/vr-edit/modules/groups.js b/scripts/vr-edit/modules/groups.js index 8cfb2b971e..fd78b1ef76 100644 --- a/scripts/vr-edit/modules/groups.js +++ b/scripts/vr-edit/modules/groups.js @@ -81,13 +81,27 @@ Groups = function () { function group() { // Groups all selections into one. - var rootID, + var DYNAMIC_AND_COLLISIONLESS = { dynamic: true, collisionless: true }, + rootID, i, - count; + lengthI, + j, + lengthJ; + + // If the first group has physics (i.e., root entity is dynamic) make all entities in child groups dynamic and + // collisionless. (Don't need to worry about other groups physics properties because only those of the the first entity + // in the linkset are used by High Fidelity.) See Selection.applyPhysics(). + if (selections[0][0].dynamic) { + for (i = 1, lengthI = selections.length; i < lengthI; i += 1) { + for (j = 0, lengthJ = selections[i].length; j < lengthJ; j += 1) { + Entities.editEntity(selections[i][j].id, DYNAMIC_AND_COLLISIONLESS); + } + } + } // Make the first entity in the first group the root and link the first entities of all other groups to it. rootID = rootEntityIDs[0]; - for (i = 1, count = rootEntityIDs.length; i < count; i += 1) { + for (i = 1, lengthI = rootEntityIDs.length; i < lengthI; i += 1) { Entities.editEntity(rootEntityIDs[i], { parentID: rootID }); @@ -95,7 +109,7 @@ Groups = function () { // Update selection. rootEntityIDs.splice(1, rootEntityIDs.length - 1); - for (i = 1, count = selections.length; i < count; i += 1) { + for (i = 1, lengthI = selections.length; i < lengthI; i += 1) { selections[i][0].parentID = rootID; selections[0] = selections[0].concat(selections[i]); } @@ -114,8 +128,11 @@ Groups = function () { hasSoloChildren = false, hasGroupChildren = false, isUngroupAll, + NONDYNAMIC_AND_NONCOLLISIONLESS = { dynamic: false, collisionless: false }, i, - count; + lengthI, + j, + lengthJ; function updateGroupInformation() { var childrenIndexesLength = childrenIndexes.length; @@ -141,7 +158,7 @@ Groups = function () { // Compile information on immediate children. rootID = rootEntityIDs[0]; - for (i = 1, count = selections[0].length; i < count; i += 1) { + for (i = 1, lengthI = selections[0].length; i < lengthI; i += 1) { if (selections[0][i].parentID === rootID) { childrenIDs.push(selections[0][i].id); childrenIndexes.push(i); @@ -163,6 +180,16 @@ Groups = function () { selections.push(selections[0].splice(childrenIndexes[i], childrenIndexes[i + 1] - childrenIndexes[i])); } } + + // If root group has physics, reset child groups to defaults for dynamic and collisionless. See + // Selection.applyPhysics(). + if (selections[0][0].dynamic) { + for (i = 1, lengthI = selections.length; i < lengthI; i += 1) { + for (j = 0, lengthJ = selections[i].length; j < lengthJ; j += 1) { + Entities.editEntity(selections[i][j].id, NONDYNAMIC_AND_NONCOLLISIONLESS); + } + } + } } function clear() { diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 578d603945..f3ce53d3e0 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -224,11 +224,11 @@ Selection = function (side) { } function finishEditing() { - var count, - i; + var i; // Restore entity set's physics. - for (i = 0, count = selection.length; i < count; i += 1) { + // Note: Need to apply children-first in order to avoid children's relative positions sometimes drifting. + for (i = selection.length - 1; i >= 0; i -= 1) { Entities.editEntity(selection[i].id, { dynamic: selection[i].dynamic, collisionless: selection[i].collisionless @@ -424,9 +424,25 @@ Selection = function (side) { } function applyPhysics(physicsProperties) { - // Apply physics to just the root entity. - var properties; + // Regarding trees of entities, when physics is to be enabled the physics engine currently: + // - Only works with physics applied to the root entity; i.e., child entities are ignored for collisions. + // - Requires child entities to be dynamic if the root entity is dynamic, otherwise child entities can drift. + // - Requires child entities to be collisionless, otherwise the entity tree can become self-propelled. + // See also: Groups.group() and ungroup(). + var properties, + i, + length; + // Make children cater to physicsProperties. + properties = { + dynamic: physicsProperties.dynamic, + collisionless: physicsProperties.dynamic || physicsProperties.collisionless + }; + for (i = 1, length = selection.length; i < length; i += 1) { + Entities.editEntity(selection[i].id, properties); + } + + // Set root per physicsProperties. properties = Object.clone(physicsProperties); properties.userData = updatePhysicsUserData(selection[intersectedEntityIndex].userData, physicsProperties.userData); Entities.editEntity(rootEntityID, properties); From 6c35a008f16a0b359acb5166ededbff84a6d972d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 6 Sep 2017 22:32:22 +1200 Subject: [PATCH 291/722] Fix scaling entities that have physics --- scripts/vr-edit/modules/selection.js | 46 ++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index f3ce53d3e0..8befc6416b 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -21,7 +21,11 @@ Selection = function (side) { rootEntityID = null, rootPosition, rootOrientation, + scaleFactor, + scaleRotation, scaleCenter, + scalePosition, + scaleOrientation, scaleRootOffset, scaleRootOrientation, ENTITY_TYPE = "entity", @@ -261,7 +265,6 @@ Selection = function (side) { function startDirectScaling(center) { // Save initial position and orientation so that can scale relative to these without accumulating float errors. - scaleCenter = center; scaleRootOffset = Vec3.subtract(rootPosition, center); scaleRootOrientation = rootOrientation; } @@ -287,10 +290,29 @@ Selection = function (side) { localPosition: Vec3.multiply(factor, selection[i].localPosition) }); } + + // Save most recent scale parameters. + scaleFactor = factor; + scaleRotation = rotation; + scaleCenter = center; } function finishDirectScaling() { - select(intersectedEntityID); // Refresh. + // Update selection with final entity properties. + var i, + length; + // Final scale, position, and orientaation of root. + rootPosition = Vec3.sum(scaleCenter, Vec3.multiply(scaleFactor, Vec3.multiplyQbyV(scaleRotation, scaleRootOffset))); + rootOrientation = Quat.multiply(scaleRotation, scaleRootOrientation); + selection[0].dimensions = Vec3.multiply(scaleFactor, selection[0].dimensions); + selection[0].position = rootPosition; + selection[0].rotation = rootOrientation; + + // Final scale and position of children. + for (i = 1, length = selection.length; i < length; i += 1) { + selection[i].dimensions = Vec3.multiply(scaleFactor, selection[i].dimensions); + selection[i].localPosition = Vec3.multiply(scaleFactor, selection[i].localPosition); + } } function startHandleScaling() { @@ -320,10 +342,28 @@ Selection = function (side) { localPosition: Vec3.multiplyVbyV(factor, selection[i].localPosition) }); } + + // Save most recent scale parameters. + scaleFactor = factor; + scalePosition = position; + scaleOrientation = orientation; } function finishHandleScaling() { - select(intersectedEntityID); // Refresh. + // Update selection with final entity properties. + var i, + length; + + // Final scale and position of root. + selection[0].dimensions = Vec3.multiplyVbyV(scaleFactor, selection[0].dimensions); + selection[0].position = scalePosition; + selection[0].rotation = scaleOrientation; + + // Final scale and position of children. + for (i = 1, length = selection.length; i < length; i += 1) { + selection[i].dimensions = Vec3.multiplyVbyV(scaleFactor, selection[i].dimensions); + selection[i].localPosition = Vec3.multiplyVbyV(scaleFactor, selection[i].localPosition); + } } function cloneEntities() { From 808f37a824e47735b48eac90421c677277a249e5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 6 Sep 2017 22:54:51 +1200 Subject: [PATCH 292/722] Prevent entities from jittering or drifting when grab a moving set --- scripts/vr-edit/modules/selection.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 8befc6416b..24717cbe62 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -219,12 +219,18 @@ Selection = function (side) { i; // Disable entity set's physics. - for (i = 0, count = selection.length; i < count; i += 1) { + //for (i = 0, count = selection.length; i < count; i += 1) { + for (i = selection.length - 1; i >= 0; i -= 1) { Entities.editEntity(selection[i].id, { - dynamic: false, // So that gravity doesn't fight with us trying to hold the entity in place. - collisionless: true // So that entity doesn't bump us about as we resize the entity. + dynamic: false, // So that gravity doesn't fight with us trying to hold the entity in place. + collisionless: true, // So that entity doesn't bump us about as we resize the entity. + velocity: Vec3.ZERO, // So that entity doesn't drift if we've grabbed a set while it was moving. + angularVelocity: Vec3.ZERO // "" }); } + + // Stop moving. + Entities.editEntity(rootEntityID, { velocity: Vec3.ZERO, angularVelocity: Vec3.ZERO }); } function finishEditing() { From 3cc0db8b40d50e314dc7fd66767ec8c167594b9e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 6 Sep 2017 23:38:38 +1200 Subject: [PATCH 293/722] Fix app icon occasionally being disabled at Interface start --- scripts/vr-edit/vr-edit.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 4bb70134b8..070901cdc0 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -19,6 +19,7 @@ ENABLED_CAPTION_COLOR_OVERRIDE = "", DISABLED_CAPTION_COLOR_OVERRIDE = "#888888", VR_EDIT_SETTING = "io.highfidelity.isVREditing", // Note: This constant is duplicated in utils.js. + START_DELAY = 2000, // ms // Application state isAppActive, @@ -1803,6 +1804,6 @@ tablet = null; } - setUp(); + Script.setTimeout(setUp, START_DELAY); // Delay start so that Entities.canRez() work; button is enabled correctly. Script.scriptEnding.connect(tearDown); }()); From 2661fb66470ba3db69b4a58196bcabb318e49c4e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 7 Sep 2017 08:48:03 +1200 Subject: [PATCH 294/722] Tidying --- scripts/vr-edit/modules/selection.js | 3 +-- scripts/vr-edit/vr-edit.js | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 24717cbe62..fe2dd5e0a5 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -215,8 +215,7 @@ Selection = function (side) { } function startEditing() { - var count, - i; + var i; // Disable entity set's physics. //for (i = 0, count = selection.length; i < count; i += 1) { diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 070901cdc0..75a8557f58 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1740,11 +1740,6 @@ Messages.messageReceived.connect(onMessageReceived); MyAvatar.dominantHandChanged.connect(onDominantHandChanged); MyAvatar.skeletonChanged.connect(onSkeletonChanged); - - // Start main update loop. - if (isAppActive) { - update(); - } } function tearDown() { From 8ca97d91c5a4127d0e556eaa57b8a5cc9b6d24bf Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 7 Sep 2017 10:26:12 +1200 Subject: [PATCH 295/722] Make able to pick color from locked entities --- scripts/vr-edit/vr-edit.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 75a8557f58..64b116993c 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -948,7 +948,7 @@ break; case EDITOR_SEARCHING: if (hand.valid() - && (!intersection.entityID || !intersection.editableEntity) + && (!intersection.entityID || !(intersection.editableEntity || toolSelected === TOOL_PICK_COLOR)) && !(intersection.overlayID && !wasTriggerClicked && isTriggerClicked && otherEditor.isHandle(intersection.overlayID))) { // No transition. @@ -963,12 +963,12 @@ intersectedEntityID = otherEditor.intersectedEntityID(); rootEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); - } else if (intersection.entityID && intersection.editableEntity + } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab) { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); - } else if (intersection.entityID && intersection.editableEntity + } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (!wasTriggerClicked || isAutoGrab) && isTriggerClicked) { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); @@ -1014,7 +1014,7 @@ break; case EDITOR_HIGHLIGHTING: if (hand.valid() - && intersection.entityID && intersection.editableEntity + && intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && !(!wasTriggerClicked && isTriggerClicked && (!otherEditor.isEditing(rootEntityID) || toolSelected !== TOOL_SCALE)) && !(!wasTriggerClicked && isTriggerClicked && intersection.overlayID @@ -1056,7 +1056,8 @@ intersectedEntityID = otherEditor.intersectedEntityID(); rootEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); - } else if (intersection.entityID && intersection.editableEntity && !wasTriggerClicked && isTriggerClicked) { + } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) + && !wasTriggerClicked && isTriggerClicked) { intersectedEntityID = intersection.entityID; // May be a different entityID. rootEntityID = Entities.rootOf(intersectedEntityID); if (otherEditor.isEditing(rootEntityID)) { From e94d2034c7f1fea9b7d45aca3f32b115a858a9af Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 7 Sep 2017 10:44:03 +1200 Subject: [PATCH 296/722] Don't grab entity behind entity just created and grip-click-deleted --- scripts/vr-edit/vr-edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 64b116993c..f69d2c3690 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1220,7 +1220,7 @@ wasTriggerClicked = isTriggerClicked; wasGripClicked = isGripClicked; - isAutoGrab = isAutoGrab && isTriggerClicked; + isAutoGrab = isAutoGrab && isTriggerClicked && !isGripClicked; if (DEBUG && editorState !== previousState) { debug(side, EDITOR_STATE_STRINGS[editorState]); From e48c456d246e2f5b0e015ac3e4c098a22292be74 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 6 Sep 2017 17:57:18 -0700 Subject: [PATCH 297/722] Trying to sync the render transform for overlay web at the right time... --- interface/src/ui/overlays/Base3DOverlay.cpp | 4 ++ interface/src/ui/overlays/Base3DOverlay.h | 4 ++ interface/src/ui/overlays/Overlay.h | 1 + interface/src/ui/overlays/Web3DOverlay.cpp | 42 +++++++++++++++++++-- interface/src/ui/overlays/Web3DOverlay.h | 5 +++ 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index c0278a6496..010be802a9 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -256,6 +256,10 @@ bool Base3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3 void Base3DOverlay::locationChanged(bool tellPhysics) { SpatiallyNestable::locationChanged(tellPhysics); + // Force the actual update of the render transform now that we notify for the change + // so it s captured for the time of rendering + notifyRenderTransformChange(); + auto itemID = getRenderItemID(); if (render::Item::isValidID(itemID)) { render::ScenePointer scene = qApp->getMain3DScene(); diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index 6377b46d7d..3f57f2e577 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -52,6 +52,9 @@ public: virtual AABox getBounds() const override = 0; + void notifyRenderTransformChange() const { _renderTransformDirty = true; } + virtual Transform evalRenderTransform() const { return Transform(); } + void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; @@ -73,6 +76,7 @@ protected: bool _ignoreRayIntersection; bool _drawInFront; bool _isGrabbable { false }; + mutable bool _renderTransformDirty{ true }; QString _name; }; diff --git a/interface/src/ui/overlays/Overlay.h b/interface/src/ui/overlays/Overlay.h index db2979b4d5..31846501ec 100644 --- a/interface/src/ui/overlays/Overlay.h +++ b/interface/src/ui/overlays/Overlay.h @@ -102,6 +102,7 @@ protected: render::ItemID _renderItemID{ render::Item::INVALID_ITEM_ID }; + bool _isLoaded; float _alpha; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 104082dee4..a1fd0414ee 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -180,12 +180,46 @@ void Web3DOverlay::buildWebSurface() { void Web3DOverlay::update(float deltatime) { + if (_renderTransformDirty) { + auto itemID = getRenderItemID(); + if (render::Item::isValidID(itemID)) { + render::ScenePointer scene = qApp->getMain3DScene(); + render::Transaction transaction; + transaction.updateItem(itemID, [](Overlay& data) { + auto web3D = dynamic_cast(&data); + if (web3D) { + web3D->evalRenderTransform(); + } + }); + scene->enqueueTransaction(transaction); + } + } + if (_webSurface) { // update globalPosition _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); } } +Transform Web3DOverlay::evalRenderTransform() const { + if (_renderTransformDirty) { + _renderTransform = getTransform(); + + // FIXME: applyTransformTo causes tablet overlay to detach from tablet entity. + // Perhaps rather than deleting the following code it should be run only if isFacingAvatar() is true? + /* + applyTransformTo(transform, true); + setTransform(transform); + */ + + if (glm::length2(getDimensions()) != 1.0f) { + _renderTransform.postScale(vec3(getDimensions(), 1.0f)); + } + _renderTransformDirty = false; + } + return _renderTransform; +} + QString Web3DOverlay::pickURL() { QUrl sourceUrl(_url); if (sourceUrl.scheme() == "http" || sourceUrl.scheme() == "https" || @@ -305,7 +339,7 @@ void Web3DOverlay::render(RenderArgs* args) { vec2 halfSize = getSize() / 2.0f; vec4 color(toGlm(getColor()), getAlpha()); - + /* Transform transform = getTransform(); // FIXME: applyTransformTo causes tablet overlay to detach from tablet entity. @@ -314,10 +348,11 @@ void Web3DOverlay::render(RenderArgs* args) { applyTransformTo(transform, true); setTransform(transform); */ - + /* if (glm::length2(getDimensions()) != 1.0f) { transform.postScale(vec3(getDimensions(), 1.0f)); } + */ if (!_texture) { _texture = gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda()); @@ -332,7 +367,8 @@ void Web3DOverlay::render(RenderArgs* args) { Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; batch.setResourceTexture(0, _texture); - batch.setModelTransform(transform); + // batch.setModelTransform(transform); + batch.setModelTransform(_renderTransform); auto geometryCache = DependencyManager::get(); if (color.a < OPAQUE_ALPHA_THRESHOLD) { geometryCache->bindWebBrowserProgram(batch, true); diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 2eae7f33da..721f86019c 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -37,6 +37,9 @@ public: virtual void update(float deltatime) override; + Transform evalRenderTransform() const override; + + QObject* getEventHandler(); void setProxyWindow(QWindow* proxyWindow); void handlePointerEvent(const PointerEvent& event); @@ -92,6 +95,8 @@ private: std::map _activeTouchPoints; QTouchDevice _touchDevice; + mutable Transform _renderTransform; + uint8_t _desiredMaxFPS { 10 }; uint8_t _currentMaxFPS { 0 }; From 8fdc2313cc2e219764fa7a8b3b4bf61d27665a17 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 7 Sep 2017 18:36:01 +1200 Subject: [PATCH 298/722] Tidying --- scripts/vr-edit/modules/feedback.js | 2 +- scripts/vr-edit/modules/toolIcon.js | 6 - scripts/vr-edit/modules/toolsMenu.js | 160 +++++++++++++-------------- scripts/vr-edit/vr-edit.js | 4 +- 4 files changed, 81 insertions(+), 91 deletions(-) diff --git a/scripts/vr-edit/modules/feedback.js b/scripts/vr-edit/modules/feedback.js index 843eebe07b..23a82e66aa 100644 --- a/scripts/vr-edit/modules/feedback.js +++ b/scripts/vr-edit/modules/feedback.js @@ -30,7 +30,7 @@ Feedback = (function () { SELECT_ENTITY: { sound: SELECT_SOUND, volume: 0.2, haptic: 0.1 }, // E.g., Group tool. CLONE_ENTITY: { sound: CLONE_SOUND, volume: 0.2, haptic: 0.1 }, CREATE_ENTITY: { sound: CREATE_SOUND, volume: 0.4, haptic: 0.2 }, - HOVER_MENU_ITEM: { sound: null, volume: 0, haptic: 0.1 } , // Tools menu. + HOVER_MENU_ITEM: { sound: null, volume: 0, haptic: 0.1 }, // Tools menu. HOVER_BUTTON: { sound: null, volume: 0, haptic: 0.075 }, // Tools options and Create palette items. EQUIP_TOOL: { sound: EQUIP_SOUND, volume: 0.3, haptic: 0.6 }, APPLY_PROPERTY: { sound: null, volume: 0, haptic: 0.3 }, diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index d03351b27c..6ea6965a36 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -86,11 +86,6 @@ ToolIcon = function (side) { setHand(side); - function update() { - // TODO: Display icon animation. - // TODO: Clear icon animation. - } - function clear() { // Deletes current tool model. if (modelOverlay) { @@ -160,7 +155,6 @@ ToolIcon = function (side) { return { setHand: setHand, - update: update, display: display, clear: clear, destroy: destroy diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index ab5a5f74c1..5825b808da 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -1843,12 +1843,11 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsItems, intersectionOverlays, intersectionEnabled, - highlightedItem, // TODO: Rename this and similar to "hovered". - highlightedItems, - highlightedSourceOverlays, - highlightedSourceItems, - highlightedElementType = null, - isHighlightingButtonElement, + hoveredItem, + hoveredSourceOverlays, + hoveredSourceItems, + hoveredElementType = null, + isHoveringButtonElement, isPicklistOpen, pressedItem = null, pressedSource = null, @@ -2583,7 +2582,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { index = optionsOverlaysIDs.indexOf("physicsPresets"); // Lower picklist. - isHighlightingPicklist = highlightedElementType === "picklist"; + isHighlightingPicklist = hoveredElementType === "picklist"; Overlays.editOverlay(optionsOverlays[index], { localPosition: isHighlightingPicklist ? Vec3.sum(optionsItems[index].properties.localPosition, OPTION_HOVER_DELTA) @@ -2865,74 +2864,74 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } - // Highlight clickable item. - if (intersectedItem !== highlightedItem || intersectionOverlays !== highlightedSourceOverlays) { + // Hover clickable item. + if (intersectedItem !== hoveredItem || intersectionOverlays !== hoveredSourceOverlays) { - if (highlightedItem !== NONE) { + if (hoveredItem !== NONE) { // Unhover old item. - switch (highlightedElementType) { + switch (hoveredElementType) { case "menuButton": - Overlays.editOverlay(menuHoverOverlays[highlightedItem], { + Overlays.editOverlay(menuHoverOverlays[hoveredItem], { localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, visible: false }); - Overlays.editOverlay(menuIconOverlays[highlightedItem], { + Overlays.editOverlay(menuIconOverlays[hoveredItem], { color: UI_ELEMENTS.menuButton.icon.properties.color }); break; case "button": - if (highlightedSourceItems[highlightedItem].enabledColor !== undefined && optionsEnabled[highlightedItem]) { - color = highlightedSourceItems[highlightedItem].enabledColor; + if (hoveredSourceItems[hoveredItem].enabledColor !== undefined && optionsEnabled[hoveredItem]) { + color = hoveredSourceItems[hoveredItem].enabledColor; } else { - color = highlightedSourceItems[highlightedItem].properties.color !== undefined - ? highlightedSourceItems[highlightedItem].properties.color + color = hoveredSourceItems[hoveredItem].properties.color !== undefined + ? hoveredSourceItems[hoveredItem].properties.color : UI_ELEMENTS.button.properties.color; } - Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { color: color, - localPosition: highlightedSourceItems[highlightedItem].properties.localPosition + localPosition: hoveredSourceItems[hoveredItem].properties.localPosition }); break; case "toggleButton": - color = optionsToggles[highlightedSourceItems[highlightedItem].id] + color = optionsToggles[hoveredSourceItems[hoveredItem].id] ? UI_ELEMENTS.toggleButton.onColor : UI_ELEMENTS.toggleButton.offColor; - Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { color: color, - localPosition: highlightedSourceItems[highlightedItem].properties.localPosition + localPosition: hoveredSourceItems[hoveredItem].properties.localPosition }); break; case "swatch": Overlays.editOverlay(swatchHighlightOverlay, { visible: false }); - color = optionsSettings[highlightedSourceItems[highlightedItem].id].value; - Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { + color = optionsSettings[hoveredSourceItems[hoveredItem].id].value; + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { dimensions: UI_ELEMENTS.swatch.properties.dimensions, color: color === "" ? EMPTY_SWATCH_COLOR : color, - localPosition: highlightedSourceItems[highlightedItem].properties.localPosition + localPosition: hoveredSourceItems[hoveredItem].properties.localPosition }); break; case "barSlider": case "imageSlider": case "colorCircle": // Lower old slider or color circle. - Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { - localPosition: highlightedSourceItems[highlightedItem].properties.localPosition + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { + localPosition: hoveredSourceItems[hoveredItem].properties.localPosition }); break; case "picklist": - if (highlightedSourceItems[highlightedItem].type !== "picklistItem" && !isPicklistOpen) { - Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { - localPosition: highlightedSourceItems[highlightedItem].properties.localPosition, + if (hoveredSourceItems[hoveredItem].type !== "picklistItem" && !isPicklistOpen) { + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { + localPosition: hoveredSourceItems[hoveredItem].properties.localPosition, color: UI_ELEMENTS.picklist.properties.color }); } else { - Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { color: UIT.colors.darkGray }); } break; case "picklistItem": - Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], { + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { color: UI_ELEMENTS.picklistItem.properties.color }); break; @@ -2940,13 +2939,13 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Nothing to do. break; default: - App.log(side, "ERROR: ToolsMenu: Unexpected hover item! " + highlightedElementType); + App.log(side, "ERROR: ToolsMenu: Unexpected hover item! " + hoveredElementType); } // Update status variables. - highlightedItem = NONE; - isHighlightingButtonElement = false; - highlightedElementType = null; + hoveredItem = NONE; + isHoveringButtonElement = false; + hoveredElementType = null; } if (intersectedItem !== NONE && intersectionItems[intersectedItem] && @@ -2954,41 +2953,40 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { || intersectionItems[intersectedItem].callback !== undefined)) { // Update status variables. - highlightedItem = intersectedItem; - highlightedItems = intersectionItems; - isHighlightingButtonElement = BUTTON_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE; - highlightedElementType = intersectionItems[highlightedItem].type; + hoveredItem = intersectedItem; + isHoveringButtonElement = BUTTON_UI_ELEMENTS.indexOf(intersectionItems[hoveredItem].type) !== NONE; + hoveredElementType = intersectionItems[hoveredItem].type; // Hover new item. - switch (highlightedElementType) { + switch (hoveredElementType) { case "menuButton": Feedback.play(otherSide, Feedback.HOVER_MENU_ITEM); - Overlays.editOverlay(menuHoverOverlays[highlightedItem], { + Overlays.editOverlay(menuHoverOverlays[hoveredItem], { localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, MENU_HOVER_DELTA), visible: true }); - Overlays.editOverlay(menuIconOverlays[highlightedItem], { + Overlays.editOverlay(menuIconOverlays[hoveredItem], { color: UI_ELEMENTS.menuButton.icon.highlightColor }); break; case "button": - if (intersectionEnabled[highlightedItem]) { + if (intersectionEnabled[hoveredItem]) { Feedback.play(otherSide, Feedback.HOVER_BUTTON); - localPosition = intersectionItems[highlightedItem].properties.localPosition; - Overlays.editOverlay(intersectionOverlays[highlightedItem], { - color: intersectionItems[highlightedItem].highlightColor !== undefined - ? intersectionItems[highlightedItem].highlightColor + localPosition = intersectionItems[hoveredItem].properties.localPosition; + Overlays.editOverlay(intersectionOverlays[hoveredItem], { + color: intersectionItems[hoveredItem].highlightColor !== undefined + ? intersectionItems[hoveredItem].highlightColor : UIT.colors.greenHighlight, localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) }); } break; case "toggleButton": - if (intersectionEnabled[highlightedItem]) { + if (intersectionEnabled[hoveredItem]) { Feedback.play(otherSide, Feedback.HOVER_BUTTON); - localPosition = intersectionItems[highlightedItem].properties.localPosition; - Overlays.editOverlay(intersectionOverlays[highlightedItem], { - color: optionsToggles[intersectionItems[highlightedItem].id] + localPosition = intersectionItems[hoveredItem].properties.localPosition; + Overlays.editOverlay(intersectionOverlays[hoveredItem], { + color: optionsToggles[intersectionItems[hoveredItem].id] ? UI_ELEMENTS.toggleButton.onHoverColor : UI_ELEMENTS.toggleButton.offHoverColor, localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) @@ -2997,22 +2995,22 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { break; case "swatch": Feedback.play(otherSide, Feedback.HOVER_BUTTON); - localPosition = intersectionItems[highlightedItem].properties.localPosition; - if (optionsSettings[intersectionItems[highlightedItem].id].value === "") { + localPosition = intersectionItems[hoveredItem].properties.localPosition; + if (optionsSettings[intersectionItems[hoveredItem].id].value === "") { // Swatch is empty; highlight it with current color. - Overlays.editOverlay(intersectionOverlays[highlightedItem], { + Overlays.editOverlay(intersectionOverlays[hoveredItem], { color: optionsSettings.currentColor.value, localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) }); } else { // Swatch is full; highlight it with ring. - Overlays.editOverlay(intersectionOverlays[highlightedItem], { + Overlays.editOverlay(intersectionOverlays[hoveredItem], { dimensions: Vec3.subtract(UI_ELEMENTS.swatch.properties.dimensions, { x: SWATCH_HIGHLIGHT_DELTA, y: 0, z: SWATCH_HIGHLIGHT_DELTA }), localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) }); Overlays.editOverlay(swatchHighlightOverlay, { - parentID: intersectionOverlays[highlightedItem], + parentID: intersectionOverlays[hoveredItem], localPosition: UI_ELEMENTS.swatchHighlight.properties.localPosition, visible: true }); @@ -3022,28 +3020,28 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { case "imageSlider": case "colorCircle": Feedback.play(otherSide, Feedback.HOVER_BUTTON); - localPosition = intersectionItems[highlightedItem].properties.localPosition; - Overlays.editOverlay(intersectionOverlays[highlightedItem], { + localPosition = intersectionItems[hoveredItem].properties.localPosition; + Overlays.editOverlay(intersectionOverlays[hoveredItem], { localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) }); break; case "picklist": Feedback.play(otherSide, Feedback.HOVER_BUTTON); if (!isPicklistOpen) { - localPosition = intersectionItems[highlightedItem].properties.localPosition; - Overlays.editOverlay(intersectionOverlays[highlightedItem], { + localPosition = intersectionItems[hoveredItem].properties.localPosition; + Overlays.editOverlay(intersectionOverlays[hoveredItem], { localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA), color: UIT.colors.greenHighlight }); } else { - Overlays.editOverlay(intersectionOverlays[highlightedItem], { + Overlays.editOverlay(intersectionOverlays[hoveredItem], { color: UIT.colors.greenHighlight }); } break; case "picklistItem": Feedback.play(otherSide, Feedback.HOVER_BUTTON); - Overlays.editOverlay(intersectionOverlays[highlightedItem], { + Overlays.editOverlay(intersectionOverlays[hoveredItem], { color: UIT.colors.greenHighlight }); break; @@ -3051,12 +3049,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Nothing to do. break; default: - App.log(side, "ERROR: ToolsMenu: Unexpected hover item! " + highlightedElementType); + App.log(side, "ERROR: ToolsMenu: Unexpected hover item! " + hoveredElementType); } } - highlightedSourceOverlays = intersectionOverlays; - highlightedSourceItems = intersectionItems; + hoveredSourceOverlays = intersectionOverlays; + hoveredSourceItems = intersectionItems; } // Press/unpress button. @@ -3065,18 +3063,18 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (pressedItem) { // Unpress previous button. Overlays.editOverlay(pressedSource[pressedItem.index], { - localPosition: isHighlightingButtonElement && highlightedItem === pressedItem.index + localPosition: isHoveringButtonElement && hoveredItem === pressedItem.index ? Vec3.sum(pressedItem.localPosition, OPTION_HOVER_DELTA) : pressedItem.localPosition }); pressedItem = null; pressedSource = null; } - if (isHighlightingButtonElement && (intersectionEnabled === null || intersectionEnabled[intersectedItem]) + if (isHoveringButtonElement && (intersectionEnabled === null || intersectionEnabled[intersectedItem]) && isTriggerClicked && !wasTriggerClicked) { // Press new button. localPosition = intersectionItems[intersectedItem].properties.localPosition; - if (highlightedElementType !== "menuButton") { + if (hoveredElementType !== "menuButton") { Overlays.editOverlay(intersectionOverlays[intersectedItem], { localPosition: Vec3.sum(localPosition, BUTTON_PRESS_DELTA) }); @@ -3105,7 +3103,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } // Picklist update. - if (intersectionItems && ((highlightedElementType === "picklist" + if (intersectionItems && ((hoveredElementType === "picklist" && controlHand.triggerClicked() !== isPicklistPressed) || (intersectionItems[intersectedItem].type !== "picklist" && isPicklistPressed))) { isPicklistPressed = controlHand.triggerClicked(); @@ -3113,7 +3111,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { doCommand(intersectionItems[intersectedItem].command.method, intersectionItems[intersectedItem].id); } } - if (intersectionItems && ((highlightedElementType === "picklistItem" + if (intersectionItems && ((hoveredElementType === "picklistItem" && controlHand.triggerClicked() !== isPicklistItemPressed) || (intersectionItems[intersectedItem].type !== "picklistItem" && isPicklistItemPressed))) { isPicklistItemPressed = controlHand.triggerClicked(); @@ -3122,7 +3120,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } if (intersectionItems && isPicklistOpen && controlHand.triggerClicked() - && highlightedElementType !== "picklist" && highlightedElementType !== "picklistItem") { + && hoveredElementType !== "picklist" && hoveredElementType !== "picklistItem") { doCommand("togglePhysicsPresets"); } @@ -3137,7 +3135,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } // Bar slider update. - if (intersectionItems && highlightedElementType === "barSlider" && controlHand.triggerClicked()) { + if (intersectionItems && hoveredElementType === "barSlider" && controlHand.triggerClicked()) { sliderProperties = Overlays.getProperties(intersection.overlayID, ["position", "orientation"]); overlayDimensions = intersectionItems[intersectedItem].properties.dimensions; if (overlayDimensions === undefined) { @@ -3155,7 +3153,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } // Image slider update. - if (intersectionItems && highlightedElementType === "imageSlider" && controlHand.triggerClicked()) { + if (intersectionItems && hoveredElementType === "imageSlider" && controlHand.triggerClicked()) { sliderProperties = Overlays.getProperties(intersection.overlayID, ["position", "orientation"]); overlayDimensions = intersectionItems[intersectedItem].properties.dimensions; if (overlayDimensions === undefined) { @@ -3176,7 +3174,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } // Color circle update. - if (intersectionItems && highlightedElementType === "colorCircle" && controlHand.triggerClicked()) { + if (intersectionItems && hoveredElementType === "colorCircle" && controlHand.triggerClicked()) { sliderProperties = Overlays.getProperties(intersection.overlayID, ["position", "orientation"]); delta = Vec3.multiplyQbyV(Quat.inverse(sliderProperties.orientation), Vec3.subtract(intersection.intersection, sliderProperties.position)); @@ -3208,7 +3206,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isGroupButtonEnabled = enableGroupButton; Overlays.editOverlay(optionsOverlays[groupButtonIndex], { color: isGroupButtonEnabled - ? (highlightedItem === groupButtonIndex + ? (hoveredItem === groupButtonIndex ? OPTONS_PANELS.groupOptions[groupButtonIndex].highlightColor : OPTONS_PANELS.groupOptions[groupButtonIndex].enabledColor) : OPTONS_PANELS.groupOptions[groupButtonIndex].properties.color @@ -3226,7 +3224,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isUngroupButtonEnabled = enableUngroupButton; Overlays.editOverlay(optionsOverlays[ungroupButtonIndex], { color: isUngroupButtonEnabled - ? (highlightedItem === ungroupButtonIndex + ? (hoveredItem === ungroupButtonIndex ? OPTONS_PANELS.groupOptions[ungroupButtonIndex].highlightColor : OPTONS_PANELS.groupOptions[ungroupButtonIndex].enabledColor) : OPTONS_PANELS.groupOptions[ungroupButtonIndex].properties.color @@ -3312,10 +3310,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsItems = null; intersectionOverlays = null; intersectionEnabled = null; - highlightedItem = NONE; - highlightedSourceOverlays = null; - isHighlightingButtonElement = false; - highlightedElementType = null; + hoveredItem = NONE; + hoveredSourceOverlays = null; + isHoveringButtonElement = false; + hoveredElementType = null; pressedItem = null; pressedSource = null; isPicklistOpen = false; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index f69d2c3690..fbf767b839 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -71,7 +71,7 @@ button, DOMAIN_CHANGED_MESSAGE = "Toolbar-DomainChanged", - DEBUG = true; // TODO: Set false. + DEBUG = false; // Utilities Script.include("./utilities/utilities.js"); @@ -275,7 +275,6 @@ intersection = getIntersection(); toolsMenu.update(intersection, grouping.groupsCount(), grouping.entitiesCount()); createPalette.update(intersection.overlayID); - toolIcon.update(); } } @@ -1037,7 +1036,6 @@ intersectedEntityID = intersection.entityID; doUpdateState = true; } - // TODO: For development testing, update intersectedEntityID so that physics can be applied to it. if ((toolSelected === TOOL_COLOR || toolSelected === TOOL_PHYSICS) && intersection.entityID !== intersectedEntityID) { intersectedEntityID = intersection.entityID; From 70581ebbc1b4fde45b2509e092bc430d13521232 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Sep 2017 10:43:43 +1200 Subject: [PATCH 299/722] Fix handle scaling's scaling and positioning of entities --- scripts/vr-edit/modules/selection.js | 7 ++-- scripts/vr-edit/vr-edit.js | 53 +++++----------------------- 2 files changed, 13 insertions(+), 47 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index fe2dd5e0a5..122465cdce 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -320,8 +320,9 @@ Selection = function (side) { } } - function startHandleScaling() { - // Nothing to do. + function startHandleScaling(position) { + // Save initial offset from hand position to root position so that can scale without accumulating float errors. + scaleRootOffset = Vec3.multiplyQbyV(Quat.inverse(rootOrientation), Vec3.subtract(rootPosition, position)); } function handleScale(factor, position, orientation) { @@ -330,7 +331,7 @@ Selection = function (side) { length; // Scale and position root. - rootPosition = position; + rootPosition = Vec3.sum(Vec3.multiplyQbyV(orientation, Vec3.multiplyVbyV(factor, scaleRootOffset)), position); rootOrientation = orientation; Entities.editEntity(selection[0].id, { dimensions: Vec3.multiplyVbyV(factor, selection[0].dimensions), diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index fbf767b839..84c6e269e3 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -383,15 +383,10 @@ isHandleScaling = false, // "" initialTargetsSeparation, initialtargetsDirection, - initialTargetToBoundingBoxCenter, otherTargetPosition, handleUnitScaleAxis, handleScaleDirections, - handleHandOffset, initialHandleDistance, - initialHandleOrientationInverse, - initialHandleRegistrationOffset, - initialSelectionOrientationInverse, laserOffset, MIN_SCALE = 0.001, @@ -533,41 +528,23 @@ function startHandleScaling(targetPosition, overlayID) { // Called on grabbing hand by scaling hand. var initialTargetPosition, - boundingBox, selectionPositionAndOrientation, - scaleAxis, - handDistance; - - isScalingWithHand = intersection.handIntersected; - - otherTargetPosition = targetPosition; + scaleAxis; // Keep grabbed handle highlighted and hide other handles. handles.grab(overlayID); - // Vector from target to bounding box center. + isScalingWithHand = intersection.handIntersected; + + otherTargetPosition = targetPosition; initialTargetPosition = getScaleTargetPosition(); - boundingBox = selection.boundingBox(); - initialTargetToBoundingBoxCenter = Vec3.subtract(boundingBox.center, initialTargetPosition); - - // Selection information. selectionPositionAndOrientation = selection.getPositionAndOrientation(); - initialSelectionOrientationInverse = Quat.inverse(selectionPositionAndOrientation.orientation); - - // Handle information. - initialHandleOrientationInverse = Quat.inverse(hand.orientation()); handleUnitScaleAxis = handles.scalingAxis(overlayID); // Unit vector in direction of scaling. handleScaleDirections = handles.scalingDirections(overlayID); // Which axes to scale the selection on. - initialHandleDistance = Vec3.length(Vec3.multiplyVbyV(boundingBox.dimensions, handleScaleDirections)) / 2; - initialHandleRegistrationOffset = Vec3.multiplyQbyV(initialSelectionOrientationInverse, - Vec3.subtract(selectionPositionAndOrientation.position, boundingBox.center)); - - // Distance from hand to handle in direction of handle. scaleAxis = Vec3.multiplyQbyV(selectionPositionAndOrientation.orientation, handleUnitScaleAxis); - handDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, boundingBox.center), scaleAxis)); - handleHandOffset = handDistance - initialHandleDistance; + initialHandleDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, initialTargetPosition), scaleAxis)); - selection.startHandleScaling(); + selection.startHandleScaling(initialTargetPosition); handles.startScaling(); isHandleScaling = true; } @@ -629,10 +606,7 @@ // Scales selection per changing position of scaling hand; positions and orients per grabbing hand. var targetPosition, deltaHandOrientation, - deltaHandleOrientation, - selectionPosition, selectionOrientation, - boundingBoxCenter, scaleAxis, handleDistance, scale, @@ -643,15 +617,10 @@ deltaHandOrientation = Quat.multiply(hand.orientation(), initialHandOrientationInverse); selectionOrientation = Quat.multiply(deltaHandOrientation, initialSelectionOrientation); - // Desired distance of handle from center of bounding box. + // Desired distance of handle from other hand targetPosition = getScaleTargetPosition(); - deltaHandleOrientation = Quat.multiply(hand.orientation(), initialHandleOrientationInverse); - boundingBoxCenter = Vec3.sum(targetPosition, - Vec3.multiplyQbyV(deltaHandleOrientation, initialTargetToBoundingBoxCenter)); scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis); - handleDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, boundingBoxCenter), scaleAxis)); - handleDistance -= handleHandOffset; - handleDistance = Math.max(handleDistance, MIN_SCALE); + handleDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, targetPosition), scaleAxis)); // Scale selection relative to initial dimensions. scale = handleDistance / initialHandleDistance; @@ -662,13 +631,9 @@ z: handleScaleDirections.z !== 0 ? scale3D.z : 1 }; - // Reposition selection per scale. - selectionPosition = Vec3.sum(boundingBoxCenter, - Vec3.multiplyQbyV(selectionOrientation, Vec3.multiplyVbyV(scale3D, initialHandleRegistrationOffset))); - // Scale. handles.scale(scale3D); - selection.handleScale(scale3D, selectionPosition, selectionOrientation); + selection.handleScale(scale3D, targetPosition, selectionOrientation); // Update grab offset. selectionPositionAndOrientation = selection.getPositionAndOrientation(); From 73ec095235dd6a5c9daea8a326e9f5751ba61765 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 7 Sep 2017 17:58:16 -0700 Subject: [PATCH 300/722] fooling around with communicating the update transform to render thread through a trasnaction, not the solution yet --- interface/src/ui/overlays/Web3DOverlay.cpp | 15 ++++++++++----- interface/src/ui/overlays/Web3DOverlay.h | 3 ++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index a1fd0414ee..9ad2cef443 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -181,14 +181,15 @@ void Web3DOverlay::buildWebSurface() { void Web3DOverlay::update(float deltatime) { if (_renderTransformDirty) { + auto updateTransform = evalRenderTransform(); auto itemID = getRenderItemID(); if (render::Item::isValidID(itemID)) { render::ScenePointer scene = qApp->getMain3DScene(); render::Transaction transaction; - transaction.updateItem(itemID, [](Overlay& data) { + transaction.updateItem(itemID, [updateTransform](Overlay& data) { auto web3D = dynamic_cast(&data); if (web3D) { - web3D->evalRenderTransform(); + web3D->setRenderTransform(updateTransform);// evalRenderTransform(); } }); scene->enqueueTransaction(transaction); @@ -203,7 +204,7 @@ void Web3DOverlay::update(float deltatime) { Transform Web3DOverlay::evalRenderTransform() const { if (_renderTransformDirty) { - _renderTransform = getTransform(); + _updateTransform = getTransform(); // FIXME: applyTransformTo causes tablet overlay to detach from tablet entity. // Perhaps rather than deleting the following code it should be run only if isFacingAvatar() is true? @@ -213,11 +214,15 @@ Transform Web3DOverlay::evalRenderTransform() const { */ if (glm::length2(getDimensions()) != 1.0f) { - _renderTransform.postScale(vec3(getDimensions(), 1.0f)); + _updateTransform.postScale(vec3(getDimensions(), 1.0f)); } _renderTransformDirty = false; } - return _renderTransform; + return _updateTransform; +} + +void Web3DOverlay::setRenderTransform(const Transform& transform) { + _renderTransform = transform; } QString Web3DOverlay::pickURL() { diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 721f86019c..ef40d88333 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -38,7 +38,7 @@ public: virtual void update(float deltatime) override; Transform evalRenderTransform() const override; - + void setRenderTransform(const Transform& transform); QObject* getEventHandler(); void setProxyWindow(QWindow* proxyWindow); @@ -95,6 +95,7 @@ private: std::map _activeTouchPoints; QTouchDevice _touchDevice; + mutable Transform _updateTransform; mutable Transform _renderTransform; uint8_t _desiredMaxFPS { 10 }; From b953c49461dc26176336770f5cd1f6afdfb26eec Mon Sep 17 00:00:00 2001 From: Cain Kilgore Date: Fri, 8 Sep 2017 03:20:55 +0100 Subject: [PATCH 301/722] Commit for Better Logger - WL 21537 --- interface/src/Application.cpp | 13 +++++++++++-- libraries/shared/src/shared/FileLogger.cpp | 11 ++++++++++- libraries/shared/src/shared/FileLogger.h | 1 + 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3650c495f2..f0ad0fe85b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -510,6 +510,13 @@ void messageHandler(QtMsgType type, const QMessageLogContext& context, const QSt OutputDebugStringA(logMessage.toLocal8Bit().constData()); OutputDebugStringA("\n"); #endif + auto avatarManager = DependencyManager::get(); + auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; + + QUuid fileLoggerSessionID = myAvatar->getSessionUUID(); + if (!fileLoggerSessionID.isNull()) { + qApp->getLogger()->setSessionID(fileLoggerSessionID); + } qApp->getLogger()->addMessage(qPrintable(logMessage + "\n")); } } @@ -804,6 +811,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _deadlockWatchdogThread = new DeadlockWatchdogThread(); _deadlockWatchdogThread->start(); + // Set File Logger Session UUID + auto avatarManager = DependencyManager::get(); + auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; + if (steamClient) { qCDebug(interfaceapp) << "[VERSION] SteamVR buildID:" << steamClient->getSteamVRBuildID(); } @@ -920,8 +931,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // send a location update immediately discoverabilityManager->updateLocation(); - auto myAvatar = getMyAvatar(); - connect(nodeList.data(), &NodeList::nodeAdded, this, &Application::nodeAdded); connect(nodeList.data(), &NodeList::nodeKilled, this, &Application::nodeKilled); connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated); diff --git a/libraries/shared/src/shared/FileLogger.cpp b/libraries/shared/src/shared/FileLogger.cpp index bea28b2b6f..1ac87907bf 100644 --- a/libraries/shared/src/shared/FileLogger.cpp +++ b/libraries/shared/src/shared/FileLogger.cpp @@ -48,6 +48,8 @@ static const QString LOGS_DIRECTORY = "Logs"; static const QString IPADDR_WILDCARD = "[0-9]*.[0-9]*.[0-9]*.[0-9]*"; static const QString DATETIME_WILDCARD = "20[0-9][0-9]-[0,1][0-9]-[0-3][0-9]_[0-2][0-9].[0-6][0-9].[0-6][0-9]"; static const QString FILENAME_WILDCARD = "hifi-log_" + IPADDR_WILDCARD + "_" + DATETIME_WILDCARD + ".txt"; +static QUuid& SESSION_ID = QUuid::QUuid("{00000000-0000-0000-0000-000000000000}"); + // Max log size is 512 KB. We send log files to our crash reporter, so we want to keep this relatively // small so it doesn't go over the 2MB zipped limit for all of the files we send. static const qint64 MAX_LOG_SIZE = 512 * 1024; @@ -62,7 +64,10 @@ QString getLogRollerFilename() { QString result = FileUtils::standardPath(LOGS_DIRECTORY); QHostAddress clientAddress = getGuessedLocalAddress(); QDateTime now = QDateTime::currentDateTime(); - result.append(QString(FILENAME_FORMAT).arg(clientAddress.toString(), now.toString(DATETIME_FORMAT))); + + auto FILE_SESSION_ID = SESSION_ID.toString().replace("{", "").replace("}", ""); + + result.append(QString(FILENAME_FORMAT).arg(FILE_SESSION_ID, now.toString(DATETIME_FORMAT))); return result; } @@ -142,6 +147,10 @@ FileLogger::~FileLogger() { _persistThreadInstance->terminate(); } +void FileLogger::setSessionID(const QUuid& message) { + SESSION_ID = message; // This is for the output of log files. It will change if the Avatar enters a different domain. +} + void FileLogger::addMessage(const QString& message) { _persistThreadInstance->queueItem(message); emit logReceived(message); diff --git a/libraries/shared/src/shared/FileLogger.h b/libraries/shared/src/shared/FileLogger.h index 15d211afe8..d9d7651147 100644 --- a/libraries/shared/src/shared/FileLogger.h +++ b/libraries/shared/src/shared/FileLogger.h @@ -26,6 +26,7 @@ public: QString getFilename() const { return _fileName; } virtual void addMessage(const QString&) override; + virtual void setSessionID(const QUuid&); virtual QString getLogData() override; virtual void locateLog() override; virtual void sync() override; From 7f4cc0ed2a6382e289eaf7907ac51537e5531fc0 Mon Sep 17 00:00:00 2001 From: Cain Kilgore Date: Fri, 8 Sep 2017 20:21:17 +0100 Subject: [PATCH 302/722] Should fix the Mac Build --- libraries/shared/src/shared/FileLogger.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/shared/src/shared/FileLogger.cpp b/libraries/shared/src/shared/FileLogger.cpp index 1ac87907bf..56393f4f32 100644 --- a/libraries/shared/src/shared/FileLogger.cpp +++ b/libraries/shared/src/shared/FileLogger.cpp @@ -48,7 +48,7 @@ static const QString LOGS_DIRECTORY = "Logs"; static const QString IPADDR_WILDCARD = "[0-9]*.[0-9]*.[0-9]*.[0-9]*"; static const QString DATETIME_WILDCARD = "20[0-9][0-9]-[0,1][0-9]-[0-3][0-9]_[0-2][0-9].[0-6][0-9].[0-6][0-9]"; static const QString FILENAME_WILDCARD = "hifi-log_" + IPADDR_WILDCARD + "_" + DATETIME_WILDCARD + ".txt"; -static QUuid& SESSION_ID = QUuid::QUuid("{00000000-0000-0000-0000-000000000000}"); +static QUuid& SESSION_ID = QUuid("{00000000-0000-0000-0000-000000000000}"); // Max log size is 512 KB. We send log files to our crash reporter, so we want to keep this relatively // small so it doesn't go over the 2MB zipped limit for all of the files we send. From dfbd25fd77ffe1e6b9b6ec5938dc250126ade69b Mon Sep 17 00:00:00 2001 From: Cain Kilgore Date: Fri, 8 Sep 2017 20:51:41 +0100 Subject: [PATCH 303/722] Now pls --- libraries/shared/src/shared/FileLogger.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/shared/src/shared/FileLogger.cpp b/libraries/shared/src/shared/FileLogger.cpp index 56393f4f32..652cca79a3 100644 --- a/libraries/shared/src/shared/FileLogger.cpp +++ b/libraries/shared/src/shared/FileLogger.cpp @@ -48,7 +48,7 @@ static const QString LOGS_DIRECTORY = "Logs"; static const QString IPADDR_WILDCARD = "[0-9]*.[0-9]*.[0-9]*.[0-9]*"; static const QString DATETIME_WILDCARD = "20[0-9][0-9]-[0,1][0-9]-[0-3][0-9]_[0-2][0-9].[0-6][0-9].[0-6][0-9]"; static const QString FILENAME_WILDCARD = "hifi-log_" + IPADDR_WILDCARD + "_" + DATETIME_WILDCARD + ".txt"; -static QUuid& SESSION_ID = QUuid("{00000000-0000-0000-0000-000000000000}"); +QUuid& SESSION_ID = QUuid("{00000000-0000-0000-0000-000000000000}"); // Max log size is 512 KB. We send log files to our crash reporter, so we want to keep this relatively // small so it doesn't go over the 2MB zipped limit for all of the files we send. From 7b39cb77916e032aa7dcb954c64b165e8f197d75 Mon Sep 17 00:00:00 2001 From: Cain Kilgore Date: Fri, 8 Sep 2017 21:44:38 +0100 Subject: [PATCH 304/722] Should work now --- libraries/shared/src/shared/FileLogger.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/shared/src/shared/FileLogger.cpp b/libraries/shared/src/shared/FileLogger.cpp index 652cca79a3..46ba45e238 100644 --- a/libraries/shared/src/shared/FileLogger.cpp +++ b/libraries/shared/src/shared/FileLogger.cpp @@ -48,7 +48,7 @@ static const QString LOGS_DIRECTORY = "Logs"; static const QString IPADDR_WILDCARD = "[0-9]*.[0-9]*.[0-9]*.[0-9]*"; static const QString DATETIME_WILDCARD = "20[0-9][0-9]-[0,1][0-9]-[0-3][0-9]_[0-2][0-9].[0-6][0-9].[0-6][0-9]"; static const QString FILENAME_WILDCARD = "hifi-log_" + IPADDR_WILDCARD + "_" + DATETIME_WILDCARD + ".txt"; -QUuid& SESSION_ID = QUuid("{00000000-0000-0000-0000-000000000000}"); +QUuid SESSION_ID = QUuid("{00000000-0000-0000-0000-000000000000}"); // Max log size is 512 KB. We send log files to our crash reporter, so we want to keep this relatively // small so it doesn't go over the 2MB zipped limit for all of the files we send. From f109a86b23fb0476e8d9b40e1dac7229a7b9c042 Mon Sep 17 00:00:00 2001 From: Cain Kilgore Date: Fri, 8 Sep 2017 23:05:22 +0100 Subject: [PATCH 305/722] Fixed small crash bug on exit --- interface/src/Application.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f0ad0fe85b..3ee5406ad0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -513,10 +513,13 @@ void messageHandler(QtMsgType type, const QMessageLogContext& context, const QSt auto avatarManager = DependencyManager::get(); auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; - QUuid fileLoggerSessionID = myAvatar->getSessionUUID(); - if (!fileLoggerSessionID.isNull()) { - qApp->getLogger()->setSessionID(fileLoggerSessionID); + if (myAvatar) { + QUuid fileLoggerSessionID = myAvatar->getSessionUUID(); + if (!fileLoggerSessionID.isNull()) { + qApp->getLogger()->setSessionID(fileLoggerSessionID); + } } + qApp->getLogger()->addMessage(qPrintable(logMessage + "\n")); } } From 387e474889fd7961d63b198a6718a7ccc2ad285f Mon Sep 17 00:00:00 2001 From: Cain Kilgore Date: Fri, 8 Sep 2017 23:48:09 +0100 Subject: [PATCH 306/722] WIP --- interface/src/Application.cpp | 24 ++++++++++++------------ libraries/avatars/src/AvatarData.h | 10 ++++++++-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3ee5406ad0..e373e321f2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -510,16 +510,6 @@ void messageHandler(QtMsgType type, const QMessageLogContext& context, const QSt OutputDebugStringA(logMessage.toLocal8Bit().constData()); OutputDebugStringA("\n"); #endif - auto avatarManager = DependencyManager::get(); - auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; - - if (myAvatar) { - QUuid fileLoggerSessionID = myAvatar->getSessionUUID(); - if (!fileLoggerSessionID.isNull()) { - qApp->getLogger()->setSessionID(fileLoggerSessionID); - } - } - qApp->getLogger()->addMessage(qPrintable(logMessage + "\n")); } } @@ -798,10 +788,19 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo installNativeEventFilter(&MyNativeEventFilter::getInstance()); #endif - _logger = new FileLogger(this); // After setting organization name in order to get correct directory - + qInstallMessageHandler(messageHandler); + _logger = new FileLogger(this); + + connect(getMyAvatar().get(), &AvatarData::sessionUUIDChanged, _logger, [this] { + auto myAvatar = getMyAvatar(); + if (myAvatar) { + _logger->setSessionID(myAvatar->getSessionUUID()); + } + }); + + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf"); _window->setWindowTitle("High Fidelity Interface"); @@ -2102,6 +2101,7 @@ Application::~Application() { _octreeProcessor.terminate(); _entityEditSender.terminate(); + disconnect(getMyAvatar().get(), &AvatarData::sessionUUIDChanged, _logger, nullptr); DependencyManager::destroy(); DependencyManager::destroy(); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index b4c36dba70..b5bce50e68 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -381,7 +381,7 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(QStringList jointNames READ getJointNames) - Q_PROPERTY(QUuid sessionUUID READ getSessionUUID) + Q_PROPERTY(QUuid sessionUUID READ getSessionUUID NOTIFY sessionUUIDChanged) Q_PROPERTY(glm::mat4 sensorToWorldMatrix READ getSensorToWorldMatrix) Q_PROPERTY(glm::mat4 controllerLeftHandMatrix READ getControllerLeftHandMatrix) @@ -667,13 +667,19 @@ public: signals: void displayNameChanged(); void lookAtSnappingChanged(bool enabled); + void sessionUUIDChanged(); public slots: void sendAvatarDataPacket(); void sendIdentityPacket(); void setJointMappingsFromNetworkReply(); - void setSessionUUID(const QUuid& sessionUUID) { setID(sessionUUID); } + void setSessionUUID(const QUuid& sessionUUID) { + if (sessionUUID != getID()) { + setID(sessionUUID); + emit sessionUUIDChanged(); + } + } virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; From 0820eadc5b154949466311ce190aad83c8b5c00e Mon Sep 17 00:00:00 2001 From: Cain Kilgore Date: Fri, 8 Sep 2017 23:52:04 +0100 Subject: [PATCH 307/722] Fixed a thing --- interface/src/Application.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e373e321f2..874cf34726 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -789,9 +789,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo #endif - qInstallMessageHandler(messageHandler); - _logger = new FileLogger(this); + qInstallMessageHandler(messageHandler); connect(getMyAvatar().get(), &AvatarData::sessionUUIDChanged, _logger, [this] { auto myAvatar = getMyAvatar(); From 561c3e4653011bd4cf961877db05d22b13b87d73 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 9 Sep 2017 17:24:07 +1200 Subject: [PATCH 308/722] Make polylines and polyvoxes able to be colored --- scripts/vr-edit/modules/selection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 122465cdce..6fdfdb80b8 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -29,7 +29,7 @@ Selection = function (side) { scaleRootOffset, scaleRootOrientation, ENTITY_TYPE = "entity", - ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "ParticleEffect"]; + ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "ParticleEffect", "PolyLine", "PolyVox"]; if (!this instanceof Selection) { From a7e04fcba66c23bccaacd9ab356d51847a82d64c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 9 Sep 2017 20:14:09 +1200 Subject: [PATCH 309/722] Don't color particle effects --- scripts/vr-edit/modules/selection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 6fdfdb80b8..c93a285065 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -29,7 +29,7 @@ Selection = function (side) { scaleRootOffset, scaleRootOrientation, ENTITY_TYPE = "entity", - ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "ParticleEffect", "PolyLine", "PolyVox"]; + ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "PolyLine", "PolyVox"]; if (!this instanceof Selection) { From 6f4f8e583b3b2c204cf537b67b8b08269f694806 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 9 Sep 2017 20:15:15 +1200 Subject: [PATCH 310/722] Don't kick off physics for Text and Web entities --- scripts/vr-edit/modules/selection.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index c93a285065..1d7fac9ed8 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -192,6 +192,7 @@ Selection = function (side) { function doKick(entityID) { var properties, + NO_KICK_ENTITY_TYPES = ["Text", "Web"], // These entities don't respond to gravity so don't kick them. DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.1, z: 0 }; @@ -200,8 +201,9 @@ Selection = function (side) { return; } - properties = Entities.getEntityProperties(entityID, ["velocity", "gravity", "parentID"]); - if (Vec3.length(properties.gravity) > 0 && Vec3.length(properties.velocity) < DYNAMIC_VELOCITY_THRESHOLD) { + properties = Entities.getEntityProperties(entityID, ["type", "velocity", "gravity"]); + if (NO_KICK_ENTITY_TYPES.indexOf(properties.type) === -1 + && Vec3.length(properties.gravity) > 0 && Vec3.length(properties.velocity) < DYNAMIC_VELOCITY_THRESHOLD) { Entities.editEntity(entityID, { velocity: DYNAMIC_VELOCITY_KICK }); } } From f6e22b0733569d27a55aac233224cb7fc33abc2c Mon Sep 17 00:00:00 2001 From: Cain Kilgore Date: Sat, 9 Sep 2017 17:21:42 +0100 Subject: [PATCH 311/722] Changes The ID no longer relies on the Avatar Session ID as this changed per domain switch. The intention of this PR is to be able to group the log files easier, hence why it now relies on Interface ID instead. Additionally, when no ID is found when the interface is first launched, the ID doesn't appear in the rolled over log file. It will just appear blank. --- interface/src/Application.cpp | 13 ++++--------- libraries/shared/src/shared/FileLogger.cpp | 9 ++++++--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 874cf34726..a063dfd0a1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -792,14 +792,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _logger = new FileLogger(this); qInstallMessageHandler(messageHandler); - connect(getMyAvatar().get(), &AvatarData::sessionUUIDChanged, _logger, [this] { - auto myAvatar = getMyAvatar(); - if (myAvatar) { - _logger->setSessionID(myAvatar->getSessionUUID()); - } - }); - - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf"); _window->setWindowTitle("High Fidelity Interface"); @@ -815,6 +807,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Set File Logger Session UUID auto avatarManager = DependencyManager::get(); auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; + auto accountManager = DependencyManager::get(); + + _logger->setSessionID(accountManager->getSessionID()); if (steamClient) { qCDebug(interfaceapp) << "[VERSION] SteamVR buildID:" << steamClient->getSteamVRBuildID(); @@ -943,7 +938,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, nodeList.data(), &NodeList::reset); // connect to appropriate slots on AccountManager - auto accountManager = DependencyManager::get(); + // auto accountManager = DependencyManager::get(); auto dialogsManager = DependencyManager::get(); connect(accountManager.data(), &AccountManager::authRequired, dialogsManager.data(), &DialogsManager::showLoginDialog); diff --git a/libraries/shared/src/shared/FileLogger.cpp b/libraries/shared/src/shared/FileLogger.cpp index 46ba45e238..f712a341fe 100644 --- a/libraries/shared/src/shared/FileLogger.cpp +++ b/libraries/shared/src/shared/FileLogger.cpp @@ -42,13 +42,13 @@ private: -static const QString FILENAME_FORMAT = "hifi-log_%1_%2.txt"; +static const QString FILENAME_FORMAT = "hifi-log%1_%2.txt"; static const QString DATETIME_FORMAT = "yyyy-MM-dd_hh.mm.ss"; static const QString LOGS_DIRECTORY = "Logs"; static const QString IPADDR_WILDCARD = "[0-9]*.[0-9]*.[0-9]*.[0-9]*"; static const QString DATETIME_WILDCARD = "20[0-9][0-9]-[0,1][0-9]-[0-3][0-9]_[0-2][0-9].[0-6][0-9].[0-6][0-9]"; static const QString FILENAME_WILDCARD = "hifi-log_" + IPADDR_WILDCARD + "_" + DATETIME_WILDCARD + ".txt"; -QUuid SESSION_ID = QUuid("{00000000-0000-0000-0000-000000000000}"); +QUuid SESSION_ID; // Max log size is 512 KB. We send log files to our crash reporter, so we want to keep this relatively // small so it doesn't go over the 2MB zipped limit for all of the files we send. @@ -64,8 +64,11 @@ QString getLogRollerFilename() { QString result = FileUtils::standardPath(LOGS_DIRECTORY); QHostAddress clientAddress = getGuessedLocalAddress(); QDateTime now = QDateTime::currentDateTime(); + QString FILE_SESSION_ID; - auto FILE_SESSION_ID = SESSION_ID.toString().replace("{", "").replace("}", ""); + if (!SESSION_ID.isNull()) { + FILE_SESSION_ID = "_" + SESSION_ID.toString().replace("{", "").replace("}", ""); + } result.append(QString(FILENAME_FORMAT).arg(FILE_SESSION_ID, now.toString(DATETIME_FORMAT))); return result; From b69bd0ef49f2c04862ca31c56a8f101082309a06 Mon Sep 17 00:00:00 2001 From: Cain Kilgore Date: Sat, 9 Sep 2017 17:23:18 +0100 Subject: [PATCH 312/722] Removed old Disconnect --- interface/src/Application.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a063dfd0a1..2fa141a44f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2095,8 +2095,6 @@ Application::~Application() { _octreeProcessor.terminate(); _entityEditSender.terminate(); - disconnect(getMyAvatar().get(), &AvatarData::sessionUUIDChanged, _logger, nullptr); - DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); From d5381a810ec508425f362fdda0dbef6b1f5bc0c7 Mon Sep 17 00:00:00 2001 From: Nick Shaffner Date: Sat, 9 Sep 2017 12:55:28 -0700 Subject: [PATCH 313/722] brew tap homebrew/versions no longer works. (or is needed) The command now outputs: > brew tap homebrew/versions Warning: homebrew/versions was deprecated. This tap is now empty as all its formulae were migrated. nonetheless: > brew install cmake openssl still works. --- BUILD_OSX.md | 1 - 1 file changed, 1 deletion(-) diff --git a/BUILD_OSX.md b/BUILD_OSX.md index 3365627b8c..586f81def3 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -4,7 +4,6 @@ Please read the [general build guide](BUILD.md) for information on dependencies [Homebrew](https://brew.sh/) is an excellent package manager for OS X. It makes install of some High Fidelity dependencies very simple. - brew tap homebrew/versions brew install cmake openssl ### OpenSSL From ce15d0ecd0ed8ea8b9a4d1b64af5ad8d12a3c01f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 11 Sep 2017 15:18:42 +1200 Subject: [PATCH 314/722] Fix color picker circle and slider images clipping at oblique angles --- scripts/vr-edit/modules/toolsMenu.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 5825b808da..43056954e4 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -2158,7 +2158,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { childProperties = Object.clone(UI_ELEMENTS.image.properties); childProperties.url = Script.resolvePath(optionsItems[i].imageURL); childProperties.dimensions = { x: properties.dimensions.x, y: properties.dimensions.y }; - imageOffset += IMAGE_OFFSET; + imageOffset += 2 * IMAGE_OFFSET; // Double offset to prevent clipping against background. if (optionsItems[i].useBaseColor) { childProperties.color = properties.color; } @@ -2209,7 +2209,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { childProperties = Object.clone(UI_ELEMENTS.image.properties); childProperties.url = Script.resolvePath(optionsItems[i].imageURL); childProperties.scale = properties.dimensions.x; - imageOffset += IMAGE_OFFSET; + imageOffset += 2 * IMAGE_OFFSET; // Double offset to prevent clipping against background. childProperties.localPosition = { x: 0, y: properties.dimensions.y / 2 + imageOffset, z: 0 }; childProperties.localRotation = Quat.fromVec3Degrees({ x: -90, y: 90, z: 0 }); childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; From 18f4a3bf35e6ae276f6c6582a6d547c0d3681902 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 11 Sep 2017 15:43:19 +1200 Subject: [PATCH 315/722] Fix hiding UI when hand is inside entity that camera isn't --- scripts/vr-edit/vr-edit.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 84c6e269e3..26fbfd392e 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -453,7 +453,7 @@ function isCameraOutsideEntity(entityID) { var cameraPosition, - entityPosition, + grabPosition, pickRay, PRECISION_PICKING = true, NO_EXCLUDE_IDS = [], @@ -461,11 +461,11 @@ intersection; cameraPosition = Camera.position; - entityPosition = Entities.getEntityProperties(entityID, "position").position; + grabPosition = side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); pickRay = { origin: cameraPosition, - direction: Vec3.normalize(Vec3.subtract(entityPosition, cameraPosition)), - length: Vec3.distance(entityPosition, cameraPosition) + direction: Vec3.normalize(Vec3.subtract(grabPosition, cameraPosition)), + length: Vec3.distance(grabPosition, cameraPosition) }; intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, [entityID], NO_EXCLUDE_IDS, VISIBLE_ONLY); From acf6a433a99a5ed8dad7925a518a2ed21f34405a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 11 Sep 2017 16:38:06 +1200 Subject: [PATCH 316/722] Don't scale z-axis of text and Web entities --- scripts/vr-edit/modules/handles.js | 32 +++++++++++++++------------- scripts/vr-edit/modules/selection.js | 17 +++++++++++---- scripts/vr-edit/vr-edit.js | 4 ++-- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/scripts/vr-edit/modules/handles.js b/scripts/vr-edit/modules/handles.js index a0279ef9a1..8e9c5694fa 100644 --- a/scripts/vr-edit/modules/handles.js +++ b/scripts/vr-edit/modules/handles.js @@ -129,7 +129,7 @@ Handles = function (side) { return FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)]; } - function display(rootEntityID, boundingBox, isMultipleEntities) { + function display(rootEntityID, boundingBox, isMultipleEntities, isSuppressZAxis) { var boundingBoxCenter, boundingBoxOrientation, cameraPosition, @@ -217,20 +217,22 @@ Handles = function (side) { faceHandleDimensions = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_DIMENSIONS); faceHandleOffsets = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_OFFSETS); for (i = 0; i < NUM_FACE_HANDLES; i += 1) { - faceHandleOverlays[i] = Overlays.addOverlay("shape", { - parentID: rootEntityID, - localPosition: Vec3.sum(boundingBoxLocalCenter, - Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], Vec3.sum(boundingBoxDimensions, faceHandleOffsets))), - localRotation: FACE_HANDLE_OVERLAY_ROTATIONS[i], - dimensions: faceHandleDimensions, - shape: "Cone", - color: HANDLE_NORMAL_COLOR, - alpha: HANDLE_NORMAL_ALPHA, - solid: true, - drawInFront: true, - ignoreRayIntersection: false, - visible: true - }); + if (!isSuppressZAxis || FACE_HANDLE_OVERLAY_AXES[i].z === 0) { + faceHandleOverlays[i] = Overlays.addOverlay("shape", { + parentID: rootEntityID, + localPosition: Vec3.sum(boundingBoxLocalCenter, + Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], Vec3.sum(boundingBoxDimensions, faceHandleOffsets))), + localRotation: FACE_HANDLE_OVERLAY_ROTATIONS[i], + dimensions: faceHandleDimensions, + shape: "Cone", + color: HANDLE_NORMAL_COLOR, + alpha: HANDLE_NORMAL_ALPHA, + solid: true, + drawInFront: true, + ignoreRayIntersection: false, + visible: true + }); + } } } else { faceHandleOverlays = []; diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 1d7fac9ed8..8023775242 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -29,7 +29,8 @@ Selection = function (side) { scaleRootOffset, scaleRootOrientation, ENTITY_TYPE = "entity", - ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "PolyLine", "PolyVox"]; + ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "PolyLine", "PolyVox"], + ENTITY_TYPES_2D = ["Text", "Web"]; if (!this instanceof Selection) { @@ -41,14 +42,15 @@ Selection = function (side) { // The root entity is always the first entry. var children, properties, - SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions", "parentID", "localPosition", - "dynamic", "collisionless", "userData"], + SELECTION_PROPERTIES = ["type", "position", "registrationPoint", "rotation", "dimensions", "parentID", + "localPosition", "dynamic", "collisionless", "userData"], i, length; properties = Entities.getEntityProperties(id, SELECTION_PROPERTIES); result.push({ id: id, + type: properties.type, position: properties.position, parentID: properties.parentID, localPosition: properties.localPosition, @@ -190,6 +192,10 @@ Selection = function (side) { }; } + function is2D() { + return selection.length === 1 && ENTITY_TYPES_2D.indexOf(selection[0].type) !== -1; + } + function doKick(entityID) { var properties, NO_KICK_ENTITY_TYPES = ["Text", "Web"], // These entities don't respond to gravity so don't kick them. @@ -278,6 +284,7 @@ Selection = function (side) { function directScale(factor, rotation, center) { // Scale, position, and rotate selection. + // We can get away with scaling the z size of 2D entities - incongruities are barely noticeable and things recover. var i, length; @@ -308,7 +315,7 @@ Selection = function (side) { // Update selection with final entity properties. var i, length; - // Final scale, position, and orientaation of root. + // Final scale, position, and orientation of root. rootPosition = Vec3.sum(scaleCenter, Vec3.multiply(scaleFactor, Vec3.multiplyQbyV(scaleRotation, scaleRootOffset))); rootOrientation = Quat.multiply(scaleRotation, scaleRootOrientation); selection[0].dimensions = Vec3.multiply(scaleFactor, selection[0].dimensions); @@ -329,6 +336,7 @@ Selection = function (side) { function handleScale(factor, position, orientation) { // Scale and reposition and orient selection. + // We can get away with scaling the z size of 2D entities - incongruities are barely noticeable and things recover. var i, length; @@ -526,6 +534,7 @@ Selection = function (side) { intersectedEntityIndex: getIntersectedEntityIndex, rootEntityID: getRootEntityID, boundingBox: getBoundingBox, + is2D: is2D, getPositionAndOrientation: getPositionAndOrientation, setPositionAndOrientation: setPositionAndOrientation, startEditing: startEditing, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 26fbfd392e..01de12c07c 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -719,7 +719,7 @@ laser.disable(); } if (toolSelected === TOOL_SCALE) { - handles.display(rootEntityID, selection.boundingBox(), selection.count() > 1); + handles.display(rootEntityID, selection.boundingBox(), selection.count() > 1, selection.is2D()); otherEditor.setHandleOverlays(handles.overlays()); } startEditing(); @@ -729,7 +729,7 @@ function updateEditorGrabbing() { selection.select(intersectedEntityID); if (toolSelected === TOOL_SCALE) { - handles.display(rootEntityID, selection.boundingBox(), selection.count() > 1); + handles.display(rootEntityID, selection.boundingBox(), selection.count() > 1, selection.is2D()); otherEditor.setHandleOverlays(handles.overlays()); } else { handles.clear(); From ec6cae4105767401543339468bc37302d948aaff Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 11 Sep 2017 17:04:50 +1200 Subject: [PATCH 317/722] Fix physics "grabbable" setting being applied without gravity --- scripts/vr-edit/vr-edit.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 01de12c07c..38f72950f8 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1470,16 +1470,20 @@ break; case "setGravityOn": + // Dynamic is true if the entity has gravity or is grabbable. if (parameter) { physicsToolPhysics.gravity = { x: 0, y: physicsToolGravity, z: 0 }; physicsToolPhysics.dynamic = true; } else { physicsToolPhysics.gravity = Vec3.ZERO; - physicsToolPhysics.dynamic = false; + physicsToolPhysics.dynamic = physicsToolPhysics.userData.grabbableKey.grabbable === true; } break; case "setGrabOn": + // Dynamic is true if the entity has gravity or is grabbable. physicsToolPhysics.userData.grabbableKey.grabbable = parameter; + physicsToolPhysics.dynamic = parameter + || (physicsToolPhysics.gravity && Vec3.length(physicsToolPhysics.gravity) > 0); break; case "setCollideOn": if (parameter) { From be049009c8023d3d74805d259351544e132a600e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 11 Sep 2017 17:51:58 +1200 Subject: [PATCH 318/722] Improve sizing of scale handles --- scripts/vr-edit/modules/handles.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/handles.js b/scripts/vr-edit/modules/handles.js index 8e9c5694fa..4c11cdfd8d 100644 --- a/scripts/vr-edit/modules/handles.js +++ b/scripts/vr-edit/modules/handles.js @@ -171,9 +171,9 @@ Handles = function (side) { // display smaller in order to give comfortable depth cue. cameraPosition = Camera.position; boundingBoxVector = Vec3.subtract(boundingBox.center, Camera.position); - distanceMultiplier = DISTANCE_MULTIPLIER_MULTIPLIER - * Vec3.dot(Quat.getForward(Camera.orientation), boundingBoxVector) - / Math.sqrt(Vec3.length(boundingBoxVector)); + distanceMultiplier = DISTANCE_MULTIPLIER_MULTIPLIER * Math.sqrt( + Math.max(Vec3.length(boundingBoxVector, Vec3.length(boundingBox.dimensions) / 2)) + ); // Corner scale handles. // At right-most and opposite corners of bounding box. From 01e4bfc53e6c7f7054cb707e3c78be116c2ff233 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 11 Sep 2017 17:36:10 -0700 Subject: [PATCH 319/722] Trying to avoid calling getTransform on nestables from render thread --- interface/src/Application.cpp | 2 +- interface/src/ui/overlays/Base3DOverlay.cpp | 32 ++++++++++++++++++--- interface/src/ui/overlays/Base3DOverlay.h | 7 ++++- interface/src/ui/overlays/Web3DOverlay.cpp | 21 ++++++-------- interface/src/ui/overlays/Web3DOverlay.h | 4 --- libraries/render-utils/src/Model.cpp | 11 ++++--- 6 files changed, 51 insertions(+), 26 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3650c495f2..63e4069c01 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -427,7 +427,7 @@ public: // Don't actually crash in debug builds, in case this apparent deadlock is simply from // the developer actively debugging code #ifdef NDEBUG - deadlockDetectionCrash(); + // deadlockDetectionCrash(); #endif } } diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 010be802a9..56fbc8d873 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -191,13 +191,14 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { // Communicate changes to the renderItem if needed if (needRenderItemUpdate) { - auto itemID = getRenderItemID(); + + /* auto itemID = getRenderItemID(); if (render::Item::isValidID(itemID)) { render::ScenePointer scene = qApp->getMain3DScene(); render::Transaction transaction; transaction.updateItem(itemID); scene->enqueueTransaction(transaction); - } + }*/ } } @@ -259,16 +260,39 @@ void Base3DOverlay::locationChanged(bool tellPhysics) { // Force the actual update of the render transform now that we notify for the change // so it s captured for the time of rendering notifyRenderTransformChange(); - +/* auto itemID = getRenderItemID(); if (render::Item::isValidID(itemID)) { render::ScenePointer scene = qApp->getMain3DScene(); render::Transaction transaction; transaction.updateItem(itemID); scene->enqueueTransaction(transaction); - } + }*/ } void Base3DOverlay::parentDeleted() { qApp->getOverlays().deleteOverlay(getOverlayID()); } + +void Base3DOverlay::update(float duration) { + if (_renderTransformDirty) { + setRenderTransform(evalRenderTransform()); + auto itemID = getRenderItemID(); + if (render::Item::isValidID(itemID)) { + render::ScenePointer scene = qApp->getMain3DScene(); + render::Transaction transaction; + + transaction.updateItem(itemID); + scene->enqueueTransaction(transaction); + } + _renderTransformDirty = false; + } +} + +Transform Base3DOverlay::evalRenderTransform() const { + return getTransform(); +} + +void Base3DOverlay::setRenderTransform(const Transform& transform) { + _renderTransform = transform; +} diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index 3f57f2e577..fa26993724 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -52,8 +52,11 @@ public: virtual AABox getBounds() const override = 0; + void update(float deltatime) override; + void notifyRenderTransformChange() const { _renderTransformDirty = true; } - virtual Transform evalRenderTransform() const { return Transform(); } + virtual Transform evalRenderTransform() const; + void setRenderTransform(const Transform& transform); void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; @@ -70,6 +73,8 @@ protected: virtual void locationChanged(bool tellPhysics = true) override; virtual void parentDeleted() override; + mutable Transform _renderTransform; + float _lineWidth; bool _isSolid; bool _isDashedLine; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 9ad2cef443..490460fdb3 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -180,7 +180,7 @@ void Web3DOverlay::buildWebSurface() { void Web3DOverlay::update(float deltatime) { - if (_renderTransformDirty) { + /* if (_renderTransformDirty) { auto updateTransform = evalRenderTransform(); auto itemID = getRenderItemID(); if (render::Item::isValidID(itemID)) { @@ -194,17 +194,19 @@ void Web3DOverlay::update(float deltatime) { }); scene->enqueueTransaction(transaction); } - } + }*/ if (_webSurface) { // update globalPosition _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); } + + Billboard3DOverlay::update(deltatime); + } Transform Web3DOverlay::evalRenderTransform() const { - if (_renderTransformDirty) { - _updateTransform = getTransform(); + auto transform = getTransform(); // FIXME: applyTransformTo causes tablet overlay to detach from tablet entity. // Perhaps rather than deleting the following code it should be run only if isFacingAvatar() is true? @@ -214,15 +216,10 @@ Transform Web3DOverlay::evalRenderTransform() const { */ if (glm::length2(getDimensions()) != 1.0f) { - _updateTransform.postScale(vec3(getDimensions(), 1.0f)); + transform.postScale(vec3(getDimensions(), 1.0f)); } - _renderTransformDirty = false; - } - return _updateTransform; -} - -void Web3DOverlay::setRenderTransform(const Transform& transform) { - _renderTransform = transform; + + return transform; } QString Web3DOverlay::pickURL() { diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index ef40d88333..7acaf5a430 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -38,7 +38,6 @@ public: virtual void update(float deltatime) override; Transform evalRenderTransform() const override; - void setRenderTransform(const Transform& transform); QObject* getEventHandler(); void setProxyWindow(QWindow* proxyWindow); @@ -95,9 +94,6 @@ private: std::map _activeTouchPoints; QTouchDevice _touchDevice; - mutable Transform _updateTransform; - mutable Transform _renderTransform; - uint8_t _desiredMaxFPS { 10 }; uint8_t _currentMaxFPS { 0 }; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 42bb91ce94..5fc7c7a1b1 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -235,15 +235,18 @@ void Model::updateRenderItems() { uint32_t deleteGeometryCounter = self->_deleteGeometryCounter; + Transform modelTransform = self->getTransform(); + // Transform modelTransform = model->getTransform(); + modelTransform.setScale(glm::vec3(1.0f)); + render::Transaction transaction; foreach (auto itemID, self->_modelMeshRenderItemsMap.keys()) { - transaction.updateItem(itemID, [deleteGeometryCounter](ModelMeshPartPayload& data) { + transaction.updateItem(itemID, [deleteGeometryCounter, modelTransform](ModelMeshPartPayload& data) { ModelPointer model = data._model.lock(); if (model && model->isLoaded()) { // Ensure the model geometry was not reset between frames if (deleteGeometryCounter == model->_deleteGeometryCounter) { - Transform modelTransform = model->getTransform(); - modelTransform.setScale(glm::vec3(1.0f)); + const Model::MeshState& state = model->getMeshState(data._meshIndex); Transform renderTransform = modelTransform; @@ -259,7 +262,7 @@ void Model::updateRenderItems() { // collision mesh does not share the same unit scale as the FBX file's mesh: only apply offset Transform collisionMeshOffset; collisionMeshOffset.setIdentity(); - Transform modelTransform = self->getTransform(); + // Transform modelTransform = self->getTransform(); foreach(auto itemID, self->_collisionRenderItemsMap.keys()) { transaction.updateItem(itemID, [modelTransform, collisionMeshOffset](MeshPartPayload& data) { // update the model transform for this render item. From 3669e66619f7b8034dc748c15730b231a27c75df Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Tue, 12 Sep 2017 00:02:54 -0700 Subject: [PATCH 320/722] trying to update the transform at the right time in overlays --- interface/src/ui/overlays/Base3DOverlay.cpp | 3 +- .../src/ui/overlays/Billboard3DOverlay.cpp | 1 + interface/src/ui/overlays/ModelOverlay.cpp | 48 ++++++++++++------- interface/src/ui/overlays/Planar3DOverlay.cpp | 10 ++++ interface/src/ui/overlays/Planar3DOverlay.h | 4 +- interface/src/ui/overlays/Web3DOverlay.cpp | 48 ------------------- interface/src/ui/overlays/Web3DOverlay.h | 2 - 7 files changed, 48 insertions(+), 68 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 56fbc8d873..af6206e819 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -191,8 +191,9 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { // Communicate changes to the renderItem if needed if (needRenderItemUpdate) { + notifyRenderTransformChange(); - /* auto itemID = getRenderItemID(); + /*auto itemID = getRenderItemID(); if (render::Item::isValidID(itemID)) { render::ScenePointer scene = qApp->getMain3DScene(); render::Transaction transaction; diff --git a/interface/src/ui/overlays/Billboard3DOverlay.cpp b/interface/src/ui/overlays/Billboard3DOverlay.cpp index f5668caa71..a333df0821 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.cpp +++ b/interface/src/ui/overlays/Billboard3DOverlay.cpp @@ -45,3 +45,4 @@ bool Billboard3DOverlay::applyTransformTo(Transform& transform, bool force) { } return transformChanged; } + diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 0bed07891e..f65faaca12 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -59,20 +59,6 @@ void ModelOverlay::update(float deltatime) { _model->simulate(deltatime); } _isLoaded = _model->isActive(); -} - -bool ModelOverlay::addToScene(Overlay::Pointer overlay, const render::ScenePointer& scene, render::Transaction& transaction) { - Volume3DOverlay::addToScene(overlay, scene, transaction); - _model->addToScene(scene, transaction); - return true; -} - -void ModelOverlay::removeFromScene(Overlay::Pointer overlay, const render::ScenePointer& scene, render::Transaction& transaction) { - Volume3DOverlay::removeFromScene(overlay, scene, transaction); - _model->removeFromScene(scene, transaction); -} - -void ModelOverlay::render(RenderArgs* args) { // check to see if when we added our model to the scene they were ready, if they were not ready, then // fix them up in the scene @@ -89,6 +75,35 @@ void ModelOverlay::render(RenderArgs* args) { scene->enqueueTransaction(transaction); } +bool ModelOverlay::addToScene(Overlay::Pointer overlay, const render::ScenePointer& scene, render::Transaction& transaction) { + Volume3DOverlay::addToScene(overlay, scene, transaction); + _model->addToScene(scene, transaction); + return true; +} + +void ModelOverlay::removeFromScene(Overlay::Pointer overlay, const render::ScenePointer& scene, render::Transaction& transaction) { + Volume3DOverlay::removeFromScene(overlay, scene, transaction); + _model->removeFromScene(scene, transaction); +} + +void ModelOverlay::render(RenderArgs* args) { +/* + // check to see if when we added our model to the scene they were ready, if they were not ready, then + // fix them up in the scene + render::ScenePointer scene = qApp->getMain3DScene(); + render::Transaction transaction; + if (_model->needsFixupInScene()) { + _model->removeFromScene(scene, transaction); + _model->addToScene(scene, transaction); + } + + _model->setVisibleInScene(_visible, scene); + _model->setLayeredInFront(getDrawInFront(), scene); + + scene->enqueueTransaction(transaction); + */ +} + void ModelOverlay::setProperties(const QVariantMap& properties) { auto origPosition = getPosition(); auto origRotation = getRotation(); @@ -280,11 +295,12 @@ ModelOverlay* ModelOverlay::createClone() const { void ModelOverlay::locationChanged(bool tellPhysics) { Base3DOverlay::locationChanged(tellPhysics); - +/* if (_model && _model->isActive()) { _model->setRotation(getRotation()); _model->setTranslation(getPosition()); - } + }*/ + _updateModel = true; } QString ModelOverlay::getName() const { diff --git a/interface/src/ui/overlays/Planar3DOverlay.cpp b/interface/src/ui/overlays/Planar3DOverlay.cpp index 58d72b100b..a979120dc6 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.cpp +++ b/interface/src/ui/overlays/Planar3DOverlay.cpp @@ -66,3 +66,13 @@ bool Planar3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::ve // FIXME - face and surfaceNormal not being returned return findRayRectangleIntersection(origin, direction, getRotation(), getPosition(), getDimensions(), distance); } + +Transform Planar3DOverlay::evalRenderTransform() const { + auto transform = getTransform(); + + if (glm::length2(getDimensions()) != 1.0f) { + transform.postScale(vec3(getDimensions(), 1.0f)); + } + + return transform; +} \ No newline at end of file diff --git a/interface/src/ui/overlays/Planar3DOverlay.h b/interface/src/ui/overlays/Planar3DOverlay.h index 8127d4ebb9..9e4babae8d 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.h +++ b/interface/src/ui/overlays/Planar3DOverlay.h @@ -32,7 +32,9 @@ public: virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) override; - + + Transform evalRenderTransform() const override; + protected: glm::vec2 _dimensions; }; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 490460fdb3..a1f68add4a 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -180,22 +180,6 @@ void Web3DOverlay::buildWebSurface() { void Web3DOverlay::update(float deltatime) { - /* if (_renderTransformDirty) { - auto updateTransform = evalRenderTransform(); - auto itemID = getRenderItemID(); - if (render::Item::isValidID(itemID)) { - render::ScenePointer scene = qApp->getMain3DScene(); - render::Transaction transaction; - transaction.updateItem(itemID, [updateTransform](Overlay& data) { - auto web3D = dynamic_cast(&data); - if (web3D) { - web3D->setRenderTransform(updateTransform);// evalRenderTransform(); - } - }); - scene->enqueueTransaction(transaction); - } - }*/ - if (_webSurface) { // update globalPosition _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); @@ -205,23 +189,6 @@ void Web3DOverlay::update(float deltatime) { } -Transform Web3DOverlay::evalRenderTransform() const { - auto transform = getTransform(); - - // FIXME: applyTransformTo causes tablet overlay to detach from tablet entity. - // Perhaps rather than deleting the following code it should be run only if isFacingAvatar() is true? - /* - applyTransformTo(transform, true); - setTransform(transform); - */ - - if (glm::length2(getDimensions()) != 1.0f) { - transform.postScale(vec3(getDimensions(), 1.0f)); - } - - return transform; -} - QString Web3DOverlay::pickURL() { QUrl sourceUrl(_url); if (sourceUrl.scheme() == "http" || sourceUrl.scheme() == "https" || @@ -341,20 +308,6 @@ void Web3DOverlay::render(RenderArgs* args) { vec2 halfSize = getSize() / 2.0f; vec4 color(toGlm(getColor()), getAlpha()); - /* - Transform transform = getTransform(); - - // FIXME: applyTransformTo causes tablet overlay to detach from tablet entity. - // Perhaps rather than deleting the following code it should be run only if isFacingAvatar() is true? - /* - applyTransformTo(transform, true); - setTransform(transform); - */ - /* - if (glm::length2(getDimensions()) != 1.0f) { - transform.postScale(vec3(getDimensions(), 1.0f)); - } - */ if (!_texture) { _texture = gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda()); @@ -369,7 +322,6 @@ void Web3DOverlay::render(RenderArgs* args) { Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; batch.setResourceTexture(0, _texture); - // batch.setModelTransform(transform); batch.setModelTransform(_renderTransform); auto geometryCache = DependencyManager::get(); if (color.a < OPAQUE_ALPHA_THRESHOLD) { diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 7acaf5a430..2eae7f33da 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -37,8 +37,6 @@ public: virtual void update(float deltatime) override; - Transform evalRenderTransform() const override; - QObject* getEventHandler(); void setProxyWindow(QWindow* proxyWindow); void handlePointerEvent(const PointerEvent& event); From afb47dcfb232108261c9968e5d12cc72d62982d7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 12 Sep 2017 20:43:11 +1200 Subject: [PATCH 321/722] Add undo and redo buttons to Tools menu --- scripts/vr-edit/assets/tools/redo-icon.svg | 7 ++ scripts/vr-edit/assets/tools/redo-label.svg | 10 ++ scripts/vr-edit/assets/tools/undo-icon.svg | 7 ++ scripts/vr-edit/assets/tools/undo-label.svg | 10 ++ scripts/vr-edit/modules/toolsMenu.js | 115 ++++++++++++++++++-- 5 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 scripts/vr-edit/assets/tools/redo-icon.svg create mode 100644 scripts/vr-edit/assets/tools/redo-label.svg create mode 100644 scripts/vr-edit/assets/tools/undo-icon.svg create mode 100644 scripts/vr-edit/assets/tools/undo-label.svg diff --git a/scripts/vr-edit/assets/tools/redo-icon.svg b/scripts/vr-edit/assets/tools/redo-icon.svg new file mode 100644 index 0000000000..bd0cea1330 --- /dev/null +++ b/scripts/vr-edit/assets/tools/redo-icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/scripts/vr-edit/assets/tools/redo-label.svg b/scripts/vr-edit/assets/tools/redo-label.svg new file mode 100644 index 0000000000..19ae558bb9 --- /dev/null +++ b/scripts/vr-edit/assets/tools/redo-label.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/scripts/vr-edit/assets/tools/undo-icon.svg b/scripts/vr-edit/assets/tools/undo-icon.svg new file mode 100644 index 0000000000..566de28906 --- /dev/null +++ b/scripts/vr-edit/assets/tools/undo-icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/scripts/vr-edit/assets/tools/undo-label.svg b/scripts/vr-edit/assets/tools/undo-label.svg new file mode 100644 index 0000000000..ca749f2765 --- /dev/null +++ b/scripts/vr-edit/assets/tools/undo-label.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 43056954e4..240dbfcfbc 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -252,8 +252,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Relative to menuButton. type: "image", properties: { - url: "../assets/tools/tool-label.svg", - scale: 0.0152, localPosition: { x: 0, y: UIT.dimensions.menuButtonSublabelYOffset, @@ -1660,6 +1658,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { scale: 0.0241 } }, + sublabel: { + properties: { + url: "../assets/tools/tool-label.svg", + scale: 0.0152 + } + }, title: { url: "../assets/tools/color-tool-heading.svg", scale: 0.0631 @@ -1693,6 +1697,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { scale: 0.0311 } }, + sublabel: { + properties: { + url: "../assets/tools/tool-label.svg", + scale: 0.0152 + } + }, title: { url: "../assets/tools/stretch-tool-heading.svg", scale: 0.0737 @@ -1725,6 +1735,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { scale: 0.0231 } }, + sublabel: { + properties: { + url: "../assets/tools/tool-label.svg", + scale: 0.0152 + } + }, title: { url: "../assets/tools/clone-tool-heading.svg", scale: 0.0621 @@ -1757,6 +1773,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { scale: 0.0250 } }, + sublabel: { + properties: { + url: "../assets/tools/tool-label.svg", + scale: 0.0152 + } + }, title: { url: "../assets/tools/group-tool-heading.svg", scale: 0.0647 @@ -1789,6 +1811,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { scale: 0.0297 } }, + sublabel: { + properties: { + url: "../assets/tools/tool-label.svg", + scale: 0.0152 + } + }, title: { url: "../assets/tools/physics-tool-heading.svg", scale: 0.0712 @@ -1821,6 +1849,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { scale: 0.0254 } }, + sublabel: { + properties: { + url: "../assets/tools/tool-label.svg", + scale: 0.0152 + } + }, title: { url: "../assets/tools/delete-tool-heading.svg", scale: 0.0653 @@ -1829,6 +1863,58 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { callback: { method: "deleteTool" } + }, + { + id: "undoButton", + type: "menuButton", + properties: { + localPosition: { + x: MENU_ITEM_XS[2], + y: MENU_ITEM_YS[2], + z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2 + } + }, + icon: { + properties: { + url: "../assets/tools/undo-icon.svg", + dimensions: { x: 0.0180, y: 0.0186 } + } + }, + label: { + properties: { + url: "../assets/tools/undo-label.svg", + scale: 0.0205 + } + }, + callback: { + method: "undoAction" + } + }, + { + id: "redoButton", + type: "menuButton", + properties: { + localPosition: { + x: MENU_ITEM_XS[3], + y: MENU_ITEM_YS[2], + z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2 + } + }, + icon: { + properties: { + url: "../assets/tools/redo-icon.svg", + dimensions: { x: 0.0180, y: 0.0186 } + } + }, + label: { + properties: { + url: "../assets/tools/redo-label.svg", + scale: 0.0192 + } + }, + callback: { + method: "redoAction" + } } ], COLOR_TOOL = 0, // Indexes of corresponding MENU_ITEMS item. @@ -1917,10 +2003,14 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { function getIconInfo(tool) { // Provides details of tool icon, label, and sublabel images for the specified tool. + var sublabelProperties; + + sublabelProperties = Object.clone(UI_ELEMENTS.menuButton.sublabel); + sublabelProperties = Object.merge(sublabelProperties, MENU_ITEMS[tool].sublabel); return { icon: MENU_ITEMS[tool].icon, label: MENU_ITEMS[tool].label, - sublabel: UI_ELEMENTS.menuButton.sublabel + sublabel: sublabelProperties }; } @@ -1977,13 +2067,16 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { menuLabelOverlays.push(overlayID); // Sublabel. - properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.sublabel.type].properties); - properties = Object.merge(properties, UI_ELEMENTS.menuButton.sublabel.properties); - properties.url = Script.resolvePath(properties.url); - properties.visible = isVisible; - properties.parentID = itemID; - overlayID = Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.sublabel.type].overlay, properties); - menuLabelOverlays.push(overlayID); + if (MENU_ITEMS[i].sublabel) { + properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.sublabel.type].properties); + properties = Object.merge(properties, UI_ELEMENTS.menuButton.sublabel.properties); + properties = Object.merge(properties, MENU_ITEMS[i].sublabel.properties); + properties.url = Script.resolvePath(properties.url); + properties.visible = isVisible; + properties.parentID = itemID; + overlayID = Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.sublabel.type].overlay, properties); + menuLabelOverlays.push(overlayID); + } } } @@ -3086,7 +3179,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }; // Button press actions. - if (intersectionOverlays === menuOverlays) { + if (intersectionOverlays === menuOverlays && intersectionItems[intersectedItem].toolOptions) { openOptions(intersectionItems[intersectedItem]); } if (intersectionItems[intersectedItem].command) { From 68c4a2f7f60d3617c3b187f44f7bf44a2f5ddd55 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 12 Sep 2017 20:53:15 +1200 Subject: [PATCH 322/722] Add History object to handle history --- scripts/vr-edit/modules/feedback.js | 6 ++ scripts/vr-edit/modules/history.js | 99 +++++++++++++++++++++++++++++ scripts/vr-edit/vr-edit.js | 18 ++++++ 3 files changed, 123 insertions(+) create mode 100644 scripts/vr-edit/modules/history.js diff --git a/scripts/vr-edit/modules/feedback.js b/scripts/vr-edit/modules/feedback.js index 23a82e66aa..e87a0867fa 100644 --- a/scripts/vr-edit/modules/feedback.js +++ b/scripts/vr-edit/modules/feedback.js @@ -23,6 +23,8 @@ Feedback = (function () { CREATE_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/create.wav")), EQUIP_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/equip.wav")), ERROR_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/error.wav")), + UNDO_SOUND = DROP_SOUND, // TODO + REDO_SOUND = DROP_SOUND, // TODO FEEDBACK_PARAMETERS = { DROP_TOOL: { sound: DROP_SOUND, volume: 0.3, haptic: 0.75 }, @@ -35,6 +37,8 @@ Feedback = (function () { EQUIP_TOOL: { sound: EQUIP_SOUND, volume: 0.3, haptic: 0.6 }, APPLY_PROPERTY: { sound: null, volume: 0, haptic: 0.3 }, APPLY_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 }, + UNDO_ACTION: { sound: UNDO_SOUND, volume: 0.2, haptic: 0.2 }, // TODO + REDO_ACTION: { sound: REDO_SOUND, volume: 0.2, haptic: 0.2 }, // TODO GENERAL_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 } }, @@ -68,6 +72,8 @@ Feedback = (function () { EQUIP_TOOL: "EQUIP_TOOL", APPLY_PROPERTY: "APPLY_PROPERTY", APPLY_ERROR: "APPLY_ERROR", + UNDO_ACTION: "UNDO_ACTION", + REDO_ACTION: "REDO_ACTION", GENERAL_ERROR: "GENERAL_ERROR", play: play }; diff --git a/scripts/vr-edit/modules/history.js b/scripts/vr-edit/modules/history.js new file mode 100644 index 0000000000..d1c03c218a --- /dev/null +++ b/scripts/vr-edit/modules/history.js @@ -0,0 +1,99 @@ +// +// history.js +// +// Created by David Rowe on 12 Sep 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global History */ + +History = (function () { + // Provides undo facility. + // Global object. + + "use strict"; + + var history = [ + /* + { + undoData: { + setProperties: [ + { + entityID: , + properties: { : , : , ... } + } + ], + createEntities: [ + { + entityID: , + properties: { : , : , ... } + } + ], + deleteEntities: [ + { + entityID: , + properties: { : , : , ... } + } + ] + }, + redoData: { + "" + } + } + */ + ], + MAX_HISTORY_ITEMS = 100, + undoPosition = -1; // The next history item to undo; the next history item to redo = undoIndex + 1. + + function push(undoData, redoData) { + // Wipe any redo history after current undo position. + if (undoPosition < history.length - 1) { + history.splice(undoPosition + 1, history.length - undoPosition - 1); + } + + // Limit the number of history items. + if (history.length >= MAX_HISTORY_ITEMS) { + history.splice(0, history.length - MAX_HISTORY_ITEMS + 1); + } + + history.push({ undoData: undoData, redoData: redoData }); + undoPosition += 1; + } + + function hasUndo() { + return undoPosition > -1; + } + + function hasRedo() { + return undoPosition < history.length - 1; + } + + function undo() { + if (undoPosition > -1) { + + // TODO + + undoPosition -= 1; + } + } + + function redo() { + if (undoPosition < history.length - 1) { + + // TODO + + undoPosition += 1; + } + } + + return { + push: push, + hasUndo: hasUndo, + hasRedo: hasRedo, + undo: undo, + redo: redo + }; +}()); diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 38f72950f8..293e85a775 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -83,6 +83,7 @@ Script.include("./modules/hand.js"); Script.include("./modules/handles.js"); Script.include("./modules/highlights.js"); + Script.include("./modules/history.js"); Script.include("./modules/laser.js"); Script.include("./modules/selection.js"); Script.include("./modules/toolIcon.js"); @@ -1535,6 +1536,23 @@ } break; + case "undoAction": + if (History.hasUndo()) { + Feedback.play(dominantHand, Feedback.UNDO_ACTION) + History.undo(); + } else { + Feedback.play(dominantHand, Feedback.GENERAL_ERROR); + } + break; + case "redoAction": + if (History.hasRedo()) { + Feedback.play(dominantHand, Feedback.REDO_ACTION) + History.redo(); + } else { + Feedback.play(dominantHand, Feedback.GENERAL_ERROR); + } + break; + default: log("ERROR: Unexpected command in onUICommand(): " + command + ", " + parameter); } From 3bb3e57f71ba943d40342fd5872387bd44a768d1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 12 Sep 2017 20:59:03 +1200 Subject: [PATCH 323/722] Add undo/redo for creating entities --- scripts/vr-edit/modules/createPalette.js | 12 ++++- scripts/vr-edit/modules/history.js | 60 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 8e46783979..44e4373476 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -317,7 +317,9 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { isTriggerClicked, properties, CREATE_OFFSET = { x: 0, y: 0.05, z: -0.02 }, - INVERSE_HAND_BASIS_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }); + INVERSE_HAND_BASIS_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }), + entityID, + createdEntities; itemIndex = paletteItemOverlays.indexOf(intersectionOverlayID); @@ -350,7 +352,13 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { Vec3.multiplyQbyV(controlHand.orientation(), Vec3.sum({ x: 0, y: properties.dimensions.z / 2, z: 0 }, CREATE_OFFSET))); properties.rotation = Quat.multiply(controlHand.orientation(), INVERSE_HAND_BASIS_ROTATION); - Entities.addEntity(properties); + entityID = Entities.addEntity(properties); + if (entityID !== Uuid.NULL) { + createdEntities = [{ entityID: entityID, properties: properties }]; + History.push({ deleteEntities: createdEntities }, { createEntities: createdEntities }); + } else { + Feedback.play(otherSide, Feedback.GENERAL_ERROR); + } // Lower and unhighlight item. Overlays.editOverlay(paletteItemHoverOverlays[itemIndex], { diff --git a/scripts/vr-edit/modules/history.js b/scripts/vr-edit/modules/history.js index d1c03c218a..a12d804e16 100644 --- a/scripts/vr-edit/modules/history.js +++ b/scripts/vr-edit/modules/history.js @@ -63,6 +63,41 @@ History = (function () { undoPosition += 1; } + function updateEntityIDs(oldEntityID, newEntityID) { + // Replace oldEntityID value with newEntityID in history. + var i, + length; + + function updateEntityIDsInProperty(properties) { + var i, + length; + + if (properties) { + for (i = 0, length = properties.length; i < length; i += 1) { + if (properties[i].entityID === oldEntityID) { + properties[i].entityID = newEntityID; + } + if (properties[i].properties && properties[i].properties.parentID === oldEntityID) { + properties[i].properties.parentID = newEntityID; + } + } + } + } + + for (i = 0, length = history.length; i < length; i += 1) { + if (history[i].undoData) { + updateEntityIDsInProperty(history[i].undoData.setProperties); + updateEntityIDsInProperty(history[i].undoData.createEntities); + updateEntityIDsInProperty(history[i].undoData.deleteEntities); + } + if (history[i].redoData) { + updateEntityIDsInProperty(history[i].redoData.setProperties); + updateEntityIDsInProperty(history[i].redoData.createEntities); + updateEntityIDsInProperty(history[i].redoData.deleteEntities); + } + } + } + function hasUndo() { return undoPosition > -1; } @@ -72,19 +107,44 @@ History = (function () { } function undo() { + var undoData, + i, + length; + if (undoPosition > -1) { + undoData = history[undoPosition].undoData; // TODO + if (undoData.deleteEntities) { + for (i = 0, length = undoData.deleteEntities.length; i < length; i += 1) { + Entities.deleteEntity(undoData.deleteEntities[i].entityID); + } + } + undoPosition -= 1; } } function redo() { + var redoData, + entityID, + i, + length; + + if (undoPosition < history.length - 1) { + redoData = history[undoPosition + 1].redoData; // TODO + if (redoData.createEntities) { + for (i = 0, length = redoData.createEntities.length; i < length; i += 1) { + entityID = Entities.addEntity(redoData.createEntities[i].properties); + updateEntityIDs(redoData.createEntities[i].entityID, entityID); + } + } + undoPosition += 1; } } From 93a5cae7d2d81d4f84b419c904d4419ef3c329d8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 12 Sep 2017 21:39:30 +1200 Subject: [PATCH 324/722] Add undo/redo for deleting entities --- scripts/vr-edit/modules/history.js | 14 ++++++++++++++ scripts/vr-edit/modules/selection.js | 21 ++++++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/scripts/vr-edit/modules/history.js b/scripts/vr-edit/modules/history.js index a12d804e16..b31009ab2f 100644 --- a/scripts/vr-edit/modules/history.js +++ b/scripts/vr-edit/modules/history.js @@ -108,6 +108,7 @@ History = (function () { function undo() { var undoData, + entityID, i, length; @@ -116,6 +117,13 @@ History = (function () { // TODO + if (undoData.createEntities) { + for (i = 0, length = undoData.createEntities.length; i < length; i += 1) { + entityID = Entities.addEntity(undoData.createEntities[i].properties); + updateEntityIDs(undoData.createEntities[i].entityID, entityID); + } + } + if (undoData.deleteEntities) { for (i = 0, length = undoData.deleteEntities.length; i < length; i += 1) { Entities.deleteEntity(undoData.deleteEntities[i].entityID); @@ -145,6 +153,12 @@ History = (function () { } } + if (redoData.deleteEntities) { + for (i = 0, length = redoData.deleteEntities.length; i < length; i += 1) { + Entities.deleteEntity(redoData.deleteEntities[i].entityID); + } + } + undoPosition += 1; } } diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 8023775242..be9ccd689c 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -15,7 +15,8 @@ Selection = function (side) { "use strict"; - var selection = [], + var selection = [], // Subset of properties to provide externally. + selectionProperties = [], // Full set of properties for history. intersectedEntityID = null, intersectedEntityIndex, rootEntityID = null, @@ -37,18 +38,17 @@ Selection = function (side) { return new Selection(side); } - function traverseEntityTree(id, result) { + function traverseEntityTree(id, selection, selectionProperties) { // Recursively traverses tree of entities and their children, gather IDs and properties. // The root entity is always the first entry. var children, properties, - SELECTION_PROPERTIES = ["type", "position", "registrationPoint", "rotation", "dimensions", "parentID", - "localPosition", "dynamic", "collisionless", "userData"], i, length; - properties = Entities.getEntityProperties(id, SELECTION_PROPERTIES); - result.push({ + properties = Entities.getEntityProperties(id); + delete properties.entityID; + selection.push({ id: id, type: properties.type, position: properties.position, @@ -61,15 +61,16 @@ Selection = function (side) { collisionless: properties.collisionless, userData: properties.userData }); + selectionProperties.push({ entityID: id, properties: properties }); if (id === intersectedEntityID) { - intersectedEntityIndex = result.length - 1; + intersectedEntityIndex = selection.length - 1; } children = Entities.getChildrenIDs(id); for (i = 0, length = children.length; i < length; i += 1) { if (Entities.getNestableType(children[i]) === ENTITY_TYPE) { - traverseEntityTree(children[i], result); + traverseEntityTree(children[i], selection, selectionProperties); } } } @@ -90,7 +91,8 @@ Selection = function (side) { // Find all children. selection = []; - traverseEntityTree(rootEntityID, selection); + selectionProperties = []; + traverseEntityTree(rootEntityID, selection, selectionProperties); } function getIntersectedEntityID() { @@ -517,6 +519,7 @@ Selection = function (side) { function deleteEntities() { if (rootEntityID) { + History.push({ createEntities: selectionProperties }, { deleteEntities: [{ entityID: rootEntityID }] }); Entities.deleteEntity(rootEntityID); // Children are automatically deleted. clear(); } From 922b712b35510d5411750686da0135eba41937fe Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 12 Sep 2017 21:58:09 +1200 Subject: [PATCH 325/722] Add undo/redo for coloring entities --- scripts/vr-edit/modules/history.js | 12 ++++++++++-- scripts/vr-edit/modules/selection.js | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/modules/history.js b/scripts/vr-edit/modules/history.js index b31009ab2f..6fb5416df3 100644 --- a/scripts/vr-edit/modules/history.js +++ b/scripts/vr-edit/modules/history.js @@ -115,7 +115,11 @@ History = (function () { if (undoPosition > -1) { undoData = history[undoPosition].undoData; - // TODO + if (undoData.setProperties) { + for (i = 0, length = undoData.setProperties.length; i < length; i += 1) { + Entities.editEntity(undoData.setProperties[i].entityID, undoData.setProperties[i].properties); + } + } if (undoData.createEntities) { for (i = 0, length = undoData.createEntities.length; i < length; i += 1) { @@ -144,7 +148,11 @@ History = (function () { if (undoPosition < history.length - 1) { redoData = history[undoPosition + 1].redoData; - // TODO + if (redoData.setProperties) { + for (i = 0, length = redoData.setProperties.length; i < length; i += 1) { + Entities.editEntity(redoData.setProperties[i].entityID, redoData.setProperties[i].properties); + } + } if (redoData.createEntities) { for (i = 0, length = redoData.createEntities.length; i < length; i += 1) { diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index be9ccd689c..0337749a8f 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -424,25 +424,36 @@ Selection = function (side) { // Entities without a color property simply ignore the edit. var properties, isOK = false, + undoData = [], + redoData = [], i, length; if (isApplyToAll) { for (i = 0, length = selection.length; i < length; i += 1) { - properties = Entities.getEntityProperties(selection[i].id, "color"); + properties = Entities.getEntityProperties(selection[i].id, ["type", "color"]); if (ENTITY_TYPES_WITH_COLOR.indexOf(properties.type) !== -1) { Entities.editEntity(selection[i].id, { color: color }); + undoData.push({ entityID: intersectedEntityID, properties: { color: properties.color } }); + redoData.push({ entityID: intersectedEntityID, properties: { color: color } }); isOK = true; } } + if (undoData.length > 0) { + History.push(undoData, redoData); + } } else { - properties = Entities.getEntityProperties(intersectedEntityID, "type"); + properties = Entities.getEntityProperties(intersectedEntityID, ["type", "color"]); if (ENTITY_TYPES_WITH_COLOR.indexOf(properties.type) !== -1) { Entities.editEntity(intersectedEntityID, { color: color }); + History.push( + { setProperties: [{ entityID: intersectedEntityID, properties: { color: properties.color } }] }, + { setProperties: [{ entityID: intersectedEntityID, properties: { color: color } }] } + ); isOK = true; } } From d718ea6928b4537b151174fc1d46a2daa42df096 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 13 Sep 2017 09:17:28 +1200 Subject: [PATCH 326/722] Add undo/redo for move --- scripts/vr-edit/modules/selection.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 0337749a8f..c0881bbf01 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -29,6 +29,8 @@ Selection = function (side) { scaleOrientation, scaleRootOffset, scaleRootOrientation, + startPosition, + startOrientation, ENTITY_TYPE = "entity", ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "PolyLine", "PolyVox"], ENTITY_TYPES_2D = ["Text", "Web"]; @@ -227,6 +229,10 @@ Selection = function (side) { function startEditing() { var i; + // Remember start properties for history entry. + startPosition = selection[0].position; + startOrientation = selection[0].rotation; + // Disable entity set's physics. //for (i = 0, count = selection.length; i < count; i += 1) { for (i = selection.length - 1; i >= 0; i -= 1) { @@ -254,6 +260,20 @@ Selection = function (side) { }); } + // Add history entry. + History.push( + { + setProperties: [ + { entityID: rootEntityID, properties: { position: startPosition, rotation: startOrientation } } + ] + }, + { + setProperties: [ + { entityID: rootEntityID, properties: { position: rootPosition, rotation: rootOrientation } } + ] + } + ); + // Kick off physics if necessary. if (selection.length > 0 && selection[0].dynamic) { kickPhysics(selection[0].id); From 10b1e3f561d5994071c51accdf7ee50dabfdf47d Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 12 Sep 2017 14:35:25 -0700 Subject: [PATCH 327/722] Trying to implement differnet solution to the transform updates problem and debug --- interface/src/ui/overlays/Base3DOverlay.cpp | 54 ++++++++++++++++----- interface/src/ui/overlays/Base3DOverlay.h | 2 +- libraries/render-utils/src/Model.cpp | 10 ++-- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index af6206e819..22448cc719 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -191,15 +191,15 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { // Communicate changes to the renderItem if needed if (needRenderItemUpdate) { - notifyRenderTransformChange(); + // notifyRenderTransformChange(); - /*auto itemID = getRenderItemID(); + auto itemID = getRenderItemID(); if (render::Item::isValidID(itemID)) { render::ScenePointer scene = qApp->getMain3DScene(); render::Transaction transaction; transaction.updateItem(itemID); scene->enqueueTransaction(transaction); - }*/ + } } } @@ -277,19 +277,51 @@ void Base3DOverlay::parentDeleted() { void Base3DOverlay::update(float duration) { if (_renderTransformDirty) { - setRenderTransform(evalRenderTransform()); - auto itemID = getRenderItemID(); - if (render::Item::isValidID(itemID)) { - render::ScenePointer scene = qApp->getMain3DScene(); - render::Transaction transaction; + auto self = this; + // queue up this work for later processing, at the end of update and just before rendering. + // the application will ensure only the last lambda is actually invoked. + /* void* key = (void*)this; + std::weak_ptr weakSelf = shared_from_this(); + AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [weakSelf]() { + // do nothing, if the model has already been destroyed. + auto spatiallyNestableSelf = weakSelf.lock(); + if (!spatiallyNestableSelf) { + return; + } + auto self = std::dynamic_pointer_cast(spatiallyNestableSelf); + */ +#ifdef UpdateInMain + self->setRenderTransform(self->evalRenderTransform()); +#else + auto renderTransform = self->evalRenderTransform(); +#endif + auto itemID = self->getRenderItemID(); - transaction.updateItem(itemID); - scene->enqueueTransaction(transaction); - } + if (render::Item::isValidID(itemID)) { + render::ScenePointer scene = qApp->getMain3DScene(); + render::Transaction transaction; + +#ifdef UpdateInMain + transaction.updateItem(itemID); +#else + transaction.updateItem(itemID, [renderTransform](Overlay& data) { + auto overlay3D = dynamic_cast(&data); + if (overlay3D) { + overlay3D->setRenderTransform(renderTransform);// evalRenderTransform(); + } + }); +#endif + scene->enqueueTransaction(transaction); + } + // }); _renderTransformDirty = false; } } +void Base3DOverlay::notifyRenderTransformChange() const { + _renderTransformDirty = true; +} + Transform Base3DOverlay::evalRenderTransform() const { return getTransform(); } diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index fa26993724..b40727a807 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -54,7 +54,7 @@ public: void update(float deltatime) override; - void notifyRenderTransformChange() const { _renderTransformDirty = true; } + void notifyRenderTransformChange() const; virtual Transform evalRenderTransform() const; void setRenderTransform(const Transform& transform); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 5fc7c7a1b1..9e552859e3 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -235,18 +235,20 @@ void Model::updateRenderItems() { uint32_t deleteGeometryCounter = self->_deleteGeometryCounter; - Transform modelTransform = self->getTransform(); + // Transform modelTransform = self->getTransform(); // Transform modelTransform = model->getTransform(); - modelTransform.setScale(glm::vec3(1.0f)); + // modelTransform.setScale(glm::vec3(1.0f)); render::Transaction transaction; foreach (auto itemID, self->_modelMeshRenderItemsMap.keys()) { - transaction.updateItem(itemID, [deleteGeometryCounter, modelTransform](ModelMeshPartPayload& data) { + transaction.updateItem(itemID, [deleteGeometryCounter /*, modelTransform*/](ModelMeshPartPayload& data) { ModelPointer model = data._model.lock(); if (model && model->isLoaded()) { // Ensure the model geometry was not reset between frames if (deleteGeometryCounter == model->_deleteGeometryCounter) { + Transform modelTransform = model->getTransform(); + modelTransform.setScale(glm::vec3(1.0f)); const Model::MeshState& state = model->getMeshState(data._meshIndex); Transform renderTransform = modelTransform; @@ -262,7 +264,7 @@ void Model::updateRenderItems() { // collision mesh does not share the same unit scale as the FBX file's mesh: only apply offset Transform collisionMeshOffset; collisionMeshOffset.setIdentity(); - // Transform modelTransform = self->getTransform(); + Transform modelTransform = self->getTransform(); foreach(auto itemID, self->_collisionRenderItemsMap.keys()) { transaction.updateItem(itemID, [modelTransform, collisionMeshOffset](MeshPartPayload& data) { // update the model transform for this render item. From 1b85a453ab4e986cd18ac03ed441f4fc6ea54da5 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 12 Sep 2017 16:49:51 -0700 Subject: [PATCH 328/722] working on text3d overlays and entities --- interface/src/ui/overlays/Text3DOverlay.cpp | 23 ++++++--- interface/src/ui/overlays/Text3DOverlay.h | 1 + .../src/RenderableTextEntityItem.cpp | 2 +- .../src/RenderableWebEntityItem.cpp | 8 +-- libraries/model/src/model/Skybox.cpp | 5 +- .../src/procedural/ProceduralSkybox.cpp | 5 +- .../render-utils/src/AntialiasingEffect.cpp | 5 +- .../src/DeferredLightingEffect.cpp | 2 +- libraries/render-utils/src/GeometryCache.cpp | 26 +++++----- libraries/render-utils/src/GeometryCache.h | 4 +- .../render-utils/src/RenderForwardTask.cpp | 9 ++-- .../render-utils/src/StencilMaskPass.cpp | 49 +++++++++++++------ libraries/render-utils/src/StencilMaskPass.h | 16 +++--- libraries/render-utils/src/sdf_text3D.slf | 17 ++++--- libraries/render-utils/src/sdf_text3D.slv | 5 +- ...overlay.slf => sdf_text3D_transparent.slf} | 21 ++++---- libraries/render-utils/src/text/Font.cpp | 26 ++++++---- libraries/render-utils/src/text/Font.h | 2 +- 18 files changed, 134 insertions(+), 92 deletions(-) rename libraries/render-utils/src/{sdf_text3D_overlay.slf => sdf_text3D_transparent.slf} (80%) diff --git a/interface/src/ui/overlays/Text3DOverlay.cpp b/interface/src/ui/overlays/Text3DOverlay.cpp index bb8c24aa11..08a66fce55 100644 --- a/interface/src/ui/overlays/Text3DOverlay.cpp +++ b/interface/src/ui/overlays/Text3DOverlay.cpp @@ -17,6 +17,8 @@ #include #include +#include + const int FIXED_FONT_POINT_SIZE = 40; const int FIXED_FONT_SCALING_RATIO = FIXED_FONT_POINT_SIZE * 80.0f; // this is a ratio determined through experimentation const float LINE_SCALE_RATIO = 1.2f; @@ -113,6 +115,7 @@ void Text3DOverlay::render(RenderArgs* args) { glm::vec3 topLeft(-halfDimensions.x, -halfDimensions.y, SLIGHTLY_BEHIND); glm::vec3 bottomRight(halfDimensions.x, halfDimensions.y, SLIGHTLY_BEHIND); + DependencyManager::get()->bindSimpleProgram(batch, false, quadColor.a < 1.0f, false, false, false, false); DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, quadColor, _geometryId); // Same font properties as textSize() @@ -132,13 +135,8 @@ void Text3DOverlay::render(RenderArgs* args) { glm::vec4 textColor = { _color.red / MAX_COLOR, _color.green / MAX_COLOR, _color.blue / MAX_COLOR, getTextAlpha() }; - // FIXME: Factor out textRenderer so that Text3DOverlay overlay parts can be grouped by pipeline - // for a gpu performance increase. Currently, - // Text renderer sets its own pipeline, - _textRenderer->draw(batch, 0, 0, getText(), textColor, glm::vec2(-1.0f), getDrawInFront()); - // so before we continue, we must reset the pipeline - batch.setPipeline(args->_shapePipeline->pipeline); - args->_shapePipeline->prepare(batch, args); + // FIXME: Factor out textRenderer so that Text3DOverlay overlay parts can be grouped by pipeline for a gpu performance increase. + _textRenderer->draw(batch, 0, 0, getText(), textColor, glm::vec2(-1.0f), true); } const render::ShapeKey Text3DOverlay::getShapeKey() { @@ -159,7 +157,18 @@ void Text3DOverlay::setProperties(const QVariantMap& properties) { auto textAlpha = properties["textAlpha"]; if (textAlpha.isValid()) { + float prevTextAlpha = getTextAlpha(); setTextAlpha(textAlpha.toFloat()); + // Update our payload key if necessary to handle transparency + if ((prevTextAlpha < 1.0f && _textAlpha >= 1.0f) || (prevTextAlpha >= 1.0f && _textAlpha < 1.0f)) { + auto itemID = getRenderItemID(); + if (render::Item::isValidID(itemID)) { + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + render::Transaction transaction; + transaction.updateItem(itemID); + scene->enqueueTransaction(transaction); + } + } } bool valid; diff --git a/interface/src/ui/overlays/Text3DOverlay.h b/interface/src/ui/overlays/Text3DOverlay.h index e7b09c9040..81551bd8f5 100644 --- a/interface/src/ui/overlays/Text3DOverlay.h +++ b/interface/src/ui/overlays/Text3DOverlay.h @@ -43,6 +43,7 @@ public: xColor getBackgroundColor(); float getTextAlpha() { return _textAlpha; } float getBackgroundAlpha() { return getAlpha(); } + bool isTransparent() override { return Overlay::isTransparent() || _textAlpha < 1.0f; } // setters void setText(const QString& text); diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index d39139257b..2f5dfdd0a4 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -113,7 +113,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) { if (!_geometryID) { _geometryID = geometryCache->allocateID(); } - geometryCache->bindSimpleProgram(batch, false, transparent, false, false, true); + geometryCache->bindSimpleProgram(batch, false, transparent, false, false, false, false); geometryCache->renderQuad(batch, minCorner, maxCorner, backgroundColor, _geometryID); float scale = _lineHeight / _textRenderer->getFontSize(); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 8b5feb15f0..208680fce8 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -171,8 +171,6 @@ void WebEntityRenderer::doRender(RenderArgs* args) { } } - //_timer.singleShot - PerformanceTimer perfTimer("WebEntityRenderer::render"); static const glm::vec2 texMin(0.0f), texMax(1.0f), topLeft(-0.5f), bottomRight(0.5f); @@ -182,11 +180,7 @@ void WebEntityRenderer::doRender(RenderArgs* args) { float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; batch._glColor4f(1.0f, 1.0f, 1.0f, fadeRatio); - if (fadeRatio < OPAQUE_ALPHA_THRESHOLD) { - DependencyManager::get()->bindWebBrowserProgram(batch, true); - } else { - DependencyManager::get()->bindWebBrowserProgram(batch); - } + DependencyManager::get()->bindWebBrowserProgram(batch, fadeRatio < OPAQUE_ALPHA_THRESHOLD); DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, fadeRatio), _geometryId); } diff --git a/libraries/model/src/model/Skybox.cpp b/libraries/model/src/model/Skybox.cpp index d327593573..fd3061afa5 100755 --- a/libraries/model/src/model/Skybox.cpp +++ b/libraries/model/src/model/Skybox.cpp @@ -97,7 +97,10 @@ void Skybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, const Sky } auto skyState = std::make_shared(); - skyState->setStencilTest(true, 0xFF, gpu::State::StencilTest(1, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + // Must match PrepareStencil::STENCIL_BACKGROUND + const int8_t STENCIL_BACKGROUND = 0; + skyState->setStencilTest(true, 0xFF, gpu::State::StencilTest(STENCIL_BACKGROUND, 0xFF, gpu::EQUAL, + gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); thePipeline = gpu::Pipeline::create(skyShader, skyState); } diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index b062fcdd63..9544759037 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -23,7 +23,10 @@ ProceduralSkybox::ProceduralSkybox() : model::Skybox() { _procedural._fragmentSource = skybox_frag; // Adjust the pipeline state for background using the stencil test _procedural.setDoesFade(false); - _procedural._opaqueState->setStencilTest(true, 0xFF, gpu::State::StencilTest(1, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + // Must match PrepareStencil::STENCIL_BACKGROUND + const int8_t STENCIL_BACKGROUND = 0; + _procedural._opaqueState->setStencilTest(true, 0xFF, gpu::State::StencilTest(STENCIL_BACKGROUND, 0xFF, gpu::EQUAL, + gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); } bool ProceduralSkybox::empty() { diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index 3013ad9ebb..70c2e3b5ce 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -70,9 +70,8 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline(RenderArgs* ar gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - PrepareStencil::testMaskNoAA(*state); - state->setDepthTest(false, false, gpu::LESS_EQUAL); + PrepareStencil::testNoAA(*state); // Good to go add the brand new pipeline _antialiasingPipeline = gpu::Pipeline::create(program, state); @@ -95,7 +94,7 @@ const gpu::PipelinePointer& Antialiasing::getBlendPipeline() { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); state->setDepthTest(false, false, gpu::LESS_EQUAL); - PrepareStencil::testMaskNoAA(*state); + PrepareStencil::testNoAA(*state); // Good to go add the brand new pipeline _blendPipeline = gpu::Pipeline::create(program, state); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 2b5fdc1d74..b0488867e9 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -420,7 +420,7 @@ void PrepareDeferred::run(const RenderContextPointer& renderContext, const Input gpu::Framebuffer::BUFFER_COLOR0 | gpu::Framebuffer::BUFFER_COLOR1 | gpu::Framebuffer::BUFFER_COLOR2 | gpu::Framebuffer::BUFFER_COLOR3 | gpu::Framebuffer::BUFFER_DEPTH | gpu::Framebuffer::BUFFER_STENCIL, - vec4(vec3(0), 0), 1.0, 1, true); + vec4(vec3(0), 0), 1.0, 0, true); // For the rest of the rendering, bind the lighting model batch.setUniformBuffer(LIGHTING_MODEL_BUFFER_SLOT, lightingModel->getParametersBuffer()); diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 3bf83d08c9..996fae4196 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -561,7 +561,7 @@ void GeometryCache::initializeShapePipelines() { render::ShapePipelinePointer GeometryCache::getShapePipeline(bool textured, bool transparent, bool culled, bool unlit, bool depthBias) { - return std::make_shared(getSimplePipeline(textured, transparent, culled, unlit, depthBias, false), nullptr, + return std::make_shared(getSimplePipeline(textured, transparent, culled, unlit, depthBias, false, true), nullptr, [](const render::ShapePipeline& , gpu::Batch& batch, render::Args*) { batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, DependencyManager::get()->getWhiteTexture()); } @@ -1877,6 +1877,7 @@ public: IS_UNLIT_FLAG, HAS_DEPTH_BIAS_FLAG, IS_FADING_FLAG, + IS_AA_FLAG, NUM_FLAGS, }; @@ -1888,6 +1889,7 @@ public: IS_UNLIT = (1 << IS_UNLIT_FLAG), HAS_DEPTH_BIAS = (1 << HAS_DEPTH_BIAS_FLAG), IS_FADING = (1 << IS_FADING_FLAG), + IS_AA = (1 << IS_AA_FLAG), }; typedef unsigned short Flags; @@ -1899,6 +1901,7 @@ public: bool isUnlit() const { return isFlag(IS_UNLIT); } bool hasDepthBias() const { return isFlag(HAS_DEPTH_BIAS); } bool isFading() const { return isFlag(IS_FADING); } + bool isAA() const { return isFlag(IS_AA); } Flags _flags = 0; short _spare = 0; @@ -1907,9 +1910,9 @@ public: SimpleProgramKey(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false, bool fading = false) { + bool unlit = false, bool depthBias = false, bool fading = false, bool isAA = true) { _flags = (textured ? IS_TEXTURED : 0) | (transparent ? IS_TRANSPARENT : 0) | (culled ? IS_CULLED : 0) | - (unlit ? IS_UNLIT : 0) | (depthBias ? HAS_DEPTH_BIAS : 0) | (fading ? IS_FADING : 0); + (unlit ? IS_UNLIT : 0) | (depthBias ? HAS_DEPTH_BIAS : 0) | (fading ? IS_FADING : 0) | (isAA ? IS_AA : 0); } SimpleProgramKey(int bitmask) : _flags(bitmask) {} @@ -1958,8 +1961,8 @@ gpu::PipelinePointer GeometryCache::getWebBrowserProgram(bool transparent) { return transparent ? _simpleTransparentWebBrowserPipelineNoAA : _simpleOpaqueWebBrowserPipelineNoAA; } -void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool transparent, bool culled, bool unlit, bool depthBiased) { - batch.setPipeline(getSimplePipeline(textured, transparent, culled, unlit, depthBiased)); +void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool transparent, bool culled, bool unlit, bool depthBiased, bool isAA) { + batch.setPipeline(getSimplePipeline(textured, transparent, culled, unlit, depthBiased, false, isAA)); // If not textured, set a default albedo map if (!textured) { @@ -1968,8 +1971,8 @@ void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool tra } } -gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transparent, bool culled, bool unlit, bool depthBiased, bool fading) { - SimpleProgramKey config { textured, transparent, culled, unlit, depthBiased, fading }; +gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transparent, bool culled, bool unlit, bool depthBiased, bool fading, bool isAA) { + SimpleProgramKey config { textured, transparent, culled, unlit, depthBiased, fading, isAA }; // If the pipeline already exists, return it auto it = _simplePrograms.find(config); @@ -2027,11 +2030,10 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - if (config.isTransparent()) { - PrepareStencil::testMask(*state); - } - else { - PrepareStencil::testMaskDrawShape(*state); + if (config.isAA()) { + config.isTransparent() ? PrepareStencil::testMask(*state) : PrepareStencil::testMaskDrawShape(*state); + } else { + PrepareStencil::testMaskDrawShapeNoAA(*state); } gpu::ShaderPointer program = (config.isUnlit()) ? (config.isFading() ? _unlitFadeShader : _unlitShader) : (config.isFading() ? _simpleFadeShader : _simpleShader); diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index a90842403b..0dc21b5adc 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -161,10 +161,10 @@ public: // Bind the pipeline and get the state to render static geometry void bindSimpleProgram(gpu::Batch& batch, bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false); + bool unlit = false, bool depthBias = false, bool isAA = true); // Get the pipeline to render static geometry static gpu::PipelinePointer getSimplePipeline(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false, bool fading = false); + bool unlit = false, bool depthBias = false, bool fading = false, bool isAA = true); void bindWebBrowserProgram(gpu::Batch& batch, bool transparent = false); gpu::PipelinePointer getWebBrowserProgram(bool transparent); diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 296eea1da8..c83251c605 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -17,6 +17,7 @@ #include #include #include +#include "StencilMaskPass.h" #include "FramebufferCache.h" #include "TextureCache.h" @@ -93,7 +94,7 @@ void PrepareFramebuffer::run(const RenderContextPointer& renderContext, gpu::Framebuffer::BUFFER_COLOR0 | gpu::Framebuffer::BUFFER_DEPTH | gpu::Framebuffer::BUFFER_STENCIL, - vec4(vec3(0), 1), 1.0, 0.0, true); + vec4(vec3(0), 1), 1.0, 0, true); }); framebuffer = _framebuffer; @@ -130,11 +131,7 @@ const gpu::PipelinePointer Stencil::getPipeline() { auto state = std::make_shared(); state->setDepthTest(true, false, gpu::LESS_EQUAL); - const gpu::int8 STENCIL_OPAQUE = 1; - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(STENCIL_OPAQUE, 0xFF, gpu::ALWAYS, - gpu::State::STENCIL_OP_REPLACE, - gpu::State::STENCIL_OP_REPLACE, - gpu::State::STENCIL_OP_KEEP)); + PrepareStencil::drawBackground(*state); _stencilPipeline = gpu::Pipeline::create(program, state); } diff --git a/libraries/render-utils/src/StencilMaskPass.cpp b/libraries/render-utils/src/StencilMaskPass.cpp index 295e124ed1..0e0d8b56b3 100644 --- a/libraries/render-utils/src/StencilMaskPass.cpp +++ b/libraries/render-utils/src/StencilMaskPass.cpp @@ -104,30 +104,51 @@ void PrepareStencil::run(const RenderContextPointer& renderContext, const gpu::F }); } +// Always draw MASK to the stencil buffer (used to always prevent drawing in certain areas later) void PrepareStencil::drawMask(gpu::State& state) { - state.setStencilTest(true, 0xFF, gpu::State::StencilTest(PrepareStencil::STENCIL_MASK, 0xFF, gpu::ALWAYS, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_REPLACE)); + state.setStencilTest(true, 0xFF, gpu::State::StencilTest(STENCIL_MASK, 0xFF, gpu::ALWAYS, + gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_REPLACE)); } +// Draw BACKGROUND to the stencil buffer behind everything else +void PrepareStencil::drawBackground(gpu::State& state) { + state.setStencilTest(true, 0xFF, gpu::State::StencilTest(STENCIL_BACKGROUND, 0xFF, gpu::ALWAYS, + gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_REPLACE, gpu::State::STENCIL_OP_KEEP)); +} + +// Pass if this area has NOT been marked as MASK void PrepareStencil::testMask(gpu::State& state) { - state.setStencilTest(true, 0x00, gpu::State::StencilTest(PrepareStencil::STENCIL_MASK, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + state.setStencilTest(true, 0x00, gpu::State::StencilTest(STENCIL_MASK, 0xFF, gpu::NOT_EQUAL, + gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); } -void PrepareStencil::testMaskNoAA(gpu::State& state) { - state.setStencilTest(true, 0x00, gpu::State::StencilTest(PrepareStencil::STENCIL_MASK | PrepareStencil::STENCIL_NO_AA, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); +// Pass if this area has NOT been marked as NO_AA or anything containing NO_AA +void PrepareStencil::testNoAA(gpu::State& state) { + state.setStencilTest(true, 0x00, gpu::State::StencilTest(STENCIL_NO_AA, STENCIL_NO_AA, gpu::NOT_EQUAL, + gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); } +// Pass if this area WAS marked as BACKGROUND +// (see: model/src/Skybox.cpp, procedural/src/ProceduralSkybox.cpp) void PrepareStencil::testBackground(gpu::State& state) { - state.setStencilTest(true, 0x00, gpu::State::StencilTest(PrepareStencil::STENCIL_BACKGROUND, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); -} - -void PrepareStencil::testMaskDrawShape(gpu::State& state) { - state.setStencilTest(true, 0xFF, gpu::State::StencilTest(PrepareStencil::STENCIL_MASK, 0xFF, gpu::NOT_EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_ZERO)); -} - -void PrepareStencil::testMaskDrawShapeNoAA(gpu::State& state) { - state.setStencilTest(true, 0xFF, gpu::State::StencilTest(PrepareStencil::STENCIL_MASK | PrepareStencil::STENCIL_NO_AA, 0xFF, gpu::ALWAYS, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_REPLACE)); + state.setStencilTest(true, 0x00, gpu::State::StencilTest(STENCIL_BACKGROUND, 0xFF, gpu::EQUAL, + gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); } +// Pass if this area WAS marked as SHAPE or anything containing SHAPE void PrepareStencil::testShape(gpu::State& state) { - state.setStencilTest(true, 0x00, gpu::State::StencilTest(PrepareStencil::STENCIL_SHAPE, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + state.setStencilTest(true, 0x00, gpu::State::StencilTest(STENCIL_SHAPE, STENCIL_SHAPE, gpu::EQUAL, + gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); +} + +// Pass if this area was NOT marked as MASK, write to SHAPE if it passes +void PrepareStencil::testMaskDrawShape(gpu::State& state) { + state.setStencilTest(true, STENCIL_SHAPE, gpu::State::StencilTest(STENCIL_MASK | STENCIL_SHAPE, STENCIL_MASK, gpu::NOT_EQUAL, + gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_REPLACE)); +} + +// Pass if this area was NOT marked as MASK, write to SHAPE and NO_AA if it passes +void PrepareStencil::testMaskDrawShapeNoAA(gpu::State& state) { + state.setStencilTest(true, STENCIL_SHAPE | STENCIL_NO_AA, gpu::State::StencilTest(STENCIL_MASK | STENCIL_SHAPE | STENCIL_NO_AA, STENCIL_MASK, gpu::NOT_EQUAL, + gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_REPLACE)); } \ No newline at end of file diff --git a/libraries/render-utils/src/StencilMaskPass.h b/libraries/render-utils/src/StencilMaskPass.h index 2c0294c471..ddbf4a7ac0 100644 --- a/libraries/render-utils/src/StencilMaskPass.h +++ b/libraries/render-utils/src/StencilMaskPass.h @@ -41,20 +41,20 @@ public: void run(const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& dstFramebuffer); - static const gpu::int8 STENCIL_SHAPE = 0; - static const gpu::int8 STENCIL_BACKGROUND = 1 << 0; - static const gpu::int8 STENCIL_MASK = 1 << 1; - static const gpu::int8 STENCIL_NO_AA = 1 << 2; - + // Always use 0 to clear the stencil buffer to set it to background + static const gpu::int8 STENCIL_BACKGROUND = 0; // must match values in Skybox.cpp and ProceduralSkybox.cpp + static const gpu::int8 STENCIL_MASK = 1 << 0; + static const gpu::int8 STENCIL_SHAPE = 1 << 1; + static const gpu::int8 STENCIL_NO_AA = 1 << 2; static void drawMask(gpu::State& state); + static void drawBackground(gpu::State& state); + static void testNoAA(gpu::State& state); static void testMask(gpu::State& state); - static void testMaskNoAA(gpu::State& state); static void testBackground(gpu::State& state); + static void testShape(gpu::State& state); static void testMaskDrawShape(gpu::State& state); static void testMaskDrawShapeNoAA(gpu::State& state); - static void testShape(gpu::State& state); - private: gpu::PipelinePointer _meshStencilPipeline; diff --git a/libraries/render-utils/src/sdf_text3D.slf b/libraries/render-utils/src/sdf_text3D.slf index f578895c85..8863e15b00 100644 --- a/libraries/render-utils/src/sdf_text3D.slf +++ b/libraries/render-utils/src/sdf_text3D.slf @@ -1,7 +1,7 @@ <@include gpu/Config.slh@> <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> -// sdf_text.frag +// sdf_text3D.frag // fragment shader // // Created by Bradley Austin Davis on 2015-02-04 @@ -38,18 +38,21 @@ void main() { // perform adaptive anti-aliasing of the edges // The larger we're rendering, the less anti-aliasing we need float s = smoothing * length(fwidth(_texCoord0)); - float w = clamp( s, 0.0, 0.5); + float w = clamp(s, 0.0, 0.5); float a = smoothstep(0.5 - w, 0.5 + w, sdf); - // discard if unvisible + // discard if invisible if (a < 0.01) { discard; } - - packDeferredFragmentTranslucent( + + packDeferredFragment( normalize(_normal), a * Color.a, Color.rgb, - DEFAULT_FRESNEL, - DEFAULT_ROUGHNESS); + DEFAULT_ROUGHNESS, + DEFAULT_METALLIC, + DEFAULT_EMISSIVE, + DEFAULT_OCCLUSION, + DEFAULT_SCATTERING); } \ No newline at end of file diff --git a/libraries/render-utils/src/sdf_text3D.slv b/libraries/render-utils/src/sdf_text3D.slv index 29bc1a9e85..d9f6e0cdda 100644 --- a/libraries/render-utils/src/sdf_text3D.slv +++ b/libraries/render-utils/src/sdf_text3D.slv @@ -1,7 +1,7 @@ <@include gpu/Config.slh@> <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> -// sdf_text.vert +// sdf_text3D.vert // vertex shader // // Created by Brad Davis on 10/14/13. @@ -27,5 +27,6 @@ void main() { TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> - <$transformModelToWorldDir(cam, obj, inNormal.xyz, _normal.xyz)$> + const vec3 normal = vec3(0, 0, 1); + <$transformModelToWorldDir(cam, obj, normal, _normal)$> } \ No newline at end of file diff --git a/libraries/render-utils/src/sdf_text3D_overlay.slf b/libraries/render-utils/src/sdf_text3D_transparent.slf similarity index 80% rename from libraries/render-utils/src/sdf_text3D_overlay.slf rename to libraries/render-utils/src/sdf_text3D_transparent.slf index d357b05e14..fc78ab3a37 100644 --- a/libraries/render-utils/src/sdf_text3D_overlay.slf +++ b/libraries/render-utils/src/sdf_text3D_transparent.slf @@ -1,7 +1,7 @@ <@include gpu/Config.slh@> <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> -// sdf_text.frag +// sdf_text3D_transparent.frag // fragment shader // // Created by Bradley Austin Davis on 2015-02-04 @@ -10,6 +10,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +<@include DeferredBufferWrite.slh@> + uniform sampler2D Font; uniform bool Outline; uniform vec4 Color; @@ -18,8 +20,6 @@ uniform vec4 Color; in vec3 _normal; in vec2 _texCoord0; -layout(location = 0) out vec4 _fragColor0; - const float gamma = 2.2; const float smoothing = 32.0; const float interiorCutoff = 0.8; @@ -38,15 +38,18 @@ void main() { // perform adaptive anti-aliasing of the edges // The larger we're rendering, the less anti-aliasing we need float s = smoothing * length(fwidth(_texCoord0)); - float w = clamp( s, 0.0, 0.5); + float w = clamp(s, 0.0, 0.5); float a = smoothstep(0.5 - w, 0.5 + w, sdf); - // gamma correction for linear attenuation - a = pow(a, 1.0 / gamma); - - // discard if unvisible + // discard if invisible if (a < 0.01) { discard; } - _fragColor0 = vec4(Color.rgb, a); + + packDeferredFragmentTranslucent( + normalize(_normal), + a * Color.a, + Color.rgb, + DEFAULT_FRESNEL, + DEFAULT_ROUGHNESS); } \ No newline at end of file diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index f51a779066..8449c58c7c 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -9,10 +9,11 @@ #include "sdf_text3D_vert.h" #include "sdf_text3D_frag.h" -#include "sdf_text3D_overlay_frag.h" +#include "sdf_text3D_transparent_frag.h" #include "../RenderUtilsLogging.h" #include "FontFamilies.h" +#include "../StencilMaskPass.h" static std::mutex fontMutex; @@ -224,13 +225,13 @@ void Font::setupGPU() { { auto vertexShader = gpu::Shader::createVertex(std::string(sdf_text3D_vert)); auto pixelShader = gpu::Shader::createPixel(std::string(sdf_text3D_frag)); - auto pixelShaderOverlay = gpu::Shader::createPixel(std::string(sdf_text3D_overlay_frag)); + auto pixelShaderTransparent = gpu::Shader::createPixel(std::string(sdf_text3D_transparent_frag)); gpu::ShaderPointer program = gpu::Shader::createProgram(vertexShader, pixelShader); - gpu::ShaderPointer programOverlay = gpu::Shader::createProgram(vertexShader, pixelShaderOverlay); + gpu::ShaderPointer programTransparent = gpu::Shader::createProgram(vertexShader, pixelShaderTransparent); gpu::Shader::BindingSet slotBindings; gpu::Shader::makeProgram(*program, slotBindings); - gpu::Shader::makeProgram(*programOverlay, slotBindings); + gpu::Shader::makeProgram(*programTransparent, slotBindings); _fontLoc = program->getTextures().findLocation("Font"); _outlineLoc = program->getUniforms().findLocation("Outline"); @@ -239,15 +240,20 @@ void Font::setupGPU() { auto state = std::make_shared(); state->setCullMode(gpu::State::CULL_BACK); state->setDepthTest(true, true, gpu::LESS_EQUAL); - state->setBlendFunction(true, + state->setBlendFunction(false, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMaskDrawShapeNoAA(*state); _pipeline = gpu::Pipeline::create(program, state); - auto layeredState = std::make_shared(); - layeredState->setCullMode(gpu::State::CULL_BACK); - layeredState->setDepthTest(true, true, gpu::LESS_EQUAL); - _layeredPipeline = gpu::Pipeline::create(programOverlay, layeredState); + auto transparentState = std::make_shared(); + transparentState->setCullMode(gpu::State::CULL_BACK); + transparentState->setDepthTest(true, true, gpu::LESS_EQUAL); + transparentState->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMaskDrawShapeNoAA(*transparentState); + _transparentPipeline = gpu::Pipeline::create(programTransparent, transparentState); } // Sanity checks @@ -361,7 +367,7 @@ void Font::drawString(gpu::Batch& batch, float x, float y, const QString& str, c setupGPU(); - batch.setPipeline(layered ? _layeredPipeline : _pipeline); + batch.setPipeline(((*color).a < 1.0f || layered) ? _transparentPipeline : _pipeline); batch.setResourceTexture(_fontLoc, _texture); batch._glUniform1i(_outlineLoc, (effectType == OUTLINE_EFFECT)); diff --git a/libraries/render-utils/src/text/Font.h b/libraries/render-utils/src/text/Font.h index 2b61f19492..a41f720f15 100644 --- a/libraries/render-utils/src/text/Font.h +++ b/libraries/render-utils/src/text/Font.h @@ -63,7 +63,7 @@ private: // gpu structures gpu::PipelinePointer _pipeline; - gpu::PipelinePointer _layeredPipeline; + gpu::PipelinePointer _transparentPipeline; gpu::TexturePointer _texture; gpu::Stream::FormatPointer _format; gpu::BufferPointer _verticesBuffer; From 97b29880050ed3fd498ebad9ac4730493ab27444 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 13 Sep 2017 14:13:23 +1200 Subject: [PATCH 329/722] Miscellaneous fixes --- scripts/vr-edit/modules/feedback.js | 4 ++-- scripts/vr-edit/modules/selection.js | 14 ++++---------- scripts/vr-edit/modules/toolsMenu.js | 2 +- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/scripts/vr-edit/modules/feedback.js b/scripts/vr-edit/modules/feedback.js index e87a0867fa..f1456df5e6 100644 --- a/scripts/vr-edit/modules/feedback.js +++ b/scripts/vr-edit/modules/feedback.js @@ -37,8 +37,8 @@ Feedback = (function () { EQUIP_TOOL: { sound: EQUIP_SOUND, volume: 0.3, haptic: 0.6 }, APPLY_PROPERTY: { sound: null, volume: 0, haptic: 0.3 }, APPLY_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 }, - UNDO_ACTION: { sound: UNDO_SOUND, volume: 0.2, haptic: 0.2 }, // TODO - REDO_ACTION: { sound: REDO_SOUND, volume: 0.2, haptic: 0.2 }, // TODO + UNDO_ACTION: { sound: UNDO_SOUND, volume: 0.1, haptic: 0.2 }, // TODO + REDO_ACTION: { sound: REDO_SOUND, volume: 0.1, haptic: 0.2 }, // TODO GENERAL_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 } }, diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index c0881bbf01..02d4b56d47 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -23,8 +23,6 @@ Selection = function (side) { rootPosition, rootOrientation, scaleFactor, - scaleRotation, - scaleCenter, scalePosition, scaleOrientation, scaleRootOffset, @@ -327,10 +325,8 @@ Selection = function (side) { }); } - // Save most recent scale parameters. + // Save most recent scale factor. scaleFactor = factor; - scaleRotation = rotation; - scaleCenter = center; } function finishDirectScaling() { @@ -338,8 +334,6 @@ Selection = function (side) { var i, length; // Final scale, position, and orientation of root. - rootPosition = Vec3.sum(scaleCenter, Vec3.multiply(scaleFactor, Vec3.multiplyQbyV(scaleRotation, scaleRootOffset))); - rootOrientation = Quat.multiply(scaleRotation, scaleRootOrientation); selection[0].dimensions = Vec3.multiply(scaleFactor, selection[0].dimensions); selection[0].position = rootPosition; selection[0].rotation = rootOrientation; @@ -394,8 +388,8 @@ Selection = function (side) { // Final scale and position of root. selection[0].dimensions = Vec3.multiplyVbyV(scaleFactor, selection[0].dimensions); - selection[0].position = scalePosition; - selection[0].rotation = scaleOrientation; + selection[0].position = rootPosition; + selection[0].rotation = rootOrientation; // Final scale and position of children. for (i = 1, length = selection.length; i < length; i += 1) { @@ -462,7 +456,7 @@ Selection = function (side) { } } if (undoData.length > 0) { - History.push(undoData, redoData); + History.push({ setProperties: undoData }, { setProperties: redoData }); } } else { properties = Entities.getEntityProperties(intersectedEntityID, ["type", "color"]); diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 240dbfcfbc..2b93c215d4 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -1561,7 +1561,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, { - id: "deleeteRule1", + id: "deleteRule1", type: "horizontalRule", properties: { localPosition: { From 8c9026c89207e084a872ce1e1f1b5842ca332383 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 13 Sep 2017 14:13:48 +1200 Subject: [PATCH 330/722] Add undo/redo for scaling --- scripts/vr-edit/modules/selection.js | 134 ++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 02d4b56d47..fdc413bdab 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -31,7 +31,10 @@ Selection = function (side) { startOrientation, ENTITY_TYPE = "entity", ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "PolyLine", "PolyVox"], - ENTITY_TYPES_2D = ["Text", "Web"]; + ENTITY_TYPES_2D = ["Text", "Web"], + MIN_HISTORY_MOVE_DISTANCE = 0.005, + MIN_HISTORY_ROTATE_ANGLE = 0.017453; // Radians = 1 degree. + if (!this instanceof Selection) { @@ -300,6 +303,26 @@ Selection = function (side) { // Save initial position and orientation so that can scale relative to these without accumulating float errors. scaleRootOffset = Vec3.subtract(rootPosition, center); scaleRootOrientation = rootOrientation; + + // User is grabbing entity; add a history entry for movement up until the start of scaling and update start position and + // orientation; unless very small movement. + if (Vec3.distance(startPosition, rootPosition) >= MIN_HISTORY_MOVE_DISTANCE + || Quat.rotationBetween(startOrientation, rootOrientation) >= MIN_HISTORY_ROTATE_ANGLE) { + History.push( + { + setProperties: [ + { entityID: rootEntityID, properties: { position: startPosition, rotation: startOrientation } } + ] + }, + { + setProperties: [ + { entityID: rootEntityID, properties: { position: rootPosition, rotation: rootOrientation } } + ] + } + ); + startPosition = rootPosition; + startOrientation = rootOrientation; + } } function directScale(factor, rotation, center) { @@ -331,23 +354,86 @@ Selection = function (side) { function finishDirectScaling() { // Update selection with final entity properties. - var i, + var undoData = [], + redoData = [], + i, length; + // Final scale, position, and orientation of root. + undoData.push({ + entityID: selection[0].id, + properties: { + dimensions: selection[0].dimensions, + position: startPosition, + rotation: startOrientation + } + }); selection[0].dimensions = Vec3.multiply(scaleFactor, selection[0].dimensions); selection[0].position = rootPosition; selection[0].rotation = rootOrientation; + redoData.push({ + entityID: selection[0].id, + properties: { + dimensions: selection[0].dimensions, + position: selection[0].position, + rotation: selection[0].rotation + } + }); // Final scale and position of children. for (i = 1, length = selection.length; i < length; i += 1) { + undoData.push({ + entityID: selection[i].id, + properties: { + dimensions: selection[i].dimensions, + localPosition: selection[i].localPosition + } + }); selection[i].dimensions = Vec3.multiply(scaleFactor, selection[i].dimensions); selection[i].localPosition = Vec3.multiply(scaleFactor, selection[i].localPosition); + redoData.push({ + entityID: selection[i].id, + properties: { + dimensions: selection[i].dimensions, + localPosition: selection[i].localPosition + } + }); } + + // Add history entry. + History.push( + { setProperties: undoData }, + { setProperties: redoData } + ); + + // Update grab start data for its undo. + startPosition = rootPosition; + startOrientation = rootOrientation; } function startHandleScaling(position) { // Save initial offset from hand position to root position so that can scale without accumulating float errors. scaleRootOffset = Vec3.multiplyQbyV(Quat.inverse(rootOrientation), Vec3.subtract(rootPosition, position)); + + // User is grabbing entity; add a history entry for movement up until the start of scaling and update start position and + // orientation; unless very small movement. + if (Vec3.distance(startPosition, rootPosition) >= MIN_HISTORY_MOVE_DISTANCE + || Quat.rotationBetween(startOrientation, rootOrientation) >= MIN_HISTORY_ROTATE_ANGLE) { + History.push( + { + setProperties: [ + { entityID: rootEntityID, properties: { position: startPosition, rotation: startOrientation } } + ] + }, + { + setProperties: [ + { entityID: rootEntityID, properties: { position: rootPosition, rotation: rootOrientation } } + ] + } + ); + startPosition = rootPosition; + startOrientation = rootOrientation; + } } function handleScale(factor, position, orientation) { @@ -383,19 +469,61 @@ Selection = function (side) { function finishHandleScaling() { // Update selection with final entity properties. - var i, + var undoData = [], + redoData = [], + i, length; // Final scale and position of root. + undoData.push({ + entityID: selection[0].id, + properties: { + dimensions: selection[0].dimensions, + position: startPosition, + rotation: startOrientation + } + }); selection[0].dimensions = Vec3.multiplyVbyV(scaleFactor, selection[0].dimensions); selection[0].position = rootPosition; selection[0].rotation = rootOrientation; + redoData.push({ + entityID: selection[0].id, + properties: { + dimensions: selection[0].dimensions, + position: selection[0].position, + rotation: selection[0].rotation + } + }); // Final scale and position of children. for (i = 1, length = selection.length; i < length; i += 1) { + undoData.push({ + entityID: selection[i].id, + properties: { + dimensions: selection[i].dimensions, + localPosition: selection[i].localPosition + } + }); selection[i].dimensions = Vec3.multiplyVbyV(scaleFactor, selection[i].dimensions); selection[i].localPosition = Vec3.multiplyVbyV(scaleFactor, selection[i].localPosition); + redoData.push({ + entityID: selection[i].id, + properties: { + dimensions: selection[i].dimensions, + localPosition: selection[i].localPosition + } + }); } + + // Add history entry. + History.push( + { setProperties: undoData }, + { setProperties: redoData } + ); + + // Update grab start data for its undo. + startPosition = rootPosition; + startOrientation = rootOrientation; } function cloneEntities() { From 003c1c7bc59889170a5f7aefff91c4f5939700cc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 13 Sep 2017 15:16:16 +1200 Subject: [PATCH 331/722] Add undo/redo for group/ungroup --- scripts/vr-edit/modules/groups.js | 62 ++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/groups.js b/scripts/vr-edit/modules/groups.js index fd78b1ef76..c3e4172ebe 100644 --- a/scripts/vr-edit/modules/groups.js +++ b/scripts/vr-edit/modules/groups.js @@ -83,18 +83,33 @@ Groups = function () { // Groups all selections into one. var DYNAMIC_AND_COLLISIONLESS = { dynamic: true, collisionless: true }, rootID, + undoData = [], + redoData = [], i, lengthI, j, lengthJ; // If the first group has physics (i.e., root entity is dynamic) make all entities in child groups dynamic and - // collisionless. (Don't need to worry about other groups physics properties because only those of the the first entity - // in the linkset are used by High Fidelity.) See Selection.applyPhysics(). + // collisionless. (Don't need to worry about other groups physics properties because only those of the first entity in + // the linkset are used by High Fidelity.) See Selection.applyPhysics(). if (selections[0][0].dynamic) { for (i = 1, lengthI = selections.length; i < lengthI; i += 1) { for (j = 0, lengthJ = selections[i].length; j < lengthJ; j += 1) { + undoData.push({ + entityID: selections[i][j].id, + properties: { + dynamic: selections[i][j].dynamic, + collisionless: selections[i][j].collisionless + } + }); Entities.editEntity(selections[i][j].id, DYNAMIC_AND_COLLISIONLESS); + selections[i][j].dynamic = true; + selections[i][j].collisionless = true; + redoData.push({ + entityID: selections[i][j].id, + properties: DYNAMIC_AND_COLLISIONLESS + }); } } } @@ -102,9 +117,17 @@ Groups = function () { // Make the first entity in the first group the root and link the first entities of all other groups to it. rootID = rootEntityIDs[0]; for (i = 1, lengthI = rootEntityIDs.length; i < lengthI; i += 1) { + undoData.push({ + entityID: rootEntityIDs[i], + properties: { parentID: Uuid.NULL } + }); Entities.editEntity(rootEntityIDs[i], { parentID: rootID }); + redoData.push({ + entityID: rootEntityIDs[i], + properties: { parentID: rootID } + }); } // Update selection. @@ -114,6 +137,12 @@ Groups = function () { selections[0] = selections[0].concat(selections[i]); } selections.splice(1, selections.length - 1); + + // Add history entry. + History.push( + { setProperties: undoData }, + { setProperties: redoData } + ); } function ungroup() { @@ -129,6 +158,8 @@ Groups = function () { hasGroupChildren = false, isUngroupAll, NONDYNAMIC_AND_NONCOLLISIONLESS = { dynamic: false, collisionless: false }, + undoData = [], + redoData = [], i, lengthI, j, @@ -172,9 +203,17 @@ Groups = function () { isUngroupAll = hasSoloChildren !== hasGroupChildren; for (i = childrenIDs.length - 1; i >= 0; i -= 1) { if (isUngroupAll || childrenIndexIsGroup[i]) { + undoData.push({ + entityID: childrenIDs[i], + properties: { parentID: selections[0][childrenIndexes[i]].parentID } + }); Entities.editEntity(childrenIDs[i], { parentID: Uuid.NULL }); + redoData.push({ + entityID: childrenIDs[i], + properties: { parentID: Uuid.NULL } + }); rootEntityIDs.push(childrenIDs[i]); selections[0][childrenIndexes[i]].parentID = Uuid.NULL; selections.push(selections[0].splice(childrenIndexes[i], childrenIndexes[i + 1] - childrenIndexes[i])); @@ -186,10 +225,29 @@ Groups = function () { if (selections[0][0].dynamic) { for (i = 1, lengthI = selections.length; i < lengthI; i += 1) { for (j = 0, lengthJ = selections[i].length; j < lengthJ; j += 1) { + undoData.push({ + entityID: selections[i][j].id, + properties: { + dynamic: selections[i][j].dynamic, + collisionless: selections[i][j].collisionless + } + }); Entities.editEntity(selections[i][j].id, NONDYNAMIC_AND_NONCOLLISIONLESS); + selections[i][j].dynamic = false; + selections[i][j].collisionless = false; + redoData.push({ + entityID: selections[i][j].id, + properties: NONDYNAMIC_AND_NONCOLLISIONLESS + }); } } } + + // Add history entry. + History.push( + { setProperties: undoData }, + { setProperties: redoData } + ); } function clear() { From 709e7e7d90a0a308092b503236d2e63b2b77af4e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 13 Sep 2017 15:25:40 +1200 Subject: [PATCH 332/722] Add undo/redo for clone --- scripts/vr-edit/modules/selection.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index fdc413bdab..60ef02285b 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -531,6 +531,8 @@ Selection = function (side) { intersectedEntityIndex = 0, parentID, properties, + undoData = [], + redoData = [], i, j, length; @@ -556,10 +558,19 @@ Selection = function (side) { properties.parentID = selection[parentIDIndexes[i]].id; } selection[i].id = Entities.addEntity(properties); + undoData.push({ entityID: selection[i].id }); + redoData.push({ entityID: selection[i].id, properties: properties }); } + // Update selection info. intersectedEntityID = selection[intersectedEntityIndex].id; rootEntityID = selection[0].id; + + // Add history entry. + History.push( + { deleteEntities: undoData }, + { createEntities: redoData } + ); } function applyColor(color, isApplyToAll) { From 1c05311056cb64385ff93f69de0d66310174e0f4 Mon Sep 17 00:00:00 2001 From: Cain Kilgore Date: Wed, 13 Sep 2017 16:48:18 +0100 Subject: [PATCH 333/722] Some code cleanup --- interface/src/Application.cpp | 3 --- libraries/shared/src/shared/FileLogger.cpp | 16 +++++++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 2fa141a44f..8de6d93dd6 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -937,9 +937,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // you might think we could just do this in NodeList but we only want this connection for Interface connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, nodeList.data(), &NodeList::reset); - // connect to appropriate slots on AccountManager - // auto accountManager = DependencyManager::get(); - auto dialogsManager = DependencyManager::get(); connect(accountManager.data(), &AccountManager::authRequired, dialogsManager.data(), &DialogsManager::showLoginDialog); connect(accountManager.data(), &AccountManager::usernameChanged, this, &Application::updateWindowTitle); diff --git a/libraries/shared/src/shared/FileLogger.cpp b/libraries/shared/src/shared/FileLogger.cpp index f712a341fe..50b0ccb43c 100644 --- a/libraries/shared/src/shared/FileLogger.cpp +++ b/libraries/shared/src/shared/FileLogger.cpp @@ -48,7 +48,7 @@ static const QString LOGS_DIRECTORY = "Logs"; static const QString IPADDR_WILDCARD = "[0-9]*.[0-9]*.[0-9]*.[0-9]*"; static const QString DATETIME_WILDCARD = "20[0-9][0-9]-[0,1][0-9]-[0-3][0-9]_[0-2][0-9].[0-6][0-9].[0-6][0-9]"; static const QString FILENAME_WILDCARD = "hifi-log_" + IPADDR_WILDCARD + "_" + DATETIME_WILDCARD + ".txt"; -QUuid SESSION_ID; +QUuid _sessionId; // Max log size is 512 KB. We send log files to our crash reporter, so we want to keep this relatively // small so it doesn't go over the 2MB zipped limit for all of the files we send. @@ -64,13 +64,13 @@ QString getLogRollerFilename() { QString result = FileUtils::standardPath(LOGS_DIRECTORY); QHostAddress clientAddress = getGuessedLocalAddress(); QDateTime now = QDateTime::currentDateTime(); - QString FILE_SESSION_ID; + QString fileSessionID; - if (!SESSION_ID.isNull()) { - FILE_SESSION_ID = "_" + SESSION_ID.toString().replace("{", "").replace("}", ""); + if (!_sessionId.isNull()) { + fileSessionID = "_" + _sessionId.toString().replace("{", "").replace("}", ""); } - result.append(QString(FILENAME_FORMAT).arg(FILE_SESSION_ID, now.toString(DATETIME_FORMAT))); + result.append(QString(FILENAME_FORMAT).arg(fileSessionID, now.toString(DATETIME_FORMAT))); return result; } @@ -151,8 +151,10 @@ FileLogger::~FileLogger() { } void FileLogger::setSessionID(const QUuid& message) { - SESSION_ID = message; // This is for the output of log files. It will change if the Avatar enters a different domain. -} + // This is for the output of log files. Once the application is first started, + // this function runs and grabs the AccountManager Session ID and saves it here. + _sessionId = message; + } void FileLogger::addMessage(const QString& message) { _persistThreadInstance->queueItem(message); From 2427008ff333ad534c2c0e3c14c7e3de0a4b90a5 Mon Sep 17 00:00:00 2001 From: Cain Kilgore Date: Wed, 13 Sep 2017 17:40:04 +0100 Subject: [PATCH 334/722] Static --- libraries/shared/src/shared/FileLogger.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/libraries/shared/src/shared/FileLogger.cpp b/libraries/shared/src/shared/FileLogger.cpp index 50b0ccb43c..b019b69fb8 100644 --- a/libraries/shared/src/shared/FileLogger.cpp +++ b/libraries/shared/src/shared/FileLogger.cpp @@ -26,7 +26,6 @@ class FilePersistThread : public GenericQueueThread < QString > { Q_OBJECT public: FilePersistThread(const FileLogger& logger); - signals: void rollingLogFile(QString newFilename); @@ -48,7 +47,7 @@ static const QString LOGS_DIRECTORY = "Logs"; static const QString IPADDR_WILDCARD = "[0-9]*.[0-9]*.[0-9]*.[0-9]*"; static const QString DATETIME_WILDCARD = "20[0-9][0-9]-[0,1][0-9]-[0-3][0-9]_[0-2][0-9].[0-6][0-9].[0-6][0-9]"; static const QString FILENAME_WILDCARD = "hifi-log_" + IPADDR_WILDCARD + "_" + DATETIME_WILDCARD + ".txt"; -QUuid _sessionId; +static QUuid SESSION_ID; // Max log size is 512 KB. We send log files to our crash reporter, so we want to keep this relatively // small so it doesn't go over the 2MB zipped limit for all of the files we send. @@ -66,8 +65,8 @@ QString getLogRollerFilename() { QDateTime now = QDateTime::currentDateTime(); QString fileSessionID; - if (!_sessionId.isNull()) { - fileSessionID = "_" + _sessionId.toString().replace("{", "").replace("}", ""); + if (!SESSION_ID.isNull()) { + fileSessionID = "_" + SESSION_ID.toString().replace("{", "").replace("}", ""); } result.append(QString(FILENAME_FORMAT).arg(fileSessionID, now.toString(DATETIME_FORMAT))); @@ -153,7 +152,7 @@ FileLogger::~FileLogger() { void FileLogger::setSessionID(const QUuid& message) { // This is for the output of log files. Once the application is first started, // this function runs and grabs the AccountManager Session ID and saves it here. - _sessionId = message; + SESSION_ID = message; } void FileLogger::addMessage(const QString& message) { From f68a588323c70a093c1135349d61f8dea0c63132 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 13 Sep 2017 10:24:10 -0700 Subject: [PATCH 335/722] rename isAA isAntiAliased --- libraries/render-utils/src/GeometryCache.cpp | 20 ++++++++++---------- libraries/render-utils/src/GeometryCache.h | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 996fae4196..6e8164ac80 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1877,7 +1877,7 @@ public: IS_UNLIT_FLAG, HAS_DEPTH_BIAS_FLAG, IS_FADING_FLAG, - IS_AA_FLAG, + IS_ANTIALIASED_FLAG, NUM_FLAGS, }; @@ -1889,7 +1889,7 @@ public: IS_UNLIT = (1 << IS_UNLIT_FLAG), HAS_DEPTH_BIAS = (1 << HAS_DEPTH_BIAS_FLAG), IS_FADING = (1 << IS_FADING_FLAG), - IS_AA = (1 << IS_AA_FLAG), + IS_ANTIALIASED = (1 << IS_ANTIALIASED_FLAG), }; typedef unsigned short Flags; @@ -1901,7 +1901,7 @@ public: bool isUnlit() const { return isFlag(IS_UNLIT); } bool hasDepthBias() const { return isFlag(HAS_DEPTH_BIAS); } bool isFading() const { return isFlag(IS_FADING); } - bool isAA() const { return isFlag(IS_AA); } + bool isAntiAliased() const { return isFlag(IS_ANTIALIASED); } Flags _flags = 0; short _spare = 0; @@ -1910,9 +1910,9 @@ public: SimpleProgramKey(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false, bool fading = false, bool isAA = true) { + bool unlit = false, bool depthBias = false, bool fading = false, bool isAntiAliased = true) { _flags = (textured ? IS_TEXTURED : 0) | (transparent ? IS_TRANSPARENT : 0) | (culled ? IS_CULLED : 0) | - (unlit ? IS_UNLIT : 0) | (depthBias ? HAS_DEPTH_BIAS : 0) | (fading ? IS_FADING : 0) | (isAA ? IS_AA : 0); + (unlit ? IS_UNLIT : 0) | (depthBias ? HAS_DEPTH_BIAS : 0) | (fading ? IS_FADING : 0) | (isAntiAliased ? IS_ANTIALIASED : 0); } SimpleProgramKey(int bitmask) : _flags(bitmask) {} @@ -1961,8 +1961,8 @@ gpu::PipelinePointer GeometryCache::getWebBrowserProgram(bool transparent) { return transparent ? _simpleTransparentWebBrowserPipelineNoAA : _simpleOpaqueWebBrowserPipelineNoAA; } -void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool transparent, bool culled, bool unlit, bool depthBiased, bool isAA) { - batch.setPipeline(getSimplePipeline(textured, transparent, culled, unlit, depthBiased, false, isAA)); +void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool transparent, bool culled, bool unlit, bool depthBiased, bool isAntiAliased) { + batch.setPipeline(getSimplePipeline(textured, transparent, culled, unlit, depthBiased, false, isAntiAliased)); // If not textured, set a default albedo map if (!textured) { @@ -1971,8 +1971,8 @@ void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool tra } } -gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transparent, bool culled, bool unlit, bool depthBiased, bool fading, bool isAA) { - SimpleProgramKey config { textured, transparent, culled, unlit, depthBiased, fading, isAA }; +gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transparent, bool culled, bool unlit, bool depthBiased, bool fading, bool isAntiAliased) { + SimpleProgramKey config { textured, transparent, culled, unlit, depthBiased, fading, isAntiAliased }; // If the pipeline already exists, return it auto it = _simplePrograms.find(config); @@ -2030,7 +2030,7 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - if (config.isAA()) { + if (config.isAntiAliased()) { config.isTransparent() ? PrepareStencil::testMask(*state) : PrepareStencil::testMaskDrawShape(*state); } else { PrepareStencil::testMaskDrawShapeNoAA(*state); diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 0dc21b5adc..288ab363f0 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -161,10 +161,10 @@ public: // Bind the pipeline and get the state to render static geometry void bindSimpleProgram(gpu::Batch& batch, bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false, bool isAA = true); + bool unlit = false, bool depthBias = false, bool isAntiAliased = true); // Get the pipeline to render static geometry static gpu::PipelinePointer getSimplePipeline(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false, bool fading = false, bool isAA = true); + bool unlit = false, bool depthBias = false, bool fading = false, bool isAntiAliased = true); void bindWebBrowserProgram(gpu::Batch& batch, bool transparent = false); gpu::PipelinePointer getWebBrowserProgram(bool transparent); From 18ce5ba30f5c1e0a0487e00a4942acd866be374f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 14 Sep 2017 08:53:20 +1200 Subject: [PATCH 336/722] Add undo/redo for physics --- scripts/vr-edit/modules/history.js | 27 +++++++++++++++ scripts/vr-edit/modules/selection.js | 50 ++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/scripts/vr-edit/modules/history.js b/scripts/vr-edit/modules/history.js index 6fb5416df3..a2b403f87a 100644 --- a/scripts/vr-edit/modules/history.js +++ b/scripts/vr-edit/modules/history.js @@ -48,6 +48,27 @@ History = (function () { MAX_HISTORY_ITEMS = 100, undoPosition = -1; // The next history item to undo; the next history item to redo = undoIndex + 1. + function doKick(entityID) { + var properties, + NO_KICK_ENTITY_TYPES = ["Text", "Web"], // These entities don't respond to gravity so don't kick them. + DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD + DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.1, z: 0 }; + + properties = Entities.getEntityProperties(entityID, ["type", "dynamic", "gravity", "velocity"]); + if (NO_KICK_ENTITY_TYPES.indexOf(properties.type) === -1 && properties.dynamic + && Vec3.length(properties.gravity) > 0 && Vec3.length(properties.velocity) < DYNAMIC_VELOCITY_THRESHOLD) { + Entities.editEntity(entityID, { velocity: DYNAMIC_VELOCITY_KICK }); + } + } + + function kickPhysics(entityID) { + // Gives entities a small kick to start off physics, if necessary. + var KICK_DELAY = 500; // ms + + // Give physics a chance to catch up. Avoids some erratic behavior. + Script.setTimeout(function () { doKick(entityID); }, KICK_DELAY); + } + function push(undoData, redoData) { // Wipe any redo history after current undo position. if (undoPosition < history.length - 1) { @@ -118,6 +139,9 @@ History = (function () { if (undoData.setProperties) { for (i = 0, length = undoData.setProperties.length; i < length; i += 1) { Entities.editEntity(undoData.setProperties[i].entityID, undoData.setProperties[i].properties); + if (undoData.setProperties[i].properties.gravity) { + kickPhysics(undoData.setProperties[i].entityID); + } } } @@ -151,6 +175,9 @@ History = (function () { if (redoData.setProperties) { for (i = 0, length = redoData.setProperties.length; i < length; i += 1) { Entities.editEntity(redoData.setProperties[i].entityID, redoData.setProperties[i].properties); + if (redoData.setProperties[i].properties.gravity) { + kickPhysics(redoData.setProperties[i].entityID); + } } } diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 60ef02285b..2b6155a032 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -652,6 +652,9 @@ Selection = function (side) { // - Requires child entities to be collisionless, otherwise the entity tree can become self-propelled. // See also: Groups.group() and ungroup(). var properties, + property, + undoData = [], + redoData = [], i, length; @@ -661,14 +664,61 @@ Selection = function (side) { collisionless: physicsProperties.dynamic || physicsProperties.collisionless }; for (i = 1, length = selection.length; i < length; i += 1) { + undoData.push({ + entityID: selection[i].id, + properties: { + dynamic: selection[i].dynamic, + collisionless: selection[i].collisionless + } + }); Entities.editEntity(selection[i].id, properties); + undoData.push({ + entityID: selection[i].id, + properties: properties + }); } + // Undo data. + properties = { + position: selection[0].position, + rotation: selection[0].rotation, + velocity: Vec3.ZERO, + angularVelocity: Vec3.ZERO + }; + for (property in physicsProperties) { + if (physicsProperties.hasOwnProperty(property)) { + properties[property] = selectionProperties[0].properties[property]; + } + } + if (properties.userData === undefined) { + properties.userData = ""; + } + undoData.push({ + entityID: selection[0].id, + properties: properties + }); + // Set root per physicsProperties. properties = Object.clone(physicsProperties); properties.userData = updatePhysicsUserData(selection[intersectedEntityIndex].userData, physicsProperties.userData); Entities.editEntity(rootEntityID, properties); + // Redo data. + properties.position = selection[0].position; + properties.rotation = selection[0].rotation; + properties.velocity = Vec3.ZERO; + properties.angularVelocity = Vec3.ZERO; + redoData.push({ + entityID: selection[0].id, + properties: properties + }); + + // Add history entry. + History.push( + { setProperties: undoData }, + { setProperties: redoData } + ); + // Kick off physics if necessary. if (physicsProperties.dynamic) { kickPhysics(rootEntityID); From 1e2a3a13da223f8f97d8e9abf9ad2f0f13b499c7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 14 Sep 2017 09:31:32 +1200 Subject: [PATCH 337/722] Fix grip-delete entity history --- scripts/vr-edit/modules/selection.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index 2b6155a032..f4033dda4a 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -262,18 +262,20 @@ Selection = function (side) { } // Add history entry. - History.push( - { - setProperties: [ - { entityID: rootEntityID, properties: { position: startPosition, rotation: startOrientation } } - ] - }, - { - setProperties: [ - { entityID: rootEntityID, properties: { position: rootPosition, rotation: rootOrientation } } - ] - } - ); + if (selection.length > 0) { + History.push( + { + setProperties: [ + { entityID: rootEntityID, properties: { position: startPosition, rotation: startOrientation } } + ] + }, + { + setProperties: [ + { entityID: rootEntityID, properties: { position: rootPosition, rotation: rootOrientation } } + ] + } + ); + } // Kick off physics if necessary. if (selection.length > 0 && selection[0].dynamic) { From d501a8f7cccf155d1733809ff321e88de3a8493f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 14 Sep 2017 11:32:58 +1200 Subject: [PATCH 338/722] Fix scaling and changing hands history --- scripts/vr-edit/modules/selection.js | 4 +++- scripts/vr-edit/vr-edit.js | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index f4033dda4a..fb05611266 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -262,7 +262,9 @@ Selection = function (side) { } // Add history entry. - if (selection.length > 0) { + if (selection.length > 0 + && (!Vec3.equal(startPosition, rootPosition) || !Quat.equal(startOrientation, rootOrientation))) { + // Positions and orientations can be identical if change grabbing hands when finish scaling. History.push( { setProperties: [ diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 293e85a775..0197fbb51f 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -522,8 +522,10 @@ function stopDirectScaling() { // Called on grabbing hand by scaling hand. - selection.finishDirectScaling(); - isDirectScaling = false; + if (isDirectScaling) { + selection.finishDirectScaling(); + isDirectScaling = false; + } } function startHandleScaling(targetPosition, overlayID) { @@ -557,10 +559,12 @@ function stopHandleScaling() { // Called on grabbing hand by scaling hand. - handles.finishScaling(); - selection.finishHandleScaling(); - handles.grab(null); // Stop highlighting grabbed handle and resume displaying all handles. - isHandleScaling = false; + if (isHandleScaling) { + handles.finishScaling(); + selection.finishHandleScaling(); + handles.grab(null); // Stop highlighting grabbed handle and resume displaying all handles. + isHandleScaling = false; + } } @@ -739,6 +743,8 @@ } function exitEditorGrabbing() { + stopDirectScaling(); + stopHandleScaling(); finishEditing(); handles.clear(); otherEditor.setHandleOverlays([]); From 6d42e82711a52e19664c055b7b6f5fed4b1add65 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 14 Sep 2017 11:52:36 +1200 Subject: [PATCH 339/722] Condense create and clone to have one history entry --- scripts/vr-edit/modules/createPalette.js | 9 +++-- scripts/vr-edit/modules/history.js | 48 ++++++++++++++++-------- scripts/vr-edit/modules/selection.js | 2 +- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 44e4373476..fff8ac34fb 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -318,8 +318,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { properties, CREATE_OFFSET = { x: 0, y: 0.05, z: -0.02 }, INVERSE_HAND_BASIS_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }), - entityID, - createdEntities; + entityID; itemIndex = paletteItemOverlays.indexOf(intersectionOverlayID); @@ -354,8 +353,10 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { properties.rotation = Quat.multiply(controlHand.orientation(), INVERSE_HAND_BASIS_ROTATION); entityID = Entities.addEntity(properties); if (entityID !== Uuid.NULL) { - createdEntities = [{ entityID: entityID, properties: properties }]; - History.push({ deleteEntities: createdEntities }, { createEntities: createdEntities }); + History.prePush( + { deleteEntities: [{ entityID: entityID }] }, + { createEntities: [{ entityID: entityID, properties: properties }] } + ); } else { Feedback.play(otherSide, Feedback.GENERAL_ERROR); } diff --git a/scripts/vr-edit/modules/history.js b/scripts/vr-edit/modules/history.js index a2b403f87a..5fb0712db3 100644 --- a/scripts/vr-edit/modules/history.js +++ b/scripts/vr-edit/modules/history.js @@ -46,7 +46,9 @@ History = (function () { */ ], MAX_HISTORY_ITEMS = 100, - undoPosition = -1; // The next history item to undo; the next history item to redo = undoIndex + 1. + undoPosition = -1, // The next history item to undo; the next history item to redo = undoIndex + 1. + undoData = {}, + redoData = {}; function doKick(entityID) { var properties, @@ -69,7 +71,17 @@ History = (function () { Script.setTimeout(function () { doKick(entityID); }, KICK_DELAY); } - function push(undoData, redoData) { + function prePush(undo, redo) { + // Stores undo and redo data to include in the next history entry. + undoData = undo; + redoData = redo; + } + + function push(undo, redo) { + // Add a history entry. + undoData = Object.merge(undoData, undo); + redoData = Object.merge(redoData, redo); + // Wipe any redo history after current undo position. if (undoPosition < history.length - 1) { history.splice(undoPosition + 1, history.length - undoPosition - 1); @@ -82,6 +94,9 @@ History = (function () { history.push({ undoData: undoData, redoData: redoData }); undoPosition += 1; + + undoData = {}; + redoData = {}; } function updateEntityIDs(oldEntityID, newEntityID) { @@ -136,6 +151,13 @@ History = (function () { if (undoPosition > -1) { undoData = history[undoPosition].undoData; + if (undoData.createEntities) { + for (i = 0, length = undoData.createEntities.length; i < length; i += 1) { + entityID = Entities.addEntity(undoData.createEntities[i].properties); + updateEntityIDs(undoData.createEntities[i].entityID, entityID); + } + } + if (undoData.setProperties) { for (i = 0, length = undoData.setProperties.length; i < length; i += 1) { Entities.editEntity(undoData.setProperties[i].entityID, undoData.setProperties[i].properties); @@ -145,13 +167,6 @@ History = (function () { } } - if (undoData.createEntities) { - for (i = 0, length = undoData.createEntities.length; i < length; i += 1) { - entityID = Entities.addEntity(undoData.createEntities[i].properties); - updateEntityIDs(undoData.createEntities[i].entityID, entityID); - } - } - if (undoData.deleteEntities) { for (i = 0, length = undoData.deleteEntities.length; i < length; i += 1) { Entities.deleteEntity(undoData.deleteEntities[i].entityID); @@ -172,6 +187,13 @@ History = (function () { if (undoPosition < history.length - 1) { redoData = history[undoPosition + 1].redoData; + if (redoData.createEntities) { + for (i = 0, length = redoData.createEntities.length; i < length; i += 1) { + entityID = Entities.addEntity(redoData.createEntities[i].properties); + updateEntityIDs(redoData.createEntities[i].entityID, entityID); + } + } + if (redoData.setProperties) { for (i = 0, length = redoData.setProperties.length; i < length; i += 1) { Entities.editEntity(redoData.setProperties[i].entityID, redoData.setProperties[i].properties); @@ -181,13 +203,6 @@ History = (function () { } } - if (redoData.createEntities) { - for (i = 0, length = redoData.createEntities.length; i < length; i += 1) { - entityID = Entities.addEntity(redoData.createEntities[i].properties); - updateEntityIDs(redoData.createEntities[i].entityID, entityID); - } - } - if (redoData.deleteEntities) { for (i = 0, length = redoData.deleteEntities.length; i < length; i += 1) { Entities.deleteEntity(redoData.deleteEntities[i].entityID); @@ -199,6 +214,7 @@ History = (function () { } return { + prePush: prePush, push: push, hasUndo: hasUndo, hasRedo: hasRedo, diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index fb05611266..b06b75fba9 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -571,7 +571,7 @@ Selection = function (side) { rootEntityID = selection[0].id; // Add history entry. - History.push( + History.prePush( { deleteEntities: undoData }, { createEntities: redoData } ); From 8e25ccac7968c9f6fd0d789fe7b328c7a83081ff Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 14 Sep 2017 12:20:33 +1200 Subject: [PATCH 340/722] Tidying --- scripts/vr-edit/utilities/utilities.js | 5 +++++ scripts/vr-edit/vr-edit.js | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/vr-edit/utilities/utilities.js b/scripts/vr-edit/utilities/utilities.js index 8dacec8c2d..e8c79b9886 100644 --- a/scripts/vr-edit/utilities/utilities.js +++ b/scripts/vr-edit/utilities/utilities.js @@ -107,6 +107,11 @@ if (typeof Object.merge !== "function") { Object.merge = function (objectA, objectB) { var a = JSON.stringify(objectA), b = JSON.stringify(objectB); + if (a === "{}") { + return JSON.parse(b); // Always return a new object. + } else if (b === "{}") { + return JSON.parse(a); // "" + } return JSON.parse(a.slice(0, -1) + "," + b.slice(1)); }; } diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 0197fbb51f..0adf6fd098 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -71,7 +71,7 @@ button, DOMAIN_CHANGED_MESSAGE = "Toolbar-DomainChanged", - DEBUG = false; + DEBUG = true; // Utilities Script.include("./utilities/utilities.js"); @@ -1544,7 +1544,7 @@ case "undoAction": if (History.hasUndo()) { - Feedback.play(dominantHand, Feedback.UNDO_ACTION) + Feedback.play(dominantHand, Feedback.UNDO_ACTION); History.undo(); } else { Feedback.play(dominantHand, Feedback.GENERAL_ERROR); @@ -1552,7 +1552,7 @@ break; case "redoAction": if (History.hasRedo()) { - Feedback.play(dominantHand, Feedback.REDO_ACTION) + Feedback.play(dominantHand, Feedback.REDO_ACTION); History.redo(); } else { Feedback.play(dominantHand, Feedback.GENERAL_ERROR); From a70f7dfc878de238c92ade1b452d30f493f631cb Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 13 Sep 2017 22:05:20 -0700 Subject: [PATCH 341/722] put updateCamera in its function --- interface/src/Application.cpp | 112 +++++++++++++++++++++++++++++++++- interface/src/Application.h | 2 + 2 files changed, 111 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 63e4069c01..4efad9000b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2371,6 +2371,111 @@ void Application::initializeUi() { offscreenSurfaceCache->reserve(Web3DOverlay::QML, 2); } +void Application::updateCamera(RenderArgs& renderArgs) { + + glm::vec3 boomOffset; + { + PROFILE_RANGE(render, "/updateCamera"); + { + PerformanceTimer perfTimer("CameraUpdates"); + + auto myAvatar = getMyAvatar(); + boomOffset = myAvatar->getScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; + + // The render mode is default or mirror if the camera is in mirror mode, assigned further below + renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; + renderArgs._boomOffset = boomOffset; + + // Always use the default eye position, not the actual head eye position. + // Using the latter will cause the camera to wobble with idle animations, + // or with changes from the face tracker + if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { + if (isHMDMode()) { + mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); + _myCamera.setPosition(extractTranslation(camMat)); + _myCamera.setOrientation(glm::quat_cast(camMat)); + } + else { + _myCamera.setPosition(myAvatar->getDefaultEyePosition()); + _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); + } + } + else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { + if (isHMDMode()) { + auto hmdWorldMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); + _myCamera.setOrientation(glm::normalize(glm::quat_cast(hmdWorldMat))); + _myCamera.setPosition(extractTranslation(hmdWorldMat) + + myAvatar->getOrientation() * boomOffset); + } + else { + _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); + if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + _myCamera.getOrientation() * boomOffset); + } + else { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + myAvatar->getOrientation() * boomOffset); + } + } + } + else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { + if (isHMDMode()) { + auto mirrorBodyOrientation = myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)); + + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD yaw and roll + glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); + mirrorHmdEulers.y = -mirrorHmdEulers.y; + mirrorHmdEulers.z = -mirrorHmdEulers.z; + glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); + + glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; + + _myCamera.setOrientation(worldMirrorRotation); + + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD lateral offsets + hmdOffset.x = -hmdOffset.x; + + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getUniformScale(), 0) + + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + + mirrorBodyOrientation * hmdOffset); + } + else { + _myCamera.setOrientation(myAvatar->getOrientation() + * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getUniformScale(), 0) + + (myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * + glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); + } + renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; + } + else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { + EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); + if (cameraEntity != nullptr) { + if (isHMDMode()) { + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + _myCamera.setOrientation(cameraEntity->getRotation() * hmdRotation); + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + _myCamera.setPosition(cameraEntity->getPosition() + (hmdRotation * hmdOffset)); + } + else { + _myCamera.setOrientation(cameraEntity->getRotation()); + _myCamera.setPosition(cameraEntity->getPosition()); + } + } + } + // Update camera position + if (!isHMDMode()) { + _myCamera.update(1.0f / _frameCounter.rate()); + } + } + } +} + void Application::paintGL() { // Some plugins process message events, allowing paintGL to be called reentrantly. if (_aboutToQuit || _window->isMinimized()) { @@ -2457,7 +2562,9 @@ void Application::paintGL() { _applicationOverlay.renderOverlay(&renderArgs); } - glm::vec3 boomOffset; + updateCamera(renderArgs); + + /* glm::vec3 boomOffset; { PROFILE_RANGE(render, "/updateCamera"); { @@ -2549,7 +2656,7 @@ void Application::paintGL() { } } } - + */ { PROFILE_RANGE(render, "/updateCompositor"); getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform()); @@ -2570,7 +2677,6 @@ void Application::paintGL() { { PROFILE_RANGE(render, "/mainRender"); PerformanceTimer perfTimer("mainRender"); - renderArgs._boomOffset = boomOffset; // FIXME is this ever going to be different from the size previously set in the render args // in the overlay render? // Viewport is assigned to the size of the framebuffer diff --git a/interface/src/Application.h b/interface/src/Application.h index c7f83ad28f..3a6a183512 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -147,6 +147,8 @@ public: void initializeGL(); void initializeUi(); + + void updateCamera(RenderArgs& renderArgs); void paintGL(); void resizeGL(); From 299a8078e3f44115c6de8081c3a7c27eb2a46dfe Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 14 Sep 2017 17:08:26 +1200 Subject: [PATCH 342/722] Revert handControllerGrab.js related changes --- .../system/controllers/handControllerGrab.js | 50 +++++++++---------- scripts/system/libraries/utils.js | 10 ++-- scripts/vr-edit/vr-edit.js | 3 +- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index d7f4ebb99d..f6eec7ab76 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -14,7 +14,7 @@ /* global getEntityCustomData, flatten, Xform, Script, Quat, Vec3, MyAvatar, Entities, Overlays, Settings, Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset, - setGrabCommunications, Menu, HMD, isInEditMode, isInVREditMode, AvatarList */ + setGrabCommunications, Menu, HMD, isInEditMode, AvatarList */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE @@ -369,8 +369,8 @@ function projectOntoOverlayXYPlane(overlayID, worldPos) { resolution.z = 1; // Circumvent divide-by-zero. var scale = Overlays.getProperty(overlayID, "dimensions"); if (scale) { - scale.z = 0.01; // overlay dimensions are 2D, not 3D. - dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); + scale.z = 0.01; // overlay dimensions are 2D, not 3D. + dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); } } else { dimensions = Overlays.getProperty(overlayID, "dimensions"); @@ -380,8 +380,8 @@ function projectOntoOverlayXYPlane(overlayID, worldPos) { } if (position && rotation && dimensions) { - return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT); -} + return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT); + } } function handLaserIntersectItem(position, rotation, start) { @@ -539,7 +539,7 @@ function storeAttachPointForHotspotInSettings(hotspot, hand, offsetPosition, off var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; function isEditing() { - return EXTERNALLY_MANAGED_2D_MINOR_MODE && (isInEditMode() || isInVREditMode()); + return EXTERNALLY_MANAGED_2D_MINOR_MODE && isInEditMode(); } function isIn2DMode() { @@ -1313,7 +1313,7 @@ function MyController(hand) { }; this.setState = function(newState, reason) { - if (((isInEditMode() || isInVREditMode()) && this.grabbedThingID !== HMD.tabletID) && + if ((isInEditMode() && this.grabbedThingID !== HMD.tabletID) && (newState !== STATE_OFF && newState !== STATE_SEARCHING && newState !== STATE_STYLUS_TOUCHING && @@ -1752,7 +1752,7 @@ function MyController(hand) { var nonTabletEntities = grabbableEntities.filter(function(entityID) { return entityID != HMD.tabletID && entityID != HMD.homeButtonID; }); - if (nonTabletEntities.length > 0 && !isInEditMode() && !isInVREditMode()) { + if (nonTabletEntities.length > 0) { Controller.triggerHapticPulse(1, 20, this.hand); } this.grabPointIntersectsEntity = true; @@ -1765,9 +1765,9 @@ function MyController(hand) { this.processStylus(); - if (isInEditMode() && !isInVREditMode() && !this.isNearStylusTarget && HMD.isHandControllerAvailable()) { + if (isInEditMode() && !this.isNearStylusTarget && HMD.isHandControllerAvailable()) { // Always showing lasers while in edit mode and hands/stylus is not active. - // But don't show lasers while in VR edit mode. + var rayPickInfo = this.calcRayPickInfo(this.hand); if (rayPickInfo.isValid) { this.intersectionDistance = (rayPickInfo.entityID || rayPickInfo.overlayID) ? rayPickInfo.distance : 0; @@ -1827,16 +1827,16 @@ function MyController(hand) { var pickRay; var valid = true - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var worldHandPosition = controllerLocation.position; - var worldHandRotation = controllerLocation.orientation; - valid = !(worldHandPosition === undefined); + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var worldHandPosition = controllerLocation.position; + var worldHandRotation = controllerLocation.orientation; + valid = !(worldHandPosition === undefined); - pickRay = { + pickRay = { origin: PICK_WITH_HAND_RAY ? worldHandPosition : MyAvatar.getHeadPosition(), direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Quat.getFront(Camera.orientation), - length: PICK_MAX_DISTANCE - }; + length: PICK_MAX_DISTANCE + }; var result = { entityID: null, @@ -2289,7 +2289,7 @@ function MyController(hand) { return aDistance - bDistance; }); entity = grabbableEntities[0]; - if ((!isInEditMode() && !isInVREditMode()) || entity == HMD.tabletID) { // tablet is grabbable, even when editing + if (!isInEditMode() || entity == HMD.tabletID) { // tablet is grabbable, even when editing name = entityPropertiesCache.getProps(entity).name; this.grabbedThingID = entity; this.grabbedIsOverlay = false; @@ -2389,7 +2389,7 @@ function MyController(hand) { equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); } - if (farGrabEnabled && farSearching && !isInVREditMode()) { + if (farGrabEnabled && farSearching) { this.updateLaserPointer(); } Reticle.setVisible(false); @@ -3431,13 +3431,13 @@ function MyController(hand) { var intersection = LaserPointers.getPrevRayPickResult(laserPointerID); if (intersection.type != RayPick.INTERSECTED_NONE) { if (intersection.objectID != this.grabbedThingID) { - this.callEntityMethodOnGrabbed("stopFarTrigger"); - this.grabbedThingID = null; - this.setState(STATE_OFF, "laser moved off of entity"); - return; - } + this.callEntityMethodOnGrabbed("stopFarTrigger"); + this.grabbedThingID = null; + this.setState(STATE_OFF, "laser moved off of entity"); + return; + } this.intersectionDistance = intersection.distance; - if (farGrabEnabled) { + if (farGrabEnabled) { this.updateLaserPointer(); } } diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index 0f367e0cfe..a5e97d8949 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -6,16 +6,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -EDIT_SETTING = "io.highfidelity.isEditing"; // Note: This constant is duplicated in edit.js. -isInEditMode = function () { +// note: this constant is currently duplicated in edit.js +EDIT_SETTING = "io.highfidelity.isEditting"; +isInEditMode = function isInEditMode() { return Settings.getValue(EDIT_SETTING); }; -VR_EDIT_SETTING = "io.highfidelity.isVREditing"; // Note: This constant is duplicated in vr-edit.js. -isInVREditMode = function () { - return HMD.active && Settings.getValue(VR_EDIT_SETTING); -} - if (!Function.prototype.bind) { Function.prototype.bind = function(oThis) { if (typeof this !== 'function') { diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 0adf6fd098..854bdea561 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -18,7 +18,6 @@ APP_ICON_DISABLED = "icons/tablet-icons/edit-disabled.svg", ENABLED_CAPTION_COLOR_OVERRIDE = "", DISABLED_CAPTION_COLOR_OVERRIDE = "#888888", - VR_EDIT_SETTING = "io.highfidelity.isVREditing", // Note: This constant is duplicated in utils.js. START_DELAY = 2000, // ms // Application state @@ -1392,7 +1391,7 @@ function updateHandControllerGrab() { // Communicate app status to handControllerGrab.js. - Settings.setValue(VR_EDIT_SETTING, isAppActive); + // TODO } function onUICommand(command, parameter) { From f22e2de52579d4871ea896567b516e57bdc8f6fb Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 14 Sep 2017 17:21:45 +1200 Subject: [PATCH 343/722] Interim method for disabling controllerDispatcher lasers, grabbing, etc. --- scripts/vr-edit/vr-edit.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 854bdea561..d94c84e9d9 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1389,9 +1389,12 @@ updateTimer = Script.setTimeout(update, UPDATE_LOOP_TIMEOUT); } - function updateHandControllerGrab() { - // Communicate app status to handControllerGrab.js. - // TODO + function updateControllerDispatcher() { + // Communicate app status to controllerDispatcher.js. + var DISABLE_HANDS = "both", + ENABLE_HANDS = "none"; + // TODO: Proper method to disable specific laser and grabbing functionality. + Messages.sendLocalMessage('Hifi-Hand-Disabler', isAppActive ? DISABLE_HANDS : ENABLE_HANDS); } function onUICommand(command, parameter) { @@ -1598,7 +1601,7 @@ } isAppActive = !isAppActive; - updateHandControllerGrab(); + updateControllerDispatcher(); button.editProperties({ isActive: isAppActive }); if (isAppActive) { @@ -1613,7 +1616,7 @@ var hasRezPermissions = Entities.canRez() || Entities.canRezTmp(); if (isAppActive && !hasRezPermissions) { isAppActive = false; - updateHandControllerGrab(); + updateControllerDispatcher(); stopApp(); } button.editProperties({ @@ -1628,7 +1631,7 @@ var hasRezPermissions = Entities.canRez() || Entities.canRezTmp(); if (isAppActive && !hasRezPermissions) { isAppActive = false; - updateHandControllerGrab(); + updateControllerDispatcher(); stopApp(); } button.editProperties({ @@ -1673,7 +1676,7 @@ // Close the app because the new avatar may have different joint numbers meaning that the UI would be attached // incorrectly. Let the user reopen the app because it can take some time for the new avatar to load. isAppActive = false; - updateHandControllerGrab(); + updateControllerDispatcher(); button.editProperties({ isActive: false }); stopApp(); } @@ -1691,7 +1694,7 @@ // Application state. isAppActive = false; - updateHandControllerGrab(); + updateControllerDispatcher(); dominantHand = MyAvatar.getDominantHand() === "left" ? LEFT_HAND : RIGHT_HAND; // Tablet/toolbar button. @@ -1751,7 +1754,7 @@ MyAvatar.skeletonChanged.disconnect(onSkeletonChanged); isAppActive = false; - updateHandControllerGrab(); + updateControllerDispatcher(); if (button) { button.clicked.disconnect(onAppButtonClicked); From 2f86658308e249eb7db7ce74fd26aa62bc56b132 Mon Sep 17 00:00:00 2001 From: vladest Date: Thu, 14 Sep 2017 16:37:02 +0200 Subject: [PATCH 344/722] Move Tablet becomes toolbar menu from General to Developer menu --- interface/src/Menu.cpp | 14 ++++++++++++++ interface/src/Menu.h | 2 ++ interface/src/ui/PreferencesDialog.cpp | 10 ---------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 005d478411..9df22ab08e 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -367,6 +367,20 @@ Menu::Menu() { QString("../../hifi/tablet/TabletGraphicsPreferences.qml"), "GraphicsPreferencesDialog"); }); + // Developer > UI >>> + MenuWrapper* uiOptionsMenu = developerMenu->addMenu("UI"); + action = addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::DesktopTabletToToolbar, 0, + qApp->getDesktopTabletBecomesToolbarSetting()); + connect(action, &QAction::triggered, [action] { + qApp->setDesktopTabletBecomesToolbarSetting(action->isChecked()); + }); + + action = addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::HMDTabletToToolbar, 0, + qApp->getHmdTabletBecomesToolbarSetting()); + connect(action, &QAction::triggered, [action] { + qApp->setHmdTabletBecomesToolbarSetting(action->isChecked()); + }); + // Developer > Render >>> MenuWrapper* renderOptionsMenu = developerMenu->addMenu("Render"); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::WorldAxes); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index d77c87a6fc..854f8d8c2b 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -200,6 +200,8 @@ namespace MenuOption { const QString VisibleToFriends = "Friends"; const QString VisibleToNoOne = "No one"; const QString WorldAxes = "World Axes"; + const QString DesktopTabletToToolbar = "Desktop Tablet Becomes Toolbar"; + const QString HMDTabletToToolbar = "HMD Tablet Becomes Toolbar"; } #endif // hifi_Menu_h diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 1c3df3210c..4d0434fe34 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -91,16 +91,6 @@ void setupPreferences() { preference->setMax(500); preferences->addPreference(preference); } - { - auto getter = []()->bool { return qApp->getDesktopTabletBecomesToolbarSetting(); }; - auto setter = [](bool value) { qApp->setDesktopTabletBecomesToolbarSetting(value); }; - preferences->addPreference(new CheckPreference(UI_CATEGORY, "Desktop Tablet Becomes Toolbar", getter, setter)); - } - { - auto getter = []()->bool { return qApp->getHmdTabletBecomesToolbarSetting(); }; - auto setter = [](bool value) { qApp->setHmdTabletBecomesToolbarSetting(value); }; - preferences->addPreference(new CheckPreference(UI_CATEGORY, "HMD Tablet Becomes Toolbar", getter, setter)); - } { auto getter = []()->bool { return qApp->getPreferAvatarFingerOverStylus(); }; auto setter = [](bool value) { qApp->setPreferAvatarFingerOverStylus(value); }; From 04305155d098365f8753a4ec315d6d1f75cb4bac Mon Sep 17 00:00:00 2001 From: beholder Date: Fri, 1 Sep 2017 00:49:02 +0300 Subject: [PATCH 345/722] make building tools (besides 'scribe') optional --- CMakeLists.txt | 5 ++--- tools/CMakeLists.txt | 30 ++++++++++++++++-------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index be513abddb..5652a20335 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,9 +101,8 @@ if (BUILD_CLIENT OR BUILD_SERVER) add_subdirectory(plugins) endif() -if (BUILD_TOOLS) - add_subdirectory(tools) -endif() +# BUILD_TOOLS option will be handled inside the tools's CMakeLists.txt because 'scribe' tool is required for build anyway +add_subdirectory(tools) if (BUILD_TESTS) add_subdirectory(tests) diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 5de44e8897..cf11ef9e7a 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -2,23 +2,25 @@ add_subdirectory(scribe) set_target_properties(scribe PROPERTIES FOLDER "Tools") -add_subdirectory(udt-test) -set_target_properties(udt-test PROPERTIES FOLDER "Tools") +if (BUILD_TOOLS) + add_subdirectory(udt-test) + set_target_properties(udt-test PROPERTIES FOLDER "Tools") -add_subdirectory(vhacd-util) -set_target_properties(vhacd-util PROPERTIES FOLDER "Tools") + add_subdirectory(vhacd-util) + set_target_properties(vhacd-util PROPERTIES FOLDER "Tools") -add_subdirectory(ice-client) -set_target_properties(ice-client PROPERTIES FOLDER "Tools") + add_subdirectory(ice-client) + set_target_properties(ice-client PROPERTIES FOLDER "Tools") -add_subdirectory(ac-client) -set_target_properties(ac-client PROPERTIES FOLDER "Tools") + add_subdirectory(ac-client) + set_target_properties(ac-client PROPERTIES FOLDER "Tools") -add_subdirectory(skeleton-dump) -set_target_properties(skeleton-dump PROPERTIES FOLDER "Tools") + add_subdirectory(skeleton-dump) + set_target_properties(skeleton-dump PROPERTIES FOLDER "Tools") -add_subdirectory(atp-client) -set_target_properties(atp-client PROPERTIES FOLDER "Tools") + add_subdirectory(atp-client) + set_target_properties(atp-client PROPERTIES FOLDER "Tools") -add_subdirectory(oven) -set_target_properties(oven PROPERTIES FOLDER "Tools") + add_subdirectory(oven) + set_target_properties(oven PROPERTIES FOLDER "Tools") +endif() From 378793d18ebae32b30a0b296801dcced19ad1c07 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 15 Sep 2017 08:48:47 +1200 Subject: [PATCH 346/722] Proper color circle --- .../assets/tools/color/color-circle-black.png | Bin 5270 -> 9588 bytes .../assets/tools/color/color-circle.png | Bin 76746 -> 129833 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/scripts/vr-edit/assets/tools/color/color-circle-black.png b/scripts/vr-edit/assets/tools/color/color-circle-black.png index 3494b63b708f8ba2945cc6ac445c0c5bc7a13444..4b62c28a4daadf7bdfb4002d2c9516bc5f8fd2e9 100644 GIT binary patch literal 9588 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 Q006+~tofNTJ=Yum2Y0~9`Tzg` diff --git a/scripts/vr-edit/assets/tools/color/color-circle.png b/scripts/vr-edit/assets/tools/color/color-circle.png index 408139972ebd4fc7254c19a67dbc9ab20c1e3cc3..8bf261338242913351cb016523b97347261013de 100644 GIT binary patch literal 129833 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�+*!U0g4-ZW-JsS$|hAS4k+F98)I6wl4O;_t!~NkRBd zz5sC>05v)ojm`qA#KDIUf!7-PT1)@nM=AcFM+0D={=(n!^5;*Tul_v((L4GL0mrq_ zALZB2KR=@%#-c&#LWDNMpRtSoZh`-=Mc)#fo2`AbaBd{N-pC~_!@ff_L{@YItOYQs zEms#^0Fn}@5U(z9qgnqOC%nB$Mg6@=vd~PJm4rAG5IKL}?@fsl{gL$&y*uEVen4pK zEJ5HysMk#-ERLr2E$M`2t@|R-hyOsR^dZH2D3g4r#4$1PL!b`p#sE-jdojfiVT(XD z_*P7T)bIZkiHDAs-c$;I55Ie?q>t`&P{m(N`Kf?epI}$i`3!lBAuBoprst$4VR|;! zkqrc)9g+8roPYA)pVfQ_0oI=$rg^Kpf5|?eremstNQO$OnR%`@$J=>fw?O45uqyW5 ziePRSehUAh{Ve`X|Gz$?2H<0_0NU?+dAM&(C;hY$@CBsb75J5S? zXMA)eRB^Lt<7p!raoZ=pV*hJc2>?Tu*%y#Qvy(^0CEaox{Xx>q4G1MX&p2zFXD#db!v*t6y=qlUT?G^p226}iY@>m z2niyF*YXBIp&cwESZbZH{EUk^be2IX!Xg78i2^~J0%4V~6m}J4M92+Jv;uG6d#$Cv z@8$o0`g1@4KCBtAPk-_C&i{Nd7rNXC*vTs}^wz*Dv!9hX&`#(9Yr6kcBamUmr|?|2 zUGx`G08hfck>~>x>v`MXHTOM!t-RkyyKbdZ_^OeQas`ACesM~J=9zPRZwGrHy8Hdu z%$gt?jsYG)SCET;E;8&VaNOT6dHJinz`8aUbL;AgULQxyIt&*1r77G-u~)7sirffQ zN@7TEzoP&+Bfv{TRZTi@BXNMk@oO`tMK5RE;F|8wutgp>fE~ zfe9~j1@UK|U%}=XU5L2>Mx5^u0Wy{l2MA|4FM>41S0l{Wx$&_3$0m3v7z&KvNAZF< zd1X_~z5r-702Y)SgVF-6mGdd9UjuvIkA|DC!a)zs1F!R$&}9rdvMi<>ORor;)(CUBwzIyl<}T7hP|7nkHB3S=}O!1hTfIPxU!icZ*`!*XL4 z1Bkf6;Jkn&WJDGVCex*7;iw35M(hi5p#my3@Lexozt+}Y`rrm2Jol3h)A^8Q!2W

    (zX`>jOyMuY(1INw1NBp2?u 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 Date: Fri, 15 Sep 2017 09:54:55 +1200 Subject: [PATCH 347/722] Fix history crash --- scripts/vr-edit/modules/history.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/vr-edit/modules/history.js b/scripts/vr-edit/modules/history.js index 5fb0712db3..039f078ad0 100644 --- a/scripts/vr-edit/modules/history.js +++ b/scripts/vr-edit/modules/history.js @@ -90,6 +90,7 @@ History = (function () { // Limit the number of history items. if (history.length >= MAX_HISTORY_ITEMS) { history.splice(0, history.length - MAX_HISTORY_ITEMS + 1); + undoPosition = history.length - 1; } history.push({ undoData: undoData, redoData: redoData }); From c6af82dedc2def76035728d1e2fcd89b03fb2f45 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 15 Sep 2017 09:55:14 +1200 Subject: [PATCH 348/722] Increase maximum number of undo levels to 1000 --- scripts/vr-edit/modules/history.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/history.js b/scripts/vr-edit/modules/history.js index 039f078ad0..92cd4d047f 100644 --- a/scripts/vr-edit/modules/history.js +++ b/scripts/vr-edit/modules/history.js @@ -45,7 +45,7 @@ History = (function () { } */ ], - MAX_HISTORY_ITEMS = 100, + MAX_HISTORY_ITEMS = 1000, undoPosition = -1, // The next history item to undo; the next history item to redo = undoIndex + 1. undoData = {}, redoData = {}; From 9da3ed0cebbb7b3daa5c01fd2b60ea02f2cdca24 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 15 Sep 2017 11:24:33 +1200 Subject: [PATCH 349/722] Emissive Create palette models --- scripts/vr-edit/assets/create/cone.fbx | Bin 19056 -> 23740 bytes scripts/vr-edit/assets/create/cube.fbx | Bin 17696 -> 17564 bytes scripts/vr-edit/assets/create/cylinder.fbx | Bin 20240 -> 24364 bytes scripts/vr-edit/assets/create/icosahedron.fbx | Bin 17904 -> 20028 bytes scripts/vr-edit/assets/create/octahedron.fbx | Bin 17024 -> 17420 bytes scripts/vr-edit/assets/create/prism.fbx | Bin 17392 -> 17548 bytes scripts/vr-edit/assets/create/sphere.fbx | Bin 29312 -> 46572 bytes scripts/vr-edit/assets/create/tetrahedron.fbx | Bin 16720 -> 17148 bytes 8 files changed, 0 insertions(+), 0 deletions(-) diff --git a/scripts/vr-edit/assets/create/cone.fbx b/scripts/vr-edit/assets/create/cone.fbx index b883b042c56337c43754ddd3fa7614304cdb7c48..d590ee04be59c63f7b77610f5902c2b05cd718dc 100644 GIT binary patch literal 23740 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 NwP|{-Z-mOU{|6d!3Ge^_ literal 19056 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 CO_Y=X diff --git a/scripts/vr-edit/assets/create/cube.fbx b/scripts/vr-edit/assets/create/cube.fbx index 530122b16efab5be9127898ba6f25bcf7d28872e..fa079219fe04503653973f859f3416d3c0212dfa 100644 GIT binary patch literal 17564 zcmc&+2~-qU)_$l6ih?ojQNhuO;sz20)D~Gp39GcAiAk{NE}&v}Ra;#xpaL#2CdO#Y zBu5h!@+Wbee;hQ?iA%y_Ox(tWQARN?;7V&W0wSP*_MBVqb$7K*H!aS8{ )m3%B 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 Lbi1KjKho_#Ti%C= literal 17696 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 diff --git a/scripts/vr-edit/assets/create/cylinder.fbx b/scripts/vr-edit/assets/create/cylinder.fbx index 250ce66773f3f970ef26c58798ac070956746bad..319cbf0a6d40d7ed2c5ac0bd05b8593106b10546 100644 GIT binary patch literal 24364 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 MZB~mI$J4r@%XV%$SNFV}LXsfM{ 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 k*{AMK^Mn_F5AMHJE diff --git a/scripts/vr-edit/assets/create/icosahedron.fbx b/scripts/vr-edit/assets/create/icosahedron.fbx index 52f948816ba00e8a1722ef375621f394d1260460..3103d6b2d6709bb33acd40fd9872bbe570a459fb 100644 GIT binary patch literal 20028 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 Q)5{AU+-*^)OYhhGzf}QULjV8( literal 17392 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 diff --git a/scripts/vr-edit/assets/create/tetrahedron.fbx b/scripts/vr-edit/assets/create/tetrahedron.fbx index 23424e0b47ff42bf2d7a8aa10adfc89aaa1d0dbb..60dd70edb1b9cd279a1b0cdb213396950e9eb994 100644 GIT binary patch literal 17148 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 Date: Thu, 14 Sep 2017 17:07:50 -0700 Subject: [PATCH 350/722] Working version that fixes the web overlay / tablet --- interface/src/ui/overlays/Base3DOverlay.cpp | 3 ++- libraries/render-utils/src/Model.cpp | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 22448cc719..7d6fa40989 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -307,7 +307,8 @@ void Base3DOverlay::update(float duration) { transaction.updateItem(itemID, [renderTransform](Overlay& data) { auto overlay3D = dynamic_cast(&data); if (overlay3D) { - overlay3D->setRenderTransform(renderTransform);// evalRenderTransform(); + auto latestTransform = overlay3D->evalRenderTransform(); + overlay3D->setRenderTransform(latestTransform);// evalRenderTransform(); } }); #endif diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 9e552859e3..5041f953a1 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -234,21 +234,28 @@ void Model::updateRenderItems() { self->updateClusterMatrices(); uint32_t deleteGeometryCounter = self->_deleteGeometryCounter; - - // Transform modelTransform = self->getTransform(); +#ifdef CAPTURE_TRANSFORM_IN_GAMEPLAY + Transform modelTransform = self->getTransform(); // Transform modelTransform = model->getTransform(); - // modelTransform.setScale(glm::vec3(1.0f)); - + modelTransform.setScale(glm::vec3(1.0f)); +#endif render::Transaction transaction; foreach (auto itemID, self->_modelMeshRenderItemsMap.keys()) { - transaction.updateItem(itemID, [deleteGeometryCounter /*, modelTransform*/](ModelMeshPartPayload& data) { + transaction.updateItem(itemID, [deleteGeometryCounter +#ifdef CAPTURE_TRANSFORM_IN_GAMEPLAY + , modelTransform +#endif + ](ModelMeshPartPayload& data) { ModelPointer model = data._model.lock(); if (model && model->isLoaded()) { // Ensure the model geometry was not reset between frames if (deleteGeometryCounter == model->_deleteGeometryCounter) { +#ifdef CAPTURE_TRANSFORM_IN_GAMEPLAY +#else Transform modelTransform = model->getTransform(); modelTransform.setScale(glm::vec3(1.0f)); +#endif const Model::MeshState& state = model->getMeshState(data._meshIndex); Transform renderTransform = modelTransform; From f02d057a83c4ca74c9ca17d2d52805d5d46c992d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 15 Sep 2017 13:43:53 +1200 Subject: [PATCH 351/722] Fix not kicking off physics if hand is inside entity --- scripts/vr-edit/modules/selection.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index b06b75fba9..a36e69aa96 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -29,6 +29,7 @@ Selection = function (side) { scaleRootOrientation, startPosition, startOrientation, + isEditing = false, ENTITY_TYPE = "entity", ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "PolyLine", "PolyVox"], ENTITY_TYPES_2D = ["Text", "Web"], @@ -207,7 +208,7 @@ Selection = function (side) { DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.1, z: 0 }; - if (entityID === rootEntityID) { + if (entityID === rootEntityID && isEditing) { // Don't kick if have started editing entity again. return; } @@ -247,6 +248,8 @@ Selection = function (side) { // Stop moving. Entities.editEntity(rootEntityID, { velocity: Vec3.ZERO, angularVelocity: Vec3.ZERO }); + + isEditing = true; } function finishEditing() { @@ -283,6 +286,8 @@ Selection = function (side) { if (selection.length > 0 && selection[0].dynamic) { kickPhysics(selection[0].id); } + + isEditing = false; } function getPositionAndOrientation() { From 4b63d8b7d91eb770b4e5815c2a648df186d27375 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 15 Sep 2017 13:48:01 +1200 Subject: [PATCH 352/722] Don't kick polylines when apply physics --- scripts/vr-edit/modules/history.js | 2 +- scripts/vr-edit/modules/selection.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/history.js b/scripts/vr-edit/modules/history.js index 92cd4d047f..c9e2f5f934 100644 --- a/scripts/vr-edit/modules/history.js +++ b/scripts/vr-edit/modules/history.js @@ -52,7 +52,7 @@ History = (function () { function doKick(entityID) { var properties, - NO_KICK_ENTITY_TYPES = ["Text", "Web"], // These entities don't respond to gravity so don't kick them. + NO_KICK_ENTITY_TYPES = ["Text", "Web", "PolyLine"], // These entities don't respond to gravity so don't kick them. DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.1, z: 0 }; diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index a36e69aa96..d0c8ca3ebd 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -204,7 +204,7 @@ Selection = function (side) { function doKick(entityID) { var properties, - NO_KICK_ENTITY_TYPES = ["Text", "Web"], // These entities don't respond to gravity so don't kick them. + NO_KICK_ENTITY_TYPES = ["Text", "Web", "PolyLine"], // These entities don't respond to gravity so don't kick them. DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.1, z: 0 }; From 1f63425e9f032edfaa73654402fb419a1ddd86b1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 15 Sep 2017 13:58:58 +1200 Subject: [PATCH 353/722] Don't kick particle effects when apply physics --- scripts/vr-edit/modules/history.js | 2 +- scripts/vr-edit/modules/selection.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/history.js b/scripts/vr-edit/modules/history.js index c9e2f5f934..88537710b9 100644 --- a/scripts/vr-edit/modules/history.js +++ b/scripts/vr-edit/modules/history.js @@ -52,7 +52,7 @@ History = (function () { function doKick(entityID) { var properties, - NO_KICK_ENTITY_TYPES = ["Text", "Web", "PolyLine"], // These entities don't respond to gravity so don't kick them. + NO_KICK_ENTITY_TYPES = ["Text", "Web", "PolyLine", "ParticleEffect"], // Don't respond to gravity so don't kick. DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.1, z: 0 }; diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index d0c8ca3ebd..c0d3333a77 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -204,7 +204,7 @@ Selection = function (side) { function doKick(entityID) { var properties, - NO_KICK_ENTITY_TYPES = ["Text", "Web", "PolyLine"], // These entities don't respond to gravity so don't kick them. + NO_KICK_ENTITY_TYPES = ["Text", "Web", "PolyLine", "ParticleEffect"], // Don't respond to gravity so don't kick. DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.1, z: 0 }; From 455db1ac829c94850e5eff716183ed91fd178183 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 15 Sep 2017 19:11:04 +1200 Subject: [PATCH 354/722] Tighten up handle scaling to keep handle 1-1 with hand/laser --- scripts/vr-edit/modules/hand.js | 12 ++++++++++-- scripts/vr-edit/modules/handles.js | 17 +++++++++++++---- scripts/vr-edit/utilities/utilities.js | 6 ++++++ scripts/vr-edit/vr-edit.js | 8 +++++++- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/scripts/vr-edit/modules/hand.js b/scripts/vr-edit/modules/hand.js index 16bd033a74..174068a3d1 100644 --- a/scripts/vr-edit/modules/hand.js +++ b/scripts/vr-edit/modules/hand.js @@ -111,6 +111,7 @@ Hand = function (side) { overlayID, overlayIDs, overlayDistance, + intersectionPosition, distance, entityID, entityIDs, @@ -169,9 +170,12 @@ Hand = function (side) { } } } - if (handleOverlayIDs.indexOf(overlayID) === -1) { + if (overlayID && handleOverlayIDs.indexOf(overlayID) === -1) { overlayID = null; } + if (overlayID) { + intersectionPosition = Overlays.getProperty(overlayID, "position"); + } } // Hand-entity intersection, if any editable, if overlay not intersected. @@ -198,6 +202,9 @@ Hand = function (side) { } } } + if (entityID) { + intersectionPosition = Entities.getEntityProperties(entityID, "position").position; + } } intersection = { @@ -205,7 +212,8 @@ Hand = function (side) { overlayID: overlayID, entityID: entityID, handIntersected: overlayID !== null || entityID !== null, - editableEntity: entityID !== null + editableEntity: entityID !== null, + intersection: intersectionPosition }; } diff --git a/scripts/vr-edit/modules/handles.js b/scripts/vr-edit/modules/handles.js index 4c11cdfd8d..f7400c6561 100644 --- a/scripts/vr-edit/modules/handles.js +++ b/scripts/vr-edit/modules/handles.js @@ -89,11 +89,11 @@ Handles = function (side) { ]; FACE_HANDLE_OVERLAY_SCALE_AXES = [ + Vec3.UNIT_NEG_X, Vec3.UNIT_X, - Vec3.UNIT_X, + Vec3.UNIT_NEG_Y, Vec3.UNIT_Y, - Vec3.UNIT_Y, - Vec3.UNIT_Z, + Vec3.UNIT_NEG_Z, Vec3.UNIT_Z ]; @@ -109,6 +109,14 @@ Handles = function (side) { return isAxisHandle(overlayID) || isCornerHandle(overlayID); } + function handleOffset(overlayID) { + // Distance from overlay position to entity surface. + if (isCornerHandle(overlayID)) { + return 0; // Corner overlays are centered on the corner. + } + return faceHandleOffsets.y / 2; + } + function getOverlays() { return [].concat(cornerHandleOverlays, faceHandleOverlays); } @@ -126,7 +134,7 @@ Handles = function (side) { if (isCornerHandle(overlayID)) { return Vec3.ONE; } - return FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)]; + return Vec3.abs(FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)]); } function display(rootEntityID, boundingBox, isMultipleEntities, isSuppressZAxis) { @@ -348,6 +356,7 @@ Handles = function (side) { display: display, overlays: getOverlays, isHandle: isHandle, + handleOffset: handleOffset, scalingAxis: scalingAxis, scalingDirections: scalingDirections, startScaling: startScaling, diff --git a/scripts/vr-edit/utilities/utilities.js b/scripts/vr-edit/utilities/utilities.js index e8c79b9886..07ee894731 100644 --- a/scripts/vr-edit/utilities/utilities.js +++ b/scripts/vr-edit/utilities/utilities.js @@ -20,6 +20,12 @@ if (typeof Vec3.max !== "function") { }; } +if (typeof Vec3.abs !== "function") { + Vec3.abs = function (a) { + return { x: Math.abs(a.x), y: Math.abs(a.y), z: Math.abs(a.z) }; + }; +} + if (typeof Quat.ZERO !== "object") { Quat.ZERO = Quat.fromVec3Radians(Vec3.ZERO); } diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index d94c84e9d9..94801335cb 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -386,6 +386,7 @@ otherTargetPosition, handleUnitScaleAxis, handleScaleDirections, + handleTargetOffset, initialHandleDistance, laserOffset, MIN_SCALE = 0.001, @@ -544,7 +545,10 @@ handleUnitScaleAxis = handles.scalingAxis(overlayID); // Unit vector in direction of scaling. handleScaleDirections = handles.scalingDirections(overlayID); // Which axes to scale the selection on. scaleAxis = Vec3.multiplyQbyV(selectionPositionAndOrientation.orientation, handleUnitScaleAxis); + handleTargetOffset = handles.handleOffset(overlayID) + + Vec3.dot(Vec3.subtract(otherTargetPosition, Overlays.getProperty(overlayID, "position")), scaleAxis); initialHandleDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, initialTargetPosition), scaleAxis)); + initialHandleDistance -= handleTargetOffset; selection.startHandleScaling(initialTargetPosition); handles.startScaling(); @@ -624,10 +628,12 @@ // Desired distance of handle from other hand targetPosition = getScaleTargetPosition(); scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis); - handleDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, targetPosition), scaleAxis)); + handleDistance = Vec3.dot(Vec3.subtract(otherTargetPosition, targetPosition), scaleAxis); + handleDistance -= handleTargetOffset; // Scale selection relative to initial dimensions. scale = handleDistance / initialHandleDistance; + scale = Math.max(scale, MIN_SCALE); scale3D = Vec3.multiply(scale, handleScaleDirections); scale3D = { x: handleScaleDirections.x !== 0 ? scale3D.x : 1, From b9f5810d996cfaca6afa4116ea02418ce6370953 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 15 Sep 2017 17:09:07 -0700 Subject: [PATCH 355/722] snapshot --- libraries/entities/src/EntityItem.h | 34 ++++++++++++++++++- libraries/entities/src/EntityItemProperties.h | 4 ++- .../src/EntityItemPropertiesDefaults.h | 12 ++++++- libraries/entities/src/EntityPropertyFlags.h | 13 ++++++- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 84742587e9..76887a8fea 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -303,8 +303,29 @@ public: uint8_t getPendingOwnershipPriority() const { return _simulationOwner.getPendingPriority(); } void rememberHasSimulationOwnershipBid() const; + // Certifiable Properties + QString getItemName() const; + void setItemName(const QString& value); + QString getItemDescription() const; + void setItemDescription(const QString& value); + QStringList getItemCategories() const; + void setItemCategories(const QStringList& value); + QString getItemArtist() const; + void setItemArtist(const QString& value); + QString getItemLicense() const; + void setItemLicense(const QString& value); + int getLimitedRun() const; + void setLimitedRun(int); QString getMarketplaceID() const; void setMarketplaceID(const QString& value); + int getEditionNumber() const; + void setEditionNumber(int); + QString getCertificateID() const; + void setCertificateID(const QString& value); + QString getStaticCertificateJSON() const; + QString getStaticCertificateHash() const; + bool verifyStaticCertificateProperties() const; + QString getVerifiedCertificateId(); bool getShouldHighlight() const; void setShouldHighlight(const bool value); @@ -525,12 +546,23 @@ protected: bool _locked { ENTITY_ITEM_DEFAULT_LOCKED }; QString _userData { ENTITY_ITEM_DEFAULT_USER_DATA }; SimulationOwner _simulationOwner; - QString _marketplaceID { ENTITY_ITEM_DEFAULT_MARKETPLACE_ID }; bool _shouldHighlight { false }; QString _name { ENTITY_ITEM_DEFAULT_NAME }; QString _href; //Hyperlink href QString _description; //Hyperlink description + // Certificate Properties + QString _itemName { ENTITY_ITEM_DEFAULT_ITEM_NAME }; + QString _itemDescription { ENTITY_ITEM_DEFAULT_ITEM_DESCRIPTION }; + QStringList _itemCategories { ENTITY_ITEM_DEFAULT_ITEM_CATEGORIES }; + QString _itemArtist { ENTITY_ITEM_DEFAULT_ITEM_ARTIST }; + QString _itemLicense { ENTITY_ITEM_DEFAULT_ITEM_LICENSE }; + int _limitedRun { ENTITY_ITEM_DEFAULT_LIMITED_RUN }; + QString _marketplaceID { ENTITY_ITEM_DEFAULT_MARKETPLACE_ID }; + int _editionNumber { ENTITY_ITEM_DEFAULT_EDITION_NUMBER }; + QString _marketplaceID { ENTITY_ITEM_DEFAULT_CERTIFICATE_ID }; + + // NOTE: Damping is applied like this: v *= pow(1 - damping, dt) // // Hence the damping coefficient must range from 0 (no damping) to 1 (immediate stop). diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index dd8ce952d3..212d707de0 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -170,7 +170,6 @@ public: DEFINE_PROPERTY(PROP_RADIUS_START, RadiusStart, radiusStart, float, particle::DEFAULT_RADIUS_START); DEFINE_PROPERTY(PROP_RADIUS_FINISH, RadiusFinish, radiusFinish, float, particle::DEFAULT_RADIUS_FINISH); DEFINE_PROPERTY(PROP_EMITTER_SHOULD_TRAIL, EmitterShouldTrail, emitterShouldTrail, bool, particle::DEFAULT_EMITTER_SHOULD_TRAIL); - DEFINE_PROPERTY_REF(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString, ENTITY_ITEM_DEFAULT_MARKETPLACE_ID); DEFINE_PROPERTY_GROUP(KeyLight, keyLight, KeyLightPropertyGroup); DEFINE_PROPERTY_REF(PROP_VOXEL_VOLUME_SIZE, VoxelVolumeSize, voxelVolumeSize, glm::vec3, PolyVoxEntityItem::DEFAULT_VOXEL_VOLUME_SIZE); DEFINE_PROPERTY_REF(PROP_VOXEL_DATA, VoxelData, voxelData, QByteArray, PolyVoxEntityItem::DEFAULT_VOXEL_DATA); @@ -203,6 +202,9 @@ public: DEFINE_PROPERTY_REF(PROP_QUERY_AA_CUBE, QueryAACube, queryAACube, AACube, AACube()); DEFINE_PROPERTY_REF(PROP_SHAPE, Shape, shape, QString, "Sphere"); + // Certifiable Properties - related to Proof of Purchase certificates + DEFINE_PROPERTY_REF(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString, ENTITY_ITEM_DEFAULT_MARKETPLACE_ID); + // these are used when bouncing location data into and out of scripts DEFINE_PROPERTY_REF(PROP_LOCAL_POSITION, LocalPosition, localPosition, glmVec3, ENTITY_ITEM_ZERO_VEC3); DEFINE_PROPERTY_REF(PROP_LOCAL_ROTATION, LocalRotation, localRotation, glmQuat, ENTITY_ITEM_DEFAULT_ROTATION); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index d52c5d9aab..5949c3aefb 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -26,9 +26,19 @@ const glm::vec3 ENTITY_ITEM_HALF_VEC3 = glm::vec3(0.5f); const bool ENTITY_ITEM_DEFAULT_LOCKED = false; const QString ENTITY_ITEM_DEFAULT_USER_DATA = QString(""); -const QString ENTITY_ITEM_DEFAULT_MARKETPLACE_ID = QString(""); const QUuid ENTITY_ITEM_DEFAULT_SIMULATOR_ID = QUuid(); +// Certificate Properties +const QString ENTITY_ITEM_DEFAULT_ITEM_NAME = QString(""); +const QString ENTITY_ITEM_DEFAULT_ITEM_DESCRIPTION = QString(""); +const QStringList ENTITY_ITEM_DEFAULT_ITEM_CATEGORIES = QStringList(); +const QString ENTITY_ITEM_DEFAULT_ITEM_ARTIST = QString(""); +const QString ENTITY_ITEM_DEFAULT_ITEM_LICENSE = QString(""); +const int ENTITY_ITEM_DEFAULT_LIMITED_RUN = -1; +const QString ENTITY_ITEM_DEFAULT_MARKETPLACE_ID = QString(""); +const int ENTITY_ITEM_DEFAULT_EDITION_NUMBER = -1; +const QString ENTITY_ITEM_DEFAULT_CERTIFICATE_ID = QString(""); + const float ENTITY_ITEM_DEFAULT_ALPHA = 1.0f; const float ENTITY_ITEM_DEFAULT_LOCAL_RENDER_ALPHA = 1.0f; const bool ENTITY_ITEM_DEFAULT_VISIBLE = true; diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index d97be6348f..3aa5423505 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -187,7 +187,18 @@ enum EntityPropertyList { PROP_SERVER_SCRIPTS, PROP_FILTER_URL, - + + // Certificable Properties + PROP_ITEM_NAME, + PROP_ITEM_DESCRIPTION, + PROP_ITEM_CATEGORIES, + PROP_ITEM_ARTIST, + PROP_ITEM_LICENSE, + PROP_LIMITED_RUN, + // PROP_MARKETPLACE_ID is above + PROP_EDITION_NUMBER, + PROP_CERTIFICATE_ID, + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, From 6c5a1c0460e9ee457810b41e33aa2c6afd7f0dd7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 16 Sep 2017 15:45:44 +1200 Subject: [PATCH 356/722] Address laser and controller interactions with HUD overlays and tablet --- .../controllerModules/inVREditMode.js | 111 ++++++++++++++++++ .../system/controllers/controllerScripts.js | 1 + scripts/vr-edit/modules/laser.js | 31 +++-- scripts/vr-edit/vr-edit.js | 3 +- 4 files changed, 133 insertions(+), 13 deletions(-) create mode 100644 scripts/system/controllers/controllerModules/inVREditMode.js diff --git a/scripts/system/controllers/controllerModules/inVREditMode.js b/scripts/system/controllers/controllerModules/inVREditMode.js new file mode 100644 index 0000000000..c48955442b --- /dev/null +++ b/scripts/system/controllers/controllerModules/inVREditMode.js @@ -0,0 +1,111 @@ +"use strict"; + +// inVREditMode.js +// +// Created by David Rowe on 16 Sep 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, MyAvatar, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, + makeDispatcherModuleParameters, makeRunningValues, getEnabledModuleByName +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); + +(function () { + + function InVREditMode(hand) { + this.hand = hand; + this.disableModules = false; + this.parameters = makeDispatcherModuleParameters( + 200, // Not too high otherwise the tablet laser doesn't work. + this.hand === RIGHT_HAND + ? ["rightHand", "rightHandEquip", "rightHandTrigger"] + : ["leftHand", "leftHandEquip", "leftHandTrigger"], + [], + 100 + ); + + this.isReady = function (controllerData) { + if (this.disableModules) { + return makeRunningValues(true, [], []); + } + return makeRunningValues(false, [], []); + }; + + this.run = function (controllerData) { + // Default behavior if disabling is not enabled. + if (!this.disableModules) { + return makeRunningValues(false, [], []); + } + + // 2D overlay lasers. + // These are automatically enabled. + + // Tablet stylus. + // Includes the tablet laser. + var tabletStylusInput = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightTabletStylusInput" + : "LeftTabletStylusInput"); + if (tabletStylusInput) { + var tabletReady = tabletStylusInput.isReady(controllerData); + if (tabletReady.active) { + return makeRunningValues(false, [], []); + } + } + + // Tablet grabbing. + var nearOverlay = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightNearParentingGrabOverlay" + : "LeftNearParentingGrabOverlay"); + if (nearOverlay) { + var nearOverlayReady = nearOverlay.isReady(controllerData); + if (nearOverlayReady.active && nearOverlay.grabbedThingID === HMD.tabletID) { + return makeRunningValues(false, [], []); + } + } + + // Teleport. + var teleporter = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightTeleporter" + : "LeftTeleporter"); + if (teleporter) { + var teleporterReady = teleporter.isReady(controllerData); + if (teleporterReady.active) { + return makeRunningValues(false, [], []); + } + } + + // Other behaviors are disabled. + return makeRunningValues(true, [], []); + }; + } + + var leftHandInVREditMode = new InVREditMode(LEFT_HAND); + var rightHandInVREditMode = new InVREditMode(RIGHT_HAND); + enableDispatcherModule("LeftHandInVREditMode", leftHandInVREditMode); + enableDispatcherModule("RightHandInVREditMode", rightHandInVREditMode); + + var INVREDIT_DISABLER_MESSAGE_CHANNEL = "Hifi-InVREdit-Disabler"; + this.handleMessage = function (channel, message, sender) { + if (sender === MyAvatar.sessionUUID && channel === INVREDIT_DISABLER_MESSAGE_CHANNEL) { + if (message === "both") { + leftHandInVREditMode.disableModules = true; + rightHandInVREditMode.disableModules = true; + } else if (message === "none") { + leftHandInVREditMode.disableModules = false; + rightHandInVREditMode.disableModules = false; + } + } + }; + Messages.subscribe(INVREDIT_DISABLER_MESSAGE_CHANNEL); + Messages.messageReceived.connect(this.handleMessage); + + this.cleanup = function () { + disableDispatcherModule("LeftHandInVREditMode"); + disableDispatcherModule("RightHandInVREditMode"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); \ No newline at end of file diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index e8b07c623d..87c349523a 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -26,6 +26,7 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/overlayLaserInput.js", "controllerModules/webEntityLaserInput.js", "controllerModules/inEditMode.js", + "controllerModules/inVREditMode.js", "controllerModules/disableOtherModule.js", "controllerModules/farTrigger.js", "controllerModules/teleport.js", diff --git a/scripts/vr-edit/modules/laser.js b/scripts/vr-edit/modules/laser.js index 9c1a6f5bb3..afd67cf364 100644 --- a/scripts/vr-edit/modules/laser.js +++ b/scripts/vr-edit/modules/laser.js @@ -169,18 +169,27 @@ Laser = function (side) { // Normal laser operation with trigger. intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, VISIBLE_ONLY); - if (!intersection.intersects) { - intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, - VISIBLE_ONLY); - intersection.editableEntity = intersection.intersects && Entities.hasEditableRoot(intersection.entityID); - intersection.overlayID = null; + if (Reticle.pointingAtSystemOverlay || (intersection.overlayID + && [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID].indexOf(intersection.overlayID) !== -1)) { + // No laser if pointing at HUD overlay or tablet; system provides lasers for these cases. + if (isLaserOn) { + isLaserOn = false; + hide(); + } + } else { + if (!intersection.intersects) { + intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, + VISIBLE_ONLY); + intersection.editableEntity = intersection.intersects && Entities.hasEditableRoot(intersection.entityID); + intersection.overlayID = null; + } + intersection.laserIntersected = intersection.intersects; + laserLength = (specifiedLaserLength !== null) + ? specifiedLaserLength + : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); + isLaserOn = true; + display(pickRay.origin, pickRay.direction, laserLength, true, hand.triggerClicked()); } - intersection.laserIntersected = intersection.intersects; - laserLength = (specifiedLaserLength !== null) - ? specifiedLaserLength - : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE); - isLaserOn = true; - display(pickRay.origin, pickRay.direction, laserLength, true, hand.triggerClicked()); } else if (uiOverlayIDs.length > 0) { diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 94801335cb..e0a54578ff 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1399,8 +1399,7 @@ // Communicate app status to controllerDispatcher.js. var DISABLE_HANDS = "both", ENABLE_HANDS = "none"; - // TODO: Proper method to disable specific laser and grabbing functionality. - Messages.sendLocalMessage('Hifi-Hand-Disabler', isAppActive ? DISABLE_HANDS : ENABLE_HANDS); + Messages.sendLocalMessage("Hifi-InVREdit-Disabler", isAppActive ? DISABLE_HANDS : ENABLE_HANDS); } function onUICommand(command, parameter) { From 3ceeb0d83edfc079b1131d9bb73fbdbc85f45e0b Mon Sep 17 00:00:00 2001 From: vladest Date: Sat, 16 Sep 2017 22:04:53 +0200 Subject: [PATCH 357/722] Rework all c++ calls --- interface/src/Application.cpp | 85 ++++---- interface/src/AvatarBookmarks.cpp | 9 +- interface/src/LocationBookmarks.cpp | 10 +- interface/src/assets/ATPAssetMigrator.cpp | 64 +++--- .../scripting/WindowScriptingInterface.cpp | 59 ++--- libraries/ui/src/OffscreenUi.cpp | 202 ++++++++++-------- libraries/ui/src/OffscreenUi.h | 83 ++++--- 7 files changed, 257 insertions(+), 255 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a2178faa99..496cbc93e4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6146,13 +6146,13 @@ bool Application::askToSetAvatarUrl(const QString& url) { bool agreeToLicense = true; // assume true //create set avatar callback auto setAvatar = [=] (QString url, QString modelName) { - auto offscreenUi = DependencyManager::get(); + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Set Avatar", + "Would you like to use '" + modelName + "' for your avatar?", + QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { - auto offscreenUi = DependencyManager::get(); - QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); - - bool ok = (QMessageBox::Ok == answer); + bool ok = (QMessageBox::Ok == static_cast(answer.toInt())); if (ok) { getMyAvatar()->useFullAvatarURL(url, modelName); emit fullAvatarURLChanged(url, modelName); @@ -6160,9 +6160,6 @@ bool Application::askToSetAvatarUrl(const QString& url) { qCDebug(interfaceapp) << "Declined to use the avatar: " << url; } }); - OffscreenUi::asyncQuestion("Set Avatar", - "Would you like to use '" + modelName + "' for your avatar?", - QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); }; if (!modelLicense.isEmpty()) { @@ -6170,12 +6167,13 @@ bool Application::askToSetAvatarUrl(const QString& url) { const int MAX_CHARACTERS_PER_LINE = 90; modelLicense = simpleWordWrap(modelLicense, MAX_CHARACTERS_PER_LINE); - auto offscreenUi = DependencyManager::get(); - QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=, &agreeToLicense] (QMessageBox::StandardButton answer) { - auto offscreenUi = DependencyManager::get(); - QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Avatar Usage License", + modelLicense + "\nDo you agree to these terms?", + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + QObject::connect(dlg, &ModalDialogListener::response, this, [=, &agreeToLicense] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - agreeToLicense = (answer == QMessageBox::Yes); + agreeToLicense = (static_cast(answer.toInt()) == QMessageBox::Yes); if (agreeToLicense) { switch (modelType) { case FSTReader::HEAD_AND_BODY_MODEL: { @@ -6192,10 +6190,6 @@ bool Application::askToSetAvatarUrl(const QString& url) { //auto offscreenUi = DependencyManager::get(); }); - - OffscreenUi::asyncQuestion("Avatar Usage License", - modelLicense + "\nDo you agree to these terms?", - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); } else { setAvatar(url, modelName); } @@ -6215,23 +6209,21 @@ bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { shortName = shortName.mid(startIndex, endIndex - startIndex); } - auto offscreenUi = DependencyManager::get(); - QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { + QString message = "Would you like to run this script:\n" + shortName; + ModalDialogListener* dlg = OffscreenUi::asyncQuestion(getWindow(), "Run Script", message, + QMessageBox::Yes | QMessageBox::No); + + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { const QString& fileName = scriptFilenameOrURL; - if (answer == QMessageBox::Yes) { + if (static_cast(answer.toInt()) == QMessageBox::Yes) { qCDebug(interfaceapp) << "Chose to run the script: " << fileName; DependencyManager::get()->loadScript(fileName); } else { qCDebug(interfaceapp) << "Declined to run the script: " << scriptFilenameOrURL; } - auto offscreenUi = DependencyManager::get(); - QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); }); - QString message = "Would you like to run this script:\n" + shortName; - - OffscreenUi::asyncQuestion(getWindow(), "Run Script", message, QMessageBox::Yes | QMessageBox::No); - return true; } @@ -6268,11 +6260,14 @@ bool Application::askToWearAvatarAttachmentUrl(const QString& url) { name = nameValue.toString(); } - auto offscreenUi = DependencyManager::get(); - QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { - auto offscreenUi = DependencyManager::get(); - QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); - if (answer == QMessageBox::Yes) { + auto avatarAttachmentConfirmationTitle = tr("Avatar Attachment Confirmation"); + auto avatarAttachmentConfirmationMessage = tr("Would you like to wear '%1' on your avatar?").arg(name); + ModalDialogListener* dlg = OffscreenUi::asyncQuestion(avatarAttachmentConfirmationTitle, + avatarAttachmentConfirmationMessage, + QMessageBox::Ok | QMessageBox::Cancel); + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + if (static_cast(answer.toInt()) == QMessageBox::Yes) { // add attachment to avatar auto myAvatar = getMyAvatar(); assert(myAvatar); @@ -6285,12 +6280,6 @@ bool Application::askToWearAvatarAttachmentUrl(const QString& url) { qCDebug(interfaceapp) << "User declined to wear the avatar attachment: " << url; } }); - - auto avatarAttachmentConfirmationTitle = tr("Avatar Attachment Confirmation"); - auto avatarAttachmentConfirmationMessage = tr("Would you like to wear '%1' on your avatar?").arg(name); - OffscreenUi::asyncQuestion(avatarAttachmentConfirmationTitle, - avatarAttachmentConfirmationMessage, - QMessageBox::Ok | QMessageBox::Cancel); } else { // json parse error auto avatarAttachmentParseErrorString = tr("Error parsing attachment JSON from url: \"%1\""); @@ -6933,17 +6922,17 @@ void Application::openUrl(const QUrl& url) const { void Application::loadDialog() { auto scriptEngines = DependencyManager::get(); - auto offscreenUi = DependencyManager::get(); - connect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, this, [=] (QString response) { - auto offscreenUi = DependencyManager::get(); - disconnect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, this, nullptr); + ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(_glWidget, tr("Open Script"), + getPreviousScriptLocation(), + tr("JavaScript Files (*.js)")); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + disconnect(dlg, &ModalDialogListener::response, this, nullptr); + const QString& response = answer.toString(); if (!response.isEmpty() && QFile(response).exists()) { setPreviousScriptLocation(QFileInfo(response).absolutePath()); DependencyManager::get()->loadScript(response, true, false, false, true); // Don't load from cache } }); - OffscreenUi::getOpenFileNameAsync(_glWidget, tr("Open Script"), getPreviousScriptLocation(), - tr("JavaScript Files (*.js)")); } QString Application::getPreviousScriptLocation() { @@ -6956,10 +6945,9 @@ void Application::setPreviousScriptLocation(const QString& location) { } void Application::loadScriptURLDialog() const { - auto offscreenUi = DependencyManager::get(); - connect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, this, [=] (QVariant response) { - disconnect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, this, nullptr); - auto offscreenUi = DependencyManager::get(); + ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + disconnect(dlg, &ModalDialogListener::response, this, nullptr); const QString& newScript = response.toString(); if (QUrl(newScript).scheme() == "atp") { OffscreenUi::asyncWarning("Error Loading Script", "Cannot load client script over ATP"); @@ -6967,7 +6955,6 @@ void Application::loadScriptURLDialog() const { DependencyManager::get()->loadScript(newScript.trimmed()); } }); - OffscreenUi::getTextAsync(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); } void Application::loadLODToolsDialog() { diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index 7c42effbc2..5b9c45eef5 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -106,10 +106,9 @@ void AvatarBookmarks::changeToBookmarkedAvatar() { } void AvatarBookmarks::addBookmark() { - auto offscreenUi = DependencyManager::get(); - connect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, this, [=] (QVariant response) { - disconnect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, this, nullptr); - auto offscreenUi = DependencyManager::get(); + ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_PLACEMARK, "Bookmark Avatar", "Name", QString()); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + disconnect(dlg, &ModalDialogListener::response, this, nullptr); auto bookmarkName = response.toString(); bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " "); if (bookmarkName.length() == 0) { @@ -130,7 +129,7 @@ void AvatarBookmarks::addBookmark() { Bookmarks::addBookmarkToFile(bookmarkName, *bookmark); }); - OffscreenUi::getTextAsync(OffscreenUi::ICON_PLACEMARK, "Bookmark Avatar", "Name", QString()); + } void AvatarBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) { diff --git a/interface/src/LocationBookmarks.cpp b/interface/src/LocationBookmarks.cpp index d1e5595c5a..285f533a7f 100644 --- a/interface/src/LocationBookmarks.cpp +++ b/interface/src/LocationBookmarks.cpp @@ -74,10 +74,10 @@ void LocationBookmarks::teleportToBookmark() { } void LocationBookmarks::addBookmark() { - auto offscreenUi = DependencyManager::get(); - connect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, this, [=] (QVariant response) { - disconnect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, this, nullptr); - auto offscreenUi = DependencyManager::get(); + ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_PLACEMARK, "Bookmark Location", "Name", QString()); + + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + disconnect(dlg, &ModalDialogListener::response, this, nullptr); auto bookmarkName = response.toString(); bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " "); @@ -89,8 +89,6 @@ void LocationBookmarks::addBookmark() { QString bookmarkAddress = addressManager->currentAddress().toString(); Bookmarks::addBookmarkToFile(bookmarkName, bookmarkAddress); }); - - OffscreenUi::getTextAsync(OffscreenUi::ICON_PLACEMARK, "Bookmark Location", "Name", QString()); } void LocationBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& address) { diff --git a/interface/src/assets/ATPAssetMigrator.cpp b/interface/src/assets/ATPAssetMigrator.cpp index 4f79d32734..8de40865b7 100644 --- a/interface/src/assets/ATPAssetMigrator.cpp +++ b/interface/src/assets/ATPAssetMigrator.cpp @@ -42,10 +42,12 @@ static const QString COMPOUND_SHAPE_URL_KEY = "compoundShapeURL"; static const QString MESSAGE_BOX_TITLE = "ATP Asset Migration"; void ATPAssetMigrator::loadEntityServerFile() { - auto offscreenUi = DependencyManager::get(); - connect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, this, [=] (QString filename) { - auto offscreenUi = DependencyManager::get(); - disconnect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, this, nullptr); + ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(_dialogParent, tr("Select an entity-server content file to migrate"), + QString(), tr("Entity-Server Content (*.gz)")); + + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + const QString& filename = response.toString(); + disconnect(dlg, &ModalDialogListener::response, this, nullptr); if (!filename.isEmpty()) { qCDebug(asset_migrator) << "Selected filename for ATP asset migration: " << filename; @@ -85,13 +87,12 @@ void ATPAssetMigrator::loadEntityServerFile() { " current asset-server\nand then save a new entity-server file with the ATP URLs.\n\nAre you ready to"\ " continue?\n\nMake sure you are connected to the right domain." }; + ModalDialogListener* migrationConfirmDialog = OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, MIGRATION_CONFIRMATION_TEXT, + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + QObject::connect(migrationConfirmDialog, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(migrationConfirmDialog, &ModalDialogListener::response, this, nullptr); - auto offscreenUi = DependencyManager::get(); - QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { - auto offscreenUi = DependencyManager::get(); - QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); - - if (QMessageBox::Yes == answer) { + if (QMessageBox::Yes == static_cast(answer.toInt())) { // try to open the file at the given filename QFile modelsFile { filename }; @@ -148,31 +149,31 @@ void ATPAssetMigrator::loadEntityServerFile() { "Select \"Yes\" to upload all discovered assets to the current asset-server immediately.\n"\ "Select \"No\" to be prompted for each discovered asset." }; - auto offscreenUi = DependencyManager::get(); - QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { - auto offscreenUi = DependencyManager::get(); - QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); - if (answer == QMessageBox::Yes) { + ModalDialogListener* migrationConfirmDialog1 = OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, + "Would you like to migrate the following resource?\n" + migrationURL.toString(), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + QObject::connect(migrationConfirmDialog1, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(migrationConfirmDialog1, &ModalDialogListener::response, this, nullptr); + if (static_cast(answer.toInt()) == + QMessageBox::Yes) { wantsCompleteMigration = true; migrateResources(migrationURL, jsonValue, isModelURL); } else { - QObject::connect(offscreenUi.data(), &OffscreenUi::response, this, [=] (QMessageBox::StandardButton answer) { - auto offscreenUi = DependencyManager::get(); - QObject::disconnect(offscreenUi.data(), &OffscreenUi::response, this, nullptr); - if (answer == QMessageBox::Yes) { + ModalDialogListener* migrationConfirmDialog2 = OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, COMPLETE_MIGRATION_TEXT, + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + QObject::connect(migrationConfirmDialog2, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(migrationConfirmDialog2, &ModalDialogListener::response, this, nullptr); + if (static_cast(answer.toInt()) == + QMessageBox::Yes) { migrateResources(migrationURL, jsonValue, isModelURL); } else { _ignoredUrls.insert(migrationURL); } }); - OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, - "Would you like to migrate the following resource?\n" + migrationURL.toString(), - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - } }); - OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, COMPLETE_MIGRATION_TEXT, - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); hasAskedForCompleteMigration = true; } if (wantsCompleteMigration) { @@ -194,13 +195,8 @@ void ATPAssetMigrator::loadEntityServerFile() { } } }); - OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, MIGRATION_CONFIRMATION_TEXT, - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - } }); - OffscreenUi::getOpenFileNameAsync(_dialogParent, tr("Select an entity-server content file to migrate"), - QString(), tr("Entity-Server Content (*.gz)")); } void ATPAssetMigrator::migrateResource(ResourceRequest* request) { @@ -333,8 +329,9 @@ bool ATPAssetMigrator::wantsToMigrateResource(const QUrl& url) { void ATPAssetMigrator::saveEntityServerFile() { // show a dialog to ask the user where they want to save the file - auto offscreenUi = DependencyManager::get(); - connect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, this, [=] (QString saveName) { + ModalDialogListener* dlg = OffscreenUi::getSaveFileNameAsync(_dialogParent, "Save Migrated Entities File"); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + const QString& saveName = response.toString(); QFile saveFile { saveName }; if (saveFile.open(QIODevice::WriteOnly)) { @@ -369,9 +366,6 @@ void ATPAssetMigrator::saveEntityServerFile() { // reset after the attempted save, success or fail reset(); }); - - OffscreenUi::getSaveFileNameAsync(_dialogParent, "Save Migrated Entities File"); - } void ATPAssetMigrator::reset() { diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index bdd99c75c2..8dacfe5569 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -127,16 +127,13 @@ QScriptValue WindowScriptingInterface::prompt(const QString& message, const QStr /// \param const QString& message message to display /// \param const QString& defaultText default text in the text box void WindowScriptingInterface::promptAsync(const QString& message, const QString& defaultText) { - auto offscreenUi = DependencyManager::get(); - connect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, - this, [=] (QVariant result) { - auto offscreenUi = DependencyManager::get(); - disconnect(offscreenUi.data(), &OffscreenUi::inputDialogResponse, - this, nullptr); + ModalDialogListener* dlg = OffscreenUi::getTextAsync(nullptr, "", message, QLineEdit::Normal, defaultText); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant result) { + disconnect(dlg, &ModalDialogListener::response, this, nullptr); emit promptTextChanged(result.toString()); }); - OffscreenUi::getTextAsync(nullptr, "", message, QLineEdit::Normal, defaultText); + } CustomPromptResult WindowScriptingInterface::customPrompt(const QVariant& config) { @@ -221,19 +218,15 @@ void WindowScriptingInterface::browseDirAsync(const QString& title, const QStrin #ifndef Q_OS_WIN path = fixupPathForMac(directory); #endif - auto offscreenUi = DependencyManager::get(); - connect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, - this, [=] (QString result) { - auto offscreenUi = DependencyManager::get(); - disconnect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, - this, nullptr); + ModalDialogListener* dlg = OffscreenUi::getExistingDirectoryAsync(nullptr, title, path); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + const QString& result = response.toString(); + disconnect(dlg, &ModalDialogListener::response, this, nullptr); if (!result.isEmpty()) { setPreviousBrowseLocation(QFileInfo(result).absolutePath()); } emit browseDirChanged(result); }); - - OffscreenUi::getExistingDirectoryAsync(nullptr, title, path); } /// \param const QString& title title of the window @@ -270,19 +263,17 @@ void WindowScriptingInterface::browseAsync(const QString& title, const QString& #ifndef Q_OS_WIN path = fixupPathForMac(directory); #endif - auto offscreenUi = DependencyManager::get(); - connect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, - this, [=] (QString result) { - auto offscreenUi = DependencyManager::get(); - disconnect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, - this, nullptr); + ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(nullptr, title, path, nameFilter); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + const QString& result = response.toString(); + disconnect(dlg, &ModalDialogListener::response, this, nullptr); if (!result.isEmpty()) { setPreviousBrowseLocation(QFileInfo(result).absolutePath()); } emit openFileChanged(result); }); - OffscreenUi::getOpenFileNameAsync(nullptr, title, path, nameFilter); + } /// Display a save file dialog. If `directory` is an invalid file or directory the browser will start at the current @@ -321,19 +312,17 @@ void WindowScriptingInterface::saveAsync(const QString& title, const QString& di #ifndef Q_OS_WIN path = fixupPathForMac(directory); #endif - auto offscreenUi = DependencyManager::get(); - connect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, - this, [=] (QString result) { - auto offscreenUi = DependencyManager::get(); - disconnect(offscreenUi.data(), &OffscreenUi::fileDialogResponse, - this, nullptr); + ModalDialogListener* dlg = OffscreenUi::getSaveFileNameAsync(nullptr, title, path, nameFilter); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + const QString& result = response.toString(); + disconnect(dlg, &ModalDialogListener::response, this, nullptr); if (!result.isEmpty()) { setPreviousBrowseLocation(QFileInfo(result).absolutePath()); } emit saveFileChanged(result); }); - OffscreenUi::getSaveFileNameAsync(nullptr, title, path, nameFilter); + } /// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid @@ -379,19 +368,15 @@ void WindowScriptingInterface::browseAssetsAsync(const QString& title, const QSt path = path + "/"; } - auto offscreenUi = DependencyManager::get(); - connect(offscreenUi.data(), &OffscreenUi::assetDialogResponse, - this, [=] (QString result) { - auto offscreenUi = DependencyManager::get(); - disconnect(offscreenUi.data(), &OffscreenUi::assetDialogResponse, - this, nullptr); + ModalDialogListener* dlg = OffscreenUi::getOpenAssetNameAsync(nullptr, title, path, nameFilter); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + const QString& result = response.toString(); + disconnect(dlg, &ModalDialogListener::response, this, nullptr); if (!result.isEmpty()) { setPreviousBrowseAssetLocation(QFileInfo(result).absolutePath()); } emit assetsDirChanged(result); }); - - OffscreenUi::getOpenAssetNameAsync(nullptr, title, path, nameFilter); } void WindowScriptingInterface::showAssetServer(const QString& upload) { diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 2048bd547f..da12d8ac31 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -151,45 +151,6 @@ bool OffscreenUi::isVisible(const QString& name) { } } -class ModalDialogListener : public QObject { - Q_OBJECT - friend class OffscreenUi; - -protected: - ModalDialogListener(QQuickItem* dialog) : _dialog(dialog) { - if (!dialog) { - _finished = true; - return; - } - connect(_dialog, SIGNAL(destroyed()), this, SLOT(onDestroyed())); - } - - ~ModalDialogListener() { - if (_dialog) { - disconnect(_dialog); - } - } - - virtual QVariant waitForResult() { - while (!_finished) { - QCoreApplication::processEvents(); - } - return _result; - } - -protected slots: - void onDestroyed() { - _finished = true; - disconnect(_dialog); - _dialog = nullptr; - } - -protected: - QQuickItem* _dialog; - bool _finished { false }; - QVariant _result; -}; - class MessageBoxListener : public ModalDialogListener { Q_OBJECT @@ -211,7 +172,7 @@ private slots: _result = button; _finished = true; auto offscreenUi = DependencyManager::get(); - emit offscreenUi->response(static_cast(_result.toInt())); + emit response(_result); offscreenUi->removeModalDialog(qobject_cast(this)); disconnect(_dialog); } @@ -272,19 +233,23 @@ QMessageBox::StandardButton OffscreenUi::messageBox(Icon icon, const QString& ti return static_cast(waitForMessageBoxResult(createMessageBox(icon, title, text, buttons, defaultButton))); } -void OffscreenUi::asyncMessageBox(Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { +ModalDialogListener* OffscreenUi::asyncMessageBox(Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { if (QThread::currentThread() != thread()) { + ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "asyncMessageBox", + Q_RETURN_ARG(ModalDialogListener*, ret), Q_ARG(Icon, icon), Q_ARG(QString, title), Q_ARG(QString, text), Q_ARG(QMessageBox::StandardButtons, buttons), Q_ARG(QMessageBox::StandardButton, defaultButton)); + return ret; } MessageBoxListener* messageBoxListener = new MessageBoxListener(createMessageBox(icon, title, text, buttons, defaultButton)); QObject* modalDialog = qobject_cast(messageBoxListener); _modalDialogListeners.push_back(modalDialog); + return messageBoxListener; } QMessageBox::StandardButton OffscreenUi::critical(const QString& title, const QString& text, @@ -296,31 +261,34 @@ QMessageBox::StandardButton OffscreenUi::information(const QString& title, const return DependencyManager::get()->messageBox(OffscreenUi::Icon::ICON_INFORMATION, title, text, buttons, defaultButton); } -void OffscreenUi::asyncCritical(const QString& title, const QString& text, +ModalDialogListener* OffscreenUi::asyncCritical(const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { - DependencyManager::get()->asyncMessageBox(OffscreenUi::Icon::ICON_CRITICAL, title, text, buttons, defaultButton); + return DependencyManager::get()->asyncMessageBox(OffscreenUi::Icon::ICON_CRITICAL, title, text, buttons, defaultButton); } -void OffscreenUi::asyncInformation(const QString& title, const QString& text, +ModalDialogListener* OffscreenUi::asyncInformation(const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { - DependencyManager::get()->asyncMessageBox(OffscreenUi::Icon::ICON_INFORMATION, title, text, buttons, defaultButton); + return DependencyManager::get()->asyncMessageBox(OffscreenUi::Icon::ICON_INFORMATION, title, text, buttons, defaultButton); } QMessageBox::StandardButton OffscreenUi::question(const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { return DependencyManager::get()->messageBox(OffscreenUi::Icon::ICON_QUESTION, title, text, buttons, defaultButton); } -void OffscreenUi::asyncQuestion(const QString& title, const QString& text, + +ModalDialogListener *OffscreenUi::asyncQuestion(const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { - DependencyManager::get()->asyncMessageBox(OffscreenUi::Icon::ICON_QUESTION, title, text, buttons, defaultButton); + return DependencyManager::get()->asyncMessageBox(OffscreenUi::Icon::ICON_QUESTION, title, text, buttons, defaultButton); } + QMessageBox::StandardButton OffscreenUi::warning(const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { return DependencyManager::get()->messageBox(OffscreenUi::Icon::ICON_WARNING, title, text, buttons, defaultButton); } -void OffscreenUi::asyncWarning(const QString& title, const QString& text, + +ModalDialogListener* OffscreenUi::asyncWarning(const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { - DependencyManager::get()->asyncMessageBox(OffscreenUi::Icon::ICON_WARNING, title, text, buttons, defaultButton); + return DependencyManager::get()->asyncMessageBox(OffscreenUi::Icon::ICON_WARNING, title, text, buttons, defaultButton); } @@ -338,8 +306,8 @@ class InputDialogListener : public ModalDialogListener { private slots: void onSelected(const QVariant& result) { _result = result; - auto offscreenUi = DependencyManager::get(); - emit offscreenUi->inputDialogResponse(_result); + auto offscreenUi = DependencyManager::get(); + emit response(_result); offscreenUi->removeModalDialog(qobject_cast(this)); _finished = true; disconnect(_dialog); @@ -394,17 +362,17 @@ QVariant OffscreenUi::getCustomInfo(const Icon icon, const QString& title, const return result; } -void OffscreenUi::getTextAsync(const Icon icon, const QString& title, const QString& label, const QString& text) { - DependencyManager::get()->inputDialogAsync(icon, title, label, text); +ModalDialogListener* OffscreenUi::getTextAsync(const Icon icon, const QString& title, const QString& label, const QString& text) { + return DependencyManager::get()->inputDialogAsync(icon, title, label, text); } -void OffscreenUi::getItemAsync(const Icon icon, const QString& title, const QString& label, const QStringList& items, +ModalDialogListener* OffscreenUi::getItemAsync(const Icon icon, const QString& title, const QString& label, const QStringList& items, int current, bool editable) { auto offscreenUi = DependencyManager::get(); auto inputDialog = offscreenUi->createInputDialog(icon, title, label, current); if (!inputDialog) { - return; + return nullptr; } inputDialog->setProperty("items", items); inputDialog->setProperty("editable", editable); @@ -412,11 +380,11 @@ void OffscreenUi::getItemAsync(const Icon icon, const QString& title, const QStr InputDialogListener* inputDialogListener = new InputDialogListener(inputDialog); offscreenUi->getModalDialogListeners().push_back(qobject_cast(inputDialogListener)); - return; + return inputDialogListener; } -void OffscreenUi::getCustomInfoAsync(const Icon icon, const QString& title, const QVariantMap& config) { - DependencyManager::get()->customInputDialog(icon, title, config); +ModalDialogListener* OffscreenUi::getCustomInfoAsync(const Icon icon, const QString& title, const QVariantMap& config) { + return DependencyManager::get()->customInputDialogAsync(icon, title, config); } QVariant OffscreenUi::inputDialog(const Icon icon, const QString& title, const QString& label, const QVariant& current) { @@ -434,19 +402,22 @@ QVariant OffscreenUi::inputDialog(const Icon icon, const QString& title, const Q return waitForInputDialogResult(createInputDialog(icon, title, label, current)); } -void OffscreenUi::inputDialogAsync(const Icon icon, const QString& title, const QString& label, const QVariant& current) { +ModalDialogListener* OffscreenUi::inputDialogAsync(const Icon icon, const QString& title, const QString& label, const QVariant& current) { if (QThread::currentThread() != thread()) { + ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "inputDialogAsync", + Q_RETURN_ARG(ModalDialogListener*, ret), Q_ARG(Icon, icon), Q_ARG(QString, title), Q_ARG(QString, label), Q_ARG(QVariant, current)); - return; + return ret; } InputDialogListener* inputDialogListener = new InputDialogListener(createInputDialog(icon, title, label, current)); QObject* inputDialog = qobject_cast(inputDialogListener); _modalDialogListeners.push_back(inputDialog); + return inputDialogListener; } QVariant OffscreenUi::customInputDialog(const Icon icon, const QString& title, const QVariantMap& config) { @@ -469,19 +440,21 @@ QVariant OffscreenUi::customInputDialog(const Icon icon, const QString& title, c return result; } -void OffscreenUi::customInputDialogAsync(const Icon icon, const QString& title, const QVariantMap& config) { +ModalDialogListener* OffscreenUi::customInputDialogAsync(const Icon icon, const QString& title, const QVariantMap& config) { if (QThread::currentThread() != thread()) { + ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "customInputDialogAsync", - Q_ARG(Icon, icon), - Q_ARG(QString, title), - Q_ARG(QVariantMap, config)); - return; + Q_RETURN_ARG(ModalDialogListener*, ret), + Q_ARG(Icon, icon), + Q_ARG(QString, title), + Q_ARG(QVariantMap, config)); + return ret; } InputDialogListener* inputDialogListener = new InputDialogListener(createCustomInputDialog(icon, title, config)); QObject* inputDialog = qobject_cast(inputDialogListener); _modalDialogListeners.push_back(inputDialog); - return; + return inputDialogListener; } void OffscreenUi::togglePinned() { @@ -655,6 +628,7 @@ void OffscreenUi::createDesktop(const QUrl& url) { #endif load(url, [=](QQmlContext* context, QObject* newObject) { + Q_UNUSED(context) _desktop = static_cast(newObject); getSurfaceContext()->setContextProperty("desktop", _desktop); _toolWindow = _desktop->findChild("ToolWindow"); @@ -703,7 +677,7 @@ private slots: _result = file; _finished = true; auto offscreenUi = DependencyManager::get(); - emit offscreenUi->fileDialogResponse(_result.toUrl().toLocalFile()); + emit response(_result); offscreenUi->removeModalDialog(qobject_cast(this)); disconnect(_dialog); } @@ -740,7 +714,7 @@ QString OffscreenUi::fileDialog(const QVariantMap& properties) { return result.toUrl().toLocalFile(); } -void OffscreenUi::fileDialogAsync(const QVariantMap& properties) { +ModalDialogListener* OffscreenUi::fileDialogAsync(const QVariantMap& properties) { QVariant buildDialogResult; bool invokeResult; auto tabletScriptingInterface = DependencyManager::get(); @@ -759,14 +733,14 @@ void OffscreenUi::fileDialogAsync(const QVariantMap& properties) { if (!invokeResult) { qWarning() << "Failed to create file open dialog"; - return; + return nullptr; } FileDialogListener* fileDialogListener = new FileDialogListener(qvariant_cast(buildDialogResult)); QObject* fileModalDialog = qobject_cast(fileDialogListener); _modalDialogListeners.push_back(fileModalDialog); - return; + return fileDialogListener; } QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { @@ -791,15 +765,17 @@ QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, return fileDialog(map); } -void OffscreenUi::fileOpenDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { +ModalDialogListener* OffscreenUi::fileOpenDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { if (QThread::currentThread() != thread()) { + ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "fileOpenDialogAsync", + Q_RETURN_ARG(ModalDialogListener*, ret), Q_ARG(QString, caption), Q_ARG(QString, dir), Q_ARG(QString, filter), Q_ARG(QString*, selectedFilter), Q_ARG(QFileDialog::Options, options)); - return; + return ret; } // FIXME support returning the selected filter... somehow? @@ -808,7 +784,7 @@ void OffscreenUi::fileOpenDialogAsync(const QString& caption, const QString& dir map.insert("dir", QUrl::fromLocalFile(dir)); map.insert("filter", filter); map.insert("options", static_cast(options)); - fileDialogAsync(map); + return fileDialogAsync(map); } QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { @@ -835,15 +811,17 @@ QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir, return fileDialog(map); } -void OffscreenUi::fileSaveDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { +ModalDialogListener* OffscreenUi::fileSaveDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { if (QThread::currentThread() != thread()) { + ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "fileSaveDialogAsync", + Q_RETURN_ARG(ModalDialogListener*, ret), Q_ARG(QString, caption), Q_ARG(QString, dir), Q_ARG(QString, filter), Q_ARG(QString*, selectedFilter), Q_ARG(QFileDialog::Options, options)); - return; + return ret; } // FIXME support returning the selected filter... somehow? @@ -879,15 +857,17 @@ QString OffscreenUi::existingDirectoryDialog(const QString& caption, const QStri return fileDialog(map); } -void OffscreenUi::existingDirectoryDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { +ModalDialogListener* OffscreenUi::existingDirectoryDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { if (QThread::currentThread() != thread()) { + ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "existingDirectoryDialogAsync", - Q_ARG(QString, caption), - Q_ARG(QString, dir), - Q_ARG(QString, filter), - Q_ARG(QString*, selectedFilter), - Q_ARG(QFileDialog::Options, options)); - return; + Q_RETURN_ARG(ModalDialogListener*, ret), + Q_ARG(QString, caption), + Q_ARG(QString, dir), + Q_ARG(QString, filter), + Q_ARG(QString*, selectedFilter), + Q_ARG(QFileDialog::Options, options)); + return ret; } QVariantMap map; @@ -900,26 +880,32 @@ void OffscreenUi::existingDirectoryDialogAsync(const QString& caption, const QSt } QString OffscreenUi::getOpenFileName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { + Q_UNUSED(ignored) return DependencyManager::get()->fileOpenDialog(caption, dir, filter, selectedFilter, options); } -void OffscreenUi::getOpenFileNameAsync(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { +ModalDialogListener* OffscreenUi::getOpenFileNameAsync(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { + Q_UNUSED(ignored) return DependencyManager::get()->fileOpenDialogAsync(caption, dir, filter, selectedFilter, options); } QString OffscreenUi::getSaveFileName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { + Q_UNUSED(ignored) return DependencyManager::get()->fileSaveDialog(caption, dir, filter, selectedFilter, options); } -void OffscreenUi::getSaveFileNameAsync(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { +ModalDialogListener* OffscreenUi::getSaveFileNameAsync(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { + Q_UNUSED(ignored) return DependencyManager::get()->fileSaveDialogAsync(caption, dir, filter, selectedFilter, options); } QString OffscreenUi::getExistingDirectory(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { + Q_UNUSED(ignored) return DependencyManager::get()->existingDirectoryDialog(caption, dir, filter, selectedFilter, options); } -void OffscreenUi::getExistingDirectoryAsync(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { +ModalDialogListener* OffscreenUi::getExistingDirectoryAsync(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { + Q_UNUSED(ignored) return DependencyManager::get()->existingDirectoryDialogAsync(caption, dir, filter, selectedFilter, options); } @@ -939,7 +925,7 @@ class AssetDialogListener : public ModalDialogListener { void onSelectedAsset(QVariant asset) { _result = asset; auto offscreenUi = DependencyManager::get(); - emit offscreenUi->assetDialogResponse(_result.toUrl().toLocalFile()); + emit response(_result); offscreenUi->removeModalDialog(qobject_cast(this)); _finished = true; disconnect(_dialog); @@ -978,7 +964,7 @@ QString OffscreenUi::assetDialog(const QVariantMap& properties) { return result.toUrl().toString(); } -void OffscreenUi::assetDialogAsync(const QVariantMap& properties) { +ModalDialogListener *OffscreenUi::assetDialogAsync(const QVariantMap& properties) { // ATP equivalent of fileDialog(). QVariant buildDialogResult; bool invokeResult; @@ -998,13 +984,13 @@ void OffscreenUi::assetDialogAsync(const QVariantMap& properties) { if (!invokeResult) { qWarning() << "Failed to create asset open dialog"; - return; + return nullptr; } AssetDialogListener* assetDialogListener = new AssetDialogListener(qvariant_cast(buildDialogResult)); QObject* assetModalDialog = qobject_cast(assetDialogListener); _modalDialogListeners.push_back(assetModalDialog); - return; + return assetDialogListener; } QList &OffscreenUi::getModalDialogListeners() { @@ -1034,16 +1020,18 @@ QString OffscreenUi::assetOpenDialog(const QString& caption, const QString& dir, return assetDialog(map); } -void OffscreenUi::assetOpenDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { +ModalDialogListener* OffscreenUi::assetOpenDialogAsync(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { // ATP equivalent of fileOpenDialog(). if (QThread::currentThread() != thread()) { + ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "assetOpenDialogAsync", + Q_RETURN_ARG(ModalDialogListener*, ret), Q_ARG(QString, caption), Q_ARG(QString, dir), Q_ARG(QString, filter), Q_ARG(QString*, selectedFilter), Q_ARG(QFileDialog::Options, options)); - return; + return ret; } // FIXME support returning the selected filter... somehow? @@ -1057,11 +1045,13 @@ void OffscreenUi::assetOpenDialogAsync(const QString& caption, const QString& di QString OffscreenUi::getOpenAssetName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { // ATP equivalent of getOpenFileName(). + Q_UNUSED(ignored) return DependencyManager::get()->assetOpenDialog(caption, dir, filter, selectedFilter, options); } -void OffscreenUi::getOpenAssetNameAsync(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { +ModalDialogListener* OffscreenUi::getOpenAssetNameAsync(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { // ATP equivalent of getOpenFileName(). + Q_UNUSED(ignored) return DependencyManager::get()->assetOpenDialogAsync(caption, dir, filter, selectedFilter, options); } @@ -1104,5 +1094,31 @@ unsigned int OffscreenUi::getMenuUserDataId() const { return _vrMenu->_userDataId; } -#include "OffscreenUi.moc" +ModalDialogListener::ModalDialogListener(QQuickItem *dialog) : _dialog(dialog) { + if (!dialog) { + _finished = true; + return; + } + connect(_dialog, SIGNAL(destroyed()), this, SLOT(onDestroyed())); +} +ModalDialogListener::~ModalDialogListener() { + if (_dialog) { + disconnect(_dialog); + } +} + +QVariant ModalDialogListener::waitForResult() { + while (!_finished) { + QCoreApplication::processEvents(); + } + return _result; +} + +void ModalDialogListener::onDestroyed() { + _finished = true; + disconnect(_dialog); + _dialog = nullptr; +} + +#include "OffscreenUi.moc" diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 9613cbc3f4..391d7da6c7 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -30,6 +30,27 @@ class VrMenu; #define OFFSCREEN_VISIBILITY_PROPERTY "shown" +class ModalDialogListener : public QObject { + Q_OBJECT + friend class OffscreenUi; + +protected: + ModalDialogListener(QQuickItem* dialog); + virtual ~ModalDialogListener(); + virtual QVariant waitForResult(); + +signals: + void response(const QVariant& value); + +protected slots: + void onDestroyed(); + +protected: + QQuickItem* _dialog; + bool _finished { false }; + QVariant _result; +}; + class OffscreenUi : public OffscreenQmlSurface, public Dependency { Q_OBJECT @@ -71,7 +92,7 @@ public: // Message box compatibility Q_INVOKABLE QMessageBox::StandardButton messageBox(Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); - Q_INVOKABLE void asyncMessageBox(Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); + Q_INVOKABLE ModalDialogListener* asyncMessageBox(Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); // Must be called from the main thread QQuickItem* createMessageBox(Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); // Must be called from the main thread @@ -89,12 +110,12 @@ public: QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) { return information(title, text, buttons, defaultButton); } - static void asyncCritical(void* ignored, const QString& title, const QString& text, + static ModalDialogListener* asyncCritical(void* ignored, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) { return asyncCritical(title, text, buttons, defaultButton); } - static void asyncInformation(void* ignored, const QString& title, const QString& text, + static ModalDialogListener* asyncInformation(void* ignored, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) { return asyncInformation(title, text, buttons, defaultButton); @@ -106,7 +127,7 @@ public: return question(title, text, buttons, defaultButton); } - static void asyncQuestion(void* ignored, const QString& title, const QString& text, + static ModalDialogListener* asyncQuestion(void* ignored, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) { return asyncQuestion(title, text, buttons, defaultButton); @@ -117,7 +138,7 @@ public: QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) { return warning(title, text, buttons, defaultButton); } - static void asyncWarning(void* ignored, const QString& title, const QString& text, + static ModalDialogListener* asyncWarning(void* ignored, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton) { return asyncWarning(title, text, buttons, defaultButton); @@ -129,53 +150,55 @@ public: static QMessageBox::StandardButton information(const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); - static void asyncCritical(const QString& title, const QString& text, + static ModalDialogListener* asyncCritical(const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); - static void asyncInformation(const QString& title, const QString& text, + static ModalDialogListener *asyncInformation(const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); static QMessageBox::StandardButton question(const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); - static void asyncQuestion (const QString& title, const QString& text, + static ModalDialogListener* asyncQuestion (const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); static QMessageBox::StandardButton warning(const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); - static void asyncWarning(const QString& title, const QString& text, + static ModalDialogListener *asyncWarning(const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); Q_INVOKABLE QString fileOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); - Q_INVOKABLE void fileOpenDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE ModalDialogListener* fileOpenDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE QString fileSaveDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); - Q_INVOKABLE void fileSaveDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE ModalDialogListener* fileSaveDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE QString existingDirectoryDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); - Q_INVOKABLE void existingDirectoryDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE ModalDialogListener* existingDirectoryDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QString assetOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); - Q_INVOKABLE void assetOpenDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE ModalDialogListener* assetOpenDialogAsync(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); // Compatibility with QFileDialog::getOpenFileName static QString getOpenFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); - static void getOpenFileNameAsync(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + static ModalDialogListener* getOpenFileNameAsync(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); // Compatibility with QFileDialog::getSaveFileName static QString getSaveFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); - static void getSaveFileNameAsync(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + static ModalDialogListener* getSaveFileNameAsync(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); // Compatibility with QFileDialog::getExistingDirectory static QString getExistingDirectory(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); - static void getExistingDirectoryAsync(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + static ModalDialogListener* getExistingDirectoryAsync(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); static QString getOpenAssetName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); - static void getOpenAssetNameAsync(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + static ModalDialogListener* getOpenAssetNameAsync(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QVariant inputDialog(const Icon icon, const QString& title, const QString& label = QString(), const QVariant& current = QVariant()); - Q_INVOKABLE void inputDialogAsync(const Icon icon, const QString& title, const QString& label = QString(), const QVariant& current = QVariant()); + Q_INVOKABLE ModalDialogListener* inputDialogAsync(const Icon icon, const QString& title, const QString& label = QString(), const QVariant& current = QVariant()); Q_INVOKABLE QVariant customInputDialog(const Icon icon, const QString& title, const QVariantMap& config); - Q_INVOKABLE void customInputDialogAsync(const Icon icon, const QString& title, const QVariantMap& config); + Q_INVOKABLE ModalDialogListener* customInputDialogAsync(const Icon icon, const QString& title, const QVariantMap& config); QQuickItem* createInputDialog(const Icon icon, const QString& title, const QString& label, const QVariant& current); QQuickItem* createCustomInputDialog(const Icon icon, const QString& title, const QVariantMap& config); QVariant waitForInputDialogResult(QQuickItem* inputDialog); @@ -194,13 +217,13 @@ public: } // Compatibility with QInputDialog::getText - static void getTextAsync(void* ignored, const QString & title, const QString & label, + static ModalDialogListener* getTextAsync(void* ignored, const QString & title, const QString & label, QLineEdit::EchoMode mode = QLineEdit::Normal, const QString & text = QString(), bool * ok = 0, Qt::WindowFlags flags = 0, Qt::InputMethodHints inputMethodHints = Qt::ImhNone) { return getTextAsync(OffscreenUi::ICON_NONE, title, label, text); } // Compatibility with QInputDialog::getItem - static void getItemAsync(void *ignored, const QString & title, const QString & label, const QStringList & items, + static ModalDialogListener* getItemAsync(void *ignored, const QString & title, const QString & label, const QStringList & items, int current = 0, bool editable = true, bool * ok = 0, Qt::WindowFlags flags = 0, Qt::InputMethodHints inputMethodHints = Qt::ImhNone) { return getItemAsync(OffscreenUi::ICON_NONE, title, label, items, current, editable); @@ -209,27 +232,27 @@ public: static QString getText(const Icon icon, const QString & title, const QString & label, const QString & text = QString(), bool * ok = 0); static QString getItem(const Icon icon, const QString & title, const QString & label, const QStringList & items, int current = 0, bool editable = true, bool * ok = 0); static QVariant getCustomInfo(const Icon icon, const QString& title, const QVariantMap& config, bool* ok = 0); - static void getTextAsync(const Icon icon, const QString & title, const QString & label, const QString & text = QString()); - static void getItemAsync(const Icon icon, const QString & title, const QString & label, const QStringList & items, int current = 0, bool editable = true); - static void getCustomInfoAsync(const Icon icon, const QString& title, const QVariantMap& config); + static ModalDialogListener* getTextAsync(const Icon icon, const QString & title, const QString & label, const QString & text = QString()); + static ModalDialogListener* getItemAsync(const Icon icon, const QString & title, const QString & label, const QStringList & items, int current = 0, bool editable = true); + static ModalDialogListener* getCustomInfoAsync(const Icon icon, const QString& title, const QVariantMap& config); unsigned int getMenuUserDataId() const; QList &getModalDialogListeners(); signals: void showDesktop(); - void response(QMessageBox::StandardButton response); - void fileDialogResponse(QString response); - void assetDialogResponse(QString response); - void inputDialogResponse(QVariant response); +// void response(QMessageBox::StandardButton response); +// void fileDialogResponse(QString response); +// void assetDialogResponse(QString response); +// void inputDialogResponse(QVariant response); public slots: void removeModalDialog(QObject* modal); private: QString fileDialog(const QVariantMap& properties); - void fileDialogAsync(const QVariantMap &properties); + ModalDialogListener *fileDialogAsync(const QVariantMap &properties); QString assetDialog(const QVariantMap& properties); - void assetDialogAsync(const QVariantMap& properties); + ModalDialogListener* assetDialogAsync(const QVariantMap& properties); QQuickItem* _desktop { nullptr }; QQuickItem* _toolWindow { nullptr }; From a8d24d9161c9bef01a730fd944b25f188f06dcc0 Mon Sep 17 00:00:00 2001 From: vladest Date: Sat, 16 Sep 2017 22:41:18 +0200 Subject: [PATCH 358/722] Cleanup --- interface/src/scripting/WindowScriptingInterface.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 8dacfe5569..292fe46a85 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -272,8 +272,6 @@ void WindowScriptingInterface::browseAsync(const QString& title, const QString& } emit openFileChanged(result); }); - - } /// Display a save file dialog. If `directory` is an invalid file or directory the browser will start at the current From 1cde504c741d146ec9751aac9a6883ce6ba1c13f Mon Sep 17 00:00:00 2001 From: vladest Date: Sat, 16 Sep 2017 22:57:26 +0200 Subject: [PATCH 359/722] Memleak fix. Cleanup --- interface/src/scripting/WindowScriptingInterface.cpp | 4 ---- libraries/ui/src/OffscreenUi.cpp | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 292fe46a85..4b981207f1 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -132,8 +132,6 @@ void WindowScriptingInterface::promptAsync(const QString& message, const QString disconnect(dlg, &ModalDialogListener::response, this, nullptr); emit promptTextChanged(result.toString()); }); - - } CustomPromptResult WindowScriptingInterface::customPrompt(const QVariant& config) { @@ -319,8 +317,6 @@ void WindowScriptingInterface::saveAsync(const QString& title, const QString& di } emit saveFileChanged(result); }); - - } /// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index da12d8ac31..9039889998 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -94,6 +94,7 @@ QObject* OffscreenUi::getFlags() { void OffscreenUi::removeModalDialog(QObject* modal) { if (modal) { _modalDialogListeners.removeOne(modal); + modal->deleteLater(); } } From e2921661e9343b1d090295a1598b453cd58fb4dd Mon Sep 17 00:00:00 2001 From: druiz17 Date: Tue, 19 Sep 2017 09:35:22 -0700 Subject: [PATCH 360/722] scaling entities --- .../controllerModules/nearParentGrabEntity.js | 10 -- .../controllerModules/scaleAvatar.js | 3 +- .../controllerModules/scaleEntity.js | 109 ++++++++++++++++++ .../system/controllers/controllerScripts.js | 3 +- .../libraries/controllerDispatcherUtils.js | 5 +- 5 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 scripts/system/controllers/controllerModules/scaleEntity.js diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js index e08b61dbd5..39e9371931 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -44,10 +44,6 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); return (this.hand === RIGHT_HAND) ? leftNearParentingGrabEntity : rightNearParentingGrabEntity; }; - this.otherHandIsParent = function(props) { - return this.getOtherModule().thisHandIsParent(props); - }; - this.thisHandIsParent = function(props) { if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== AVATAR_SELF_ID) { return false; @@ -99,12 +95,6 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); // this should never happen, but if it does, don't set previous parent to be this hand. // this.previousParentID[targetProps.id] = NULL; // this.previousParentJointIndex[targetProps.id] = -1; - } else if (this.otherHandIsParent(targetProps)) { - // the other hand is parent. Steal the object and information - var otherModule = this.getOtherModule(); - this.previousParentID[targetProps.id] = otherModule.previousParentID[targetProps.id]; - this.previousParentJointIndex[targetProps.id] = otherModule.previousParentJointIndex[targetProps.id]; - otherModule.endNearParentingGrabEntity(); } else { this.previousParentID[targetProps.id] = targetProps.parentID; this.previousParentJointIndex[targetProps.id] = targetProps.parentJointIndex; diff --git a/scripts/system/controllers/controllerModules/scaleAvatar.js b/scripts/system/controllers/controllerModules/scaleAvatar.js index 05804c967b..fc28f4a00f 100644 --- a/scripts/system/controllers/controllerModules/scaleAvatar.js +++ b/scripts/system/controllers/controllerModules/scaleAvatar.js @@ -1,4 +1,4 @@ -// handControllerGrab.js +// scaleAvatar.js // // Created by Dante Ruiz on 9/11/17 // @@ -80,4 +80,5 @@ dispatcherUtils.disableDispatcherModule("LeftScaleAvatar"); dispatcherUtils.disableDispatcherModule("RightScaleAvatar"); }; + Script.scriptEnding.connect(this.cleanup); })(); diff --git a/scripts/system/controllers/controllerModules/scaleEntity.js b/scripts/system/controllers/controllerModules/scaleEntity.js new file mode 100644 index 0000000000..4b504c3733 --- /dev/null +++ b/scripts/system/controllers/controllerModules/scaleEntity.js @@ -0,0 +1,109 @@ +// scaleEntity.js +// +// Created by Dante Ruiz on 9/18/17 +// +// Grabs physically moveable entities with hydra-like controllers; it works for either near or far objects. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, Vec3, MyAvatar, RIGHT_HAND */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ + +(function() { + var dispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js"); + + function ScaleEntity(hand) { + this.hand = hand; + this.grabbedThingID = false; + this.scalingStartDistance = false; + this.scalingStartDimensions = false; + + this.parameters = dispatcherUtils.makeDispatcherModuleParameters( + 120, + this.hand === RIGHT_HAND ? ["rightHandTrigger"] : ["leftHandTrigger"], + [], + 100 + ); + + this.otherHand = function() { + return this.hand === dispatcherUtils.RIGHT_HAND ? dispatcherUtils.LEFT_HAND : dispatcherUtils.RIGHT_HAND; + }; + + this.otherModule = function() { + return this.hand === dispatcherUtils.RIGHT_HAND ? leftScaleEntity : rightScaleEntity; + }; + + this.bumperPressed = function(controllerData) { + if ( controllerData.secondaryValues[this.hand] > dispatcherUtils.BUMPER_ON_VALUE) { + return true; + } + return false; + }; + + this.getTargetProps = function(controllerData) { + // nearbyEntityProperties is already sorted by length from controller + var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; + var sensorScaleFactor = MyAvatar.sensorToWorldScale; + for (var i = 0; i < nearbyEntityProperties.length; i++) { + var props = nearbyEntityProperties[i]; + var handPosition = controllerData.controllerLocations[this.hand].position; + var distance = Vec3.distance(props.position, handPosition); + if (distance > dispatcherUtils.NEAR_GRAB_RADIUS * sensorScaleFactor) { + continue; + } + if ((dispatcherUtils.entityIsGrabbable(props) || + dispatcherUtils.propsArePhysical(props)) && !props.locked) { + return props; + } + } + return null; + }; + + this.isReady = function(controllerData) { + var otherModule = this.otherModule(); + if (this.bumperPressed(controllerData) && otherModule.bumperPressed(controllerData)) { + var thisHandTargetProps = this.getTargetProps(controllerData); + var otherHandTargetProps = otherModule.getTargetProps(controllerData); + if (thisHandTargetProps && otherHandTargetProps) { + if (thisHandTargetProps.id === otherHandTargetProps.id) { + this.grabbedThingID = thisHandTargetProps.id; + this.scalingStartDistance = Vec3.length(Vec3.subtract(controllerData.controllerLocations[this.hand].position, + controllerData.controllerLocations[this.otherHand()].position)); + this.scalingStartDimensions = thisHandTargetProps.dimensions; + return dispatcherUtils.makeRunningValues(true, [], []); + } + } + } + return dispatcherUtils.makeRunningValues(false, [], []); + }; + + this.run = function(controllerData) { + var otherModule = this.otherModule(); + if (this.bumperPressed(controllerData) && otherModule.bumperPressed(controllerData)) { + if (this.hand === dispatcherUtils.RIGHT_HAND) { + var scalingCurrentDistance = + Vec3.length(Vec3.subtract(controllerData.controllerLocations[this.hand].position, + controllerData.controllerLocations[this.otherHand()].position)); + var currentRescale = scalingCurrentDistance / this.scalingStartDistance; + var newDimensions = Vec3.multiply(currentRescale, this.scalingStartDimensions); + Entities.editEntity(this.grabbedThingID, { dimensions: newDimensions }); + } + return dispatcherUtils.makeRunningValues(true, [], []); + } + return dispatcherUtils.makeRunningValues(false, [], []); + }; + } + + var leftScaleEntity = new ScaleEntity(dispatcherUtils.LEFT_HAND); + var rightScaleEntity = new ScaleEntity(dispatcherUtils.RIGHT_HAND); + + dispatcherUtils.enableDispatcherModule("LeftScaleEntity", leftScaleEntity); + dispatcherUtils.enableDispatcherModule("RightScaleEntity", rightScaleEntity); + + this.cleanup = function() { + dispatcherUtils.disableDispatcherModule("LeftScaleEntity"); + dispatcherUtils.disableDispatcherModule("RightScaleEntity"); + }; + Script.scriptEnding.connect(this.cleanup); +})(); diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index e8b07c623d..6b140173b8 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -29,7 +29,8 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/disableOtherModule.js", "controllerModules/farTrigger.js", "controllerModules/teleport.js", - "controllerModules/scaleAvatar.js" + "controllerModules/scaleAvatar.js", + "controllerModules/scaleEntity.js" ]; var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 33eec74111..1570f71605 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -318,6 +318,9 @@ if (typeof module !== 'undefined') { makeRunningValues: makeRunningValues, LEFT_HAND: LEFT_HAND, RIGHT_HAND: RIGHT_HAND, - BUMPER_ON_VALUE: BUMPER_ON_VALUE + BUMPER_ON_VALUE: BUMPER_ON_VALUE, + propsArePhysical: propsArePhysical, + entityIsGrabbable: entityIsGrabbable, + NEAR_GRAB_RADIUS: NEAR_GRAB_RADIUS }; } From 50ee41e0955982bacd3141ee98be81ef76a3a1dd Mon Sep 17 00:00:00 2001 From: vladest Date: Tue, 19 Sep 2017 22:42:15 +0200 Subject: [PATCH 361/722] Fix file loading dialogs --- libraries/ui/src/OffscreenUi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 9039889998..9dfe831081 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -675,7 +675,7 @@ class FileDialogListener : public ModalDialogListener { private slots: void onSelectedFile(QVariant file) { - _result = file; + _result = file.toUrl().toLocalFile(); _finished = true; auto offscreenUi = DependencyManager::get(); emit response(_result); From b93e91b9f3b757ce76411eb1380f41dc459d3f6b Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Tue, 12 Sep 2017 14:38:27 -0700 Subject: [PATCH 362/722] New android toolchain --- .gitattributes | 1 + .gitignore | 5 + BUILD_ANDROID.md | 59 +- CMakeLists.txt | 17 +- android/app/CMakeLists.txt | 8 + android/app/build.gradle | 57 + android/app/proguard-rules.pro | 25 + android/app/src/main/AndroidManifest.xml | 37 + android/app/src/main/cpp/GoogleVRHelpers.h | 50 + android/app/src/main/cpp/native-lib.cpp | 78 + android/app/src/main/cpp/renderer.cpp | 636 ++++++ android/app/src/main/cpp/renderer.h | 60 + .../saintandreas/testapp/MainActivity.java | 105 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4208 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2555 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6114 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10056 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10486 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 14696 bytes android/app/src/main/res/values/colors.xml | 4 + android/app/src/main/res/values/strings.xml | 3 + android/app/src/main/res/values/styles.xml | 15 + android/build.gradle | 91 + android/gradle.properties | 17 + android/settings.gradle | 1 + cmake/android/AndroidManifest.xml.in | 82 - cmake/android/QtCreateAPK.cmake | 159 -- cmake/android/android.toolchain.cmake | 1725 ----------------- cmake/android/deployment-file.json.in | 13 - cmake/android/strings.xml.in | 11 - cmake/externals/glm/CMakeLists.txt | 2 +- cmake/externals/tbb/CMakeLists.txt | 4 +- cmake/init.cmake | 21 +- cmake/macros/AutoScribeShader.cmake | 4 +- cmake/macros/SetPackagingParameters.cmake | 1 + cmake/macros/SetupHifiLibrary.cmake | 4 +- cmake/macros/SetupQt.cmake | 2 +- cmake/macros/TargetGlew.cmake | 12 +- cmake/macros/TargetOpenGL.cmake | 6 +- cmake/macros/TargetOpenSSL.cmake | 32 + cmake/macros/TargetTBB.cmake | 24 + interface/CMakeLists.txt | 6 - libraries/audio-client/src/AudioClient.cpp | 30 + libraries/audio-client/src/AudioClient.h | 9 +- libraries/audio/src/AudioGate.cpp | 7 +- libraries/avatars/src/AvatarData.cpp | 2 +- libraries/controllers/CMakeLists.txt | 4 - .../src/display-plugins/DisplayPlugin.cpp | 3 + .../hmd/DebugHmdDisplayPlugin.cpp | 10 +- .../hmd/DebugHmdDisplayPlugin.h | 1 - .../entities-renderer/src/paintStroke.slf | 2 +- .../entities/src/EntityScriptingInterface.cpp | 28 +- .../entities/src/EntityScriptingInterface.h | 12 +- libraries/entities/src/EntityTree.cpp | 2 +- libraries/entities/src/PolyLineEntityItem.h | 12 +- libraries/entities/src/ShapeEntityItem.cpp | 2 +- libraries/fbx/src/OBJReader.cpp | 13 +- libraries/fbx/src/OBJReader.h | 2 +- libraries/gl/CMakeLists.txt | 6 +- libraries/gl/src/gl/Config.cpp | 35 + libraries/gl/src/gl/Config.h | 66 +- libraries/gl/src/gl/GLHelpers.cpp | 7 + libraries/gl/src/gl/GLHelpers.h | 7 +- libraries/gl/src/gl/OffscreenGLCanvas.cpp | 2 +- libraries/gl/src/gl/OpenGLVersionChecker.cpp | 3 - libraries/gpu-gl/CMakeLists.txt | 5 - .../gpu-gl/src/gpu/gl41/GL41BackendShader.cpp | 4 +- .../gpu-gl/src/gpu/gl45/GL45BackendShader.cpp | 3 +- libraries/gpu-gles/CMakeLists.txt | 11 + libraries/gpu-gles/src/gpu/gl/GLBackend.cpp | 722 +++++++ libraries/gpu-gles/src/gpu/gl/GLBackend.h | 427 ++++ .../gpu-gles/src/gpu/gl/GLBackendInput.cpp | 338 ++++ .../gpu-gles/src/gpu/gl/GLBackendOutput.cpp | 169 ++ .../gpu-gles/src/gpu/gl/GLBackendPipeline.cpp | 250 +++ .../gpu-gles/src/gpu/gl/GLBackendQuery.cpp | 93 + .../gpu-gles/src/gpu/gl/GLBackendState.cpp | 334 ++++ .../gpu-gles/src/gpu/gl/GLBackendTexture.cpp | 40 + .../src/gpu/gl/GLBackendTransform.cpp | 212 ++ libraries/gpu-gles/src/gpu/gl/GLBuffer.cpp | 35 + libraries/gpu-gles/src/gpu/gl/GLBuffer.h | 66 + .../gpu-gles/src/gpu/gl/GLFramebuffer.cpp | 48 + libraries/gpu-gles/src/gpu/gl/GLFramebuffer.h | 77 + .../gpu-gles/src/gpu/gl/GLInputFormat.cpp | 33 + libraries/gpu-gles/src/gpu/gl/GLInputFormat.h | 29 + libraries/gpu-gles/src/gpu/gl/GLPipeline.cpp | 62 + libraries/gpu-gles/src/gpu/gl/GLPipeline.h | 29 + libraries/gpu-gles/src/gpu/gl/GLQuery.h | 67 + libraries/gpu-gles/src/gpu/gl/GLShader.cpp | 224 +++ libraries/gpu-gles/src/gpu/gl/GLShader.h | 59 + libraries/gpu-gles/src/gpu/gl/GLShared.cpp | 879 +++++++++ libraries/gpu-gles/src/gpu/gl/GLShared.h | 167 ++ libraries/gpu-gles/src/gpu/gl/GLState.cpp | 248 +++ libraries/gpu-gles/src/gpu/gl/GLState.h | 73 + .../gpu-gles/src/gpu/gl/GLTexelFormat.cpp | 648 +++++++ libraries/gpu-gles/src/gpu/gl/GLTexelFormat.h | 32 + libraries/gpu-gles/src/gpu/gl/GLTexture.cpp | 323 +++ libraries/gpu-gles/src/gpu/gl/GLTexture.h | 233 +++ .../gpu-gles/src/gpu/gl/GLTextureTransfer.cpp | 207 ++ .../gpu-gles/src/gpu/gl/GLTextureTransfer.h | 78 + .../gpu-gles/src/gpu/gles/GLESBackend.cpp | 197 ++ libraries/gpu-gles/src/gpu/gles/GLESBackend.h | 99 + .../src/gpu/gles/GLESBackendBuffer.cpp | 69 + .../src/gpu/gles/GLESBackendInput.cpp | 29 + .../src/gpu/gles/GLESBackendOutput.cpp | 169 ++ .../src/gpu/gles/GLESBackendQuery.cpp | 38 + .../src/gpu/gles/GLESBackendTexture.cpp | 176 ++ .../src/gpu/gles/GLESBackendTransform.cpp | 67 + libraries/gpu/src/gpu/Buffer.h | 1 + .../gpu/DrawTexcoordRectTransformUnitQuad.slv | 2 +- .../gpu/src/gpu/DrawTransformUnitQuad.slv | 2 +- .../gpu/src/gpu/DrawUnitQuadTexcoord.slv | 2 +- .../gpu/DrawViewportQuadTransformTexcoord.slv | 2 +- libraries/gpu/src/gpu/Forward.h | 5 + libraries/gpu/src/gpu/Frame.cpp | 4 + libraries/gpu/src/gpu/Frame.h | 2 +- libraries/gpu/src/gpu/Framebuffer.cpp | 1 + libraries/gpu/src/gpu/Framebuffer.h | 4 + libraries/gpu/src/gpu/Query.cpp | 4 +- libraries/gpu/src/gpu/Transform.slh | 15 +- libraries/image/CMakeLists.txt | 14 +- libraries/image/src/image/Image.cpp | 13 +- .../src/input-plugins/TouchscreenDevice.h | 2 +- libraries/ktx/CMakeLists.txt | 2 +- libraries/midi/src/Midi.h | 2 +- .../src/model-networking/ModelCache.cpp | 22 +- libraries/model/src/model/Light.slh | 2 +- .../src/model/LightIrradiance.shared.slh | 2 +- libraries/networking/CMakeLists.txt | 46 +- libraries/networking/src/AddressManager.cpp | 4 + libraries/networking/src/AssetClient.cpp | 4 + .../networking/src/AssetResourceRequest.cpp | 2 +- .../networking/src/HTTPResourceRequest.cpp | 6 +- libraries/networking/src/LimitedNodeList.cpp | 4 + libraries/networking/src/NodeList.cpp | 2 +- libraries/networking/src/NodePermissions.cpp | 21 +- libraries/networking/src/NodePermissions.h | 9 +- .../networking/src/udt/CongestionControl.h | 4 +- .../networking/src/udt/ControlPacket.cpp | 2 +- libraries/physics/src/CharacterRayResult.h | 26 +- .../physics/src/CharacterSweepResult.cpp | 6 +- libraries/physics/src/CharacterSweepResult.h | 4 +- .../physics/src/CollisionRenderMeshCache.h | 2 +- libraries/procedural/CMakeLists.txt | 2 +- libraries/render-utils/CMakeLists.txt | 2 +- .../render-utils/src/DeferredBufferRead.slh | 2 +- libraries/render-utils/src/FadeEffect.h | 4 +- libraries/render-utils/src/ForwardBuffer.slh | 68 + .../render-utils/src/ForwardBufferWrite.slh | 63 + .../render-utils/src/ForwardGlobalLight.slh | 198 ++ libraries/render-utils/src/GeometryCache.cpp | 4 +- .../src/LightClusterGrid_shared.slh | 14 +- libraries/render-utils/src/LightPoint.slh | 4 +- libraries/render-utils/src/LightSpot.slh | 4 +- libraries/render-utils/src/LightingModel.slh | 4 +- .../render-utils/src/MaterialTextures.slh | 4 +- .../render-utils/src/MeshPartPayload.cpp | 1 - libraries/render-utils/src/Model.h | 4 +- libraries/render-utils/src/PickItemsJob.cpp | 36 +- libraries/render-utils/src/PickItemsJob.h | 14 +- .../render-utils/src/SubsurfaceScattering.slh | 2 +- libraries/render-utils/src/forward_model.slf | 55 +- .../src/forward_model_normal_map.slf | 8 +- .../src/forward_model_normal_specular_map.slf | 8 +- .../src/forward_model_specular_map.slf | 6 +- .../src/forward_model_translucent.slf | 81 + .../render-utils/src/forward_model_unlit.slf | 6 +- libraries/render-utils/src/glowLine.slf | 2 +- .../src/lightClusters_drawClusterContent.slv | 6 +- .../lightClusters_drawClusterFromDepth.slv | 4 +- .../src/lightClusters_drawGrid.slv | 6 +- libraries/render-utils/src/simple.slf | 4 +- .../src/surfaceGeometry_makeCurvature.slf | 2 +- libraries/render/CMakeLists.txt | 3 +- libraries/render/src/render/Engine.h | 2 +- libraries/render/src/render/Item.h | 2 +- libraries/render/src/render/TransitionStage.h | 18 +- .../render/src/render/drawItemBounds.slf | 2 +- .../src/UsersScriptingInterface.cpp | 2 +- libraries/script-engine/src/Vec3.cpp | 2 +- .../script-engine/src/WebSocketClass.cpp | 2 +- libraries/shared/CMakeLists.txt | 2 - libraries/shared/src/DependencyManager.h | 16 +- libraries/shared/src/PathUtils.cpp | 12 +- libraries/shared/src/Preferences.h | 8 +- libraries/shared/src/SettingHandle.h | 2 +- libraries/shared/src/shared/PlatformHacks.h | 18 + .../shared/src/shared/platform/AndroidHacks.h | 50 + libraries/ui/CMakeLists.txt | 6 +- libraries/ui/src/VrMenu.cpp | 2 +- libraries/ui/src/ui/OffscreenQmlSurface.cpp | 8 +- plugins/oculus/src/OculusHelpers.cpp | 2 +- .../src/OculusLegacyDisplayPlugin.cpp | 2 +- .../src/OculusLegacyDisplayPlugin.h | 2 +- tests/shaders/src/main.cpp | 33 +- tools/oven/src/ui/BakeWidget.cpp | 2 +- 199 files changed, 9414 insertions(+), 2358 deletions(-) create mode 100644 android/app/CMakeLists.txt create mode 100644 android/app/build.gradle create mode 100644 android/app/proguard-rules.pro create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/cpp/GoogleVRHelpers.h create mode 100644 android/app/src/main/cpp/native-lib.cpp create mode 100644 android/app/src/main/cpp/renderer.cpp create mode 100644 android/app/src/main/cpp/renderer.h create mode 100644 android/app/src/main/java/org/saintandreas/testapp/MainActivity.java create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/values/colors.xml create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/build.gradle create mode 100644 android/gradle.properties create mode 100644 android/settings.gradle delete mode 100755 cmake/android/AndroidManifest.xml.in delete mode 100644 cmake/android/QtCreateAPK.cmake delete mode 100755 cmake/android/android.toolchain.cmake delete mode 100644 cmake/android/deployment-file.json.in delete mode 100644 cmake/android/strings.xml.in create mode 100644 cmake/macros/TargetOpenSSL.cmake create mode 100644 cmake/macros/TargetTBB.cmake create mode 100644 libraries/gl/src/gl/Config.cpp create mode 100644 libraries/gpu-gles/CMakeLists.txt create mode 100644 libraries/gpu-gles/src/gpu/gl/GLBackend.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLBackend.h create mode 100644 libraries/gpu-gles/src/gpu/gl/GLBackendInput.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLBackendOutput.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLBackendPipeline.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLBackendQuery.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLBackendState.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLBackendTexture.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLBackendTransform.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLBuffer.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLBuffer.h create mode 100644 libraries/gpu-gles/src/gpu/gl/GLFramebuffer.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLFramebuffer.h create mode 100644 libraries/gpu-gles/src/gpu/gl/GLInputFormat.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLInputFormat.h create mode 100644 libraries/gpu-gles/src/gpu/gl/GLPipeline.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLPipeline.h create mode 100644 libraries/gpu-gles/src/gpu/gl/GLQuery.h create mode 100644 libraries/gpu-gles/src/gpu/gl/GLShader.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLShader.h create mode 100644 libraries/gpu-gles/src/gpu/gl/GLShared.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLShared.h create mode 100644 libraries/gpu-gles/src/gpu/gl/GLState.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLState.h create mode 100644 libraries/gpu-gles/src/gpu/gl/GLTexelFormat.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLTexelFormat.h create mode 100644 libraries/gpu-gles/src/gpu/gl/GLTexture.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLTexture.h create mode 100644 libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.cpp create mode 100644 libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.h create mode 100644 libraries/gpu-gles/src/gpu/gles/GLESBackend.cpp create mode 100644 libraries/gpu-gles/src/gpu/gles/GLESBackend.h create mode 100644 libraries/gpu-gles/src/gpu/gles/GLESBackendBuffer.cpp create mode 100644 libraries/gpu-gles/src/gpu/gles/GLESBackendInput.cpp create mode 100644 libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp create mode 100644 libraries/gpu-gles/src/gpu/gles/GLESBackendQuery.cpp create mode 100644 libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp create mode 100644 libraries/gpu-gles/src/gpu/gles/GLESBackendTransform.cpp create mode 100644 libraries/render-utils/src/ForwardBuffer.slh create mode 100644 libraries/render-utils/src/ForwardBufferWrite.slh create mode 100644 libraries/render-utils/src/ForwardGlobalLight.slh create mode 100644 libraries/render-utils/src/forward_model_translucent.slf create mode 100644 libraries/shared/src/shared/PlatformHacks.h create mode 100644 libraries/shared/src/shared/platform/AndroidHacks.h diff --git a/.gitattributes b/.gitattributes index 406780d20a..4a06c4288a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,6 +10,7 @@ *.json text *.js text *.qml text +*.qrc text *.slf text *.slh text *.slv text diff --git a/.gitignore b/.gitignore index d6227f1f30..8aa82865a4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,11 @@ ext/ Makefile *.user +# Android Studio +*.iml +local.properties +android/libraries + # Xcode *.xcodeproj *.xcworkspace diff --git a/BUILD_ANDROID.md b/BUILD_ANDROID.md index d69d20ee8a..cc51e58b1d 100644 --- a/BUILD_ANDROID.md +++ b/BUILD_ANDROID.md @@ -1,19 +1,56 @@ Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Android specific instructions are found in this file. -### Android Dependencies +# Android Dependencies You will need the following tools to build our Android targets. -* [cmake](http://www.cmake.org/download/) ~> 3.5.1 -* [Qt](http://www.qt.io/download-open-source/#) ~> 5.6.2 -* [ant](http://ant.apache.org/bindownload.cgi) ~> 1.9.4 -* [Android NDK](https://developer.android.com/tools/sdk/ndk/index.html) ~> r10d -* [Android SDK](http://developer.android.com/sdk/installing/index.html) ~> 24.4.1.1 - * Install the latest Platform-tools - * Install the latest Build-tools - * Install the SDK Platform for API Level 19 - * Install Sources for Android SDK for API Level 19 - * Install the ARM EABI v7a System Image if you want to run an emulator. +* [Qt](http://www.qt.io/download-open-source/#) ~> 5.9.1 +* [Android Studio](https://developer.android.com/studio/index.html) +* [Google VR SDK](https://github.com/googlevr/gvr-android-sdk/releases) +* [Gradle](https://gradle.org/releases/) + +### Qt + +Download the Qt online installer. Run the installer and select the android_armv7 binaries. Installing to the default path is recommended + +### Android Studio + +Download the Android Studio installer and run it. Once installed, at the welcome screen, click configure in the lower right corner and select SDK manager + +From the SDK Platforms tab, select API level 26. + +* Install the ARM EABI v7a System Image if you want to run an emulator. + +From the SDK Tools tab select the following + +* Android SDK Build-Tools +* GPU Debugging Tools +* CMake (even if you have a separate CMake installation) +* LLDB +* Android SDK Platform-Tools +* Android SDK Tools +* Android SDK Tools +* NDK (even if you have the NDK installed separately) + +### Google VR SDK + +Download the 1.8 Google VR SDK [release](https://github.com/googlevr/gvr-android-sdk/archive/v1.80.0.zip). Unzip the archive to a location on your drive. + +### Gradle + +Download [Gradle 4.1](https://services.gradle.org/distributions/gradle-4.1-all.zip) and unzip it on your local drive. You may wish to add the location of the bin directory within the archive to your path + +#### Set up machine specific Gradle properties + +Create a `gradle.properties` file in ~/.gradle. Edit the file to contain the following + + QT5_ROOT=C\:\\Qt\\5.9.1\\android_armv7 + GVR_ROOT=C\:\\Android\\gvr-android-sdk + +Replace the paths with your local installations of Qt5 and the Google VR SDK + + +# TODO fix the rest You will also need to cross-compile the dependencies required for all platforms for Android, and help CMake find these compiled libraries on your machine. diff --git a/CMakeLists.txt b/CMakeLists.txt index be513abddb..9d3296a168 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,16 @@ -if (WIN32) +# If we're running under the gradle build, HIFI_ANDROID will be set here, but +# ANDROID will not be set until after the `project` statement. This is the *ONLY* +# place you need to use `HIFI_ANDROID` instead of `ANDROID` +if (WIN32 AND NOT HIFI_ANDROID) cmake_minimum_required(VERSION 3.7) else() cmake_minimum_required(VERSION 3.2) endif() -include("cmake/init.cmake") - project(hifi) +include("cmake/init.cmake") + include("cmake/compiler.cmake") if (NOT DEFINED SERVER_ONLY) @@ -54,11 +57,13 @@ endif() file(GLOB_RECURSE CMAKE_SRC cmake/*.cmake cmake/CMakeLists.txt) add_custom_target(cmake SOURCES ${CMAKE_SRC}) GroupSources("cmake") +unset(CMAKE_SRC) file(GLOB_RECURSE JS_SRC scripts/*.js unpublishedScripts/*.js) add_custom_target(js SOURCES ${JS_SRC}) GroupSources("scripts") GroupSources("unpublishedScripts") +unset(JS_SRC) # Locate the required Qt build on the filesystem setup_qt() @@ -77,6 +82,12 @@ option(USE_NSIGHT "Attempt to find the nSight libraries" 1) set_packaging_parameters() +# FIXME hack to work on the proper Android toolchain +if (ANDROID) + add_subdirectory(android/app) + return() +endif() + # add subdirectories for all targets if (BUILD_SERVER) add_subdirectory(assignment-client) diff --git a/android/app/CMakeLists.txt b/android/app/CMakeLists.txt new file mode 100644 index 0000000000..2d6df925e9 --- /dev/null +++ b/android/app/CMakeLists.txt @@ -0,0 +1,8 @@ +set(TARGET_NAME native-lib) +setup_hifi_library() +link_hifi_libraries(shared networking gl gpu gpu-gles render-utils) +autoscribe_shader_lib(gpu model render render-utils) +target_opengl() +target_link_libraries(native-lib android log m) +target_include_directories(native-lib PRIVATE "${GVR_ROOT}/libraries/headers") +target_link_libraries(native-lib "C:/Users/bdavis/Git/hifi/android/libraries/jni/armeabi-v7a/libgvr.so") diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000000..bd1c596bf3 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,57 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 26 + buildToolsVersion "26.0.1" + defaultConfig { + applicationId "org.saintandreas.testapp" + minSdkVersion 24 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + ndk { abiFilters 'armeabi-v7a' } + externalNativeBuild { + cmake { + arguments '-DHIFI_ANDROID=1', + '-DANDROID_PLATFORM=android-24', + '-DANDROID_TOOLCHAIN=clang', + '-DANDROID_STL=gnustl_shared', + '-DGVR_ROOT=' + GVR_ROOT, + '-DNATIVE_SCRIBE=c:/bin/scribe.exe', + "-DHIFI_ANDROID_PRECOMPILED=${project.rootDir}/libraries/jni/armeabi-v7a" + } + } + jackOptions { enabled true } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + sourceSets { + main { + jniLibs.srcDirs += '../libraries/jni'; + } + } + externalNativeBuild { + cmake { + path '../../CMakeLists.txt' + } + } +} + +dependencies { + compile fileTree(dir: "${project.rootDir}/libraries/jar", include: 'QtAndroid-bundled.jar') + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.google.vr:sdk-audio:1.80.0' + compile 'com.google.vr:sdk-base:1.80.0' +} + +build.dependsOn(':extractQt5') diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000000..b3c0078513 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\Android\SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..05547bd5ae --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/cpp/GoogleVRHelpers.h b/android/app/src/main/cpp/GoogleVRHelpers.h new file mode 100644 index 0000000000..10c46b036f --- /dev/null +++ b/android/app/src/main/cpp/GoogleVRHelpers.h @@ -0,0 +1,50 @@ +#include +#include +#include + +namespace googlevr { + + // Convert a GVR matrix to GLM matrix + glm::mat4 toGlm(const gvr::Mat4f &matrix) { + glm::mat4 result; + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + result[j][i] = matrix.m[i][j]; + } + } + return result; + } + + // Given a field of view in degrees, compute the corresponding projection +// matrix. + glm::mat4 perspectiveMatrixFromView(const gvr::Rectf& fov, float z_near, float z_far) { + const float x_left = -std::tan(fov.left * M_PI / 180.0f) * z_near; + const float x_right = std::tan(fov.right * M_PI / 180.0f) * z_near; + const float y_bottom = -std::tan(fov.bottom * M_PI / 180.0f) * z_near; + const float y_top = std::tan(fov.top * M_PI / 180.0f) * z_near; + const float Y = (2 * z_near) / (y_top - y_bottom); + const float A = (x_right + x_left) / (x_right - x_left); + const float B = (y_top + y_bottom) / (y_top - y_bottom); + const float C = (z_near + z_far) / (z_near - z_far); + const float D = (2 * z_near * z_far) / (z_near - z_far); + + glm::mat4 result { 0 }; + result[2][0] = A; + result[1][1] = Y; + result[2][1] = B; + result[2][2] = C; + result[3][2] = D; + result[2][3] = -1; + return result; + } + + glm::quat toGlm(const gvr::ControllerQuat& q) { + glm::quat result; + result.w = q.qw; + result.x = q.qx; + result.y = q.qy; + result.z = q.qz; + return result; + } + +} diff --git a/android/app/src/main/cpp/native-lib.cpp b/android/app/src/main/cpp/native-lib.cpp new file mode 100644 index 0000000000..156d43d849 --- /dev/null +++ b/android/app/src/main/cpp/native-lib.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include + +#include "renderer.h" + +int QtMsgTypeToAndroidPriority(QtMsgType type) { + int priority = ANDROID_LOG_UNKNOWN; + switch (type) { + case QtDebugMsg: priority = ANDROID_LOG_DEBUG; break; + case QtWarningMsg: priority = ANDROID_LOG_WARN; break; + case QtCriticalMsg: priority = ANDROID_LOG_ERROR; break; + case QtFatalMsg: priority = ANDROID_LOG_FATAL; break; + case QtInfoMsg: priority = ANDROID_LOG_INFO; break; + default: break; + } + return priority; +} + +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + __android_log_write(QtMsgTypeToAndroidPriority(type), "Interface", message.toStdString().c_str()); +} + +static jlong toJni(NativeRenderer *renderer) { + return reinterpret_cast(renderer); +} + +static NativeRenderer *fromJni(jlong renderer) { + return reinterpret_cast(renderer); +} + +#define JNI_METHOD(r, name) JNIEXPORT r JNICALL Java_org_saintandreas_testapp_MainActivity_##name + +extern "C" { + +JNI_METHOD(jlong, nativeCreateRenderer) +(JNIEnv *env, jclass clazz, jobject class_loader, jobject android_context, jlong native_gvr_api) { + qInstallMessageHandler(messageHandler); +#if defined(GVR) + auto gvrContext = reinterpret_cast(native_gvr_api); + return toJni(new NativeRenderer(gvrContext)); +#else + return toJni(new NativeRenderer(nullptr)); +#endif +} + +JNI_METHOD(void, nativeDestroyRenderer) +(JNIEnv *env, jclass clazz, jlong renderer) { + delete fromJni(renderer); +} + +JNI_METHOD(void, nativeInitializeGl) +(JNIEnv *env, jobject obj, jlong renderer) { + fromJni(renderer)->InitializeGl(); +} + +JNI_METHOD(void, nativeDrawFrame) +(JNIEnv *env, jobject obj, jlong renderer) { + fromJni(renderer)->DrawFrame(); +} + +JNI_METHOD(void, nativeOnTriggerEvent) +(JNIEnv *env, jobject obj, jlong renderer) { + fromJni(renderer)->OnTriggerEvent(); +} + +JNI_METHOD(void, nativeOnPause) +(JNIEnv *env, jobject obj, jlong renderer) { + fromJni(renderer)->OnPause(); +} + +JNI_METHOD(void, nativeOnResume) +(JNIEnv *env, jobject obj, jlong renderer) { + fromJni(renderer)->OnResume(); +} + +} // extern "C" diff --git a/android/app/src/main/cpp/renderer.cpp b/android/app/src/main/cpp/renderer.cpp new file mode 100644 index 0000000000..a877ebd777 --- /dev/null +++ b/android/app/src/main/cpp/renderer.cpp @@ -0,0 +1,636 @@ +#include "renderer.h" + +#include + +#include +#include + +#include "GoogleVRHelpers.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#if 0 +#include +#include +#include +#include +#include +#include +#include +#include +#endif + + +template +void withFrameBuffer(gvr::Frame& frame, int32_t index, F f) { + frame.BindBuffer(index); + f(); + frame.Unbind(); +} + + +static const uint64_t kPredictionTimeWithoutVsyncNanos = 50000000; + +// Each shader has two variants: a single-eye ES 2.0 variant, and a multiview +// ES 3.0 variant. The multiview vertex shaders use transforms defined by +// arrays of mat4 uniforms, using gl_ViewID_OVR to determine the array index. + +#define UNIFORM_LIGHT_POS 20 +#define UNIFORM_M 16 +#define UNIFORM_MV 8 +#define UNIFORM_MVP 0 + +#if 0 +uniform Transform { // API uses “Transform[2]” to refer to instance 2 + mat4 u_MVP[2]; + mat4 u_MVMatrix[2]; + mat4 u_Model; + vec3 u_LightPos[2]; +}; +static const char *kDiffuseLightingVertexShader = R"glsl( +#version 300 es +#extension GL_OVR_multiview2 : enable + +layout(num_views=2) in; + +layout(location = 0) uniform mat4 u_MVP[2]; +layout(location = 8) uniform mat4 u_MVMatrix[2]; +layout(location = 16) uniform mat4 u_Model; +layout(location = 20) uniform vec3 u_LightPos[2]; + +layout(location = 0) in vec4 a_Position; +layout(location = 1) in vec4 a_Color; +layout(location = 2) in vec3 a_Normal; + +out vec4 v_Color; +out vec3 v_Grid; + +void main() { + mat4 mvp = u_MVP[gl_ViewID_OVR]; + mat4 modelview = u_MVMatrix[gl_ViewID_OVR]; + vec3 lightpos = u_LightPos[gl_ViewID_OVR]; + v_Grid = vec3(u_Model * a_Position); + vec3 modelViewVertex = vec3(modelview * a_Position); + vec3 modelViewNormal = vec3(modelview * vec4(a_Normal, 0.0)); + float distance = length(lightpos - modelViewVertex); + vec3 lightVector = normalize(lightpos - modelViewVertex); + float diffuse = max(dot(modelViewNormal, lightVector), 0.5); + diffuse = diffuse * (1.0 / (1.0 + (0.00001 * distance * distance))); + v_Color = vec4(a_Color.rgb * diffuse, a_Color.a); + gl_Position = mvp * a_Position; +} +)glsl"; +#endif + + +static const char *kSimepleVertexShader = R"glsl( +#version 300 es +#extension GL_OVR_multiview2 : enable + +layout(num_views=2) in; + +layout(location = 0) in vec4 a_Position; + +out vec4 v_Color; + +void main() { + v_Color = vec4(a_Position.xyz, 1.0); + gl_Position = vec4(a_Position.xyz, 1.0); +} +)glsl"; + + +static const char *kPassthroughFragmentShader = R"glsl( +#version 300 es +precision mediump float; +in vec4 v_Color; +out vec4 FragColor; + +void main() { FragColor = v_Color; } +)glsl"; + +static void CheckGLError(const char* label) { + int gl_error = glGetError(); + if (gl_error != GL_NO_ERROR) { + qWarning("GL error @ %s: %d", label, gl_error); + // Crash immediately to make OpenGL errors obvious. + abort(); + } +} + +// Contains vertex, normal and other data. +namespace cube { + const std::array CUBE_COORDS{{ + // Front face + -1.0f, 1.0f, 1.0f, + -1.0f, -1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + -1.0f, -1.0f, 1.0f, + 1.0f, -1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + + // Right face + 1.0f, 1.0f, 1.0f, + 1.0f, -1.0f, 1.0f, + 1.0f, 1.0f, -1.0f, + 1.0f, -1.0f, 1.0f, + 1.0f, -1.0f, -1.0f, + 1.0f, 1.0f, -1.0f, + + // Back face + 1.0f, 1.0f, -1.0f, + 1.0f, -1.0f, -1.0f, + -1.0f, 1.0f, -1.0f, + 1.0f, -1.0f, -1.0f, + -1.0f, -1.0f, -1.0f, + -1.0f, 1.0f, -1.0f, + + // Left face + -1.0f, 1.0f, -1.0f, + -1.0f, -1.0f, -1.0f, + -1.0f, 1.0f, 1.0f, + -1.0f, -1.0f, -1.0f, + -1.0f, -1.0f, 1.0f, + -1.0f, 1.0f, 1.0f, + + // Top face + -1.0f, 1.0f, -1.0f, + -1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, -1.0f, + -1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, -1.0f, + + // Bottom face + 1.0f, -1.0f, -1.0f, + 1.0f, -1.0f, 1.0f, + -1.0f, -1.0f, -1.0f, + 1.0f, -1.0f, 1.0f, + -1.0f, -1.0f, 1.0f, + -1.0f, -1.0f, -1.0f + }}; + + const std::array CUBE_COLORS{{ + // front, green + 0.0f, 0.5273f, 0.2656f, + 0.0f, 0.5273f, 0.2656f, + 0.0f, 0.5273f, 0.2656f, + 0.0f, 0.5273f, 0.2656f, + 0.0f, 0.5273f, 0.2656f, + 0.0f, 0.5273f, 0.2656f, + + // right, blue + 0.0f, 0.3398f, 0.9023f, + 0.0f, 0.3398f, 0.9023f, + 0.0f, 0.3398f, 0.9023f, + 0.0f, 0.3398f, 0.9023f, + 0.0f, 0.3398f, 0.9023f, + 0.0f, 0.3398f, 0.9023f, + + // back, also green + 0.0f, 0.5273f, 0.2656f, + 0.0f, 0.5273f, 0.2656f, + 0.0f, 0.5273f, 0.2656f, + 0.0f, 0.5273f, 0.2656f, + 0.0f, 0.5273f, 0.2656f, + 0.0f, 0.5273f, 0.2656f, + + // left, also blue + 0.0f, 0.3398f, 0.9023f, + 0.0f, 0.3398f, 0.9023f, + 0.0f, 0.3398f, 0.9023f, + 0.0f, 0.3398f, 0.9023f, + 0.0f, 0.3398f, 0.9023f, + 0.0f, 0.3398f, 0.9023f, + + // top, red + 0.8359375f, 0.17578125f, 0.125f, + 0.8359375f, 0.17578125f, 0.125f, + 0.8359375f, 0.17578125f, 0.125f, + 0.8359375f, 0.17578125f, 0.125f, + 0.8359375f, 0.17578125f, 0.125f, + 0.8359375f, 0.17578125f, 0.125f, + + // bottom, also red + 0.8359375f, 0.17578125f, 0.125f, + 0.8359375f, 0.17578125f, 0.125f, + 0.8359375f, 0.17578125f, 0.125f, + 0.8359375f, 0.17578125f, 0.125f, + 0.8359375f, 0.17578125f, 0.125f, + 0.8359375f, 0.17578125f, 0.125f + }}; + + const std::array CUBE_NORMALS{{ + // Front face + 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + + // Right face + 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + + // Back face + 0.0f, 0.0f, -1.0f, + 0.0f, 0.0f, -1.0f, + 0.0f, 0.0f, -1.0f, + 0.0f, 0.0f, -1.0f, + 0.0f, 0.0f, -1.0f, + 0.0f, 0.0f, -1.0f, + + // Left face + -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, + + // Top face + 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + + // Bottom face + 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f + }}; +} + +namespace triangle { + static std::array TRIANGLE_VERTS {{ + -0.5f, -0.5f, 0.0f, + 0.5f, -0.5f, 0.0f, + 0.0f, 0.5f, 0.0f + }}; +} + +std::array buildViewports(const std::unique_ptr &gvrapi) { + return { {gvrapi->CreateBufferViewport(), gvrapi->CreateBufferViewport()} }; +}; + +const std::string VERTEX_SHADER_DEFINES{ R"GLSL( +#version 300 es +#extension GL_EXT_clip_cull_distance : enable +#define GPU_VERTEX_SHADER +#define GPU_SSBO_TRANSFORM_OBJECT 1 +#define GPU_TRANSFORM_IS_STEREO +#define GPU_TRANSFORM_STEREO_CAMERA +#define GPU_TRANSFORM_STEREO_CAMERA_INSTANCED +#define GPU_TRANSFORM_STEREO_SPLIT_SCREEN +)GLSL" }; + +const std::string PIXEL_SHADER_DEFINES{ R"GLSL( +#version 300 es +precision mediump float; +#define GPU_PIXEL_SHADER +#define GPU_TRANSFORM_IS_STEREO +#define GPU_TRANSFORM_STEREO_CAMERA +#define GPU_TRANSFORM_STEREO_CAMERA_INSTANCED +#define GPU_TRANSFORM_STEREO_SPLIT_SCREEN +)GLSL" }; + + +#if defined(GVR) +NativeRenderer::NativeRenderer(gvr_context *vrContext) : + _gvrapi(new gvr::GvrApi(vrContext, false)), + _viewports(buildViewports(_gvrapi)), + _gvr_viewer_type(_gvrapi->GetViewerType()) +#else +NativeRenderer::NativeRenderer(void *vrContext) +#endif +{ + start = std::chrono::system_clock::now(); + qDebug() << "QQQ" << __FUNCTION__; +} + + +/** + * Converts a raw text file, saved as a resource, into an OpenGL ES shader. + * + * @param type The type of shader we will be creating. + * @param resId The resource ID of the raw text file. + * @return The shader object handler. + */ +int LoadGLShader(int type, const char *shadercode) { + GLuint result = 0; + std::string shaderError; + static const std::string SHADER_DEFINES; + if (!gl::compileShader(type, shadercode, SHADER_DEFINES, result, shaderError)) { + qWarning() << "QQQ" << __FUNCTION__ << "Shader compile failure" << shaderError.c_str(); + } + return result; +} + +// Computes a texture size that has approximately half as many pixels. This is +// equivalent to scaling each dimension by approximately sqrt(2)/2. +static gvr::Sizei HalfPixelCount(const gvr::Sizei &in) { + // Scale each dimension by sqrt(2)/2 ~= 7/10ths. + gvr::Sizei out; + out.width = (7 * in.width) / 10; + out.height = (7 * in.height) / 10; + return out; +} + + +#if defined(GVR) +void NativeRenderer::InitializeVR() { + _gvrapi->InitializeGl(); + bool multiviewEnabled = _gvrapi->IsFeatureSupported(GVR_FEATURE_MULTIVIEW); + qWarning() << "QQQ" << __FUNCTION__ << "Multiview enabled " << multiviewEnabled; + // Because we are using 2X MSAA, we can render to half as many pixels and + // achieve similar quality. + _renderSize = HalfPixelCount(_gvrapi->GetMaximumEffectiveRenderTargetSize()); + + std::vector specs; + specs.push_back(_gvrapi->CreateBufferSpec()); + specs[0].SetColorFormat(GVR_COLOR_FORMAT_RGBA_8888); + specs[0].SetDepthStencilFormat(GVR_DEPTH_STENCIL_FORMAT_DEPTH_16); + specs[0].SetSamples(2); + gvr::Sizei half_size = {_renderSize.width / 2, _renderSize.height}; + specs[0].SetMultiviewLayers(2); + specs[0].SetSize(half_size); + + _swapchain.reset(new gvr::SwapChain(_gvrapi->CreateSwapChain(specs))); + _viewportlist.reset(new gvr::BufferViewportList(_gvrapi->CreateEmptyBufferViewportList())); +} +void NativeRenderer::PrepareFramebuffer() { + const gvr::Sizei recommended_size = HalfPixelCount( + _gvrapi->GetMaximumEffectiveRenderTargetSize()); + if (_renderSize.width != recommended_size.width || + _renderSize.height != recommended_size.height) { + // We need to resize the framebuffer. Note that multiview uses two texture + // layers, each with half the render width. + gvr::Sizei framebuffer_size = recommended_size; + framebuffer_size.width /= 2; + _swapchain->ResizeBuffer(0, framebuffer_size); + _renderSize = recommended_size; + } +} +#endif + +void testShaderBuild(const char* vs_src, const char * fs_src) { + std::string error; + GLuint vs, fs; + if (!gl::compileShader(GL_VERTEX_SHADER, vs_src, VERTEX_SHADER_DEFINES, vs, error) || + !gl::compileShader(GL_FRAGMENT_SHADER, fs_src, PIXEL_SHADER_DEFINES, fs, error)) { + throw std::runtime_error("Failed to compile shader"); + } + auto pr = gl::compileProgram({ vs, fs }, error); + if (!pr) { + throw std::runtime_error("Failed to link shader"); + } +} + +void NativeRenderer::InitializeGl() { + qDebug() << "QQQ" << __FUNCTION__; + //gl::initModuleGl(); +#if defined(GVR) + InitializeVR(); +#endif + + glDisable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_BLEND); + + + + const uint32_t vertShader = LoadGLShader(GL_VERTEX_SHADER, kSimepleVertexShader); + //const uint32_t vertShader = LoadGLShader(GL_VERTEX_SHADER, kDiffuseLightingVertexShader); + const uint32_t fragShader = LoadGLShader(GL_FRAGMENT_SHADER, kPassthroughFragmentShader); + std::string error; + _cubeProgram = gl::compileProgram({ vertShader, fragShader }, error); + CheckGLError("build program"); + + glGenBuffers(1, &_cubeBuffer); + glBindBuffer(GL_ARRAY_BUFFER, _cubeBuffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 9, triangle::TRIANGLE_VERTS.data(), GL_STATIC_DRAW); + /* + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 108 * 3, NULL, GL_STATIC_DRAW); + glBufferSubData(GL_ARRAY_BUFFER, sizeof(float) * 108 * 0, sizeof(float) * 108, cube::CUBE_COORDS.data()); + glBufferSubData(GL_ARRAY_BUFFER, sizeof(float) * 108 * 1, sizeof(float) * 108, cube::CUBE_COLORS.data()); + glBufferSubData(GL_ARRAY_BUFFER, sizeof(float) * 108 * 2, sizeof(float) * 108, cube::CUBE_NORMALS.data()); + */ + glBindBuffer(GL_ARRAY_BUFFER, 0); + CheckGLError("upload vertices"); + + glGenVertexArrays(1, &_cubeVao); + glBindBuffer(GL_ARRAY_BUFFER, _cubeBuffer); + glBindVertexArray(_cubeVao); + + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + /* + glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0); + glEnableVertexAttribArray(0); + glVertexAttribPointer(1, 3, GL_FLOAT, false, 0, (const void*)(sizeof(float) * 108 * 1) ); + glEnableVertexAttribArray(1); + glVertexAttribPointer(2, 3, GL_FLOAT, false, 0, (const void*)(sizeof(float) * 108 * 2)); + glEnableVertexAttribArray(2); + */ + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + CheckGLError("build vao "); + + static std::once_flag once; + std::call_once(once, [&]{ + testShaderBuild(sdf_text3D_vert, sdf_text3D_frag); + + testShaderBuild(DrawTransformUnitQuad_vert, DrawTexture_frag); + testShaderBuild(DrawTexcoordRectTransformUnitQuad_vert, DrawTexture_frag); + testShaderBuild(DrawViewportQuadTransformTexcoord_vert, DrawTexture_frag); + testShaderBuild(DrawTransformUnitQuad_vert, DrawTextureOpaque_frag); + testShaderBuild(DrawTransformUnitQuad_vert, DrawColoredTexture_frag); + + testShaderBuild(simple_vert, simple_frag); + testShaderBuild(simple_vert, simple_textured_frag); + testShaderBuild(simple_vert, simple_textured_unlit_frag); + testShaderBuild(deferred_light_vert, directional_ambient_light_frag); + testShaderBuild(deferred_light_vert, directional_skybox_light_frag); + testShaderBuild(standardTransformPNTC_vert, standardDrawTexture_frag); + testShaderBuild(standardTransformPNTC_vert, DrawTextureOpaque_frag); + + testShaderBuild(model_vert, model_frag); + testShaderBuild(model_normal_map_vert, model_normal_map_frag); + testShaderBuild(model_vert, model_specular_map_frag); + testShaderBuild(model_normal_map_vert, model_normal_specular_map_frag); + testShaderBuild(model_vert, model_translucent_frag); + testShaderBuild(model_normal_map_vert, model_translucent_frag); + testShaderBuild(model_lightmap_vert, model_lightmap_frag); + testShaderBuild(model_lightmap_normal_map_vert, model_lightmap_normal_map_frag); + testShaderBuild(model_lightmap_vert, model_lightmap_specular_map_frag); + testShaderBuild(model_lightmap_normal_map_vert, model_lightmap_normal_specular_map_frag); + + testShaderBuild(skin_model_vert, model_frag); + testShaderBuild(skin_model_normal_map_vert, model_normal_map_frag); + testShaderBuild(skin_model_vert, model_specular_map_frag); + testShaderBuild(skin_model_normal_map_vert, model_normal_specular_map_frag); + testShaderBuild(skin_model_vert, model_translucent_frag); + testShaderBuild(skin_model_normal_map_vert, model_translucent_frag); + + testShaderBuild(model_shadow_vert, model_shadow_frag); + + testShaderBuild(overlay3D_vert, overlay3D_frag); + +#if 0 + testShaderBuild(textured_particle_vert, textured_particle_frag); + testShaderBuild(skybox_vert, skybox_frag); + testShaderBuild(paintStroke_vert,paintStroke_frag); + testShaderBuild(polyvox_vert, polyvox_frag); +#endif + + }); + + qDebug() << "done"; +} + +static const float kZNear = 1.0f; +static const float kZFar = 100.0f; +static const gvr_rectf fullscreen = {0, 1, 0, 1}; + +void NativeRenderer::DrawFrame() { + auto now = std::chrono::duration_cast( + std::chrono::system_clock::now() - start); + glm::vec3 v; + v.r = (float) (now.count() % 1000) / 1000.0f; + v.g = 1.0f - v.r; + v.b = 1.0f; + + PrepareFramebuffer(); + + // A client app does its rendering here. + gvr::ClockTimePoint target_time = gvr::GvrApi::GetTimePointNow(); + target_time.monotonic_system_time_nanos += kPredictionTimeWithoutVsyncNanos; + + using namespace googlevr; + using namespace bilateral; + const auto gvrHeadPose = _gvrapi->GetHeadSpaceFromStartSpaceRotation(target_time); + _head_view = toGlm(gvrHeadPose); + _viewportlist->SetToRecommendedBufferViewports(); + + glm::mat4 eye_views[2]; + for_each_side([&](bilateral::Side side) { + int eye = index(side); + const gvr::Eye gvr_eye = eye == 0 ? GVR_LEFT_EYE : GVR_RIGHT_EYE; + const auto& eyeView = eye_views[eye] = toGlm(_gvrapi->GetEyeFromHeadMatrix(gvr_eye)) * _head_view; + auto& viewport = _viewports[eye]; + + _viewportlist->GetBufferViewport(eye, &viewport); + viewport.SetSourceUv(fullscreen); + viewport.SetSourceLayer(eye); + _viewportlist->SetBufferViewport(eye, viewport); + const auto &mvc = _modelview_cube[eye] = eyeView * _model_cube; + const auto &mvf = _modelview_floor[eye] = eyeView * _model_floor; + const gvr_rectf fov = viewport.GetSourceFov(); + const glm::mat4 perspective = perspectiveMatrixFromView(fov, kZNear, kZFar); + _modelview_projection_cube[eye] = perspective * mvc; + _modelview_projection_floor[eye] = perspective * mvf; + _light_pos_eye_space[eye] = glm::vec3(eyeView * _light_pos_world_space); + }); + + + gvr::Frame frame = _swapchain->AcquireFrame(); + withFrameBuffer(frame, 0, [&]{ + glClearColor(v.r, v.g, v.b, 1); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glViewport(0, 0, _renderSize.width / 2, _renderSize.height); + glUseProgram(_cubeProgram); + glBindVertexArray(_cubeVao); + glDrawArrays(GL_TRIANGLES, 0, 3); + /* + float* fp; + fp = (float*)&_light_pos_eye_space[0]; + glUniform3fv(UNIFORM_LIGHT_POS, 2, fp); + fp = (float*)&_modelview_cube[0]; + glUniformMatrix4fv(UNIFORM_MV, 2, GL_FALSE, fp); + fp = (float*)&_modelview_projection_cube[0]; + glUniformMatrix4fv(UNIFORM_MVP, 2, GL_FALSE, fp); + fp = (float*)&_model_cube; + glUniformMatrix4fv(UNIFORM_M, 1, GL_FALSE, fp); + glDrawArrays(GL_TRIANGLES, 0, 36); + */ + glBindVertexArray(0); + }); + + frame.Submit(*_viewportlist, gvrHeadPose); + CheckGLError("onDrawFrame"); + +} + +void NativeRenderer::OnTriggerEvent() { + qDebug() << "QQQ" << __FUNCTION__; +} + +void NativeRenderer::OnPause() { + qDebug() << "QQQ" << __FUNCTION__; + _gvrapi->PauseTracking(); +} + +void NativeRenderer::OnResume() { + qDebug() << "QQQ" << __FUNCTION__; + _gvrapi->ResumeTracking(); + _gvrapi->RefreshViewerProfile(); +} diff --git a/android/app/src/main/cpp/renderer.h b/android/app/src/main/cpp/renderer.h new file mode 100644 index 0000000000..df7c51cab4 --- /dev/null +++ b/android/app/src/main/cpp/renderer.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + +#define GVR + +#if defined(GVR) +#include +#endif + +class NativeRenderer { +public: + +#if defined(GVR) + NativeRenderer(gvr_context* vrContext); +#else + NativeRenderer(void* vrContext); +#endif + + void InitializeGl(); + void DrawFrame(); + void OnTriggerEvent(); + void OnPause(); + void OnResume(); + +private: + + + std::chrono::time_point start; +#if defined(GVR) + void InitializeVR(); + void PrepareFramebuffer(); + + std::unique_ptr _gvrapi; + gvr::ViewerType _gvr_viewer_type; + std::unique_ptr _viewportlist; + std::unique_ptr _swapchain; + std::array _viewports; + gvr::Sizei _renderSize; +#endif + + uint32_t _cubeBuffer { 0 }; + uint32_t _cubeVao { 0 }; + uint32_t _cubeProgram { 0 }; + + glm::mat4 _head_view; + glm::mat4 _model_cube; + glm::mat4 _camera; + glm::mat4 _view; + glm::mat4 _model_floor; + + std::array _modelview_cube; + std::array _modelview_floor; + std::array _modelview_projection_cube; + std::array _modelview_projection_floor; + std::array _light_pos_eye_space; + const glm::vec4 _light_pos_world_space{ 0, 2, 0, 1}; +}; diff --git a/android/app/src/main/java/org/saintandreas/testapp/MainActivity.java b/android/app/src/main/java/org/saintandreas/testapp/MainActivity.java new file mode 100644 index 0000000000..7eea14dce9 --- /dev/null +++ b/android/app/src/main/java/org/saintandreas/testapp/MainActivity.java @@ -0,0 +1,105 @@ +package org.saintandreas.testapp; + +import android.app.Activity; +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.os.Bundle; +import android.view.View; + +import com.google.vr.ndk.base.AndroidCompat; +import com.google.vr.ndk.base.GvrLayout; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +public class MainActivity extends Activity { + private final static int IMMERSIVE_STICKY_VIEW_FLAGS = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + + static { + System.loadLibrary("gvr"); + System.loadLibrary("native-lib"); + } + + private long nativeRenderer; + private GvrLayout gvrLayout; + private GLSurfaceView surfaceView; + + private native long nativeCreateRenderer(ClassLoader appClassLoader, Context context, long nativeGvrContext); + private native void nativeDestroyRenderer(long renderer); + private native void nativeInitializeGl(long renderer); + private native void nativeDrawFrame(long renderer); + private native void nativeOnTriggerEvent(long renderer); + private native void nativeOnPause(long renderer); + private native void nativeOnResume(long renderer); + + class NativeRenderer implements GLSurfaceView.Renderer { + @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { nativeInitializeGl(nativeRenderer); } + @Override public void onSurfaceChanged(GL10 gl, int width, int height) { } + @Override public void onDrawFrame(GL10 gl) { + nativeDrawFrame(nativeRenderer); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setImmersiveSticky(); + getWindow() + .getDecorView() + .setOnSystemUiVisibilityChangeListener((int visibility)->{ + if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { setImmersiveSticky(); } + }); + + gvrLayout = new GvrLayout(this); + nativeRenderer = nativeCreateRenderer( + getClass().getClassLoader(), + getApplicationContext(), + gvrLayout.getGvrApi().getNativeGvrContext()); + + surfaceView = new GLSurfaceView(this); + surfaceView.setEGLContextClientVersion(3); + surfaceView.setEGLConfigChooser(8, 8, 8, 0, 0, 0); + surfaceView.setPreserveEGLContextOnPause(true); + surfaceView.setRenderer(new NativeRenderer()); + + gvrLayout.setPresentationView(surfaceView); + setContentView(gvrLayout); + if (gvrLayout.setAsyncReprojectionEnabled(true)) { + AndroidCompat.setSustainedPerformanceMode(this, true); + } + AndroidCompat.setVrModeEnabled(this, true); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + gvrLayout.shutdown(); + nativeDestroyRenderer(nativeRenderer); + nativeRenderer = 0; + } + + @Override + protected void onPause() { + surfaceView.queueEvent(()->nativeOnPause(nativeRenderer)); + surfaceView.onPause(); + gvrLayout.onPause(); + super.onPause(); + } + + @Override + protected void onResume() { + super.onResume(); + gvrLayout.onResume(); + surfaceView.onResume(); + surfaceView.queueEvent(()->nativeOnResume(nativeRenderer)); + } + + private void setImmersiveSticky() { + getWindow().getDecorView().setSystemUiVisibility(IMMERSIVE_STICKY_VIEW_FLAGS); + } +} diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cde69bcccec65160d92116f20ffce4fce0b5245c GIT binary patch literal 3418 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..9a078e3e1a42d474c78470a73c7987cf7ac5d9a0 GIT binary patch literal 4208 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..3af2608a4492ef9ae63a77ec3305aedda89594cb GIT binary patch literal 6114 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..324e72cdd7480cb983fa1bcc7ce686e51ef87fe7 GIT binary patch literal 7718 zcmZ{p$IRXXZTL&R=k%^>oyT@agdZ03gy(S2h3uAn`xo;Q#=zt-9U<005SQ!gB=x zs7oODXN3&_0K`V!;5h&UZ~;I_7y#S?01&bR06xM1fVKnx=?nm%asS+=4+Q`K$68BG z8TjAbD76Oz004GZf9VAP1Qh=P1IYSJ3jj>q8p;ZWev1e2H;t5YO%LN0=$mni7RU$f zLQr88EfIGVt%NG&lR}Ru5f8tqGA@rxI*CpW-RBk}=$Ithq2wP3Vls&Vb!*0TQun?P zvP5Cr%)>u6MmbkSZEasFoe$CI&SH7vnfLAEUnk^VAAFzCD?>`7%ZuXw%Mmjq^3GRu zh%FI{QSjyKKU+*{0 z>wJT3cAWqaXqsK13(v&oB(A7rFxfzUOyi|(IETRwboZ0W*#s^rzG*HJA0Og&q;JjbZ>9hk;D_-&I`8g;WWARonr^_GWb! zT^Xw((8voS4L0z*{ivjkynrjAynQ8~i1WTJsz!dskVz`vpqt6Hl$-G6@!MiCsW&Ha zUuyPO(*F?Ulfdv#<9J(Z#xO&hVN$>wU|83iGz5X20^MZlbsTIV{HS)0 z*6SLok8QqXzG#Y%NvPCp=A2+TR7l&Oe7)5hC#|6h^k1Xe#9^%*)tvO1v;*7%%G2Yk zfA6~c@k+SR6#L6*%a^EoH=%ItTuNIIwlp0B${?a%r^vM6!B7*gtKM&q$sx$$pAcBh z5KL!AY)kU_bZ#No@6Ey&nX{WHf|7C?`ind*>6_WJ(t$x8`UR-Bwi-oT(ma+5`(Vmf zfEowTv6dp^&lx}co@v+;Lm3{Qw~$g>c8pnCOglkO@uGx?cHUIr({ z1YCBz?xW#IU0XwJRH27d8C<&k+`Q<3~< zGX5q-d7_*fD;{1?Afc5YvB=M<>g$oUFzjL zO2vX6eRbe|k}(Q}1~}L-ToUG=uODZ;N}Cf1;56|u4kh7oc-C#j(-3g6TGU5KXjJOh zKW`EL&QlkON3cK?R9S3Fj{)icgY(S5K$qM3HEKs~S>0=R8&adWdNbSf!h{=`@3poFp8L3j+;y*7% zq_oQN zbqPyz*^Ja~5})zdQhm_2JS#;xjb|V?+mC{FdIcR0GbE6YXg(Ta^gDK!w)M8A*Vm5{ zAyXo8k=nW_ll=EIdn}nwjM%N^rmmqjODY)Epnjf{LygEfFG@9w;NDEM8QyX)^4qGK zA-TEy>^~DQU$b(rF>%PeI5|lw`#9g5P9QF8niDDVw}|SFIbdU1_M!FH4t$8nT2lz%@JW(DF6PJ4l`8Kas zvp5b)3mVmDCYzsaa8|md7U5|67OS~hQrqr@c(?bRrmGQ>Mt-T-EO%dy0h)`!2AyKm zLDVS~t?yZ1J7%kSllk;Q9{Sv09HOdH3B`=5KEla_xmY6&IacCl$g5FU`rj;A@qZk# zthgTxH08NPxV*o$g7OkEHhpzflvT>XNetYqFk*Q)E6Pnj2V;w1_M`pkh?|HNMp&QN zYj&<)EBrc@4DPZgiV+(}3T0~}Opa0l_DMYV#jFDFC`cUFFWMen8>| zd{a9O(Af5&{GIC{GYdij2WFYVf#>drrE24?Qaxv#!zMn@5z74ICz7)7E`2GOs<9SJ zVzL|!nm#zH;Ml3N+-$|25zA~&)$FqLQi~

    TYSD#W4maIvLbRYGJNG}V|+SXfTh>av49 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..9bec2e623103ac9713b00cad8502a057c1efda61 GIT binary patch literal 10056 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

  • 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 literal 0 HcmV?d00001 diff --git a/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml b/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml new file mode 100644 index 0000000000..b5d7efe818 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml @@ -0,0 +1,196 @@ +// +// FirstUseTutorial.qml +// qml/hifi/commerce/purchases +// +// FirstUseTutorial +// +// Created by Zach Fox on 2017-09-13 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Rectangle { + HifiConstants { id: hifi; } + + id: root; + property string activeView: "step_1"; + // Style + color: hifi.colors.baseGray; + + // + // "STEP 1" START + // + Item { + id: step_1; + visible: root.activeView === "step_1"; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: tutorialActionButtonsContainer.top; + + RalewayRegular { + id: step1text; + text: "This is the first-time Purchases tutorial.

    Here is some bold text " + + "inside Step 1."; + // Text size + size: 24; + // Anchors + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + } + // + // "STEP 1" END + // + + // + // "STEP 2" START + // + Item { + id: step_2; + visible: root.activeView === "step_2"; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: tutorialActionButtonsContainer.top; + + RalewayRegular { + id: step2text; + text: "STEP TWOOO!!!"; + // Text size + size: 24; + // Anchors + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + } + // + // "STEP 2" END + // + + Item { + id: tutorialActionButtonsContainer; + // Size + width: root.width; + height: 70; + // Anchors + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 24; + + // "Skip" or "Back" button + HifiControlsUit.Button { + id: skipOrBackButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: parent.width/2 - anchors.leftMargin*2; + text: root.activeView === "step_1" ? "Skip" : "Back"; + onClicked: { + if (root.activeView === "step_1") { + sendSignalToParent({method: 'tutorial_skipClicked'}); + } else { + root.activeView = "step_" + (parseInt(root.activeView.split("_")[1]) - 1); + } + } + } + + // "Next" or "Finish" button + HifiControlsUit.Button { + id: nextButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.right: parent.right; + anchors.rightMargin: 20; + width: parent.width/2 - anchors.rightMargin*2; + text: root.activeView === "step_2" ? "Finish" : "Next"; + onClicked: { + // If this is the final step... + if (root.activeView === "step_2") { + sendSignalToParent({method: 'tutorial_finished'}); + } else { + root.activeView = "step_" + (parseInt(root.activeView.split("_")[1]) + 1); + } + } + } + } + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the JavaScript, in this case the Marketplaces JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from a script. + // + function fromScript(message) { + switch (message.method) { + case 'updatePurchases': + referrerURL = message.referrerURL; + break; + case 'purchases_getIsFirstUseResult': + if (message.isFirstUseOfPurchases && root.activeView !== "firstUseTutorial") { + root.activeView = "firstUseTutorial"; + } else if (!message.isFirstUseOfPurchases && root.activeView === "initialize") { + root.activeView = "purchasesMain"; + commerce.inventory(); + } + break; + default: + console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); + } + } + signal sendSignalToParent(var message); + + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index af32f5cfb7..1186687a82 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -13,7 +13,9 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 +import QtGraphicalEffects 1.0 import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls @@ -21,116 +23,395 @@ import "../wallet" as HifiWallet // references XXX from root context -Rectangle { +Item { HifiConstants { id: hifi; } id: root; - property string itemName: ""; - property string itemId: ""; - property string itemPreviewImageUrl: ""; - property string itemHref: ""; - // Style - color: hifi.colors.white; - // Size - width: parent.width; - height: 120; + property string purchaseStatus; + property bool purchaseStatusChanged; + property bool canRezCertifiedItems: false; + property string itemName; + property string itemId; + property string itemPreviewImageUrl; + property string itemHref; + property int ownedItemCount; + property int itemEdition; - Image { - id: itemPreviewImage; - source: root.itemPreviewImageUrl; + height: 110; + width: parent.width; + + onPurchaseStatusChangedChanged: { + if (root.purchaseStatusChanged === true && root.purchaseStatus === "confirmed") { + statusText.text = "CONFIRMED!"; + statusText.color = hifi.colors.blueAccent; + confirmedTimer.start(); + root.purchaseStatusChanged = false; + } + } + + Timer { + id: confirmedTimer; + interval: 3000; + onTriggered: { + root.purchaseStatus = ""; + } + } + + Rectangle { + id: mainContainer; + // Style + color: hifi.colors.white; + // Size anchors.left: parent.left; anchors.leftMargin: 8; + anchors.right: parent.right; + anchors.rightMargin: 8; anchors.top: parent.top; - anchors.topMargin: 8; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 8; - width: 180; - fillMode: Image.PreserveAspectFit; + height: root.height - 10; - MouseArea { - anchors.fill: parent; - onClicked: { - sendToPurchases({method: 'purchases_itemInfoClicked', itemId: root.itemId}); - } - } - } - - - RalewayRegular { - id: itemName; - anchors.top: itemPreviewImage.top; - anchors.left: itemPreviewImage.right; - anchors.leftMargin: 8; - anchors.right: parent.right; - anchors.rightMargin: 8; - height: 30; - // Text size - size: 20; - // Style - color: hifi.colors.blueAccent; - text: root.itemName; - elide: Text.ElideRight; - // Alignment - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - - MouseArea { - anchors.fill: parent; - hoverEnabled: enabled; - onClicked: { - sendToPurchases({method: 'purchases_itemInfoClicked', itemId: root.itemId}); - } - onEntered: { - itemName.color = hifi.colors.blueHighlight; - } - onExited: { - itemName.color = hifi.colors.blueAccent; - } - } - } - - Item { - id: buttonContainer; - anchors.top: itemName.bottom; - anchors.topMargin: 8; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 8; - anchors.left: itemPreviewImage.right; - anchors.leftMargin: 8; - anchors.right: parent.right; - anchors.rightMargin: 8; - - // "Rez" button - HifiControlsUit.Button { - id: rezButton; - color: hifi.buttons.blue; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; + Image { + id: itemPreviewImage; + source: root.itemPreviewImageUrl; anchors.left: parent.left; - anchors.right: parent.right; - height: parent.height/2 - 4; - text: "Rez Item" - onClicked: { - if (urlHandler.canHandleUrl(root.itemHref)) { - urlHandler.handleUrl(root.itemHref); + anchors.top: parent.top; + anchors.bottom: parent.bottom; + width: height; + fillMode: Image.PreserveAspectCrop; + + MouseArea { + anchors.fill: parent; + onClicked: { + sendToPurchases({method: 'purchases_itemInfoClicked', itemId: root.itemId}); } } } - // "More Info" button - HifiControlsUit.Button { - id: moreInfoButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.right: parent.right; - height: parent.height/2 - 4; - text: "More Info" - onClicked: { - sendToPurchases({method: 'purchases_itemInfoClicked', itemId: root.itemId}); + + RalewaySemiBold { + id: itemName; + anchors.top: itemPreviewImage.top; + anchors.topMargin: 4; + anchors.left: itemPreviewImage.right; + anchors.leftMargin: 8; + anchors.right: buttonContainer.left; + anchors.rightMargin: 8; + height: paintedHeight; + // Text size + size: 24; + // Style + color: hifi.colors.blueAccent; + text: root.itemName; + elide: Text.ElideRight; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + sendToPurchases({method: 'purchases_itemInfoClicked', itemId: root.itemId}); + } + onEntered: { + itemName.color = hifi.colors.blueHighlight; + } + onExited: { + itemName.color = hifi.colors.blueAccent; + } } } + + Item { + id: certificateContainer; + anchors.top: itemName.bottom; + anchors.topMargin: 4; + anchors.left: itemName.left; + anchors.right: buttonContainer.left; + anchors.rightMargin: 2; + height: 24; + + HiFiGlyphs { + id: certificateIcon; + text: hifi.glyphs.scriptNew; + // Size + size: 30; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: 32; + // Style + color: hifi.colors.lightGray; + } + + RalewayRegular { + id: viewCertificateText; + text: "VIEW CERTIFICATE"; + size: 14; + anchors.left: certificateIcon.right; + anchors.leftMargin: 4; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + color: hifi.colors.lightGray; + } + + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + sendToPurchases({method: 'purchases_itemCertificateClicked', itemMarketplaceId: root.itemId}); + } + onEntered: { + certificateIcon.color = hifi.colors.black; + viewCertificateText.color = hifi.colors.black; + } + onExited: { + certificateIcon.color = hifi.colors.lightGray; + viewCertificateText.color = hifi.colors.lightGray; + } + } + } + + Item { + id: statusContainer; + + visible: root.purchaseStatus || root.ownedItemCount > 1; + anchors.left: itemName.left; + anchors.top: certificateContainer.bottom; + anchors.topMargin: 8; + anchors.bottom: parent.bottom; + anchors.right: buttonContainer.left; + anchors.rightMargin: 2; + + RalewaySemiBold { + id: statusText; + anchors.left: parent.left; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + width: paintedWidth; + text: { + if (root.purchaseStatus === "pending") { + "PENDING..." + } else if (root.purchaseStatus === "invalidated") { + "INVALIDATED" + } else if (root.ownedItemCount > 1) { + "(#" + root.itemEdition + ") You own " + root.ownedItemCount + " others" + } else { + "" + } + } + size: 18; + color: { + if (root.purchaseStatus === "pending") { + hifi.colors.blueAccent + } else if (root.purchaseStatus === "invalidated") { + hifi.colors.redAccent + } else if (root.ownedItemCount > 1) { + hifi.colors.blueAccent + } else { + hifi.colors.baseGray + } + } + verticalAlignment: Text.AlignTop; + } + + HiFiGlyphs { + id: statusIcon; + text: { + if (root.purchaseStatus === "pending") { + hifi.glyphs.question + } else if (root.purchaseStatus === "invalidated") { + hifi.glyphs.question + } else { + "" + } + } + // Size + size: 36; + // Anchors + anchors.top: parent.top; + anchors.topMargin: -8; + anchors.left: statusText.right; + anchors.bottom: parent.bottom; + // Style + color: { + if (root.purchaseStatus === "pending") { + hifi.colors.blueAccent + } else if (root.purchaseStatus === "invalidated") { + hifi.colors.redAccent + } else if (root.ownedItemCount > 1) { + hifi.colors.blueAccent + } else { + hifi.colors.baseGray + } + } + verticalAlignment: Text.AlignTop; + } + + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + if (root.purchaseStatus === "pending") { + sendToPurchases({method: 'showPendingLightbox'}); + } else if (root.purchaseStatus === "invalidated") { + sendToPurchases({method: 'showInvalidatedLightbox'}); + } else if (root.ownedItemCount > 1) { + sendToPurchases({method: 'setFilterText', filterText: root.itemName}); + } + } + onEntered: { + if (root.purchaseStatus === "pending") { + statusText.color = hifi.colors.blueHighlight; + statusIcon.color = hifi.colors.blueHighlight; + } else if (root.purchaseStatus === "invalidated") { + statusText.color = hifi.colors.redAccent; + statusIcon.color = hifi.colors.redAccent; + } else if (root.ownedItemCount > 1) { + statusText.color = hifi.colors.blueHighlight; + statusIcon.color = hifi.colors.blueHighlight; + } + } + onExited: { + if (root.purchaseStatus === "pending") { + statusText.color = hifi.colors.blueAccent; + statusIcon.color = hifi.colors.blueAccent; + } else if (root.purchaseStatus === "invalidated") { + statusText.color = hifi.colors.redHighlight; + statusIcon.color = hifi.colors.redHighlight; + } else if (root.ownedItemCount > 1) { + statusText.color = hifi.colors.blueAccent; + statusIcon.color = hifi.colors.blueAccent; + } + } + } + } + + Rectangle { + id: rezzedNotifContainer; + z: 998; + visible: false; + color: hifi.colors.blueHighlight; + anchors.fill: buttonContainer; + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + } + + RalewayBold { + anchors.fill: parent; + text: "REZZED"; + size: 18; + color: hifi.colors.white; + verticalAlignment: Text.AlignVCenter; + horizontalAlignment: Text.AlignHCenter; + } + + Timer { + id: rezzedNotifContainerTimer; + interval: 2000; + onTriggered: rezzedNotifContainer.visible = false + } + } + + Button { + id: buttonContainer; + property int color: hifi.buttons.red; + property int colorScheme: hifi.colorSchemes.light; + + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + width: height; + enabled: root.canRezCertifiedItems && root.purchaseStatus !== "invalidated"; + + onClicked: { + if (urlHandler.canHandleUrl(root.itemHref)) { + urlHandler.handleUrl(root.itemHref); + } + rezzedNotifContainer.visible = true; + rezzedNotifContainerTimer.start(); + } + + style: ButtonStyle { + + background: Rectangle { + gradient: Gradient { + GradientStop { + position: 0.2 + color: { + if (!control.enabled) { + hifi.buttons.disabledColorStart[control.colorScheme] + } else if (control.pressed) { + hifi.buttons.pressedColor[control.color] + } else if (control.hovered) { + hifi.buttons.hoveredColor[control.color] + } else { + hifi.buttons.colorStart[control.color] + } + } + } + GradientStop { + position: 1.0 + color: { + if (!control.enabled) { + hifi.buttons.disabledColorFinish[control.colorScheme] + } else if (control.pressed) { + hifi.buttons.pressedColor[control.color] + } else if (control.hovered) { + hifi.buttons.hoveredColor[control.color] + } else { + hifi.buttons.colorFinish[control.color] + } + } + } + } + } + + label: Item { + HiFiGlyphs { + id: lightningIcon; + text: hifi.glyphs.lightning; + // Size + size: 32; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 12; + anchors.left: parent.left; + anchors.right: parent.right; + horizontalAlignment: Text.AlignHCenter; + // Style + color: enabled ? hifi.buttons.textColor[control.color] + : hifi.buttons.disabledTextColor[control.colorScheme] + } + RalewayBold { + anchors.top: lightningIcon.bottom; + anchors.topMargin: -20; + anchors.right: parent.right; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + font.capitalization: Font.AllUppercase + color: enabled ? hifi.buttons.textColor[control.color] + : hifi.buttons.disabledTextColor[control.colorScheme] + size: 16; + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + text: "Rez It" + } + } + } + } + } + + DropShadow { + anchors.fill: mainContainer; + horizontalOffset: 3; + verticalOffset: 3; + radius: 8.0; + samples: 17; + color: "#80000000"; + source: mainContainer; } // diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 697249f740..1002fb881b 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -18,6 +18,7 @@ import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls import "../wallet" as HifiWallet +import "../common" as HifiCommerceCommon // references XXX from root context @@ -30,19 +31,13 @@ Rectangle { property bool securityImageResultReceived: false; property bool purchasesReceived: false; property bool punctuationMode: false; + property bool canRezCertifiedItems: false; + property bool pendingInventoryReply: true; // Style - color: hifi.colors.baseGray; + color: hifi.colors.white; Hifi.QmlCommerce { id: commerce; - onAccountResult: { - if (result.status === "success") { - commerce.getKeyFilePathIfExists(); - } else { - // unsure how to handle a failure here. We definitely cannot proceed. - } - } - onLoginStatusResult: { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; @@ -52,9 +47,18 @@ Rectangle { } } + onAccountResult: { + if (result.status === "success") { + commerce.getKeyFilePathIfExists(); + } else { + // unsure how to handle a failure here. We definitely cannot proceed. + } + } + onKeyFilePathIfExistsResult: { if (path === "" && root.activeView !== "notSetUp") { root.activeView = "notSetUp"; + notSetUpTimer.start(); } else if (path !== "" && root.activeView === "initialize") { commerce.getSecurityImage(); } @@ -64,103 +68,101 @@ Rectangle { securityImageResultReceived = true; if (!exists && root.activeView !== "notSetUp") { // "If security image is not set up" root.activeView = "notSetUp"; + notSetUpTimer.start(); } else if (exists && root.activeView === "initialize") { commerce.getWalletAuthenticatedStatus(); - } else if (exists) { - // just set the source again (to be sure the change was noticed) - securityImage.source = ""; - securityImage.source = "image://security/securityImage"; } } onWalletAuthenticatedStatusResult: { - if (!isAuthenticated && !passphraseModal.visible) { - passphraseModal.visible = true; + if (!isAuthenticated && root.activeView !== "passphraseModal") { + root.activeView = "passphraseModal"; } else if (isAuthenticated) { - root.activeView = "purchasesMain"; - commerce.inventory(); + sendToScript({method: 'purchases_getIsFirstUse'}); } } onInventoryResult: { purchasesReceived = true; + if (result.status !== 'success') { console.log("Failed to get purchases", result.message); } else { purchasesModel.clear(); purchasesModel.append(result.data.assets); - filteredPurchasesModel.clear(); - filteredPurchasesModel.append(result.data.assets); + + if (previousPurchasesModel.count !== 0) { + checkIfAnyItemStatusChanged(); + } else { + // Fill statusChanged default value + // Not doing this results in the default being true... + for (var i = 0; i < purchasesModel.count; i++) { + purchasesModel.setProperty(i, "statusChanged", false); + } + } + previousPurchasesModel.append(result.data.assets); + + buildFilteredPurchasesModel(); + + if (root.pendingInventoryReply) { + inventoryTimer.start(); + } } + + root.pendingInventoryReply = false; } } - HifiWallet.SecurityImageModel { - id: securityImageModel; + Timer { + id: notSetUpTimer; + interval: 200; + onTriggered: { + sendToScript({method: 'checkout_walletNotSetUp'}); + } + } + + HifiCommerceCommon.CommerceLightbox { + id: lightboxPopup; + visible: false; + anchors.fill: parent; + + Connections { + onSendToParent: { + sendToScript(msg); + } + } } // // TITLE BAR START // - Item { + HifiCommerceCommon.EmulatedMarketplaceHeader { id: titleBarContainer; + z: 998; visible: !needsLogIn.visible; // Size - height: 50; + width: parent.width; // Anchors anchors.left: parent.left; - anchors.right: parent.right; anchors.top: parent.top; - // Title Bar text - RalewaySemiBold { - id: titleBarText; - text: "PURCHASES"; - // Text size - size: hifi.fontSizes.overlayTitle; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.bottom: parent.bottom; - width: paintedWidth; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - // Security Image (TEMPORARY!) - Image { - id: securityImage; - // Anchors - anchors.top: parent.top; - anchors.right: parent.right; - anchors.verticalCenter: parent.verticalCenter; - height: parent.height - 10; - width: height; - fillMode: Image.PreserveAspectFit; - mipmap: true; - cache: false; - source: "image://security/securityImage"; - } - Image { - id: securityImageOverlay; - source: "../wallet/images/lockOverlay.png"; - width: securityImage.width * 0.45; - height: securityImage.height * 0.45; - anchors.bottom: securityImage.bottom; - anchors.right: securityImage.right; - mipmap: true; - opacity: 0.9; - } - - // Separator - HifiControlsUit.Separator { - anchors.left: parent.left; - anchors.right: parent.right; - anchors.bottom: parent.bottom; + Connections { + onSendToParent: { + if (msg.method === 'needsLogIn' && root.activeView !== "needsLogIn") { + root.activeView = "needsLogIn"; + } else if (msg.method === 'showSecurityPicLightbox') { + lightboxPopup.titleText = "Your Security Pic"; + lightboxPopup.bodyImageSource = msg.securityImageSource; + lightboxPopup.bodyText = lightboxPopup.securityPicBodyText; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.button2text = "GO TO WALLET"; + lightboxPopup.button2method = "sendToParent({method: 'purchases_openWallet'});"; + lightboxPopup.visible = true; + } else { + sendToScript(msg); + } + } } } // @@ -171,10 +173,11 @@ Rectangle { id: initialize; visible: root.activeView === "initialize"; anchors.top: titleBarContainer.bottom; + anchors.topMargin: -titleBarContainer.additionalDropdownHeight; anchors.bottom: parent.bottom; anchors.left: parent.left; anchors.right: parent.right; - color: hifi.colors.baseGray; + color: hifi.colors.white; Component.onCompleted: { securityImageResultReceived = false; @@ -206,101 +209,45 @@ Rectangle { HifiWallet.PassphraseModal { id: passphraseModal; - visible: false; + visible: root.activeView === "passphraseModal"; + anchors.fill: parent; + titleBarText: "Purchases"; + titleBarIcon: hifi.glyphs.wallet; + + Connections { + onSendSignalToParent: { + if (msg.method === "authSuccess") { + root.activeView = "initialize"; + sendToScript({method: 'purchases_getIsFirstUse'}); + } else { + sendToScript(msg); + } + } + } + } + + FirstUseTutorial { + id: firstUseTutorial; + visible: root.activeView === "firstUseTutorial"; anchors.top: titleBarContainer.bottom; + anchors.topMargin: -titleBarContainer.additionalDropdownHeight; anchors.bottom: parent.bottom; anchors.left: parent.left; anchors.right: parent.right; Connections { onSendSignalToParent: { - sendToScript(msg); - } - } - } - - // - // "WALLET NOT SET UP" START - // - Item { - id: notSetUp; - visible: root.activeView === "notSetUp"; - anchors.top: titleBarContainer.bottom; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.right: parent.right; - - RalewayRegular { - id: notSetUpText; - text: "Your wallet isn't set up.

    Set up your Wallet (no credit card necessary) to claim your free HFC " + - "and get items from the Marketplace."; - // Text size - size: 24; - // Anchors - anchors.top: parent.top; - anchors.bottom: notSetUpActionButtonsContainer.top; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - // Style - color: hifi.colors.faintGray; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - } - - Item { - id: notSetUpActionButtonsContainer; - // Size - width: root.width; - height: 70; - // Anchors - anchors.left: parent.left; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 24; - - // "Cancel" button - HifiControlsUit.Button { - id: cancelButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.left: parent.left; - anchors.leftMargin: 20; - width: parent.width/2 - anchors.leftMargin*2; - text: "Cancel" - onClicked: { - sendToScript({method: 'purchases_backClicked', referrerURL: referrerURL}); - } - } - - // "Set Up" button - HifiControlsUit.Button { - id: setUpButton; - color: hifi.buttons.blue; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: parent.width/2 - anchors.rightMargin*2; - text: "Set Up Wallet" - onClicked: { - sendToScript({method: 'checkout_setUpClicked'}); + switch (message.method) { + case 'tutorial_skipClicked': + case 'tutorial_finished': + sendToScript({method: 'purchases_setIsFirstUse'}); + root.activeView = "purchasesMain"; + commerce.inventory(); + break; } } } } - // - // "WALLET NOT SET UP" END - // // // PURCHASES CONTENTS START @@ -310,13 +257,10 @@ Rectangle { visible: root.activeView === "purchasesMain"; // Anchors anchors.left: parent.left; - anchors.leftMargin: 4; anchors.right: parent.right; - anchors.rightMargin: 4; anchors.top: titleBarContainer.bottom; - anchors.topMargin: 8; - anchors.bottom: actionButtonsContainer.top; - anchors.bottomMargin: 8; + anchors.topMargin: 8 - titleBarContainer.additionalDropdownHeight; + anchors.bottom: parent.bottom; // // FILTER BAR START @@ -329,32 +273,38 @@ Rectangle { anchors.left: parent.left; anchors.leftMargin: 8; anchors.right: parent.right; - anchors.rightMargin: 8; + anchors.rightMargin: 12; anchors.top: parent.top; anchors.topMargin: 4; + RalewayRegular { + id: myPurchasesText; + anchors.top: parent.top; + anchors.topMargin: 10; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 10; + anchors.left: parent.left; + anchors.leftMargin: 4; + width: paintedWidth; + text: "My Purchases"; + color: hifi.colors.baseGray; + size: 28; + } + HifiControlsUit.TextField { id: filterBar; - property int previousLength: 0; - anchors.fill: parent; - placeholderText: "Filter"; + colorScheme: hifi.colorSchemes.faintGray; + hasClearButton: true; + hasRoundedBorder: true; + anchors.left: myPurchasesText.right; + anchors.leftMargin: 16; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + placeholderText: "filter items"; onTextChanged: { - if (filterBar.text.length < previousLength) { - filteredPurchasesModel.clear(); - - for (var i = 0; i < purchasesModel.count; i++) { - filteredPurchasesModel.append(purchasesModel.get(i)); - } - } - - for (var i = 0; i < filteredPurchasesModel.count; i++) { - if (filteredPurchasesModel.get(i).title.toLowerCase().indexOf(filterBar.text.toLowerCase()) === -1) { - filteredPurchasesModel.remove(i); - i--; - } - } - previousLength = filterBar.text.length; + buildFilteredPurchasesModel(); } onAccepted: { @@ -366,29 +316,107 @@ Rectangle { // FILTER BAR END // + HifiControlsUit.Separator { + id: separator; + colorScheme: 1; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: filterBarContainer.bottom; + anchors.topMargin: 16; + } + ListModel { id: purchasesModel; } + ListModel { + id: previousPurchasesModel; + } ListModel { id: filteredPurchasesModel; } + Rectangle { + id: cantRezCertified; + visible: !root.canRezCertifiedItems; + color: "#FFC3CD"; + radius: 4; + border.color: hifi.colors.redAccent; + border.width: 1; + anchors.top: separator.bottom; + anchors.topMargin: 12; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 80; + + HiFiGlyphs { + id: lightningIcon; + text: hifi.glyphs.lightning; + // Size + size: 36; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 18; + anchors.left: parent.left; + anchors.leftMargin: 12; + horizontalAlignment: Text.AlignHCenter; + // Style + color: hifi.colors.lightGray; + } + + RalewayRegular { + text: "You don't have permission to rez certified items in this domain. " + + 'Learn More'; + // Text size + size: 18; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 4; + anchors.left: lightningIcon.right; + anchors.leftMargin: 8; + anchors.right: parent.right; + anchors.rightMargin: 8; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 4; + // Style + color: hifi.colors.baseGray; + wrapMode: Text.WordWrap; + // Alignment + verticalAlignment: Text.AlignVCenter; + + onLinkActivated: { + lightboxPopup.titleText = "Rez Permission Required"; + lightboxPopup.bodyText = "You don't have permission to rez certified items in this domain.

    " + + "Use the GOTO app to visit another domain or go to your own sandbox."; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.button2text = "OPEN GOTO"; + lightboxPopup.button2method = "sendToParent({method: 'purchases_openGoTo'});"; + lightboxPopup.visible = true; + } + } + } + ListView { id: purchasesContentsList; visible: purchasesModel.count !== 0; clip: true; model: filteredPurchasesModel; // Anchors - anchors.top: filterBarContainer.bottom; + anchors.top: root.canRezCertifiedItems ? separator.bottom : cantRezCertified.bottom; anchors.topMargin: 12; anchors.left: parent.left; anchors.bottom: parent.bottom; width: parent.width; delegate: PurchasedItem { + canRezCertifiedItems: root.canRezCertifiedItems; itemName: title; itemId: id; itemPreviewImageUrl: preview; itemHref: root_file_url; + purchaseStatus: status; + purchaseStatusChanged: statusChanged; anchors.topMargin: 12; anchors.bottomMargin: 12; @@ -396,6 +424,24 @@ Rectangle { onSendToPurchases: { if (msg.method === 'purchases_itemInfoClicked') { sendToScript({method: 'purchases_itemInfoClicked', itemId: itemId}); + } else if (msg.method === 'purchases_itemCertificateClicked') { + sendToScript(msg); + } else if (msg.method === "showInvalidatedLightbox") { + lightboxPopup.titleText = "Item Invalidated"; + lightboxPopup.bodyText = 'Your item is marked "invalidated" because this item has been suspended ' + + "from the Marketplace due to a claim against its author."; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.visible = true; + } else if (msg.method === "showPendingLightbox") { + lightboxPopup.titleText = "Item Pending"; + lightboxPopup.bodyText = 'Your item is marked "pending" while your purchase is being confirmed. ' + + "Usually, purchases take about 90 seconds to confirm."; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.visible = true; + } else if (msg.method === "setFilterText") { + filterBar.text = msg.filterText; } } } @@ -414,7 +460,7 @@ Rectangle { // Explanitory text RalewayRegular { id: haventPurchasedYet; - text: "You haven't purchased anything yet!

    Get an item from Marketplace to add it to your Purchases."; + text: "You haven't purchased anything yet!

    Get an item from Marketplace to add it to My Purchases."; // Text size size: 22; // Anchors @@ -426,7 +472,7 @@ Rectangle { anchors.rightMargin: 24; height: paintedHeight; // Style - color: hifi.colors.faintGray; + color: hifi.colors.baseGray; wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; @@ -452,42 +498,6 @@ Rectangle { // PURCHASES CONTENTS END // - // - // ACTION BUTTONS START - // - Item { - id: actionButtonsContainer; - visible: purchasesContentsContainer.visible; - // Size - width: parent.width; - height: 40; - // Anchors - anchors.left: parent.left; - anchors.bottom: keyboard.top; - anchors.bottomMargin: 8; - - // "Back" button - HifiControlsUit.Button { - id: backButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.left: parent.left; - anchors.leftMargin: 20; - width: parent.width/2 - anchors.leftMargin*2; - text: "Back" - onClicked: { - sendToScript({method: 'purchases_backClicked', referrerURL: referrerURL}); - } - } - } - // - // ACTION BUTTONS END - // - HifiControlsUit.Keyboard { id: keyboard; raised: HMD.mounted && filterBar.focus; @@ -499,9 +509,60 @@ Rectangle { } } + onVisibleChanged: { + if (!visible) { + inventoryTimer.stop(); + } + } + + Timer { + id: inventoryTimer; + interval: 90000; + onTriggered: { + if (root.activeView === "purchasesMain" && !root.pendingInventoryReply) { + root.pendingInventoryReply = true; + commerce.inventory(); + } + } + } + // // FUNCTION DEFINITIONS START // + + function buildFilteredPurchasesModel() { + filteredPurchasesModel.clear(); + for (var i = 0; i < purchasesModel.count; i++) { + if (purchasesModel.get(i).title.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) { + if (purchasesModel.get(i).status !== "confirmed") { + filteredPurchasesModel.insert(0, purchasesModel.get(i)); + } else { + filteredPurchasesModel.append(purchasesModel.get(i)); + } + } + } + } + + function checkIfAnyItemStatusChanged() { + var currentPurchasesModelId, currentPurchasesModelEdition, currentPurchasesModelStatus; + var previousPurchasesModelStatus; + for (var i = 0; i < purchasesModel.count; i++) { + currentPurchasesModelId = purchasesModel.get(i).id; + currentPurchasesModelEdition = purchasesModel.get(i).edition_number; + currentPurchasesModelStatus = purchasesModel.get(i).status; + + for (var j = 0; j < previousPurchasesModel.count; j++) { + previousPurchasesModelStatus = previousPurchasesModel.get(j).status; + if (currentPurchasesModelId === previousPurchasesModel.get(j).id && + currentPurchasesModelEdition === previousPurchasesModel.get(j).edition_number && + currentPurchasesModelStatus !== previousPurchasesModelStatus) { + + purchasesModel.setProperty(i, "statusChanged", true); + } + } + } + } + // // Function Name: fromScript() // @@ -519,6 +580,17 @@ Rectangle { switch (message.method) { case 'updatePurchases': referrerURL = message.referrerURL; + titleBarContainer.referrerURL = message.referrerURL; + root.canRezCertifiedItems = message.canRezCertifiedItems; + filterBar.text = message.filterText ? message.filterText : ""; + break; + case 'purchases_getIsFirstUseResult': + if (message.isFirstUseOfPurchases && root.activeView !== "firstUseTutorial") { + root.activeView = "firstUseTutorial"; + } else if (!message.isFirstUseOfPurchases && root.activeView === "initialize") { + root.activeView = "purchasesMain"; + commerce.inventory(); + } break; default: console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); diff --git a/interface/resources/qml/hifi/commerce/wallet/Help.qml b/interface/resources/qml/hifi/commerce/wallet/Help.qml index 19273298b6..21548ea788 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Help.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Help.qml @@ -1,8 +1,8 @@ // -// SendMoney.qml +// Help.qml // qml/hifi/commerce/wallet // -// SendMoney +// Help // // Created by Zach Fox on 2017-08-18 // Copyright 2017 High Fidelity, Inc. @@ -12,8 +12,8 @@ // import Hifi 1.0 as Hifi -import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick 2.7 +import QtQuick.Controls 2.2 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls @@ -24,35 +24,44 @@ Item { HifiConstants { id: hifi; } id: root; + property string keyFilePath; Hifi.QmlCommerce { id: commerce; + + onKeyFilePathIfExistsResult: { + keyFilePath = path; + } } - // "Unavailable" - RalewayRegular { - id: helpText; - text: "Help me!"; + Component.onCompleted: { + commerce.getKeyFilePathIfExists(); + } + + RalewaySemiBold { + id: helpTitleText; + text: "Help Topics"; // Anchors - anchors.fill: parent; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: paintedWidth; + height: 30; // Text size - size: 24; + size: 18; // Style - color: hifi.colors.faintGray; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + color: hifi.colors.blueHighlight; } HifiControlsUit.Button { + id: clearCachedPassphraseButton; color: hifi.buttons.black; colorScheme: hifi.colorSchemes.dark; - anchors.bottom: resetButton.top; - anchors.bottomMargin: 15; - anchors.horizontalCenter: parent.horizontalCenter; - height: 50; - width: 250; - text: "DEBUG: Clear Cached Passphrase"; + anchors.top: parent.top; + anchors.left: helpTitleText.right; + anchors.leftMargin: 20; + height: 40; + width: 150; + text: "DBG: Clear Pass"; onClicked: { commerce.setPassphrase(""); sendSignalToWallet({method: 'passphraseReset'}); @@ -62,18 +71,184 @@ Item { id: resetButton; color: hifi.buttons.red; colorScheme: hifi.colorSchemes.dark; - anchors.bottom: helpText.bottom; - anchors.bottomMargin: 15; - anchors.horizontalCenter: parent.horizontalCenter; - height: 50; - width: 250; - text: "DEBUG: Reset Wallet!"; + anchors.top: clearCachedPassphraseButton.top; + anchors.left: clearCachedPassphraseButton.right; + height: 40; + width: 150; + text: "DBG: RST Wallet"; onClicked: { commerce.reset(); sendSignalToWallet({method: 'walletReset'}); } } + ListModel { + id: helpModel; + + ListElement { + isExpanded: false; + question: "What are private keys?" + answer: qsTr("A private key is a secret piece of text that is used to decrypt code.

    In High Fidelity, your private keys are used to decrypt the contents of your Wallet and Purchases."); + } + ListElement { + isExpanded: false; + question: "Where are my private keys stored?" + answer: qsTr('Your private keys are only stored on your hard drive in High Fidelity Interface\'s AppData directory.

    Tap here to open the file path of your hifikey in your file explorer.

    You may backup this file by copying it to a USB flash drive, or to a service like Dropbox or Google Drive. Restore your backup by replacing the file in Interface\'s AppData directory with your backed-up copy.'); + } + ListElement { + isExpanded: false; + question: "What happens if I lose my passphrase?" + answer: qsTr("If you lose your passphrase, you will no longer have access to the contents of your Wallet or My Purchases.

    Nobody can help you recover your passphrase, including High Fidelity. Please write it down and store it securely."); + } + ListElement { + isExpanded: false; + question: "What is a 'Security Pic'?" + answer: qsTr("Your Security Pic is an encrypted image that you selected during Wallet Setup. It acts as an extra layer of Wallet security.

    When you see your Security Pic, you know that your actions and data are securely making use of your private keys.

    If you don't see your Security Pic on a page that is asking you for your Wallet passphrase, someone untrustworthy may be trying to gain access to your Wallet.

    The Pic is stored on your hard drive inside the same file as your private keys."); + } + ListElement { + isExpanded: false; + question: "My HFC balance isn't what I expect it to be. Why?" + answer: qsTr('High Fidelity Coin (HFC) transactions are backed by a blockchain, which takes time to update. The status of a transaction usually updates within 90 seconds.

    Tap here to learn more about the blockchain.'); + } + ListElement { + isExpanded: false; + question: "My friend purchased my item from the Marketplace, but I still haven't received the money from the sale. Why not?" + answer: qsTr('High Fidelity Coin (HFC) transactions are backed by a blockchain, which takes time to update. The status of a transaction usually updates within 90 seconds, at which point you will receive your money.

    Tap here to learn more about the blockchain.'); + } + ListElement { + isExpanded: false; + question: "Do I get charged money if a transaction fails?" + answer: qsTr("No. Your HFC balance only changes after a transaction is confirmed."); + } + ListElement { + isExpanded: false; + question: "How do I convert HFC to other currencies?" + answer: qsTr("We are still building the tools needed to support a vibrant economy in High Fidelity. There is currently no way to convert HFC to other currencies."); + } + } + + ListView { + id: helpListView; + ScrollBar.vertical: ScrollBar { + policy: helpListView.contentHeight > helpListView.height ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded; + parent: helpListView.parent; + anchors.top: helpListView.top; + anchors.right: helpListView.right; + anchors.bottom: helpListView.bottom; + width: 20; + } + anchors.top: helpTitleText.bottom; + anchors.topMargin: 30; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.right: parent.right + clip: true; + model: helpModel; + delegate: Item { + width: parent.width; + height: model.isExpanded ? questionContainer.height + answerContainer.height : questionContainer.height; + + HifiControlsUit.Separator { + colorScheme: 1; + visible: index === 0; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + } + + Item { + id: questionContainer; + anchors.top: parent.top; + anchors.left: parent.left; + width: parent.width; + height: questionText.paintedHeight + 50; + + RalewaySemiBold { + id: plusMinusButton; + text: model.isExpanded ? "-" : "+"; + // Anchors + anchors.top: parent.top; + anchors.topMargin: model.isExpanded ? -9 : 0; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + width: 60; + // Text size + size: 60; + // Style + color: hifi.colors.white; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + RalewaySemiBold { + id: questionText; + text: model.question; + size: 18; + anchors.verticalCenter: parent.verticalCenter; + anchors.left: plusMinusButton.right; + anchors.leftMargin: 4; + anchors.right: parent.right; + anchors.rightMargin: 10; + wrapMode: Text.WordWrap; + height: paintedHeight; + color: hifi.colors.white; + verticalAlignment: Text.AlignVCenter; + } + + MouseArea { + id: securityTabMouseArea; + anchors.fill: parent; + onClicked: { + model.isExpanded = !model.isExpanded; + if (model.isExpanded) { + collapseAllOtherHelpItems(index); + } + } + } + } + + Rectangle { + id: answerContainer; + visible: model.isExpanded; + color: Qt.rgba(0, 0, 0, 0.5); + anchors.top: questionContainer.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: answerText.paintedHeight + 50; + + RalewayRegular { + id: answerText; + text: model.answer; + size: 18; + anchors.verticalCenter: parent.verticalCenter; + anchors.left: parent.left; + anchors.leftMargin: 32; + anchors.right: parent.right; + anchors.rightMargin: 32; + wrapMode: Text.WordWrap; + height: paintedHeight; + color: hifi.colors.white; + + onLinkActivated: { + if (link === "#privateKeyPath") { + Qt.openUrlExternally("file:///" + root.keyFilePath.substring(0, root.keyFilePath.lastIndexOf('/'))); + } else if (link === "#blockchain") { + Qt.openUrlExternally("https://www.highfidelity.com/"); + } + } + } + } + + HifiControlsUit.Separator { + colorScheme: 1; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + } + } + } + + // // FUNCTION DEFINITIONS START // @@ -97,6 +272,14 @@ Item { } } signal sendSignalToWallet(var msg); + + function collapseAllOtherHelpItems(thisIndex) { + for (var i = 0; i < helpModel.count; i++) { + if (i !== thisIndex) { + helpModel.setProperty(i, "isExpanded", false); + } + } + } // // FUNCTION DEFINITIONS END // diff --git a/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml b/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml index 1e95aaa297..7ce0cf3853 100644 --- a/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml +++ b/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml @@ -24,6 +24,12 @@ Item { HifiConstants { id: hifi; } id: root; + + Image { + anchors.fill: parent; + source: "images/wallet-bg.jpg"; + } + Hifi.QmlCommerce { id: commerce; } diff --git a/interface/resources/qml/hifi/commerce/wallet/NotSetUp.qml b/interface/resources/qml/hifi/commerce/wallet/NotSetUp.qml deleted file mode 100644 index 42b8526a8a..0000000000 --- a/interface/resources/qml/hifi/commerce/wallet/NotSetUp.qml +++ /dev/null @@ -1,127 +0,0 @@ -// -// NotSetUp.qml -// qml/hifi/commerce/wallet -// -// NotSetUp -// -// Created by Zach Fox on 2017-08-18 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import Hifi 1.0 as Hifi -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit -import "../../../controls" as HifiControls - -// references XXX from root context - -Item { - HifiConstants { id: hifi; } - - id: root; - Hifi.QmlCommerce { - id: commerce; - } - - // - // TAB CONTENTS START - // - - // Text below title bar - RalewaySemiBold { - id: notSetUpText; - text: "Your Wallet Account Has Not Been Set Up"; - // Text size - size: 22; - // Anchors - anchors.top: parent.top; - anchors.topMargin: 100; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: 50; - width: paintedWidth; - // Style - color: hifi.colors.faintGray; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - } - - // Explanitory text - RalewayRegular { - text: "To buy and sell items in High Fidelity Coin (HFC), you first need " + - "to set up your wallet.
    You do not need to submit a credit card or personal information to set up your wallet."; - // Text size - size: 18; - // Anchors - anchors.top: notSetUpText.bottom; - anchors.topMargin: 16; - anchors.left: parent.left; - anchors.leftMargin: 30; - anchors.right: parent.right; - anchors.rightMargin: 30; - height: 100; - width: paintedWidth; - // Style - color: hifi.colors.faintGray; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - } - - // "Set Up" button - HifiControlsUit.Button { - color: hifi.buttons.blue; - colorScheme: hifi.colorSchemes.dark; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 150; - anchors.horizontalCenter: parent.horizontalCenter; - width: parent.width/2; - height: 50; - text: "Set Up My Wallet"; - onClicked: { - sendSignalToWallet({method: 'setUpClicked'}); - } - } - - - // - // TAB CONTENTS END - // - - // - // FUNCTION DEFINITIONS START - // - // - // Function Name: fromScript() - // - // Relevant Variables: - // None - // - // Arguments: - // message: The message sent from the JavaScript. - // Messages are in format "{method, params}", like json-rpc. - // - // Description: - // Called when a message is received from a script. - // - function fromScript(message) { - switch (message.method) { - default: - console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); - } - } - signal sendSignalToWallet(var msg); - // - // FUNCTION DEFINITIONS END - // -} diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelectionLightbox.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml similarity index 62% rename from interface/resources/qml/hifi/commerce/wallet/PassphraseSelectionLightbox.qml rename to interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml index 608cb229b0..e560384807 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelectionLightbox.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml @@ -1,8 +1,8 @@ // -// PassphraseSelectionLightbox.qml +// PassphraseChange.qml // qml/hifi/commerce/wallet // -// PassphraseSelectionLightbox +// PassphraseChange // // Created by Zach Fox on 2017-08-18 // Copyright 2017 High Fidelity, Inc. @@ -20,12 +20,31 @@ import "../../../controls" as HifiControls // references XXX from root context -Rectangle { +Item { HifiConstants { id: hifi; } id: root; - // Style - color: hifi.colors.baseGray; + + SecurityImageModel { + id: securityImageModel; + } + + // Username Text + RalewayRegular { + id: usernameText; + text: Account.username; + // Text size + size: 24; + // Style + color: hifi.colors.white; + elide: Text.ElideRight; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: parent.width/2; + height: 80; + } // // SECURE PASSPHRASE SELECTION START @@ -33,60 +52,31 @@ Rectangle { Item { id: choosePassphraseContainer; // Anchors - anchors.fill: parent; + anchors.top: usernameText.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; - Item { - id: passphraseTitle; - // Size - width: parent.width; - height: 50; - // Anchors - anchors.left: parent.left; - anchors.top: parent.top; - - // Title Bar text - RalewaySemiBold { - text: "CHANGE PASSPHRASE"; - // Text size - size: hifi.fontSizes.overlayTitle; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.bottom: parent.bottom; - width: paintedWidth; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - } - - // Text below title bar + // "Change Passphrase" text RalewaySemiBold { - id: passphraseTitleHelper; - text: "Choose a Secure Passphrase"; + id: passphraseTitle; + text: "Change Passphrase:"; // Text size - size: 24; - // Anchors - anchors.top: passphraseTitle.bottom; + size: 18; + anchors.top: parent.top; anchors.left: parent.left; - anchors.leftMargin: 16; + anchors.leftMargin: 20; anchors.right: parent.right; - anchors.rightMargin: 16; - height: 50; + height: 30; // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; + color: hifi.colors.blueHighlight; } PassphraseSelection { id: passphraseSelection; - anchors.top: passphraseTitleHelper.bottom; - anchors.topMargin: 30; + isChangingPassphrase: true; + anchors.top: passphraseTitle.bottom; + anchors.topMargin: 8; anchors.left: parent.left; anchors.right: parent.right; anchors.bottom: passphraseNavBar.top; @@ -96,11 +86,10 @@ Rectangle { if (msg.method === 'statusResult') { if (msg.status) { // Success submitting new passphrase - root.resetSubmitButton(); - root.visible = false; + sendSignalToWallet({method: "walletSecurity_changePassphraseSuccess"}); } else { // Error submitting new passphrase - root.resetSubmitButton(); + resetSubmitButton(); passphraseSelection.setErrorText("Backend error"); } } else { @@ -115,40 +104,37 @@ Rectangle { id: passphraseNavBar; // Size width: parent.width; - height: 100; + height: 40; // Anchors: anchors.left: parent.left; anchors.bottom: parent.bottom; + anchors.bottomMargin: 30; // "Cancel" button HifiControlsUit.Button { - color: hifi.buttons.black; + color: hifi.buttons.noneBorderlessWhite; colorScheme: hifi.colorSchemes.dark; anchors.top: parent.top; - anchors.topMargin: 3; anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; anchors.left: parent.left; anchors.leftMargin: 20; - width: 100; + width: 150; text: "Cancel" onClicked: { - root.visible = false; + sendSignalToWallet({method: "walletSecurity_changePassphraseCancelled"}); } } // "Submit" button HifiControlsUit.Button { id: passphraseSubmitButton; - color: hifi.buttons.black; + color: hifi.buttons.blue; colorScheme: hifi.colorSchemes.dark; anchors.top: parent.top; - anchors.topMargin: 3; anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; anchors.right: parent.right; anchors.rightMargin: 20; - width: 100; + width: 150; text: "Submit"; onClicked: { if (passphraseSelection.validateAndSubmitPassphrase()) { diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml index 4157e4082f..8ab0c3af60 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml @@ -17,6 +17,7 @@ import QtQuick.Controls 1.4 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls +import "../common" as HifiCommerceCommon // references XXX from root context @@ -26,11 +27,20 @@ Item { id: root; z: 998; property bool keyboardRaised: false; + property string titleBarIcon: ""; + property string titleBarText: ""; + + Image { + anchors.fill: parent; + source: "images/wallet-bg.jpg"; + } Hifi.QmlCommerce { id: commerce; onSecurityImageResult: { + titleBarSecurityImage.source = ""; + titleBarSecurityImage.source = "image://security/securityImage"; passphraseModalSecurityImage.source = ""; passphraseModalSecurityImage.source = "image://security/securityImage"; } @@ -40,7 +50,7 @@ Item { if (!isAuthenticated) { errorText.text = "Authentication failed - please try again."; } else { - root.visible = false; + sendSignalToParent({method: 'authSuccess'});; } } } @@ -66,24 +76,88 @@ Item { } } - // Background rectangle - Rectangle { + HifiCommerceCommon.CommerceLightbox { + id: lightboxPopup; + visible: false; anchors.fill: parent; - color: "black"; - opacity: 0.9; } - Rectangle { + Item { + id: titleBar; anchors.top: parent.top; anchors.left: parent.left; anchors.right: parent.right; + height: 50; + + // Wallet icon + HiFiGlyphs { + id: titleBarIcon; + text: root.titleBarIcon; + // Size + size: parent.height * 0.8; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 8; + anchors.verticalCenter: parent.verticalCenter; + // Style + color: hifi.colors.blueHighlight; + } + + RalewaySemiBold { + id: titleBarText; + text: root.titleBarText; + anchors.top: parent.top; + anchors.left: titleBarIcon.right; + anchors.leftMargin: 4; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + size: 20; + color: hifi.colors.white; + verticalAlignment: Text.AlignVCenter; + } + + Image { + id: titleBarSecurityImage; + source: ""; + visible: titleBarSecurityImage.source !== ""; + anchors.right: parent.right; + anchors.rightMargin: 6; + anchors.top: parent.top; + anchors.topMargin: 6; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 6; + width: height; + fillMode: Image.PreserveAspectFit; + mipmap: true; + + MouseArea { + enabled: titleBarSecurityImage.visible; + anchors.fill: parent; + onClicked: { + lightboxPopup.titleText = "Your Security Pic"; + lightboxPopup.bodyImageSource = titleBarSecurityImage.source; + lightboxPopup.bodyText = lightboxPopup.securityPicBodyText; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.visible = true; + } + } + } + } + + Item { + id: passphraseContainer; + anchors.top: titleBar.bottom; + anchors.left: parent.left; + anchors.leftMargin: 8; + anchors.right: parent.right; + anchors.rightMargin: 8; height: 250; - color: hifi.colors.baseGray; RalewaySemiBold { id: instructionsText; - text: "Enter Wallet Passphrase"; - size: 16; + text: "Please Enter Your Passphrase"; + size: 24; anchors.top: parent.top; anchors.topMargin: 30; anchors.left: parent.left; @@ -91,17 +165,34 @@ Item { width: passphraseField.width; height: paintedHeight; // Style - color: hifi.colors.faintGray; + color: hifi.colors.white; // Alignment horizontalAlignment: Text.AlignLeft; } + // Error text above buttons + RalewaySemiBold { + id: errorText; + text: ""; + // Text size + size: 15; + // Anchors + anchors.bottom: passphraseField.top; + anchors.bottomMargin: 4; + anchors.left: passphraseField.left; + anchors.right: parent.right; + height: 20; + // Style + color: hifi.colors.redHighlight; + } + HifiControlsUit.TextField { id: passphraseField; + colorScheme: hifi.colorSchemes.dark; anchors.top: instructionsText.bottom; - anchors.topMargin: 4; + anchors.topMargin: 40; anchors.left: instructionsText.left; - width: 280; + width: 260; height: 50; echoMode: TextInput.Password; placeholderText: "passphrase"; @@ -133,7 +224,7 @@ Item { anchors.top: passphraseField.bottom; anchors.topMargin: 8; height: 30; - text: "Show passphrase as plain text"; + text: "Show passphrase"; boxSize: 24; onClicked: { passphraseField.echoMode = checked ? TextInput.Normal : TextInput.Password; @@ -144,16 +235,18 @@ Item { Item { id: securityImageContainer; // Anchors - anchors.top: instructionsText.top; + anchors.top: passphraseField.top; anchors.left: passphraseField.right; - anchors.leftMargin: 12; + anchors.leftMargin: 8; anchors.right: parent.right; + anchors.rightMargin: 8; + height: 145; Image { id: passphraseModalSecurityImage; anchors.top: parent.top; - anchors.horizontalCenter: parent.horizontalCenter; - height: 75; - width: height; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: iconAndTextContainer.top; fillMode: Image.PreserveAspectFit; mipmap: true; source: "image://security/securityImage"; @@ -162,54 +255,43 @@ Item { commerce.getSecurityImage(); } } - Image { - id: passphraseModalSecurityImageOverlay; - source: "images/lockOverlay.png"; - width: passphraseModalSecurityImage.width * 0.45; - height: passphraseModalSecurityImage.height * 0.45; - anchors.bottom: passphraseModalSecurityImage.bottom; + Item { + id: iconAndTextContainer; + anchors.left: passphraseModalSecurityImage.left; anchors.right: passphraseModalSecurityImage.right; - mipmap: true; - opacity: 0.9; + anchors.bottom: parent.bottom; + height: 24; + // Lock icon + HiFiGlyphs { + id: lockIcon; + text: hifi.glyphs.lock; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: 30; + size: 20; + width: height; + verticalAlignment: Text.AlignBottom; + color: hifi.colors.white; + } + // "Security image" text below pic + RalewayRegular { + id: securityImageText; + text: "SECURITY PIC"; + // Text size + size: 12; + // Anchors + anchors.bottom: parent.bottom; + anchors.right: parent.right; + anchors.rightMargin: lockIcon.anchors.leftMargin; + width: paintedWidth; + height: 22; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignRight; + verticalAlignment: Text.AlignBottom; + } } - // "Security image" text below pic - RalewayRegular { - text: "security image"; - // Text size - size: 12; - // Anchors - anchors.top: passphraseModalSecurityImage.bottom; - anchors.topMargin: 4; - anchors.left: securityImageContainer.left; - anchors.right: securityImageContainer.right; - height: paintedHeight; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - } - } - - // Error text above buttons - RalewaySemiBold { - id: errorText; - text: ""; - // Text size - size: 16; - // Anchors - anchors.bottom: passphrasePopupActionButtonsContainer.top; - anchors.bottomMargin: 4; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: 30; - // Style - color: hifi.colors.redHighlight; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; } // @@ -217,29 +299,10 @@ Item { // Item { id: passphrasePopupActionButtonsContainer; - // Size - width: root.width; - height: 50; // Anchors anchors.left: parent.left; + anchors.right: parent.right; anchors.bottom: parent.bottom; - anchors.bottomMargin: 10; - - // "Cancel" button - HifiControlsUit.Button { - id: cancelPassphraseInputButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - height: 40; - anchors.left: parent.left; - anchors.leftMargin: 20; - width: parent.width/2 - anchors.leftMargin*2; - text: "Cancel" - onClicked: { - sendSignalToParent({method: 'passphrasePopup_cancelClicked'}); - } - } // "Submit" button HifiControlsUit.Button { @@ -247,16 +310,38 @@ Item { color: hifi.buttons.blue; colorScheme: hifi.colorSchemes.dark; anchors.top: parent.top; + anchors.topMargin: 20; height: 40; + anchors.left: parent.left; + anchors.leftMargin: 16; anchors.right: parent.right; - anchors.rightMargin: 20; - width: parent.width/2 - anchors.rightMargin*2; + anchors.rightMargin: 16; + width: parent.width/2 -4; text: "Submit" onClicked: { submitPassphraseInputButton.enabled = false; commerce.setPassphrase(passphraseField.text); } } + + // "Cancel" button + HifiControlsUit.Button { + id: cancelPassphraseInputButton; + color: hifi.buttons.noneBorderlessWhite; + colorScheme: hifi.colorSchemes.dark; + anchors.top: submitPassphraseInputButton.bottom; + anchors.topMargin: 20; + height: 40; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + width: parent.width/2 - 4; + text: "Cancel" + onClicked: { + sendSignalToParent({method: 'passphrasePopup_cancelClicked'}); + } + } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml index 7dca5a75b8..e7ff8489d1 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -24,6 +24,8 @@ Item { HifiConstants { id: hifi; } id: root; + property bool isChangingPassphrase: false; + property bool isShowingTip: false; // This object is always used in a popup. // This MouseArea is used to prevent a user from being @@ -51,23 +53,62 @@ Item { // TODO: Fix this unlikely bug onVisibleChanged: { if (visible) { - passphraseField.focus = true; + if (root.isChangingPassphrase) { + currentPassphraseField.focus = true; + } else { + passphraseField.focus = true; + } sendMessageToLightbox({method: 'disableHmdPreview'}); } else { sendMessageToLightbox({method: 'maybeEnableHmdPreview'}); } } + + + HifiControlsUit.TextField { + id: currentPassphraseField; + colorScheme: hifi.colorSchemes.dark; + visible: root.isChangingPassphrase; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: passphraseField.right; + height: 50; + echoMode: TextInput.Password; + placeholderText: "enter current passphrase"; + + onFocusChanged: { + if (focus) { + sendSignalToWallet({method: 'walletSetup_raiseKeyboard'}); + } else if (!passphraseFieldAgain.focus) { + sendSignalToWallet({method: 'walletSetup_lowerKeyboard'}); + } + } + + MouseArea { + anchors.fill: parent; + onClicked: { + parent.focus = true; + sendSignalToWallet({method: 'walletSetup_raiseKeyboard'}); + } + } + + onAccepted: { + passphraseField.focus = true; + } + } HifiControlsUit.TextField { id: passphraseField; - anchors.top: parent.top; - anchors.topMargin: 30; + colorScheme: hifi.colorSchemes.dark; + anchors.top: root.isChangingPassphrase ? currentPassphraseField.bottom : parent.top; + anchors.topMargin: root.isChangingPassphrase ? 40 : 0; anchors.left: parent.left; - anchors.leftMargin: 16; - width: 280; + anchors.leftMargin: 20; + width: 285; height: 50; echoMode: TextInput.Password; - placeholderText: "passphrase"; + placeholderText: root.isShowingTip ? "" : "enter new passphrase"; onFocusChanged: { if (focus) { @@ -91,13 +132,14 @@ Item { } HifiControlsUit.TextField { id: passphraseFieldAgain; + colorScheme: hifi.colorSchemes.dark; anchors.top: passphraseField.bottom; - anchors.topMargin: 10; + anchors.topMargin: root.isChangingPassphrase ? 20 : 40; anchors.left: passphraseField.left; anchors.right: passphraseField.right; height: 50; echoMode: TextInput.Password; - placeholderText: "re-enter passphrase"; + placeholderText: root.isShowingTip ? "" : "re-enter new passphrase"; onFocusChanged: { if (focus) { @@ -124,16 +166,16 @@ Item { Item { id: securityImageContainer; // Anchors - anchors.top: passphraseField.top; + anchors.top: root.isChangingPassphrase ? currentPassphraseField.top : passphraseField.top; anchors.left: passphraseField.right; - anchors.leftMargin: 12; anchors.right: parent.right; + anchors.bottom: root.isChangingPassphrase ? passphraseField.bottom : passphraseFieldAgain.bottom; Image { id: passphrasePageSecurityImage; anchors.top: parent.top; - anchors.horizontalCenter: parent.horizontalCenter; - height: 75; - width: height; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: iconAndTextContainer.top; fillMode: Image.PreserveAspectFit; mipmap: true; source: "image://security/securityImage"; @@ -142,123 +184,125 @@ Item { commerce.getSecurityImage(); } } - Image { - id: topSecurityImageOverlay; - source: "images/lockOverlay.png"; - width: passphrasePageSecurityImage.width * 0.45; - height: passphrasePageSecurityImage.height * 0.45; - anchors.bottom: passphrasePageSecurityImage.bottom; + Item { + id: iconAndTextContainer; + anchors.left: passphrasePageSecurityImage.left; anchors.right: passphrasePageSecurityImage.right; - mipmap: true; - opacity: 0.9; - } - // "Security image" text below pic - RalewayRegular { - text: "security image"; - // Text size - size: 12; - // Anchors - anchors.top: passphrasePageSecurityImage.bottom; - anchors.topMargin: 4; - anchors.left: securityImageContainer.left; - anchors.right: securityImageContainer.right; - height: paintedHeight; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + anchors.bottom: parent.bottom; + height: 22; + // Lock icon + HiFiGlyphs { + id: lockIcon; + text: hifi.glyphs.lock; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: 35; + size: 20; + width: height; + verticalAlignment: Text.AlignBottom; + color: hifi.colors.white; + } + // "Security image" text below pic + RalewayRegular { + id: securityImageText; + text: "SECURITY PIC"; + // Text size + size: 12; + // Anchors + anchors.bottom: parent.bottom; + anchors.right: parent.right; + anchors.rightMargin: lockIcon.anchors.leftMargin; + width: paintedWidth; + height: 22; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignRight; + verticalAlignment: Text.AlignBottom; + } } } - // Error text below TextFields + // Error text above TextFields RalewaySemiBold { id: errorText; text: ""; // Text size - size: 16; + size: 15; // Anchors - anchors.top: passphraseFieldAgain.bottom; - anchors.topMargin: 0; + anchors.bottom: passphraseField.top; + anchors.bottomMargin: 4; anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; + anchors.leftMargin: 20; + anchors.right: securityImageContainer.left; + anchors.rightMargin: 4; height: 30; // Style color: hifi.colors.redHighlight; // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - // Text below TextFields - RalewaySemiBold { - id: passwordReqs; - text: "Passphrase must be at least 3 characters"; - // Text size - size: 16; - // Anchors - anchors.top: passphraseFieldAgain.bottom; - anchors.topMargin: 16; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: 30; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHLeft; verticalAlignment: Text.AlignVCenter; } // Show passphrase text HifiControlsUit.CheckBox { id: showPassphrase; + visible: !root.isShowingTip; colorScheme: hifi.colorSchemes.dark; anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.top: passwordReqs.bottom; + anchors.leftMargin: 20; + anchors.top: passphraseFieldAgain.bottom; anchors.topMargin: 16; height: 30; - text: "Show passphrase as plain text"; + text: "Show passphrase"; boxSize: 24; onClicked: { passphraseField.echoMode = checked ? TextInput.Normal : TextInput.Password; passphraseFieldAgain.echoMode = checked ? TextInput.Normal : TextInput.Password; + if (root.isChangingPassphrase) { + currentPassphraseField.echoMode = checked ? TextInput.Normal : TextInput.Password; + } } } // Text below checkbox RalewayRegular { - text: "Your passphrase is used to encrypt your private keys. Please write it down. If it is lost, you will not be able to recover it."; + visible: !root.isShowingTip; + text: "Your passphrase is used to encrypt your private keys. Only you have it.

    Please write it down.

    If it is lost, you will not be able to recover it."; // Text size - size: 16; + size: 18; // Anchors anchors.top: showPassphrase.bottom; anchors.topMargin: 16; anchors.left: parent.left; - anchors.leftMargin: 16; + anchors.leftMargin: 24; anchors.right: parent.right; - anchors.rightMargin: 16; + anchors.rightMargin: 24; height: paintedHeight; // Style - color: hifi.colors.faintGray; + color: hifi.colors.white; wrapMode: Text.WordWrap; // Alignment - horizontalAlignment: Text.AlignLeft; + horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; } function validateAndSubmitPassphrase() { if (passphraseField.text.length < 3) { - setErrorText("Passphrase too short."); + setErrorText("Passphrase must be at least 3 characters."); + passphraseField.error = true; + passphraseFieldAgain.error = true; + currentPassphraseField.error = true; return false; } else if (passphraseField.text !== passphraseFieldAgain.text) { setErrorText("Passphrases don't match."); + passphraseField.error = true; + passphraseFieldAgain.error = true; + currentPassphraseField.error = true; return false; } else { + passphraseField.error = false; + passphraseFieldAgain.error = false; + currentPassphraseField.error = false; setErrorText(""); commerce.setPassphrase(passphraseField.text); return true; @@ -270,6 +314,7 @@ Item { } function clearPassphraseFields() { + currentPassphraseField.text = ""; passphraseField.text = ""; passphraseFieldAgain.text = ""; setErrorText(""); diff --git a/interface/resources/qml/hifi/commerce/wallet/Security.qml b/interface/resources/qml/hifi/commerce/wallet/Security.qml index b5d52d57e2..fcce798646 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Security.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Security.qml @@ -25,29 +25,16 @@ Item { HifiConstants { id: hifi; } id: root; + property string keyFilePath: ""; Hifi.QmlCommerce { id: commerce; - onSecurityImageResult: { - if (exists) { // "If security image is set up" - var path = "image://security/securityImage"; - topSecurityImage.source = ""; - topSecurityImage.source = path; - changeSecurityImageImage.source = ""; - changeSecurityImageImage.source = path; - } - } - onKeyFilePathIfExistsResult: { - keyFilePath.text = path; + keyFilePath = path; } } - SecurityImageModel { - id: securityImageModel; - } - // Username Text RalewayRegular { id: usernameText; @@ -55,253 +42,258 @@ Item { // Text size size: 24; // Style - color: hifi.colors.faintGray; + color: hifi.colors.white; elide: Text.ElideRight; // Anchors - anchors.top: securityImageContainer.top; - anchors.bottom: securityImageContainer.bottom; - anchors.left: parent.left; - anchors.right: securityImageContainer.left; - } - - // Security Image - Item { - id: securityImageContainer; - // Anchors anchors.top: parent.top; - anchors.right: parent.right; - width: 75; - height: childrenRect.height; - - onVisibleChanged: { - if (visible) { - commerce.getSecurityImage(); - } - } - - Image { - id: topSecurityImage; - // Anchors - anchors.top: parent.top; - anchors.horizontalCenter: parent.horizontalCenter; - height: parent.width - 10; - width: height; - fillMode: Image.PreserveAspectFit; - mipmap: true; - source: "image://security/securityImage"; - cache: false; - } - Image { - id: topSecurityImageMask; - source: "images/lockOverlay.png"; - width: topSecurityImage.width * 0.45; - height: topSecurityImage.height * 0.45; - anchors.bottom: topSecurityImage.bottom; - anchors.right: topSecurityImage.right; - mipmap: true; - opacity: 0.9; - } - // "Security image" text below pic - RalewayRegular { - text: "security image"; - // Text size - size: 12; - // Anchors - anchors.top: topSecurityImage.bottom; - anchors.topMargin: 4; - anchors.left: securityImageContainer.left; - anchors.right: securityImageContainer.right; - height: paintedHeight; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - } + anchors.left: parent.left; + anchors.leftMargin: 20; + width: parent.width/2; + height: 80; } Item { id: securityContainer; - anchors.top: securityImageContainer.bottom; + anchors.top: usernameText.bottom; anchors.topMargin: 20; anchors.left: parent.left; anchors.right: parent.right; - height: childrenRect.height; + anchors.bottom: parent.bottom; - RalewayRegular { + RalewaySemiBold { id: securityText; text: "Security"; // Anchors anchors.top: parent.top; anchors.left: parent.left; + anchors.leftMargin: 20; anchors.right: parent.right; height: 30; // Text size - size: 22; + size: 18; + // Style + color: hifi.colors.blueHighlight; + } + + Rectangle { + id: securityTextSeparator; + // Size + width: parent.width; + height: 1; + // Anchors + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: securityText.bottom; + anchors.topMargin: 8; // Style color: hifi.colors.faintGray; } Item { id: changePassphraseContainer; - anchors.top: securityText.bottom; - anchors.topMargin: 16; + anchors.top: securityTextSeparator.bottom; + anchors.topMargin: 8; anchors.left: parent.left; + anchors.leftMargin: 40; anchors.right: parent.right; + anchors.rightMargin: 55; height: 75; - Image { + HiFiGlyphs { id: changePassphraseImage; + text: hifi.glyphs.passphrase; + // Size + size: 80; // Anchors anchors.top: parent.top; + anchors.bottom: parent.bottom; anchors.left: parent.left; - height: parent.height; - width: height; - source: "images/lockOverlay.png"; - fillMode: Image.PreserveAspectFit; - mipmap: true; - cache: false; - visible: false; + // Style + color: hifi.colors.white; } - ColorOverlay { - anchors.fill: changePassphraseImage; - source: changePassphraseImage; - color: "white" + + RalewaySemiBold { + text: "Passphrase"; + // Anchors + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: changePassphraseImage.right; + anchors.leftMargin: 30; + width: 50; + // Text size + size: 18; + // Style + color: hifi.colors.white; } + // "Change Passphrase" button HifiControlsUit.Button { id: changePassphraseButton; - color: hifi.buttons.black; + color: hifi.buttons.blue; colorScheme: hifi.colorSchemes.dark; + anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; - anchors.left: changePassphraseImage.right; - anchors.leftMargin: 16; - width: 250; - height: 50; - text: "Change My Passphrase"; + width: 140; + height: 40; + text: "Change"; onClicked: { sendSignalToWallet({method: 'walletSecurity_changePassphrase'}); } } } - Item { - id: changeSecurityImageContainer; - anchors.top: changePassphraseContainer.bottom; - anchors.topMargin: 8; + Rectangle { + id: changePassphraseSeparator; + // Size + width: parent.width; + height: 1; + // Anchors anchors.left: parent.left; anchors.right: parent.right; + anchors.top: changePassphraseContainer.bottom; + anchors.topMargin: 8; + // Style + color: hifi.colors.faintGray; + } + + Item { + id: changeSecurityImageContainer; + anchors.top: changePassphraseSeparator.bottom; + anchors.topMargin: 8; + anchors.left: parent.left; + anchors.leftMargin: 40; + anchors.right: parent.right; + anchors.rightMargin: 55; height: 75; - Image { + HiFiGlyphs { id: changeSecurityImageImage; + text: hifi.glyphs.securityImage; + // Size + size: 80; // Anchors anchors.top: parent.top; + anchors.bottom: parent.bottom; anchors.left: parent.left; - height: parent.height; - width: height; - fillMode: Image.PreserveAspectFit; - mipmap: true; - cache: false; - source: "image://security/securityImage"; + // Style + color: hifi.colors.white; } - // "Change Security Image" button + + RalewaySemiBold { + text: "Security Pic"; + // Anchors + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: changeSecurityImageImage.right; + anchors.leftMargin: 30; + width: 50; + // Text size + size: 18; + // Style + color: hifi.colors.white; + } + + // "Change Passphrase" button HifiControlsUit.Button { id: changeSecurityImageButton; - color: hifi.buttons.black; + color: hifi.buttons.blue; colorScheme: hifi.colorSchemes.dark; + anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; - anchors.left: changeSecurityImageImage.right; - anchors.leftMargin: 16; - width: 250; - height: 50; - text: "Change My Security Image"; + width: 140; + height: 40; + text: "Change"; onClicked: { sendSignalToWallet({method: 'walletSecurity_changeSecurityImage'}); } } } - } - Item { - id: yourPrivateKeysContainer; - anchors.top: securityContainer.bottom; - anchors.topMargin: 20; - anchors.left: parent.left; - anchors.right: parent.right; - height: childrenRect.height; - - RalewaySemiBold { - id: yourPrivateKeysText; - text: "Your Private Keys"; + Rectangle { + id: privateKeysSeparator; + // Size + width: parent.width; + height: 1; // Anchors - anchors.top: parent.top; anchors.left: parent.left; anchors.right: parent.right; - height: 30; - // Text size - size: 22; + anchors.top: changeSecurityImageContainer.bottom; + anchors.topMargin: 8; // Style color: hifi.colors.faintGray; } - // Text below "your private keys" - RalewayRegular { - id: explanitoryText; - text: "Your money and purchases are secured with private keys that only you " + - "have access to. If they are lost, you will not be able to access your money or purchases. " + - "To safeguard your private keys, back up this file regularly:"; - // Text size - size: 18; - // Anchors - anchors.top: yourPrivateKeysText.bottom; - anchors.topMargin: 10; + Item { + id: yourPrivateKeysContainer; + anchors.top: privateKeysSeparator.bottom; anchors.left: parent.left; + anchors.leftMargin: 40; anchors.right: parent.right; - height: paintedHeight; - // Style - color: hifi.colors.faintGray; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - } - HifiControlsUit.TextField { - id: keyFilePath; - anchors.top: explanitoryText.bottom; - anchors.topMargin: 10; - anchors.left: parent.left; - anchors.right: clipboardButton.left; - height: 40; - readOnly: true; + anchors.rightMargin: 55; + anchors.bottom: parent.bottom; - onVisibleChanged: { - if (visible) { - commerce.getKeyFilePathIfExists(); - } - } - } - HifiControlsUit.Button { - id: clipboardButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.right: parent.right; - anchors.top: keyFilePath.top; - anchors.bottom: keyFilePath.bottom; - width: height; HiFiGlyphs { - text: hifi.glyphs.question; + id: yourPrivateKeysImage; + text: hifi.glyphs.walletKey; // Size - size: parent.height*1.3; + size: 80; // Anchors - anchors.fill: parent; + anchors.top: parent.top; + anchors.topMargin: 20; + anchors.left: parent.left; // Style - horizontalAlignment: Text.AlignHCenter; - color: enabled ? hifi.colors.white : hifi.colors.faintGray; + color: hifi.colors.white; } - onClicked: { - Window.copyToClipboard(keyFilePath.text); + RalewaySemiBold { + id: yourPrivateKeysText; + text: "Private Keys"; + size: 18; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 32; + anchors.left: yourPrivateKeysImage.right; + anchors.leftMargin: 30; + anchors.right: parent.right; + height: 30; + // Style + color: hifi.colors.white; + } + + // Text below "private keys" + RalewayRegular { + id: explanitoryText; + text: "Your money and purchases are secured with private keys that only you have access to."; + // Text size + size: 18; + // Anchors + anchors.top: yourPrivateKeysText.bottom; + anchors.topMargin: 10; + anchors.left: yourPrivateKeysText.left; + anchors.right: yourPrivateKeysText.right; + height: paintedHeight; + // Style + color: hifi.colors.white; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + } + + HifiControlsUit.Button { + id: backupInstructionsButton; + text: "View Backup Instructions"; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.left: explanitoryText.left; + anchors.right: explanitoryText.right; + anchors.top: explanitoryText.bottom; + anchors.topMargin: 16; + height: 40; + + onClicked: { + Qt.openUrlExternally("https://www.highfidelity.com/"); + } } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelectionLightbox.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml similarity index 57% rename from interface/resources/qml/hifi/commerce/wallet/SecurityImageSelectionLightbox.qml rename to interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml index ff7156dd6c..7f767060f6 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelectionLightbox.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml @@ -1,8 +1,8 @@ // -// SecurityImageSelectionLightbox.qml +// SecurityImageChange.qml // qml/hifi/commerce/wallet // -// SecurityImageSelectionLightbox +// SecurityImageChange // // Created by Zach Fox on 2017-08-18 // Copyright 2017 High Fidelity, Inc. @@ -20,22 +20,26 @@ import "../../../controls" as HifiControls // references XXX from root context -Rectangle { +Item { HifiConstants { id: hifi; } id: root; property bool justSubmitted: false; - // Style - color: hifi.colors.baseGray; + + SecurityImageModel { + id: securityImageModel; + } Hifi.QmlCommerce { id: commerce; - + onSecurityImageResult: { + securityImageChangePageSecurityImage.source = ""; + securityImageChangePageSecurityImage.source = "image://security/securityImage"; if (exists) { // Success submitting new security image if (root.justSubmitted) { root.resetSubmitButton(); - root.visible = false; + sendSignalToWallet({method: "walletSecurity_changeSecurityImageSuccess"}); root.justSubmitted = false; } } else if (root.justSubmitted) { @@ -45,71 +49,104 @@ Rectangle { } } } + + + // Security Image + Item { + // Anchors + anchors.top: parent.top; + anchors.right: parent.right; + anchors.rightMargin: 25; + width: 130; + height: 150; + Image { + id: securityImageChangePageSecurityImage; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: iconAndTextContainer.top; + fillMode: Image.PreserveAspectFit; + mipmap: true; + source: "image://security/securityImage"; + cache: false; + onVisibleChanged: { + commerce.getSecurityImage(); + } + } + Item { + id: iconAndTextContainer; + anchors.left: securityImageChangePageSecurityImage.left; + anchors.right: securityImageChangePageSecurityImage.right; + anchors.bottom: parent.bottom; + height: 22; + // Lock icon + HiFiGlyphs { + id: lockIcon; + text: hifi.glyphs.lock; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: 10; + size: 20; + width: height; + verticalAlignment: Text.AlignBottom; + color: hifi.colors.white; + } + // "Security image" text below pic + RalewayRegular { + id: securityImageText; + text: "SECURITY PIC"; + // Text size + size: 12; + // Anchors + anchors.bottom: parent.bottom; + anchors.right: parent.right; + anchors.rightMargin: lockIcon.anchors.leftMargin; + width: paintedWidth; + height: 22; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignRight; + verticalAlignment: Text.AlignBottom; + } + } + } // // SECURITY IMAGE SELECTION START // Item { - id: securityImageContainer; // Anchors - anchors.fill: parent; + anchors.top: parent.top; + anchors.topMargin: 135; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; - Item { - id: securityImageTitle; - // Size - width: parent.width; - height: 50; - // Anchors - anchors.left: parent.left; - anchors.top: parent.top; - - // Title Bar text - RalewaySemiBold { - text: "CHANGE SECURITY IMAGE"; - // Text size - size: hifi.fontSizes.overlayTitle; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.bottom: parent.bottom; - width: paintedWidth; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - } - - // Text below title bar + // "Change Security Image" text RalewaySemiBold { - id: securityImageTitleHelper; - text: "Choose a Security Picture:"; + id: securityImageTitle; + text: "Change Security Pic:"; // Text size - size: 24; - // Anchors - anchors.top: securityImageTitle.bottom; + size: 18; + anchors.top: parent.top; anchors.left: parent.left; - anchors.leftMargin: 16; - height: 50; - width: paintedWidth; + anchors.leftMargin: 20; + anchors.right: parent.right; + height: 30; // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; + color: hifi.colors.blueHighlight; } SecurityImageSelection { id: securityImageSelection; // Anchors - anchors.top: securityImageTitleHelper.bottom; + anchors.top: securityImageTitle.bottom; anchors.left: parent.left; anchors.leftMargin: 16; anchors.right: parent.right; anchors.rightMargin: 16; - height: 280; + height: 300; Connections { onSendSignalToWallet: { @@ -118,66 +155,43 @@ Rectangle { } } - // Text below security images - RalewayRegular { - text: "Your security picture shows you that the service asking for your passphrase is authorized. You can change your secure picture at any time."; - // Text size - size: 18; - // Anchors - anchors.top: securityImageSelection.bottom; - anchors.topMargin: 40; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: paintedHeight; - // Style - color: hifi.colors.faintGray; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - // Navigation Bar Item { id: securityImageNavBar; // Size width: parent.width; - height: 100; + height: 40; // Anchors: anchors.left: parent.left; anchors.bottom: parent.bottom; + anchors.bottomMargin: 30; // "Cancel" button HifiControlsUit.Button { - color: hifi.buttons.black; + color: hifi.buttons.noneBorderlessWhite; colorScheme: hifi.colorSchemes.dark; anchors.top: parent.top; - anchors.topMargin: 3; anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; anchors.left: parent.left; anchors.leftMargin: 20; - width: 100; + width: 150; text: "Cancel" onClicked: { - root.visible = false; + sendSignalToWallet({method: "walletSecurity_changeSecurityImageCancelled"}); } } // "Submit" button HifiControlsUit.Button { id: securityImageSubmitButton; - color: hifi.buttons.black; + enabled: securityImageSelection.currentIndex !== -1; + color: hifi.buttons.blue; colorScheme: hifi.colorSchemes.dark; anchors.top: parent.top; - anchors.topMargin: 3; anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; anchors.right: parent.right; anchors.rightMargin: 20; - width: 100; + width: 150; text: "Submit"; onClicked: { root.justSubmitted = true; diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml index 5f5a3e8247..706684ed39 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml @@ -24,6 +24,7 @@ Item { HifiConstants { id: hifi; } id: root; + property int currentIndex: securityImageGrid.currentIndex; // This will cause a bug -- if you bring up security image selection in HUD mode while // in HMD while having HMD preview enabled, then move, then finish passphrase selection, @@ -43,6 +44,7 @@ Item { GridView { id: securityImageGrid; + interactive: false; clip: true; // Anchors anchors.fill: parent; @@ -56,8 +58,8 @@ Item { Item { anchors.fill: parent; Image { - width: parent.width - 8; - height: parent.height - 8; + width: parent.width - 12; + height: parent.height - 12; source: sourcePath; anchors.horizontalCenter: parent.horizontalCenter; anchors.verticalCenter: parent.verticalCenter; diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 87cde5f43d..4cc1f2f8ec 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -13,10 +13,12 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 +import QtGraphicalEffects 1.0 import QtQuick.Controls 1.4 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls +import "../common" as HifiCommerceCommon // references XXX from root context @@ -28,18 +30,14 @@ Rectangle { property string activeView: "initialize"; property bool keyboardRaised: false; - // Style - color: hifi.colors.baseGray; + Image { + anchors.fill: parent; + source: "images/wallet-bg.jpg"; + } + Hifi.QmlCommerce { id: commerce; - onAccountResult: { - if (result.status === "success") { - commerce.getKeyFilePathIfExists(); - } else { - // unsure how to handle a failure here. We definitely cannot proceed. - } - } onLoginStatusResult: { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; @@ -49,25 +47,35 @@ Rectangle { } } + onAccountResult: { + if (result.status === "success") { + commerce.getKeyFilePathIfExists(); + } else { + // unsure how to handle a failure here. We definitely cannot proceed. + } + } + onKeyFilePathIfExistsResult: { - if (path === "" && root.activeView !== "notSetUp") { - root.activeView = "notSetUp"; + if (path === "" && root.activeView !== "walletSetup") { + root.activeView = "walletSetup"; } else if (path !== "" && root.activeView === "initialize") { commerce.getSecurityImage(); } } onSecurityImageResult: { - if (!exists && root.activeView !== "notSetUp") { // "If security image is not set up" - root.activeView = "notSetUp"; + if (!exists && root.activeView !== "walletSetup") { // "If security image is not set up" + root.activeView = "walletSetup"; } else if (exists && root.activeView === "initialize") { commerce.getWalletAuthenticatedStatus(); + titleBarSecurityImage.source = ""; + titleBarSecurityImage.source = "image://security/securityImage"; } } onWalletAuthenticatedStatusResult: { - if (!isAuthenticated && passphraseModal && !passphraseModal.visible) { - passphraseModal.visible = true; + if (!isAuthenticated && passphraseModal && root.activeView !== "passphraseModal") { + root.activeView = "passphraseModal"; } else if (isAuthenticated) { root.activeView = "walletHome"; } @@ -78,74 +86,11 @@ Rectangle { id: securityImageModel; } - Rectangle { - id: walletSetupLightboxContainer; - visible: walletSetupLightbox.visible || passphraseSelectionLightbox.visible || securityImageSelectionLightbox.visible; - z: 998; + HifiCommerceCommon.CommerceLightbox { + id: lightboxPopup; + visible: false; anchors.fill: parent; - color: "black"; - opacity: 0.5; } - WalletSetupLightbox { - id: walletSetupLightbox; - visible: false; - z: 998; - anchors.centerIn: walletSetupLightboxContainer; - width: walletSetupLightboxContainer.width - 50; - height: walletSetupLightboxContainer.height - 50; - - Connections { - onSendSignalToWallet: { - if (msg.method === 'walletSetup_cancelClicked') { - walletSetupLightbox.visible = false; - } else if (msg.method === 'walletSetup_finished') { - root.activeView = "initialize"; - commerce.getLoginStatus(); - } else if (msg.method === 'walletSetup_raiseKeyboard') { - root.keyboardRaised = true; - } else if (msg.method === 'walletSetup_lowerKeyboard') { - root.keyboardRaised = false; - } else { - sendToScript(msg); - } - } - } - } - PassphraseSelectionLightbox { - id: passphraseSelectionLightbox; - visible: false; - z: 998; - anchors.centerIn: walletSetupLightboxContainer; - width: walletSetupLightboxContainer.width - 50; - height: walletSetupLightboxContainer.height - 50; - - Connections { - onSendSignalToWallet: { - if (msg.method === 'walletSetup_raiseKeyboard') { - root.keyboardRaised = true; - } else if (msg.method === 'walletSetup_lowerKeyboard') { - root.keyboardRaised = false; - } else { - sendToScript(msg); - } - } - } - } - SecurityImageSelectionLightbox { - id: securityImageSelectionLightbox; - visible: false; - z: 998; - anchors.centerIn: walletSetupLightboxContainer; - width: walletSetupLightboxContainer.width - 50; - height: walletSetupLightboxContainer.height - 50; - - Connections { - onSendSignalToWallet: { - sendToScript(msg); - } - } - } - // // TITLE BAR START @@ -160,6 +105,20 @@ Rectangle { anchors.left: parent.left; anchors.top: parent.top; + // Wallet icon + HiFiGlyphs { + id: walletIcon; + text: hifi.glyphs.wallet; + // Size + size: parent.height * 0.8; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 8; + anchors.verticalCenter: parent.verticalCenter; + // Style + color: hifi.colors.blueHighlight; + } + // Title Bar text RalewaySemiBold { id: titleBarText; @@ -168,28 +127,119 @@ Rectangle { size: hifi.fontSizes.overlayTitle; // Anchors anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 16; + anchors.left: walletIcon.right; + anchors.leftMargin: 4; anchors.bottom: parent.bottom; width: paintedWidth; // Style - color: hifi.colors.faintGray; + color: hifi.colors.white; // Alignment - horizontalAlignment: Text.AlignHLeft; verticalAlignment: Text.AlignVCenter; } - // Separator - HifiControlsUit.Separator { - anchors.left: parent.left; + Image { + id: titleBarSecurityImage; + source: ""; + visible: titleBarSecurityImage.source !== "" && !securityImageChange.visible; anchors.right: parent.right; + anchors.rightMargin: 6; + anchors.top: parent.top; + anchors.topMargin: 6; anchors.bottom: parent.bottom; + anchors.bottomMargin: 6; + width: height; + mipmap: true; + + MouseArea { + enabled: titleBarSecurityImage.visible; + anchors.fill: parent; + onClicked: { + lightboxPopup.titleText = "Your Security Pic"; + lightboxPopup.bodyImageSource = titleBarSecurityImage.source; + lightboxPopup.bodyText = lightboxPopup.securityPicBodyText; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.visible = true; + } + } } } // // TITLE BAR END // + WalletSetup { + id: walletSetup; + visible: root.activeView === "walletSetup"; + z: 998; + anchors.fill: parent; + + Connections { + onSendSignalToWallet: { + if (msg.method === 'walletSetup_finished') { + if (msg.referrer === '') { + root.activeView = "initialize"; + commerce.getLoginStatus(); + } else if (msg.referrer === 'purchases') { + sendToScript({method: 'goToPurchases'}); + } + } else if (msg.method === 'walletSetup_raiseKeyboard') { + root.keyboardRaised = true; + } else if (msg.method === 'walletSetup_lowerKeyboard') { + root.keyboardRaised = false; + } else { + sendToScript(msg); + } + } + } + } + PassphraseChange { + id: passphraseChange; + visible: root.activeView === "passphraseChange"; + z: 998; + anchors.top: titleBarContainer.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + + Connections { + onSendSignalToWallet: { + if (msg.method === 'walletSetup_raiseKeyboard') { + root.keyboardRaised = true; + } else if (msg.method === 'walletSetup_lowerKeyboard') { + root.keyboardRaised = false; + } else if (msg.method === 'walletSecurity_changePassphraseCancelled') { + root.activeView = "security"; + } else if (msg.method === 'walletSecurity_changePassphraseSuccess') { + root.activeView = "security"; + } else { + sendToScript(msg); + } + } + } + } + SecurityImageChange { + id: securityImageChange; + visible: root.activeView === "securityImageChange"; + z: 998; + anchors.top: titleBarContainer.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + + Connections { + onSendSignalToWallet: { + if (msg.method === 'walletSecurity_changeSecurityImageCancelled') { + root.activeView = "security"; + } else if (msg.method === 'walletSecurity_changeSecurityImageSuccess') { + root.activeView = "security"; + } else { + sendToScript(msg); + } + } + } + } + // // TAB CONTENTS START // @@ -231,31 +281,17 @@ Rectangle { PassphraseModal { id: passphraseModal; - visible: false; - anchors.top: titleBarContainer.bottom; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.right: parent.right; + visible: root.activeView === "passphraseModal"; + anchors.fill: parent; + titleBarText: "Wallet"; + titleBarIcon: hifi.glyphs.wallet; Connections { onSendSignalToParent: { - sendToScript(msg); - } - } - } - - NotSetUp { - id: notSetUp; - visible: root.activeView === "notSetUp"; - anchors.top: titleBarContainer.bottom; - anchors.bottom: tabButtonsContainer.top; - anchors.left: parent.left; - anchors.right: parent.right; - - Connections { - onSendSignalToWallet: { - if (msg.method === 'setUpClicked') { - walletSetupLightbox.visible = true; + if (msg.method === "authSuccess") { + root.activeView = "walletHome"; + } else { + sendToScript(msg); } } } @@ -265,47 +301,42 @@ Rectangle { id: walletHome; visible: root.activeView === "walletHome"; anchors.top: titleBarContainer.bottom; - anchors.topMargin: 16; anchors.bottom: tabButtonsContainer.top; - anchors.bottomMargin: 16; anchors.left: parent.left; - anchors.leftMargin: 16; anchors.right: parent.right; - anchors.rightMargin: 16; + + Connections { + onSendSignalToWallet: { + sendToScript(msg); + } + } } SendMoney { id: sendMoney; visible: root.activeView === "sendMoney"; anchors.top: titleBarContainer.bottom; - anchors.topMargin: 16; anchors.bottom: tabButtonsContainer.top; - anchors.bottomMargin: 16; anchors.left: parent.left; - anchors.leftMargin: 16; anchors.right: parent.right; - anchors.rightMargin: 16; } Security { id: security; visible: root.activeView === "security"; anchors.top: titleBarContainer.bottom; - anchors.topMargin: 16; anchors.bottom: tabButtonsContainer.top; - anchors.bottomMargin: 16; anchors.left: parent.left; - anchors.leftMargin: 16; anchors.right: parent.right; - anchors.rightMargin: 16; Connections { onSendSignalToWallet: { if (msg.method === 'walletSecurity_changePassphrase') { - passphraseSelectionLightbox.visible = true; - passphraseSelectionLightbox.clearPassphraseFields(); + root.activeView = "passphraseChange"; + passphraseChange.clearPassphraseFields(); + passphraseChange.resetSubmitButton(); } else if (msg.method === 'walletSecurity_changeSecurityImage') { - securityImageSelectionLightbox.visible = true; + root.activeView = "securityImageChange"; } } } @@ -315,13 +346,9 @@ Rectangle { id: help; visible: root.activeView === "help"; anchors.top: titleBarContainer.bottom; - anchors.topMargin: 16; anchors.bottom: tabButtonsContainer.top; - anchors.bottomMargin: 16; anchors.left: parent.left; - anchors.leftMargin: 16; anchors.right: parent.right; - anchors.rightMargin: 16; Connections { onSendSignalToWallet: { @@ -342,11 +369,11 @@ Rectangle { // Item { id: tabButtonsContainer; - visible: !needsLogIn.visible; + visible: !needsLogIn.visible && root.activeView !== "passphraseChange" && root.activeView !== "securityImageChange"; property int numTabs: 5; // Size width: root.width; - height: 80; + height: 90; // Anchors anchors.left: parent.left; anchors.bottom: parent.bottom; @@ -361,30 +388,46 @@ Rectangle { // "WALLET HOME" tab button Rectangle { id: walletHomeButtonContainer; - visible: !notSetUp.visible; + visible: !walletSetup.visible; color: root.activeView === "walletHome" ? hifi.colors.blueAccent : hifi.colors.black; anchors.top: parent.top; anchors.left: parent.left; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; + + HiFiGlyphs { + id: homeTabIcon; + text: hifi.glyphs.home2; + // Size + size: 50; + // Anchors + anchors.horizontalCenter: parent.horizontalCenter; + anchors.top: parent.top; + anchors.topMargin: -2; + // Style + color: root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + } RalewaySemiBold { text: "WALLET HOME"; // Text size - size: hifi.fontSizes.overlayTitle; + size: 16; // Anchors - anchors.fill: parent; + anchors.bottom: parent.bottom; + height: parent.height/2; + anchors.left: parent.left; anchors.leftMargin: 4; + anchors.right: parent.right; anchors.rightMargin: 4; // Style - color: hifi.colors.faintGray; + color: root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + verticalAlignment: Text.AlignTop; } MouseArea { - enabled: !walletSetupLightboxContainer.visible; + id: walletHomeTabMouseArea; anchors.fill: parent; hoverEnabled: enabled; onClicked: { @@ -396,87 +439,136 @@ Rectangle { } } - // "SEND MONEY" tab button + // "EXCHANGE MONEY" tab button Rectangle { - id: sendMoneyButtonContainer; - visible: !notSetUp.visible; + id: exchangeMoneyButtonContainer; + visible: !walletSetup.visible; color: hifi.colors.black; anchors.top: parent.top; anchors.left: walletHomeButtonContainer.right; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; - - RalewaySemiBold { - text: "SEND MONEY"; - // Text size - size: 14; + + HiFiGlyphs { + id: exchangeMoneyTabIcon; + text: hifi.glyphs.leftRightArrows; + // Size + size: 50; // Anchors - anchors.fill: parent; - anchors.leftMargin: 4; - anchors.rightMargin: 4; + anchors.horizontalCenter: parent.horizontalCenter; + anchors.top: parent.top; + anchors.topMargin: -2; // Style color: hifi.colors.lightGray50; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; } - } - - // "EXCHANGE MONEY" tab button - Rectangle { - id: exchangeMoneyButtonContainer; - visible: !notSetUp.visible; - color: hifi.colors.black; - anchors.top: parent.top; - anchors.left: sendMoneyButtonContainer.right; - anchors.bottom: parent.bottom; - width: parent.width / tabButtonsContainer.numTabs; RalewaySemiBold { text: "EXCHANGE MONEY"; // Text size - size: 14; + size: 16; // Anchors - anchors.fill: parent; + anchors.bottom: parent.bottom; + height: parent.height/2; + anchors.left: parent.left; anchors.leftMargin: 4; + anchors.right: parent.right; anchors.rightMargin: 4; // Style color: hifi.colors.lightGray50; wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + verticalAlignment: Text.AlignTop; + } + } + + + // "SEND MONEY" tab button + Rectangle { + id: sendMoneyButtonContainer; + visible: !walletSetup.visible; + color: hifi.colors.black; + anchors.top: parent.top; + anchors.left: exchangeMoneyButtonContainer.right; + anchors.bottom: parent.bottom; + width: parent.width / tabButtonsContainer.numTabs; + + HiFiGlyphs { + id: sendMoneyTabIcon; + text: hifi.glyphs.paperPlane; + // Size + size: 46; + // Anchors + anchors.horizontalCenter: parent.horizontalCenter; + anchors.top: parent.top; + anchors.topMargin: -2; + // Style + color: hifi.colors.lightGray50; + } + + RalewaySemiBold { + text: "SEND MONEY"; + // Text size + size: 16; + // Anchors + anchors.bottom: parent.bottom; + height: parent.height/2; + anchors.left: parent.left; + anchors.leftMargin: 4; + anchors.right: parent.right; + anchors.rightMargin: 4; + // Style + color: hifi.colors.lightGray50; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignTop; } } // "SECURITY" tab button Rectangle { id: securityButtonContainer; - visible: !notSetUp.visible; + visible: !walletSetup.visible; color: root.activeView === "security" ? hifi.colors.blueAccent : hifi.colors.black; anchors.top: parent.top; - anchors.left: exchangeMoneyButtonContainer.right; + anchors.left: sendMoneyButtonContainer.right; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; + + HiFiGlyphs { + id: securityTabIcon; + text: hifi.glyphs.lock; + // Size + size: 38; + // Anchors + anchors.horizontalCenter: parent.horizontalCenter; + anchors.top: parent.top; + anchors.topMargin: 2; + // Style + color: root.activeView === "security" || securityTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + } RalewaySemiBold { text: "SECURITY"; // Text size - size: hifi.fontSizes.overlayTitle; + size: 16; // Anchors - anchors.fill: parent; + anchors.bottom: parent.bottom; + height: parent.height/2; + anchors.left: parent.left; anchors.leftMargin: 4; + anchors.right: parent.right; anchors.rightMargin: 4; // Style - color: hifi.colors.faintGray; + color: root.activeView === "security" || securityTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + verticalAlignment: Text.AlignTop; } MouseArea { - enabled: !walletSetupLightboxContainer.visible; + id: securityTabMouseArea; anchors.fill: parent; hoverEnabled: enabled; onClicked: { @@ -487,34 +579,50 @@ Rectangle { onExited: parent.color = root.activeView === "security" ? hifi.colors.blueAccent : hifi.colors.black; } } - + // "HELP" tab button Rectangle { id: helpButtonContainer; - visible: !notSetUp.visible; + visible: !walletSetup.visible; color: root.activeView === "help" ? hifi.colors.blueAccent : hifi.colors.black; anchors.top: parent.top; anchors.left: securityButtonContainer.right; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; + + HiFiGlyphs { + id: helpTabIcon; + text: hifi.glyphs.question; + // Size + size: 55; + // Anchors + anchors.horizontalCenter: parent.horizontalCenter; + anchors.top: parent.top; + anchors.topMargin: -6; + // Style + color: root.activeView === "help" || helpTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + } RalewaySemiBold { text: "HELP"; // Text size - size: hifi.fontSizes.overlayTitle; + size: 16; // Anchors - anchors.fill: parent; + anchors.bottom: parent.bottom; + height: parent.height/2; + anchors.left: parent.left; anchors.leftMargin: 4; + anchors.right: parent.right; anchors.rightMargin: 4; // Style - color: hifi.colors.faintGray; + color: root.activeView === "help" || helpTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + verticalAlignment: Text.AlignTop; } MouseArea { - enabled: !walletSetupLightboxContainer.visible; + id: helpTabMouseArea; anchors.fill: parent; hoverEnabled: enabled; onClicked: { @@ -526,6 +634,7 @@ Rectangle { } } + function resetTabButtonColors() { walletHomeButtonContainer.color = hifi.colors.black; sendMoneyButtonContainer.color = hifi.colors.black; @@ -604,6 +713,9 @@ Rectangle { // function fromScript(message) { switch (message.method) { + case 'updateWalletReferrer': + walletSetup.referrer = message.referrer; + break; default: console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index a7050febfa..7083592c1d 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -13,7 +13,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtGraphicalEffects 1.0 +import QtQuick.Controls 2.2 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls @@ -29,14 +30,6 @@ Item { Hifi.QmlCommerce { id: commerce; - onSecurityImageResult: { - if (exists) { - // just set the source again (to be sure the change was noticed) - securityImage.source = ""; - securityImage.source = "image://security/securityImage"; - } - } - onBalanceResult : { balanceText.text = result.data.balance; } @@ -50,10 +43,6 @@ Item { } } - SecurityImageModel { - id: securityImageModel; - } - Connections { target: GlobalServices onMyUsernameChanged: { @@ -68,213 +57,195 @@ Item { // Text size size: 24; // Style - color: hifi.colors.faintGray; + color: hifi.colors.white; elide: Text.ElideRight; // Anchors - anchors.top: securityImageContainer.top; - anchors.bottom: securityImageContainer.bottom; + anchors.top: parent.top; anchors.left: parent.left; - anchors.right: hfcBalanceContainer.left; + anchors.leftMargin: 20; + width: parent.width/2; + height: 80; } // HFC Balance Container Item { id: hfcBalanceContainer; // Anchors - anchors.top: securityImageContainer.top; - anchors.right: securityImageContainer.left; - anchors.rightMargin: 16; - width: 175; - height: 60; - Rectangle { - id: hfcBalanceField; - color: hifi.colors.darkGray; - anchors.right: parent.right; - anchors.left: parent.left; - anchors.bottom: parent.bottom; - height: parent.height - 15; - - // "HFC" balance label - FiraSansRegular { - id: balanceLabel; - text: "HFC"; - // Text size - size: 20; - // Anchors - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.right: hfcBalanceField.right; - anchors.rightMargin: 4; - width: paintedWidth; - // Style - color: hifi.colors.lightGrayText; - // Alignment - horizontalAlignment: Text.AlignRight; - verticalAlignment: Text.AlignVCenter; - - onVisibleChanged: { - if (visible) { - historyReceived = false; - commerce.balance(); - commerce.history(); - } - } - } - - // Balance Text - FiraSansSemiBold { - id: balanceText; - text: "--"; - // Text size - size: 28; - // Anchors - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.right: balanceLabel.left; - anchors.rightMargin: 4; - // Style - color: hifi.colors.lightGrayText; - // Alignment - horizontalAlignment: Text.AlignRight; - verticalAlignment: Text.AlignVCenter; - } - } - // "balance" text above field - RalewayRegular { - text: "balance"; - // Text size - size: 12; - // Anchors - anchors.top: parent.top; - anchors.bottom: hfcBalanceField.top; - anchors.bottomMargin: 4; - anchors.left: hfcBalanceField.left; - anchors.right: hfcBalanceField.right; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - } - } - - // Security Image - Item { - id: securityImageContainer; - // Anchors anchors.top: parent.top; anchors.right: parent.right; - width: 75; - height: childrenRect.height; + anchors.leftMargin: 20; + width: parent.width/2; + height: 80; + + // "HFC" balance label + HiFiGlyphs { + id: balanceLabel; + text: hifi.glyphs.hfc; + // Size + size: 40; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + // Style + color: hifi.colors.white; + } - onVisibleChanged: { - if (visible) { - commerce.getSecurityImage(); + // Balance Text + FiraSansRegular { + id: balanceText; + text: "--"; + // Text size + size: 28; + // Anchors + anchors.top: balanceLabel.top; + anchors.bottom: balanceLabel.bottom; + anchors.left: balanceLabel.right; + anchors.leftMargin: 10; + anchors.right: parent.right; + anchors.rightMargin: 4; + // Style + color: hifi.colors.white; + // Alignment + verticalAlignment: Text.AlignVCenter; + + onVisibleChanged: { + if (visible) { + historyReceived = false; + commerce.balance(); + commerce.history(); + } } } - Image { - id: securityImage; - // Anchors - anchors.top: parent.top; - anchors.horizontalCenter: parent.horizontalCenter; - height: parent.width - 10; - width: height; - fillMode: Image.PreserveAspectFit; - mipmap: true; - cache: false; - source: "image://security/securityImage"; - } - Image { - id: securityImageOverlay; - source: "images/lockOverlay.png"; - width: securityImage.width * 0.45; - height: securityImage.height * 0.45; - anchors.bottom: securityImage.bottom; - anchors.right: securityImage.right; - mipmap: true; - opacity: 0.9; - } - // "Security image" text below pic + // "balance" text below field RalewayRegular { - text: "security image"; + text: "BALANCE (HFC)"; // Text size - size: 12; + size: 14; // Anchors - anchors.top: securityImage.bottom; - anchors.topMargin: 4; - anchors.left: securityImageContainer.left; - anchors.right: securityImageContainer.right; + anchors.top: balanceLabel.top; + anchors.topMargin: balanceText.paintedHeight + 20; + anchors.bottom: balanceLabel.bottom; + anchors.left: balanceText.left; + anchors.right: balanceText.right; height: paintedHeight; // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + color: hifi.colors.white; } } // Recent Activity - Item { + Rectangle { id: recentActivityContainer; - anchors.top: securityImageContainer.bottom; - anchors.topMargin: 8; anchors.left: parent.left; anchors.right: parent.right; anchors.bottom: parent.bottom; + height: 440; + + + LinearGradient { + anchors.fill: parent; + start: Qt.point(0, 0); + end: Qt.point(0, height); + gradient: Gradient { + GradientStop { position: 0.0; color: hifi.colors.white } + GradientStop { position: 1.0; color: hifi.colors.faintGray } + } + } - RalewayRegular { + RalewaySemiBold { id: recentActivityText; text: "Recent Activity"; // Anchors anchors.top: parent.top; + anchors.topMargin: 26; anchors.left: parent.left; + anchors.leftMargin: 30; anchors.right: parent.right; + anchors.rightMargin: 30; height: 30; // Text size size: 22; // Style - color: hifi.colors.faintGray; + color: hifi.colors.baseGrayHighlight; } ListModel { id: transactionHistoryModel; } - Rectangle { + Item { anchors.top: recentActivityText.bottom; - anchors.topMargin: 4; + anchors.topMargin: 26; anchors.bottom: parent.bottom; anchors.left: parent.left; anchors.right: parent.right; - color: "white"; - + anchors.rightMargin: 24; + ListView { id: transactionHistory; + ScrollBar.vertical: ScrollBar { + policy: transactionHistory.contentHeight > parent.parent.height ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded; + parent: transactionHistory.parent; + anchors.top: transactionHistory.top; + anchors.left: transactionHistory.right; + anchors.leftMargin: 4; + anchors.bottom: transactionHistory.bottom; + width: 20; + } anchors.centerIn: parent; width: parent.width - 12; - height: parent.height - 12; + height: parent.height; visible: transactionHistoryModel.count !== 0; clip: true; model: transactionHistoryModel; delegate: Item { width: parent.width; height: transactionText.height + 30; - FiraSansRegular { - id: transactionText; - text: model.text; + + HifiControlsUit.Separator { + visible: index === 0; + colorScheme: 1; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + } + + AnonymousProRegular { + id: dateText; + text: getFormattedDate(model.created_at * 1000); // Style size: 18; - width: parent.width; + anchors.left: parent.left; + anchors.top: parent.top; + anchors.topMargin: 15; + width: 118; height: paintedHeight; - anchors.verticalCenter: parent.verticalCenter; - color: "black"; + color: hifi.colors.blueAccent; wrapMode: Text.WordWrap; // Alignment - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; + horizontalAlignment: Text.AlignRight; + } + + AnonymousProRegular { + id: transactionText; + text: model.text; + size: 18; + anchors.top: parent.top; + anchors.topMargin: 15; + anchors.left: dateText.right; + anchors.leftMargin: 20; + anchors.right: parent.right; + height: paintedHeight; + color: hifi.colors.baseGrayHighlight; + wrapMode: Text.WordWrap; + + onLinkActivated: { + sendSignalToWallet({method: 'transactionHistory_linkClicked', marketplaceLink: link}); + } } HifiControlsUit.Separator { + colorScheme: 1; anchors.left: parent.left; anchors.right: parent.right; anchors.bottom: parent.bottom; @@ -304,6 +275,30 @@ Item { // // FUNCTION DEFINITIONS START // + + function getFormattedDate(timestamp) { + var a = new Date(timestamp); + var year = a.getFullYear(); + var month = a.getMonth(); + var day = a.getDate(); + var hour = a.getHours(); + var drawnHour = hour; + if (hour === 0) { + drawnHour = 12; + } else if (hour > 12) { + drawnHour -= 12; + } + + var amOrPm = "AM"; + if (hour >= 12) { + amOrPm = "PM"; + } + + var min = a.getMinutes(); + var sec = a.getSeconds(); + return year + '-' + month + '-' + day + '
    ' + drawnHour + ':' + min + amOrPm; + } + // // Function Name: fromScript() // diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml new file mode 100644 index 0000000000..c2b20a37ef --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml @@ -0,0 +1,746 @@ +// +// modalContainer.qml +// qml/hifi/commerce/wallet +// +// modalContainer +// +// Created by Zach Fox on 2017-08-17 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtGraphicalEffects 1.0 +import QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls +import "../common" as HifiCommerceCommon + +// references XXX from root context + +Item { + HifiConstants { id: hifi; } + + id: root; + property string activeView: "step_1"; + property string lastPage; + property bool hasShownSecurityImageTip: false; + property string referrer; + + Image { + anchors.fill: parent; + source: "images/wallet-bg.jpg"; + } + + Hifi.QmlCommerce { + id: commerce; + + onSecurityImageResult: { + if (!exists && root.lastPage === "step_2") { + // ERROR! Invalid security image. + root.activeView = "step_2"; + } else { + titleBarSecurityImage.source = ""; + titleBarSecurityImage.source = "image://security/securityImage"; + } + } + + onWalletAuthenticatedStatusResult: { + if (isAuthenticated) { + root.activeView = "step_4"; + } else { + root.activeView = "step_3"; + } + } + + onKeyFilePathIfExistsResult: { + keyFilePath.text = path; + } + } + + HifiCommerceCommon.CommerceLightbox { + id: lightboxPopup; + visible: false; + anchors.fill: parent; + } + + // + // TITLE BAR START + // + Item { + id: titleBarContainer; + // Size + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + anchors.right: parent.right; + + // Wallet icon + HiFiGlyphs { + id: walletIcon; + text: hifi.glyphs.wallet; + // Size + size: parent.height * 0.8; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 8; + anchors.verticalCenter: parent.verticalCenter; + // Style + color: hifi.colors.blueHighlight; + } + + // Title Bar text + RalewayRegular { + id: titleBarText; + text: "Wallet Setup" + (securityImageTip.visible ? "" : " - Step " + root.activeView.split("_")[1] + " of 4"); + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.left: walletIcon.right; + anchors.leftMargin: 8; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + Image { + id: titleBarSecurityImage; + source: ""; + visible: !securityImageTip.visible && titleBarSecurityImage.source !== ""; + anchors.right: parent.right; + anchors.rightMargin: 6; + anchors.top: parent.top; + anchors.topMargin: 6; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 6; + width: height; + mipmap: true; + + MouseArea { + enabled: titleBarSecurityImage.visible; + anchors.fill: parent; + onClicked: { + lightboxPopup.titleText = "Your Security Pic"; + lightboxPopup.bodyImageSource = titleBarSecurityImage.source; + lightboxPopup.bodyText = lightboxPopup.securityPicBodyText; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.visible = true; + } + } + } + } + // + // TITLE BAR END + // + + // + // FIRST PAGE START + // + Item { + id: firstPageContainer; + visible: root.activeView === "step_1"; + // Anchors + anchors.top: titleBarContainer.bottom; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + + HiFiGlyphs { + id: bigWalletIcon; + text: hifi.glyphs.wallet; + // Size + size: 180; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 40; + anchors.horizontalCenter: parent.horizontalCenter; + // Style + color: hifi.colors.white; + } + + RalewayRegular { + id: firstPage_text01; + text: "Let's set up your wallet!"; + // Text size + size: 26; + // Anchors + anchors.top: bigWalletIcon.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: paintedHeight; + width: paintedWidth; + // Style + color: hifi.colors.white; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + RalewayRegular { + id: firstPage_text02; + text: "Set up your wallet to claim your free High Fidelity Coin (HFC) and get items from the Marketplace.

    " + + "No credit card is required."; + // Text size + size: 18; + // Anchors + anchors.top: firstPage_text01.bottom; + anchors.topMargin: 40; + anchors.left: parent.left; + anchors.leftMargin: 65; + anchors.right: parent.right; + anchors.rightMargin: 65; + height: paintedHeight; + width: paintedWidth; + // Style + color: hifi.colors.white; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + // "Set Up" button + HifiControlsUit.Button { + id: firstPage_setUpButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: firstPage_text02.bottom; + anchors.topMargin: 40; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width/2; + height: 50; + text: "Set Up Wallet"; + onClicked: { + root.activeView = "step_2"; + } + } + + // "Cancel" button + HifiControlsUit.Button { + color: hifi.buttons.none; + colorScheme: hifi.colorSchemes.dark; + anchors.top: firstPage_setUpButton.bottom; + anchors.topMargin: 20; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width/2; + height: 50; + text: "Cancel"; + onClicked: { + sendSignalToWallet({method: 'walletSetup_cancelClicked'}); + } + } + } + // + // FIRST PAGE END + // + + // + // SECURITY IMAGE SELECTION START + // + Item { + id: securityImageContainer; + visible: root.activeView === "step_2"; + // Anchors + anchors.top: titleBarContainer.bottom; + anchors.topMargin: 30; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + + // Text below title bar + RalewayRegular { + id: securityImageTitleHelper; + text: "Choose a Security Pic:"; + // Text size + size: 24; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + height: 50; + width: paintedWidth; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + SecurityImageSelection { + id: securityImageSelection; + // Anchors + anchors.top: securityImageTitleHelper.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: 300; + + Connections { + onSendSignalToWallet: { + sendSignalToWallet(msg); + } + } + } + + // Text below security images + RalewayRegular { + text: "Your security picture shows you that the service asking for your passphrase is authorized. You can change your secure picture at any time."; + // Text size + size: 18; + // Anchors + anchors.top: securityImageSelection.bottom; + anchors.topMargin: 40; + anchors.left: parent.left; + anchors.right: parent.right; + height: paintedHeight; + // Style + color: hifi.colors.white; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Navigation Bar + Item { + // Size + width: parent.width; + height: 50; + // Anchors: + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 50; + + // "Back" button + HifiControlsUit.Button { + color: hifi.buttons.noneBorderlessWhite; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: 200; + text: "Back" + onClicked: { + root.activeView = "step_1"; + } + } + + // "Next" button + HifiControlsUit.Button { + enabled: securityImageSelection.currentIndex !== -1; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + anchors.rightMargin: 20; + width: 200; + text: "Next"; + onClicked: { + root.lastPage = "step_2"; + var securityImagePath = securityImageSelection.getImagePathFromImageID(securityImageSelection.getSelectedImageIndex()) + commerce.chooseSecurityImage(securityImagePath); + root.activeView = "step_3"; + passphraseSelection.clearPassphraseFields(); + } + } + } + } + // + // SECURITY IMAGE SELECTION END + // + + // + // SECURE PASSPHRASE SELECTION START + // + + Item { + id: securityImageTip; + visible: false; + z: 999; + anchors.fill: root; + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + } + + Image { + source: "images/wallet-tip-bg.png"; + anchors.fill: parent; + } + + RalewayRegular { + id: tipText; + text: 'Tip:

    When you see your security picture like this, you know ' + + "the page asking for your passphrase is legitimate."; + // Text size + size: 18; + // Anchors + anchors.bottom: parent.bottom; + anchors.bottomMargin: 230; + anchors.left: parent.left; + anchors.leftMargin: 60; + height: paintedHeight; + width: 210; + // Style + color: hifi.colors.white; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + } + + // "Got It" button + HifiControlsUit.Button { + id: tipGotItButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: tipText.bottom; + anchors.topMargin: 20; + anchors.horizontalCenter: tipText.horizontalCenter; + height: 50; + width: 150; + text: "Got It"; + onClicked: { + root.hasShownSecurityImageTip = true; + securityImageTip.visible = false; + } + } + } + Item { + id: choosePassphraseContainer; + visible: root.activeView === "step_3"; + // Anchors + anchors.top: titleBarContainer.bottom; + anchors.topMargin: 30; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + + onVisibleChanged: { + if (visible) { + commerce.getWalletAuthenticatedStatus(); + if (!root.hasShownSecurityImageTip) { + securityImageTip.visible = true; + } + } + } + + // Text below title bar + RalewayRegular { + id: passphraseTitleHelper; + text: "Set Your Passphrase:"; + // Text size + size: 24; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 50; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + PassphraseSelection { + id: passphraseSelection; + isShowingTip: securityImageTip.visible; + anchors.top: passphraseTitleHelper.bottom; + anchors.topMargin: 30; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: passphraseNavBar.top; + + Connections { + onSendMessageToLightbox: { + if (msg.method === 'statusResult') { + } else { + sendSignalToWallet(msg); + } + } + } + } + + // Navigation Bar + Item { + id: passphraseNavBar; + visible: !securityImageTip.visible; + // Size + width: parent.width; + height: 50; + // Anchors: + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 50; + + // "Back" button + HifiControlsUit.Button { + color: hifi.buttons.noneBorderlessWhite; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: 200; + text: "Back" + onClicked: { + root.lastPage = "step_3"; + root.activeView = "step_2"; + } + } + + // "Next" button + HifiControlsUit.Button { + id: passphrasePageNextButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + anchors.rightMargin: 20; + width: 200; + text: "Next"; + onClicked: { + if (passphraseSelection.validateAndSubmitPassphrase()) { + root.lastPage = "step_3"; + commerce.generateKeyPair(); + root.activeView = "step_4"; + } + } + } + } + } + // + // SECURE PASSPHRASE SELECTION END + // + + // + // PRIVATE KEYS READY START + // + Item { + id: privateKeysReadyContainer; + visible: root.activeView === "step_4"; + // Anchors + anchors.top: titleBarContainer.bottom; + anchors.topMargin: 30; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + + // Text below title bar + RalewayRegular { + id: keysReadyTitleHelper; + text: "Back Up Your Private Keys"; + // Text size + size: 24; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 50; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + RalewayRegular { + id: explanationText; + text: "To protect your privacy, you control your private keys. High Fidelity has no access to your private keys and cannot " + + "recover them for you.

    If they are lost, you will not be able to access your money or purchases."; + // Text size + size: 20; + // Anchors + anchors.top: keysReadyTitleHelper.bottom; + anchors.topMargin: 24; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: paintedHeight; + // Style + color: hifi.colors.white; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + Rectangle { + id: pathAndInstructionsContainer; + anchors.top: explanationText.bottom; + anchors.topMargin: 24; + anchors.left: parent.left; + anchors.right: parent.right; + height: 240; + color: Qt.rgba(0, 0, 0, 0.5); + + Item { + id: instructions01Container; + anchors.fill: parent; + + RalewaySemiBold { + id: keyFilePathText; + text: "Private Key File Location:"; + size: 18; + anchors.top: parent.top; + anchors.topMargin: 40; + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + height: paintedHeight; + color: hifi.colors.white; + } + + HifiControlsUit.Button { + id: clipboardButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.top: keyFilePathText.bottom; + anchors.topMargin: 8; + height: 24; + width: height; + HiFiGlyphs { + text: hifi.glyphs.folderLg; + // Size + size: parent.height; + // Anchors + anchors.fill: parent; + // Style + horizontalAlignment: Text.AlignHCenter; + color: enabled ? hifi.colors.blueHighlight : hifi.colors.faintGray; + } + + onClicked: { + Qt.openUrlExternally("file:///" + keyFilePath.text.substring(0, keyFilePath.text.lastIndexOf('/'))); + } + } + RalewayRegular { + id: keyFilePath; + size: 18; + anchors.top: clipboardButton.top; + anchors.left: clipboardButton.right; + anchors.leftMargin: 8; + anchors.right: parent.right; + anchors.rightMargin: 30; + height: paintedHeight; + wrapMode: Text.WordWrap; + color: hifi.colors.blueHighlight; + + onVisibleChanged: { + if (visible) { + commerce.getKeyFilePathIfExists(); + } + } + } + + // "Open Instructions" button + HifiControlsUit.Button { + id: openInstructionsButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: keyFilePath.bottom; + anchors.topMargin: 30; + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + height: 40; + text: "Open Instructions for Later"; + onClicked: { + instructions01Container.visible = false; + instructions02Container.visible = true; + keysReadyPageFinishButton.visible = true; + Qt.openUrlExternally("https://www.highfidelity.com/"); + } + } + } + + Item { + id: instructions02Container; + anchors.fill: parent; + visible: false; + + RalewayRegular { + text: "All set!
    Instructions for backing up your keys have been opened on your desktop. " + + "Be sure to look them over after your session."; + size: 22; + anchors.fill: parent; + anchors.leftMargin: 30; + anchors.rightMargin: 30; + wrapMode: Text.WordWrap; + color: hifi.colors.white; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + } + } + + // Navigation Bar + Item { + // Size + width: parent.width; + height: 50; + // Anchors: + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 50; + // "Finish" button + HifiControlsUit.Button { + id: keysReadyPageFinishButton; + visible: false; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + anchors.rightMargin: 20; + width: 200; + text: "Finish"; + onClicked: { + root.visible = false; + sendSignalToWallet({method: 'walletSetup_finished', referrer: root.referrer ? root.referrer : ""}); + } + } + } + } + // + // PRIVATE KEYS READY END + // + + // + // FUNCTION DEFINITIONS START + // + signal sendSignalToWallet(var msg); + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetupLightbox.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetupLightbox.qml deleted file mode 100644 index a623c2bcf7..0000000000 --- a/interface/resources/qml/hifi/commerce/wallet/WalletSetupLightbox.qml +++ /dev/null @@ -1,502 +0,0 @@ -// -// WalletSetupLightbox.qml -// qml/hifi/commerce/wallet -// -// WalletSetupLightbox -// -// Created by Zach Fox on 2017-08-17 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import Hifi 1.0 as Hifi -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit -import "../../../controls" as HifiControls - -// references XXX from root context - -Rectangle { - HifiConstants { id: hifi; } - - id: root; - property string lastPage: "initialize"; - // Style - color: hifi.colors.baseGray; - - Hifi.QmlCommerce { - id: commerce; - - onSecurityImageResult: { - if (!exists && root.lastPage === "securityImage") { - // ERROR! Invalid security image. - securityImageContainer.visible = true; - choosePassphraseContainer.visible = false; - } - } - - onWalletAuthenticatedStatusResult: { - securityImageContainer.visible = false; - if (isAuthenticated) { - privateKeysReadyContainer.visible = true; - } else { - choosePassphraseContainer.visible = true; - } - } - - onKeyFilePathIfExistsResult: { - keyFilePath.text = path; - } - } - - // - // SECURITY IMAGE SELECTION START - // - Item { - id: securityImageContainer; - // Anchors - anchors.fill: parent; - - Item { - id: securityImageTitle; - // Size - width: parent.width; - height: 50; - // Anchors - anchors.left: parent.left; - anchors.top: parent.top; - - // Title Bar text - RalewaySemiBold { - text: "WALLET SETUP - STEP 1 OF 3"; - // Text size - size: hifi.fontSizes.overlayTitle; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.bottom: parent.bottom; - width: paintedWidth; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - } - - // Text below title bar - RalewaySemiBold { - id: securityImageTitleHelper; - text: "Choose a Security Picture:"; - // Text size - size: 24; - // Anchors - anchors.top: securityImageTitle.bottom; - anchors.left: parent.left; - anchors.leftMargin: 16; - height: 50; - width: paintedWidth; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - SecurityImageSelection { - id: securityImageSelection; - // Anchors - anchors.top: securityImageTitleHelper.bottom; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: 280; - - Connections { - onSendSignalToWallet: { - sendSignalToWallet(msg); - } - } - } - - // Text below security images - RalewayRegular { - text: "Your security picture shows you that the service asking for your passphrase is authorized. You can change your secure picture at any time."; - // Text size - size: 18; - // Anchors - anchors.top: securityImageSelection.bottom; - anchors.topMargin: 40; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: paintedHeight; - // Style - color: hifi.colors.faintGray; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - // Navigation Bar - Item { - // Size - width: parent.width; - height: 100; - // Anchors: - anchors.left: parent.left; - anchors.bottom: parent.bottom; - - // "Cancel" button - HifiControlsUit.Button { - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.left: parent.left; - anchors.leftMargin: 20; - width: 100; - text: "Cancel" - onClicked: { - sendSignalToWallet({method: 'walletSetup_cancelClicked'}); - } - } - - // "Next" button - HifiControlsUit.Button { - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: 100; - text: "Next"; - onClicked: { - root.lastPage = "securityImage"; - var securityImagePath = securityImageSelection.getImagePathFromImageID(securityImageSelection.getSelectedImageIndex()) - commerce.chooseSecurityImage(securityImagePath); - securityImageContainer.visible = false; - choosePassphraseContainer.visible = true; - passphraseSelection.clearPassphraseFields(); - } - } - } - } - // - // SECURITY IMAGE SELECTION END - // - - // - // SECURE PASSPHRASE SELECTION START - // - Item { - id: choosePassphraseContainer; - visible: false; - // Anchors - anchors.fill: parent; - - onVisibleChanged: { - if (visible) { - commerce.getWalletAuthenticatedStatus(); - } - } - - Item { - id: passphraseTitle; - // Size - width: parent.width; - height: 50; - // Anchors - anchors.left: parent.left; - anchors.top: parent.top; - - // Title Bar text - RalewaySemiBold { - text: "WALLET SETUP - STEP 2 OF 3"; - // Text size - size: hifi.fontSizes.overlayTitle; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.bottom: parent.bottom; - width: paintedWidth; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - } - - // Text below title bar - RalewaySemiBold { - id: passphraseTitleHelper; - text: "Choose a Secure Passphrase"; - // Text size - size: 24; - // Anchors - anchors.top: passphraseTitle.bottom; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: 50; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - PassphraseSelection { - id: passphraseSelection; - anchors.top: passphraseTitleHelper.bottom; - anchors.topMargin: 30; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.bottom: passphraseNavBar.top; - - Connections { - onSendMessageToLightbox: { - if (msg.method === 'statusResult') { - } else { - sendSignalToWallet(msg); - } - } - } - } - - // Navigation Bar - Item { - id: passphraseNavBar; - // Size - width: parent.width; - height: 100; - // Anchors: - anchors.left: parent.left; - anchors.bottom: parent.bottom; - - // "Back" button - HifiControlsUit.Button { - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.left: parent.left; - anchors.leftMargin: 20; - width: 100; - text: "Back" - onClicked: { - root.lastPage = "choosePassphrase"; - choosePassphraseContainer.visible = false; - securityImageContainer.visible = true; - } - } - - // "Next" button - HifiControlsUit.Button { - id: passphrasePageNextButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: 100; - text: "Next"; - onClicked: { - if (passphraseSelection.validateAndSubmitPassphrase()) { - root.lastPage = "choosePassphrase"; - commerce.generateKeyPair(); - choosePassphraseContainer.visible = false; - privateKeysReadyContainer.visible = true; - } - } - } - } - } - // - // SECURE PASSPHRASE SELECTION END - // - - // - // PRIVATE KEYS READY START - // - Item { - id: privateKeysReadyContainer; - visible: false; - // Anchors - anchors.fill: parent; - - Item { - id: keysReadyTitle; - // Size - width: parent.width; - height: 50; - // Anchors - anchors.left: parent.left; - anchors.top: parent.top; - - // Title Bar text - RalewaySemiBold { - text: "WALLET SETUP - STEP 3 OF 3"; - // Text size - size: hifi.fontSizes.overlayTitle; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.bottom: parent.bottom; - width: paintedWidth; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - } - - // Text below title bar - RalewaySemiBold { - id: keysReadyTitleHelper; - text: "Your Private Keys are Ready"; - // Text size - size: 24; - // Anchors - anchors.top: keysReadyTitle.bottom; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: 50; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - // Text below checkbox - RalewayRegular { - id: explanationText; - text: "Your money and purchases are secured with private keys that only you have access to. " + - "If they are lost, you will not be able to access your money or purchases.

    " + - "To protect your privacy, High Fidelity has no access to your private keys and cannot " + - "recover them for any reason.

    To safeguard your private keys, backup this file on a regular basis:
    "; - // Text size - size: 16; - // Anchors - anchors.top: keysReadyTitleHelper.bottom; - anchors.topMargin: 16; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: paintedHeight; - // Style - color: hifi.colors.faintGray; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - HifiControlsUit.TextField { - id: keyFilePath; - anchors.top: explanationText.bottom; - anchors.topMargin: 10; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: clipboardButton.left; - height: 40; - readOnly: true; - - onVisibleChanged: { - if (visible) { - commerce.getKeyFilePathIfExists(); - } - } - } - HifiControlsUit.Button { - id: clipboardButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.right: parent.right; - anchors.rightMargin: 16; - anchors.top: keyFilePath.top; - anchors.bottom: keyFilePath.bottom; - width: height; - HiFiGlyphs { - text: hifi.glyphs.question; - // Size - size: parent.height*1.3; - // Anchors - anchors.fill: parent; - // Style - horizontalAlignment: Text.AlignHCenter; - color: enabled ? hifi.colors.white : hifi.colors.faintGray; - } - - onClicked: { - Window.copyToClipboard(keyFilePath.text); - } - } - - // Navigation Bar - Item { - // Size - width: parent.width; - height: 100; - // Anchors: - anchors.left: parent.left; - anchors.bottom: parent.bottom; - // "Next" button - HifiControlsUit.Button { - id: keysReadyPageNextButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: 100; - text: "Finish"; - onClicked: { - root.visible = false; - sendSignalToWallet({method: 'walletSetup_finished'}); - } - } - } - } - // - // PRIVATE KEYS READY END - // - - // - // FUNCTION DEFINITIONS START - // - signal sendSignalToWallet(var msg); - // - // FUNCTION DEFINITIONS END - // -} diff --git a/interface/resources/qml/hifi/commerce/wallet/images/01.jpg b/interface/resources/qml/hifi/commerce/wallet/images/01.jpg index 6e7897cb82e3c8319ea81e291a57a00d25429b90..821be6c584d86455df30a882481ae210048bb294 100644 GIT binary patch delta 10568 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Ʀ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 Y@w|w@^7Bl@?=cg${{U!+`gjlj*==C}6951J diff --git a/interface/resources/qml/hifi/commerce/wallet/images/02.jpg b/interface/resources/qml/hifi/commerce/wallet/images/02.jpg index e210d0dc825a2e56fc8f5104b9379af808e808a9..cfcd3e38bf5e0bfd32e3d84652ed3d90ef9047b2 100644 GIT binary patch delta 12887 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 delta 29857 zcma%?1x#IC)TZyny?AkVcP&t$T-;rYySw(_6nA%rixv0M;_hxO?oeEcOup~`|C!88 zM&9J??31VPcf`*HZhKhi0Omz0Q{jFgCkgo1{Ro`Q;nnuLUbpOJ-w zlZS_goL*2wfa?PrHxJi8n}Cs#k%tKVb0zau5g%5B^u6 z{}dPo77iW(5eXRu^{pS&j0u7P!@|PA!NSAC!GS>F05A+J2o4J#o03x;0Y}vYk;(;^ zD=4uLiCUumC!X5O9~y2`*I;B6{PzTeM6`7D42(=XynOrufbXhUODb4zPm zdq;0y|G?nT@W|-w-2B4g((=mc*7nZs-u}Vi(edTg_08?w{lnwaTR(^#41)a+E-?5% z(f$`OED#JBfPsU9g+u&@3=HG-PX-noJS8Urwzw*yi3<)DR}d1eL}Fq6Ph@ItwLf^K zt}`h3G(20hm;b>1%l3an`=7{y{|{)u|DEl>qx}!ERS+617zBd_iv{V@ zApOK+ECqgFF)&@GLk`5`^?DZe^l~Qi=2qMHSO1N*Iwic+(ES~ z!_Z;~-PUSA9HM&_vTJlUP!h6V3fBsr2S*AD3$4ilm8FD|x_3^=dgc}ku<+qyA-7fa zIYBChbSvCyYdPBq69nJSq+G7ieCH#3=W#fvNC9h@iXb9n%bj~G0>P2;Ps@WsScz)g z^9c+w4q#Wl#FIrR^_d-{@3s_q>dW0>@t6` zhS!N!u3sCn)DXf<9gr;HcGU2Ew#rO1{cSg!I zo75Mi;Y7M+-|(AkI|G-^k^-A4#nthPvIz9%uMj)n@QHEjoUM>qF?+B&QO(>4=O;?yi!9(AZZjEeJGtTcQE$0S)k+HN+ zURQS#d`PIH7>`IOe*m6tp){}|*ZjV4QY?%MrfAZh`$>#^IZm+wDe*_Dh8Kx0BSR*d z_73zZJYRd5asq77mRZ|@pdO|wD#+&q)d-hY+!fG0K>PGd|Mc0UGezMSW7TiEfOeH>};f{yJo88I1E3a}uR24rN z)t|C2DW#hmU&VSxBHLCWCaE`5sR`3~rXHlnI9tg~jgPK-|L*U)uU52{M#Y4H>U@qS z1&>1x-jHt{G?i~nsKxCDsA9&Je6Nrolm!VF+#)0pIA#QoOEb#ylQn*EvR}o5YZWb zJ?6aIdzypMb^1g@`l~xR@l?ciuPL8GuonA3-NvJxT#Uk@LIl?tV1_od|025n82eX0 zRGR4l+_@MC!eA!aTsepM(%;8gqY?xMPEeaIOwe)lH5B2S!UQELy7H$px_Gf9#!!CH zdf7~C#pJQ!smxm&Ay0MF_EcXMU6DpC#t2V&#=`Ncue>r3ts1~> z@9fTN$Lb)EP~DNsY?fg$Qq-Zo?M1&obK@ymHep@vqVuGLx7zIBt0C?=eFaBqXO@w2 z15Luh?KNy4pf*IigEG<^ zMA7L23~u20B(J-ZDqu@9)mvqUBEHmNbJ$RZ1a@hDN!S~*4UbQ#WtXjsnHo62?hupO-9O$Rmez2_v5`Ul@U)E-0Qv1qdThTcr9S`_#x(Z(zRz zD|z0dHZ%c2S?-FRK<5}=`@MDtq}0g;?7@mHhg!+PfppGq`mK65y~BOWL`m&_@}lnx zTgxTf_3Bq6)C`C&ae}nmW?O6AF+!B)X#Q;1#g8un-{+e%c~~tc1?p7zNO5dveZ$GV z-yQ*i(6Y6->d6KF5*8w{--8q0>FhiO^fAY--sF%}gv+NfatJj+l^1wIHNz^1ZwjfQu+ehpP z*FlLL)Iktzj5h1G@Uj{Lt7CNl@^-6XV-~R6rThjWn^I?uikiRfhFr;a2G3#*fxv-? z%^C=aljgSKi}gZkG^-4is*o8ecybLzzV)ER#=X>xYdw9~Tf&O0dHlQD|*O{-42Du*q+mS+>|*)03mTM5Asg#N(|?jcb=uTmX{RfvEOaDv*w#Dp)r8@ z?!%LInZXGkpY(gTYSD|i>q?XPYBp{6_bpfKP`UD_SzB%<>|7Dm(Ffrlu?3$Mt8BIN z9Tw;&&42bDR30QLdbU?cjiVv!77Lt&BjU;3Ye&IRv2V(*O>34UO-IU(02`TkR4K9n zB*q70MqkR3m7~oOBo`h&m)D4+cW%hcjVsN%PQ2_KGX0gKjGv%DbB=xuVEkHM*=bAZrOZyXsk^87k8&h&D3Y#d(nuYW6f&Ohief_riJ6UhU%Q~?)P^h|&ji^kq z#gs;-%k%L%8Ml{O2M`vjjs6A_F%^}6rLa)m)8qR>HM=*Kt^emeaa5Rbk9wjkL^$0? z%C#o$`sWjFhh-41E4;2b?RpwQ%j*!Peb2?aa&d(@$C}A(aHK(@c+78&8rSom>h>Qm z6PK=Q536Y6`IN34BDqVLGkt^l&Enrcnkw)Yh8e?!L#X6RB0%Nb;OJ@Hya$x^svCwr zQ7*R{iMqZF-V)JzLdG#m##5&7@?Pf__jmQ|01Tynf?WJ|8G^zIlb}O6*}~+BqhjX^ zbNkYkj8$ve>T-Vu(S$w&lW1?(317ZM1HWh*a#> zT2QmrgK7jI=fBVmK>XG#UYoNmWAWlj+Tqh}_mrd|M>;yk-6LX6_N=l|#7Lzdzx;(J z0T-ooQH#@}q;^J>v&kyW93^Z6+T->(ac>|wrs|O{<)RF84P(#paD+H+y_|vyM4n^j z_x;}~Li(P}3}nO;+hWDKtYIORl#vt(%v8rOIDUYq>TH8Msjs2jhlUWg#yz6ANdq-T+tNDN&8Wc8mfv&VgI4vf3CQlG5!K}#t@p{WnYo6H7 z(vqB=I?mTck7(Dq3u_Fuc7H24f3b|Q;-uZRF)y3sxv@Di zcgLQLxkPK_cjTJb3A-jhOaHX6%;qZ=+yzKEc$7uk(5tc-80Rf4Myd{K{(K^O9G%#p zMT^sT3Nf_IuHO6^V<1Qtx(MxIYJ{+(?4DWp@ zTtV?6E1s=6_L_Z6T$g0zYeoIeotFm<7@#*g5g$UBYZMBm5+4tVau@3NOnOYr7zYYH zpI?w_Nsh6>xx25twDU&@F^%KgB4b|;aqd+-ocb@-&r0ripGxo#96hu;o2*fOP_VY0 z(c~P6HiuOEAD+c6#BUMq=H2bNDwv3i>1{0hz~rCpU>wWh7l6*)d61~?toFRWV(%uh zSX~_*V;JW`ujR}ijJ0}|c&CwGY6;l4h@dK6evWJ!L$bfg9#gl#K8xn4r0~mPp6cTu z;Ut%o60UcpKGsh9_)HR&L>w`)$s8Zw-{{o0_N^w`(8LkvJR#u3EoOt?wt43>ZteO0 z!dA-eD%F<5op;B&qHun}_cdYllJq#n93H|&qqCfAimj32-USMvbo zu0+FiXhJ7Mo-9y1QM}mbaP_uBziI?`5>Mt8(s}0A?_#4>iRC;BArqh6zj@N;dD+li zc;{z|ZgS?xT7pCEzFT>sUAV;Rcq3yG8t4g6lP69>bR8?-U*kERNS<`XAOI@}myb(%iWL)174jsmsd19S2*u%d5c}aF(knZmJ5}C3 zt~gIPUd=eB(^1$N#l3O zYSZsF5K7!-+J#c^U&|G@yBkr8QYkY#j{vSd2OME?aor|&rZh;A5{cP|#oW}r z{xoHacKprSJ&+@Y!R1rn!z5$?k&nK;)AGAbk-CzTl z-IKJWnUj+{KX~bGzq~ff`f7!DaQY8c(8~?va^goh7bQy1NGrvVX=r@Yqq6u4mUhzX zs*V#@w?cxcwJX_T)I%=o_IB7dEKX7uS6q->38ase`LOGjHTO-r)hm&jm5`kCNsB$& zi7f>(VLRp!M-J4{FOf39GHvUGHN|7B%XGSJxE&ebciu(&z5mLd>(~|OL!q_q6?5c4 zl%H;6@(A2k!I$QFXVYN%hUhA%)+pMHs(MM|r`L1uA(5>deM&B8nM4*%ZSZF{acl46 zarAlAYdq(HYlb6~H(PAHvtC(BULQ$1)BRSka3(C>wE*O>R%2|wK$Dxa&H@Xv?%2WA z%H^Mqqmc1zv$#BTe~$e5YY>BQ1^dpjlIItiZ5sdKwLXVrs@YN2y|nl@7Y*80S-e#V zY+6FkA?4R@?M8{a20`TrBDTz}zb7Z%W>%~Bp{;D~y zB2U5y5eZOW$60hj%G==tFi;UxGl6J-4kFB z&esnC2#k3qaLJRt`-PBF`lur5Obu69oI_s4$*B#8 zrRz04;zEcdaLZd}k+h|TPZ8)%PW|nNO7R#i`5tnu&K0gK?DSK2?ATYc2%FUWOp|2(SC6yANrYgK{hY`@~dUd%ULw( z5(3~4mpSG@o*g4Iaj*9BSW(k0tAPH->)E?>ip}D=7BLR%N?ws8CyP~$Ap*p|RZ&qd zwX{Lzp;z5fI#*$$JWM~pId@Ps>s=e1U$&lND{8Ca6;g16R8gnMKL<2g7j9e%hsr-6 zJCAR(v##ZNzow+)6UIG01lEcr?$1txxf>7~gt&`T`-V!n@z7Yn*j|6+3LlRB8q+nG++K4I!fNVwnq zUh8X@!)Q?)D!<(IhLcjJ?x@hPhD8Dl!8-$@87V4#`(1jowye8f;bWar!LrEUHWff9 z>(afcy*o$B0WMia38pB?ZtV;4M@N&!$n3FCp$PObc)#OoZNA>Q#ZH^%6T39YC3$-7jmG7i#EJ_2at24`}EP3^mn%Ryc<9Wce6)%6L3M;nf$kluP z@_7c!dfwpft|KuaQ7JobTH%9SOVQzql$(Ah*egG80@ zigbgYHf&4#&eWt4s=rmQt?&Vr69&kMitc~A^^F=@8X4fktt=BPAV~D0U3b+^k!h=8 z>#JUolvn{@TUUO@qBJDz`mdb245-d~v^KK^>lPiuC465b+uUwvmF#yz$`oDkaa|dW z+@-~R+G(%(%3ard2AP|jt8ql{Zs@cyQ3>7M4VI?Q*E|3I-l(nFJO2Ps?7#9Qy7Dr; z!Z%A&lI1=8g+Y7O7k^iAjM!40U0_D!T9v2hs+YTH82Mpw)ToIw@_ka*&f+)r8uO0! z#IqfX(45*=F%*02;k2DB({+u;qicxsp~l|@zQ}}m{LgV;d>59>?yGrS%W4M1x{<$b zMmhQ}4DVHEYgma>x5bG8>Uv0tn0)IbdD(NI1#Cm@a%tiTicf@e&_UuUEKU?oiH>u;(&v%x;@Gg0O-N)x0f2a>Ro?gcQl2-@lUxnk3bjLTd zIUA+8J@7K?Z{c5{tM=BpalTa~3yjq8pQ40r@fvBN>-uR7YT`Lsp6lZA>Po7D>WmI$ zDOFN7r8YWV48v^*?Pq$56$`WLXpFUDpVQ49CT!q7#fvWPZ&14##Ex0SIvrnILTYV2 zM|dldzM?gIy~3gc2Y93t&d#+5cqcvRH-tpoK2!-c_fiyAg)2G{(X7L;0yQGsimg_u z=LKB5CazrY%1%g;E^(Y8T8c}lD1($NxCczjQ%4^s9rq^yKU78(E)9OoTE`vvHka?h zUKS!qM$OB%OVP}P3MM4A>j;wpC+R*`>Y=Igz6b@D=mTNEc!0y+8|XW_B%EtNk-!F_ z7yl&;v#VX_4LR$4-{F2kty7(m%=q;PQ?VmB&Q@2Z^)5YID-JKVRKvN5mfR09>&#!> zJ=yxQT8o<*(&xhIaMd>XZl5`Gx>=tTQ9WO^BiN=N`k^dLd{x@5%cHPW=}w6Wqs!W|%7@9CT}@*;cb z)Q~lp<*MJlC^Oh4cs~^rQ&lc0WoNT^@5$(5NB3iTa@fnyn8eRADw^2f34zb@L;cZ- zLdx^QDML{R47Gxp6fRilFt@Bt47=0#S_;pZPCCQZ-mBSU*wwnII`}RXd&~!$A7YFi zz`X_d$r#TS%FKZnilSgEr#k|}8_o#=ofUD_3FDplaq?_4W8G$>7zoj3=!CR1$C3P0 zr6^OeA+3)pd&+gV$o+`xVXQX5>>$x~BY3k2yP6lu_Dd=PJ7-w->q8RH5 zmXbfH1-A7)SHE-7S<)hGvw`3H4a!eY@VupZego+-RPWm0z^csG{_ajAu(Y1#Zi2r! z4>&A}3ef=GRvN|0UkCu0f4!`KMwovD2m8+m|Brcv0FQw1?}Lbpgn)>Qh=72EiiC{v zj}VYh(NIy){(1S=GW>Tb3_L6>JPINL;=d{Xp920P@2wYvg#=m$t;2$`KrmQfSS;{c zKZq0rf`R$RZ2K<<5d@C_hX@0U1V#qE^@E7Pp#M1v`Y$K$zsxL5SWFNIeE8}Z&26E7 z8stONC#eJC1HYS93DrT5wWWcl1{{dMKd9IjvCPGhNXhdpv?tChbw3&A)Sfglr*fHg zAlS!&(KF#ZL(6(-cs}HqBe!sogtZO-MH7fo9{yHMu@n=Gi|;VMB-Tu|zUF@2Avb=9 zFPqlU+Rb~6BEW1 z3v-q5{;CYiYB8e79J;Fd7m7&1YVkd#E!;};oSGnZ6 z6TA#U*0|po(?@*X$g^_LkAS$R%^$tlWX?*s?cshTyfHDlLw0aL+hIbk2_%bMbc-v7 znfXZ=;0?B%M@(AHnYY9v1|`W_6f!IpxKybqHMdvxq6|bEPtWW!@Yf>*HS5fOYA9`at+o$pyJ zYRdIdnpE(pKwb2)$xVAeerZNxgz?=saws|2(#;YuGis*!Nsml z@o5>wg$3JU!1a_&9?Cv2m%cMUQ85hP`;p73_VLB?g`i*!t~#5OEegnjY4B;{k+bwke`n=i8@FwiMB^4NiwFt0sbd zp)2QO3fE6nnp0A)RfT0uYso67#CBR!-HWl(#b&QDfZfX7x6f%U<=V==`FRV|t!Xaq z0rZEjCGO96;v60g0#@|$jjnno`dY@v*{!5jQa5!W&bg>dN6SBvlVV~c zw@-V|iH+=O{%r=vXq`wF{i^R0Tt z1`qdG;L(Pq^S8DaRD^VTSL9dLOs-wS*L`V}xEhlB*lWzX3}I4t&nUT&`x8$Lm0uf!##qCq-e9TC^{4b6iWf9*{&%L5vZbhxBhRS zK+YK6@8%zc>}$7pgjICLc?xG2)#p87eww!T0fdS@9Q+7qiLNc7%GvYbQah( z047=(slX0Jh|R*hO)Ydzv-kDZpljv3jJ=NB9=4q5s$J)CMGADwAW_&-k-$TP|v|aO3SE-DP+^t>Pq1xlR8Q>2mH@EiM1s5 zWb3tGgX9uayl^HJ`k#4^r#|u35#q=nUdIkGhGJJNA>wgXR)4}HRIue1NlK;_pb{^I z^hfcO_Fla8gIF;^U^o~s91IK?fQA3}bb|%Mz`=tMun@6vC~>LqIH@u5xsl$}aEa5t zt%8uiU=SD{2Jj&@6ml-;mlSyeJ#=l?%01D9Dk+^0NO(2nYk%Tr$hTa7N7(~=CjAD| zjrti~aF(U%@Sb=NCIXx+hAGyOWJe zz#XXzFfv+9-zyxg=V&^oiNmz06=JAX(03%>{ia$a#pWRA{T6DPhq;W9dTOdvFOZAT zt3JI5XUk=8#+Wri$V5ohWpGWnWnw-a%f_!FeMQYBe)+d2V{Xy4vrVZg7F#jR%ww)s_{kvTnP96u5wF26sE3~{zwIFetuR>@lBmiI6 zaH@@7Vk%-RIOWMFxseU0Jr-*vF7(|z+nU$6Ing<%s)&oILT}!#VPKsPC)HpnIr=9- zXwc-~c7uSq0OU=pVu=qbR^lf~2F?EF$Qdpu_)Zjrq&ou+1C%LTQ>2$c9 zA7kCDnY{7;#_c~6vHf(ZS4*(xIJ46#8)v&wwh9I((oL$hXfy=nig)GQb-zs79uUNw z4J8u9=K%A)quTVNcS)c&UFx$&x37)sQAcQ_$&?;1zYWuKsbN3+OggGU>1PZUoqS8D zq=ZsUdKRv;`&8c{#|bL!4t9odU-@2<6*3hk6aO`Jo*RakN3wg;`=!FYlJV~8RDNx@ z728SjuvzbZgeI26xBPfaec{VUz4zb$Tl({t#|3Z~0maKkRCw}0E!SAzKlDuUAP86$ z8-TqjJD{cibBpPn!E?8ugzLbD|Bh)HXwl{)>^rmsI0m@YEAg@?1QDWYf zYKv)$T|8dH6DVP~dh?p*rD~@$BiEdP6~S!h#^_m8gRi`bhLk}$9) z)u#3SNHTg~A!hDYUu)M;#=~kkj9?##evGXQi$lY|CfF}6L}8}Ui#B@rjrNyb0~CM{ zzuQ46_?%F96gM`j=p=U_98l!)!a_d~o7PBxX}F9>ZKGCQ_@mo}TA|~UIh2q!wT`rl z?Uuq?I-}yF6*_u_bpiw zOhsCNN?dKza@uq&^7c2_Ybd+&`626>J1jHK7334m#ks9=R&EovE}B}|$hUMD?xMvD z=6)OU1LZ|RL1)}kzw$dPHUImLd0nnUM_%EId#qE!-;s@*6;$a!kei~gbs(^}Qrd3c z70(CR8re$rGiB;_B~z#jC<=|U4Yn#&Cevsu;r-Gq^b|v6wB_<%W7^JRfd9oSWuGVi zy7hLD+rHiGgwIuG1fS^LO6Ca;M7K$Y9Opv}^Z*Uu_JY%hex?1vms$N-1w0(e4IBh)uc}(6Q@)^ z`p0?R8F<9kA-Be&XS?v&PD|F=gr!fwkx3iJRR?H)zo*H z$zl>cFP2iCt<1yLl$tz@h=T<&N^#@Ho7eO1?2El;9ptmLenr?(ArZA5wi&M^KG#h+ z3+xaQ;#L}#MZt#B;cht{LN5a*y*4v)j3?8RC)Ns?Pf>de&M$LhVbZ+YX~mC3HSjy7 zHKlsm3g|hw^2oanMCrgYbkq*@h*(+kw(YjXj|4jgE|yhdNHL}p?to67s{I8;O(v2A zd7C=~eaSVN$@p^+8$=Xonaj#k(79(;da<@gtGOq$Va`!F*P|P~w%M78sdN2dBuHsI zU6ZjdSH0P|l8z3t7-8A~T}4~bp(wq?lcm0&QS;aPh%`~!>;?GC*6hX_ahU%ghVe(P zFPS`L+EGXRxufae{&Z$8T6}Y$yy~0WV3(`7%%`7;&~P>wJ$yu3#Q0S~SU9KVY>fE* zXbj&#!)ix5Mnk!CympCpNRe0beq+1~NA0o&f8>zTUQ$kjgt2l^VxTD6a%itG_16fR zyKtOxnGhCYDNmqwh5Jq==EF8IH6EN86B}-Zwu#}fdr#bPO@zw)uX*|IcdkJWj7g_L zd`e?_M<3ofxtZbXt{=3;NtrCc(u1jCsp07fzPK`=^OU3!ND5!+{|&bsFs$8V->GSz zf+bBtdjq}q6ee3wK5D)4gLm=cqgvn8Q8MeUlSkfau1N#x&<~I18m})TrwE%OR#eM?QNT<26(E#$lF7(ayvl^DD{hRArTg1mR-j-X;VQpe; z39nkhez`NbGRdVMgM%=4yHKeXjR)M#g_5{B=_DqRF(B>rt;+Rb3mKFhO>MGiCR}AG z5Ffv6+RgGAF}sGwqg&Dq%T{a;e^q+F5_W>NHYwoNdGGQZ&MTlez5NZ;G01yxRBA5Z zerBfH7$9VO9YL)9fqLX4Ira`JftuvtFBtgsAV&G4x4{{_CQ5jxzr{zNQsK;77*G2fPp@q9;8~he3ej0V?s2G$U4L&`$1#AOF zSnYl7BWHH{#(7|&YP4Oi*Cx8jkB4R6VDPOL@{lrb7wZO}ie9zBi>hsd*KKe3`35({ zkWjB9v3TplR1$?`ZSjNF@k(9NiZyU#s4Wn~@Zg0U$4#vZ{zRHEkZ199maq@gW3ZNLT}jGHDYg_(R1B zO;I+n`pFTEl}hWcGfhpQ()-&S2Fx-0i4|SWjD+x)Qh5x#S(g>!c%%*Wiygqa!%pHT z2?sv-2bt*6|K%Ch1H6Fi;nCPc z_bsXXYcH5?B);VYs>d?p`VGLM>nyKrH6%snO zRC+`v)Is))LNMi{RBO(aXe%~OGf?|6-GJgj`hgc64r6lk(O+2Pb?6^ZS5?N$o7 zZJsm>hikw;BWJN&pkpPrg89Q!R`J2>PyuEU2`)7=i zcyIdi$_M|`a0#S&CQBjaiY(%y$};Q^nAtLe=j!tM4cW3dnhafxH6P(EE6xN2#FS~n45geM$LU`OiLKyliykH1e*liT8f-CVcReZnOgh=Z z;tT@m=^~OU4^qicbB4Cu>p_(-k+q2Stsg1Ys%hA619%M~p+>U%_}}S>8IH-L2JrP& zvK+BYKH&-mho(tw39ffvA%Ewv&)A1ZepPTYSPP43bo>Nw(E!4gO4J_3yj(#lNepDr zd(aVAaLi;KE(a)k#QcYZy~Y<0--=xI_C^?Xw)&r#7YO%^B>2 z`)ljQ5588M6S381`qwHAtBiRBV6E6>@vMNq;Ki*~Eg!~pMbJ}!m#mcJU^vDnnGle` z#?O)MCzGT!y5gUlCsF@M3B2`#Sg=4aFksk!Zl7?lz<<}{`oFDfPfPG?cEvj19(ShYNyH>Jlf=~eHTt@)|zxJ6Y8uV`-O z&_DBK41aVs;~~1fx+e+`)OonTz}$XJr+r_($D zpT<0#`=Wd2_Fb*W^;J2IF?sj=4EP=M%IM{5rJi*8ihww8n$H)1-uTc+WQ#J?BNl{I zaqe2Y=P5Xo=Fmv95$Q!`SgA%YxHIyQ;9dUL<@IY>Djfvp{PGPnRW1;=Di^ec{+5>WXuv}l%GVty=a$OpKK78%7D z+FwqVK(e@AJ630#CmyLV9?XFq{Fn_(M)N3}KjU+{3}eik6TPr`y3r(Ejqf>#xL$Xo z7=Wvt^SiE?0oAVfrN7v2iE$!rvs@3$O+~6riti$^xA`nbMn0SkC5|+i#FOM!ByoyQ zZ(*xAEzhh9B!-v^c?sOpNEVOq4uv4f#=jH28QC$tU_24{Oea%vmdOQJWK&1}#g1=m zVTiIyZYHC0zlu^CkM)l&vP&Y^OPr;g5oaGZ( zd5Ol&B+JbwmL5oc5d9D`L>ECmSYYv^A-ZhnG=`d-c;7QtYn5h1v3O5S*wjy$FpgRK zb_9EXi2adhH_m;4hu;F|B&3nYd!R&CC9om)?I}DSVU&-i$#S1uV~EMZT_rvz$&ncu z4@|4S@|$^yQw^=oR`*B0uG;$J#o0;m@rL#NsaCw{ufyR)U@%Q;wqbCLGl4XZ=W|P) z{^k@!YWSwGRkM+Z^ECccgrTTeDq6ct!pMEH&;ZZ6D2hUIa9|g3l4_{X*NKc{g^UXO z6_04BN?}p&SY_JjO-~UUCGEO=hl@8`!SY@qC*C|LiJKRrf9nUyVg74${L|?L50CuM zLH|Emr!aA-#8pjjxmv#tH8L_3>uze% zfn|T}mGsUb;4mz(3qWDKBv{Dlvt{%ai9PPsBWW&PR2+BEoB8)X9d97tM1PclyTaVL zP^&S{x{TD$Q1ejS>}eikE*7Z9WjX^eoC7FO*3k_0YzRql=M;HEn=)x?s9Ns^e>q}e z#)K!MaF`VH&B&-L;ii_uux0(NdN-mtUTfHH_T`H)N(Irb=nU^4PVU|Qi!{E`@ub;R zEe{kL1KIwxdR{s{#rJ);Ie5?ra?zb=TE@Xs#>tpQj+&OQpDP$UuQm;AmIPN3WI3X>W^>giTW`ItNGjVt^}m4IVb*L}lvr#w5RNg()gzfi9t)uZSj*JtUt!z4I zb{P%rCxeDRbWpM>G-*~%V3yr7b^BJTN-?6UnaEO8BPB`H(pR69(@k>&8jPOQFoxkt z#HRajK__IE9L<)%kcOuXlMq`aZ#<+pRUIA!z$bJI4%^2Nf=SFfmX;<~76{;K|I1ln zqIs7lss~ReBm5_{B~8NdF9+rddSVwH{^(bzrlic_HA2m~XurEv93r%qWJ_kjS8w)l z<9(gkAX}w#314u9uBRp-E;w)YCoafJSU2<6!@z7hCIDXi|(o+MWHeNXzxkOH$ngtike@ znO+Avwt9M=Mw;BLpXK&*+d@tBv~P2?)O;s0-$3=&17rOT<1Ny+%u^+Xt&q_%j^L;qrO;04>=^9X* zYwFLt+m;ic61RN?mU!e=?pn)RB}lY1S5G7M7EXygW8B}XM1`6bqcm9(=%*_E{7B5n zxjUA`-H5}im(KQ-rAVZ!DPD8RUG7h%8Np5)-skm@SPbJoN`S6mNN7T8vW08Eb8q6N zY^q^f*A$S!4em`{Y)!I(&m3I!Lr=$#ExGTHf8~E#!m$+tZk2$HURt6PRt?Nd%T`P8 z-g2XjU_TR_MT$i&Lr13k(B266>-z&?;W=TqRV4K|D+@ zYGa}yAQ!dvrtY_K569W!Tt3e5M~jkm8$k+jb-NOL-g`ASyr_-Cn|0%F65ypuET`s6T$YltKhQjq%M{AGyj&-RmpDMrvBy44EIM zxdq^gVuq_{rGO$f2@w{*d=}&Gxg$6%!9h_L!NIo$I(``}dFgnYJ#%an%%UFwko$Fd z^M`q_uMYCU%*)!qfg?-wP@$qno-F0(Uttr{_DA7mCWi zZD?*tJZ~(_e}r^bz|AtN!f%Kb8Jt@7SMH=7*cbc}fN?^b*ZVNq^$r$Z%}y?sLtGP0 zaAyDDbjfZ(GTJ(S5Qk+cbq;b_P&0QGPJ2L~7jUNYy_BgG!md!ICEz;)HKA;oi;5Jz zGOniz!&i-tD%a1q%5q2cao_ez4%{~$TWu-M+RZIUO_^)s>xuV_vL5A%tB5Xv3`Z9M zU_@$|-xph`raBrGs*Z{I=1iUwz8Sudd8V*9IH$mcl5+b4uLRU;^4s#q0&YwFhPmpe zntGws_&+0c9Yp%y75NhV9bPb}@7JU7LOr30i)>`bKfDJPj1!Y^QtSwwORl0sAcl88 zRz$ieTRI_K23&tLL-C(%rl(UTnNo}ZFs%u0JOWnIYz-` zUyB(obsGK8j$%`DC`LC&n5}G=h&qa|tufCSpi}b_DCrwR%o^o(7J?$PnM`q{>&qD>hy*V(v|h|IrR8sGx_dS1V5Z7W2Wni= zQH>1-%60gAgJiEhq;7=FpAM1$rnPCwKrUO%O1(wmbLwL+jf0P=96WouzrC>1@}bf= z4eeZm?1)OGSH~fbN^I!I!y)S&Z2q6Nz7vIV728f~3A_@qvz0LKR}fp^#v2X`FG4u2 zSL7C}YTS+LuHWU`t3o)0NSo-^ksV6#7%~(K3>h6?7Kwjn6la!Os6-zE-B6vbIL2z- zU{fuZMs{mT>jm7|wsJC^UKCx%g=8POP0Ar`Q`^3#yaJR(`R9HH4(7>vO#;=s`ssG_ z*JHz!H_(ZK83B{%w}+wG-;0h4a7ynS$e^zfq3JhJN}~qrNEFAr+RNhyE23{2J9X^c zpZXNod3eBKcM_fM3#d^*#TH@zKs?*o?6F(m`X~yC$wnbcK-g_Iib(w&iC#$5Q)qbv z4uXk&LuoOVw^Ml8crQ-CH;t@|Fu0Dq!z0!#GgYERjB!m!N)Rhe_7a3i5ZpglJKHjv zb4S#B5v+X`w#sMm&1&Ap(841kUG&K{#$8n|jBkFT-{7XI7R(87JL;q{H*(E3cipwo zLW0YV1MDfAWV%{Pv^wwU6xo;?g!H4?$LCJ8I~yem4Lcm$nK#i4^l1pt{dC0*`?gRr zGV+(0>E^R31T#8J+mZlQs|E91UFTvRKio-isxG#vGw zV-Ia?tRkYEZ(RjgXMAB&j@q7eoPj_coXV2r%x4s*RXgV=@6PT+_3-nPt{_G_W9eq&JqzZZ~jX;@Q=VMDU0Hrd-4^r^aN$rQ}nH z-CzAu+TR2}pCx(Ke=7sJoUPDy%qy`k3$#MQj@eY@cm4%D{LJ(;BtS_>{u;(p@8J*V z_Q}BURfU4XvvDBAk4do`@SS2-#q>(1Zdk1?lCUi}Nx0Q|60ZW_s zVCA8|M>z>l_$6&lK{*$$@c;&Q3XP{4qcz)q9W=g&e1rbCBF@1p^RQjpPquBlCQr68 z*-f@Rar*V-nrw5j?Iyb>O}1_GTkpHq-rqlQulqcY$qYUE#8)+`dH`~xqSZf9YjjamNt!>`c<-_oQlE^`qVWy$+o zp`4;qy-a|6w~t=|u^_>gA|h0ZJ20p|Aof=MlWOf^B@4{X# z+24!voWss-y%h35XTCpwVyM5^lqzMN|2LWHsKYQOOlrDC$f>^YN}zTf(ezo(XR zla?MxS+7}Pue9N282ISe{`#xXrqA()K&pg6$dY$Ha=!1(-iXVLw@LB>J>s|L8gtSa zUaIUuwEfRLZ`Tq1__KdHo4*Q_8s|HxMD?OOMwy5nnH1lq=_NI6%KNwVBx__7x9PV6 zUVW#p4bUH>Y6CkQXP=X+{m!?I;=K`tRM!ipKRLV`HmBvKA ziVZVDDdp6@nHr7${p~~KSXp#j$x4g>UGP0DF zV-&aQ)Q~z0URb{&}d~)H=hbT(8s*Lx)3)h%n_7)Wy&acuzZ? zm+>OISKD9ol62b|K42p&>ow2*o85%gog=~0O9|m$ZxX(6v6xtrvYd-+Wg+ON*ORC! z;!T_?M1A^UQogSCV3$pylHjrrvEW~}w_*R*dA2J=Sd69qCy78oNRtlE=%Ec&4_Ovr z*`0s3csiBHg&P`6Hlrc-t+Q_xxZ;|T-mGz_G}3mb%JF;qXNKqy)~sZ$QufN}da}ja;%5ZLmCakq`5wG_E97+GCeE+G0 z?u4xVXHtWNK^b3EWeT%cA4jYdj+tDbOll#bOG1qzX+u`+sZJ&fB`{)Bq<$H^q)-SfPlX81xn_vUfue&uNpQEK8rp8=_6F4qo1c)iNnG z_}#kF=J+U{(WPHb*=^DYIAA9iG&b{anHFE<_P#bLG-g&8HO(GZZJ0u@ zCw`5VAA&fUC&d?Ybjd^6RZ`i%Fq>e@*Wve1_8>y&MTa4;ipv}@^TAyxN-FZ`_VwNR zx~a)0t@#LKYY!@TLBXkWw82=ZI|)AY67kEWhL&Uzn53Y0CX%p1h1?U` z7vJ+?%z@m+t09{IOVPhe@SI{0wbtX%7IHFw_~b5y~ zkN5n0X@?%6D!zJs=0>uQotipVWBgDi^;4|70UJ6;kzYU35Nh!6OSUn5$H1+q&WS?$ zliqp8fP=&&6Xt0;U=gq4`q$-`PCoDAiJuhq?b!0l=uBb32z@2V`Xw4gXS$ee}^X| zV>Kcdqm`~`qPl|~!-KSJ_jQomJ6;F%@krl|51-wLJ4tLcZ|QMk(toADUm3qc0*OTv z?uWK8!JENe#_w!|b_hTo|;pFU*?xej9N%|hw?&PQ9T9%EIO)ogmBi;pOqh?+| zxm^=>d?nkkV5iT-4@CUetF8payiEX0$IW(y(yA$%rG}qU7jOR)CBxY zuM}2i%HM9R0b4G-2;8L{;)H1104c_llsNsOC*v-t6TWo=1A29@?t8anOP%0(U$Mx; z;V`1S!deZ*gaaO_-tTpmgFMRhcObP3&=oqyKF(5%E*PQIhbfK1@nyaZYJ*zi;zT<- zF8P-`DP~v~;+5;^iUv%T$@F=|mM?DZzW5jYWzCLbM3?5QPY}C0E4SO-lIa? zPv|F~Kenl_{>)Cxr5|Mk7QLtH>JYIxZ{X@$av3w zypBcRR|ngvayCNYUwV#wD?H_QciCvISnE{a;F~F4sZp)uB!0Rfuwk^DW~iz9J@y;eiwA)oC^CjdE)`CP9O|eTdnA%>Sv=F+v?6Z!{pMBp`023vfJ{@)rvkUILT#m1D%)O z^;zc=L=2aVONH;ea7KRCo&uM*l#j;dSW()S(6c0G_g7HwVf6Tpmo)#B()Oh=H=nOZ zuKQ9;fSJSo!B~z7+Td5?d09HmoRFx=tUhjm%07v7b>D-)?HH2j2U;^tWL1xEa$CP@ z0eJOjzbzK9-kT>wCgEr?eT_fnk=o~d#hV%K=HQ^|y>M@h6@B(qN`yMqs5zw_S*A1u~Iu;Xp+(S@r>4WZ^@ULaHHKsC?FxR$Fdl;U@FN^c-O|_e$veN z9J9TjidRjIlgh98dcIU77lg{*m>m(8Oz9j{yY76#Kk1dP~?p zj1yaZB9@|L|H9kgpXJ)bQSF%ZNT7v@TbYz4SpQ*%XJFAI(b1e4gHsB$gzSmj-}>2F zY^bg6H(?aoxntWV53q20`h3avT|ae0dl`XSVSZqLCJQ98sVeo~IHR;Ymk+P9_?hbt zRF=5(?G*IsRe<*ETiU@^Y_0s+l&7|);WFX6+47IiytQxfw(sgxD~7Si6B@RDQYZsV z@!9B?7yIKy_va**)amIQ-Qt5gonHo}*(bQIBv#;*Zpm0cehgZ`)!|IoegJ>#X6?s6z z9JA^@pn)s62&9pFD$L(2rhFNPRHLI}wyG~%_k3L~&rrGZgq?@r!9_&L`(j{UW7f4E z`}mQMt3sYN-+p8EG4?Upq=$5MKoy--VG3Bcf>o^z@-*KRj5xZ;CUdofOixCmPknmP zG&!<1wqe_a|53tNuoi;@wtNH|aaYnZKCsR1DY$?hzUO01H^DR$-W zbmwv&j9#_&rle!1lpY*}8V`MECmtb3cW^(>cyL3q6t;s&KKfM>*bHde>gX);#>=`6 z4=m**t)DS4%Xg$T#jr47nymgQP)vM-_A8>@$08NsG;r&?_`<0(qo>!vLaG@3u(S@b zSMQ%z#k@$yzNv1GE^;L!>~=Ww5=a*p&WJ)(_e>psD+Ny#`1gh7kU`EDhrsMBe$R;9 z>Vh6n5LMb1iWx#*JtNTZy9>6BYeR6t{}lCja2_V(Sn$jsd`xCKSTi$aZu6C+EH1LL zb?~$kT9(Vh;8h->3S)OdSR<553LT#)GFAHo2`B0eL7Qg(X8XMtCc7<{cdmx^Wnig^ zE&W;H0E8vl|6*0l-E)MOk?xfd(ez{!`6ZM!P}}TL$cU8c1ptbR5po$DuytWq1eX*b zTgZ3I|0(2$h{jccR1P3z*WxdEWtJa;S{<@C)6<5f6rmY=9B$VGq*l<-q^&Hm>Z|uR zLbFd)@>!Qe4R=WG?-jr5R`2t%Ct!ssxK%5QY`sowBAf*CKXIiQs6+=5Y}D~X!%R)w zgenDcA15*dtpcrY;2(7DZg2C!J0#>)59*-4<;3(EH=~npOd0G~BJg!vHKzb6O@H}H zCG}cMZk}5B+60!ayR6Q73!LB;6Y(+$r}HF|!6cCEvc28wK?MWHOXzCS5R~;-hH?du z)!1}pL=RLyRh+|Ywx)pnL7v#CH2#G4+leu6`a=RC4cNUzY?|Kg%P>iFQ~JWj+hyRW zLIp8FLua)luQCq{Rcy+lTbh1Bu9=wkZONANgR!%u4&7o*@|5_%la^(C+wGXI8C$yA z69$NhQ)NRKT9)uDCz!OZuG!+wws0K7_mH8_yEERdt9LkEJ+`=OZ(a0~2>9z3HQyR? z!P+601Ne$AtngJdjFt8}M^-|ZOxhN%ut>)7o|k9p5oc44;l<6`#3I&ckvZ$yIfY)( zbc?4JMn)a#TB=V}t#zcYr({NNQP;D@5eO~#iVmU@B@YWFtRFKL`IYH)p@{SU%&hzs z0OubhFV9_>f!_~{u0>9zhalO*&UWEo!Sb^f1L%NU@Ct6{1?zHk3f?E5eci)jMub@B z8STJ@vu(<+U~ee+HXELX1V%=3Y>l3vl6J*Da~R+L7LwV&@Y0`Ww3VIbrX)?#kNQY| zV6j%a){$=RI0{a|GQZ-#b?Ie;Az3>I0*dG)wH5poFNjVtjx@VyO zj2KD~@Z5=JHPG%irRN}_f6qfTP67Y(qH!Nqe;{+ONfQmr-6pLd7KmP#oBRZYO`GD( ze1cS%@(i_gFv4ph5>?O`4X}h`A&&f$0rrQr!bz)t9s{cFhVYWDfuduUw00SvpjvK{ zQg0N@P$DYzjEDe>G0WV=@0PcbHlY=y3hwwsTMdmJt}UMHe>M#p?@O^;gg3!Z`YGFN z+5<#QteP&3<%9)W2$>N%k>7?h?gE>wKD>nCyW`K&XmYf^IK7Rdv55y9$ir@Z2OM^e zArBkdq#mwIf-nYMhocsZ&d1Sqvj*(HTzD>WQTi-BJq99woc*iF8yN^wifII;tRr1Q zm#Q*gmLIJP>E38CTA++Sr1S0Y^4{#Ncc8MrP+tD_rVJlX$Yfm-NWtaKLVGl5>vHNO-Ts+3m5VL8-Ufl2Y`ITO;drMv31mY%;Zf%K*3qCN3C)&Ja~il@6Kgli7Y{ZKq% z8EeqsWUT1NMU#G$QlZ0**Q;V5q78Pbt6t-e#eDzAGiBBS{T+S`F4xqo4v1G}X4b7Y z{ROYIooQXG=QH>B=a9-r((Cd|AF_#r=n*Wu8AfQt#86{(TB=ruPztw*(D{Mb#U;3bEF6AP~O znuL7u{Tu)R^cepM(*N;+{k-yn@Hrb=#d^`9(V*E_S#_CHbjKc*l$BzqxYr%%E& z6Yq-Yq4VF8wpMymbJ$LkaaBf=0sPC^wp}JnB$_8Wmyj17zL-;7%zugl-P7(+hZ}fs zVa21(HWNz7&aOUbrcRd}a)JQ9$O1JX4QQ;JU9|ObwjyJnHClmuuvG9cdyFp{qYi7# zn+9u>j)A_Kk zF!5|$;GJFnuj^&}0P;>PL^{BV&p&v4%hU!F zE+E~n{FVrUgOdBKSolN8Kq2mAKQKReH3VhMkaDg23owq+qh1sn zLFoOcBKEy0?DwA_Gg+`#DbC;T=m(^YcgBfb)B8~|#D{C&SB z?tTlZCcM5k!t4j46-Lqs_oL@(j>wEZL1yIFWhzeUE8&@GVlq_6h&-=_Wwz)j9>+xz z{;Dhyi65UJAh%6ZRuf6%OuS=sZd2u`a&d=tB={C*0I?35?aQ*7_lIqchEfi4hnC$6 zJ~uyjNxpa3DdO8p1JUcCe)x50cw~A>&|zakvgD;i>nuT0Mll(RBwFA4y3X=bR7l6^ z=HJON70DSUCv4@9OpxK|&KxR=S;PSSB;yc_Hd2W$q z#0<>#7`7!FW|zb;8>9^)&j;x>cAlwFLoa_$UWc~pWdG$%L|4#Z+j}pJ)#>5v`<4V| z8pIlQ(IJ@*!wZg8QQf>P%+W1ZnVMx&0? zTI9`nBqU=Q;a3((BhZICf3rj0M`I6INcSt>GM81vuCzJ9(R?op`vlFy7UVD%n+3N+ zLzmK2q%apw@5uL>8Vvp}S}UfbJ@kt=!fYW2{$kYR3J$g-LLysg;yG`Ew#M;rLN15{ zsZE`ek2|q`QtBn*6up?P`eBIJeK)360hzKW2Gu0N7 zKt4*U3RL50DQ)cIA21yl1#=7hDrNsV9*hYK5kR0!pA$%Xlzg&z!lE+Kh&X`j{jC8Z z35$F1C@`F3iA{(53wbY&rVA4WXD1) zOKl#%(k-7x1yRNFDG3f|Svud7I$tHrMQKPZpi3-A0Dgh~sLpAm8=~g!d*<0LJ3L{W zVeuBz<_q{gJfe~_mPB;#T7~w$K3nSdoH+l%^K`8rT~sj}3tZd$u`ROna|8?O${T%50tNL7pI-*@eutIV#+ zR0Rw80FY*@Lsza){Hf~mcYl!1JZTw=Fj+NP>aqJr_8}%o=HT%YG}mTwR?qLv z@9nJ+wT@8=<4(R`wVVD2i3gXh`nNlP5*|zTH&KYrz9X}G1O@^yH;K%vu|{xt2! zs=u=OUfL)hP9gLUw97Bk{-2=Z#}9dbc-WYfRZkBzv`)X+)Djg#`PRu@1-TJF-!CWa z0M&W*WHXj_$wzM0V)n`A>l5H_u!8g4^$E(!ef)a%M?hr2EDTS`t&NQKHnzThk&RU< zta2C6+|CA+lNyJzz4ZJXM`U$+`U#>weVm(l$GW_vm|%VV1kHTdk1G#dK7E4rg1Ugj z_e1<<^H-vTzjb@qVQNEHoOOq;RnW-%_Al*c=FUi!$u@{@wUO!2#`Y%G%2T$sW6-!S|o(6HIX!M zhs2bAMcJ4Q=bCGlp41fDwbBU_j#+$zrL34oefX|11k(FeI% zgFT57VT<*wi+RBguO874GdimhNPL46x1gS9;Su4b205O95h*dZv62!88r!)o;^O)l_%n9G0vijYwMy@3B8?Gyv_f6V2-$OZlX zGZ%DQl0L+ehidB!MUr1AwX{Qq>~*EPM}lCawN}on!Av708eKzJ;De*DoMo}Z?>1}u zD;g|sU@w?o+zdh@F%UrJbkc=hm8MFEAB1!^Z&mvut?vhM5DQv4WrW{a=8A2gTc3k~ zYrv6ET|n@F{GR7pH!yCpCG zTHRY45x!+7`x7bT&79irq}NfLtIT+OhEe5b(*nGJ&Jy|-BK*-&dmrcD&}1D}3doc3 zX!gy{Sl`RFXC(15au?`WkC@d z=y_Zc`Kyq7iB{)w zPFwF;SnmRaxNpU{#Z-GmKUhk1y0lt;6Z@uZgg;#bwk#))XhB-*<1QmY4G|npMo5OP z>#Wbor(K#qvo#MBS z`|w-JZdE~$?NMeQ_fV1_RPTAqB7TKA1K(AWi1#O`1XVI@i!Jjn=nn}0n)zt)V6&6G zhZT&@bBucB@&%eQ*^pP~eq`zfw8{2PM^!?_Y>jh8fOn!7lMw=!IAZp75armqlH79Irh{4sC#7C#}oXN~T}{F-St zTJ8ZCqP5p{Y#_C!(S1UnOA-UanQ|0@EFYvXRj^0Tk~90@#tQmiSI+wmyZdD&Jh@(A z6V{Kt0|fPXxLW+A#B)uE-g2Le%fvT^fd#@1_7& zRqEA;|F!H9WW_IIm1$kiE;Bi?X*7(>_coYIo`(-Wx8}&K&!|y&5w1Vhk*(R(;fSvP z6XZyYnx;YS9V6`K9XJC$0xp@t>kC=trL6h=^cWdGrT11R5e7#)t;sKVJD^eVZ(pIw z@Vf=8u8^H)#_c1n5TD+0=P}+vSP$Tm)@6b?26c&YPP&kf^kbS;eq_$#s0=4hOe!t2&FTo+*dN9JW< zBMzJ)c)-tpF`F@I*POVdR6h`n@*K~dK31%6Fm}+>RwW$;uY+{{g zX^%Z0mzDSy77wlLS3CGTzgFPEx%7f0=kMW$yethpn!jIGDoL;sSC6)@@h3>o2~i%W z6i#0QApk3pB|ssKvGjaFs6W0%q1-R5Zj4qHFVpJwZRoyP>XJp0xRfb%TJz3iGbjF) z0f6R3&iREEGX2xWzm}vpAVvz?4W@)$FYyTrQI63;a%}J!iIQ%Jsu76R+At{9Zfe6^ z`>8@kC{hhVre~XrS8tJ-3zai;IlQ>Je*IoI0{sjP;7?moW1y#aiy%vG5pxsp4!x#V znFMbKxbNaG#Z0hGF3DJu@L(heX1vh#|H66bI8Gykm%KR~eRiTs58T;xL*HDN{MqOL(YQP zI(a(6{f2#sH!x3dzP(9B>5hw-XoYHKw43-Lxi8N>&l^h#SpWOdwmRbN-tg$?%o~Y{ zI!^|ghA}|Qj}V+cGv#czzPmCNmHlG_}C6(cKz@S$aFO4ZpDC#F@mw6_1rfUaF zMs5o6p0%2c_jJS~R%XW}xHxV~d&qxdNO4o$eL%lf~ zzE&p5Kje1##*@)eLP)42CBt4-l`{2JU2c124FgmS{+jdO`hJ6kH?ajfy3ZV5N-Xgl?II0b0!r7+W=)RFVEJ-1jJ0Zs+S|DQxwl$^b}x& z_5~8DLrYtx6lKrd;17fbo=OBhyGLHQycc5%ykIr2vOo)FlEr4%j+;$P-Q>Gz79TAe}n0a_3k!MPm{(w`Dftkq$>^gT^3!5 zu3+&`Hg93V^S(6%rQb+(S#S1@(_N(KS+zuXw%$TGzwKxF(A&`(JI#x+u&3695IiV= zhS63pjk+jfv}RplFF4+2RI*N7L4VP-8k%jl(qd`ym96Dv^N&AoBKUgAC_(cnJb4GJ z5z5vlq(s%vj2z&2!4?ZG^Bv-5MYXu8?MOcJJ6>3Mttjbwh?{zs-Az6M1TTeODe-KG zP;xl7EzT^Zh0DLtNe^kSB#|`cd@w@&6^+aIJ*|AZQ=O38}Q$_;}c~|+$ z#9OsC%E|UW8(;fzn9;mq(qeMhm8DZ9xc1(+J7I{;ns0WM^{&=l%U=7REqlmIs8Vz? z-TmFPr3VZ|z(4M8ESd(yiQlqgO?hpgMrnD?7jYrJC{Ro$y5T)K$v~RJo~Ex(*;{QT z>HGe;e%90`!FU?-R!@5j1xWyE1r8XJe$VOcwZf$a6`oNRz5`Gr>u(+j+djzK$mu4P zg1Nm(N)H2-GOiW(3k(7U_sdt|?^}GohQP}3P)j6imPZVM=b-9<{3#n#4th0r@*4P# z?puU}X90s&-M6)@{@#1JpT+Zt}b?t{#ONEW+H9xrgPI}9oD7&^^R zsZ{B!xU}GDhyC48^=qRVKF=@dU#6RL&n zz*?Z29Wg;?8jc1vu?$F<0R#)M`J zq#+04NQN_r8bg+7L?dfTx)ktPi;g9MT5+85FIG#uW`*p~QOt1$H=21QZ6P!9i;U^D z`PNb5^5h>RoJYMf`()DkzB&9JvW$cG&a&Fr^n#e884Q>;ss#2{0DCd+r=B$OeMze4 z;poZSnWR*v?CWSLO=4k>aE!k?JH)Ze3p^{#@T6!E1%k6d=<(ML`8AX4!=v~Qik3Ih z0nhyvmO~{QilIBipP+*}wtq`=H^w|q>iZngr!PHy8fdkI7HfPM%5Hf!vwPfG3XGU% zU?>pV?$kn#Ht@n10CKfAHZ&8C+*ZOIejqYx8d%7$RX6QqofR0Qo_n)p3HX}i5hqZK z0`gi@uLusBV+%2$txfHBCbvyQfJ@AxU%aHsx%p;n!eP25MPErw6Qhg9o$(xo zD_rm%d4*afy7IfFK0&-{o8(3Z-1}1D_Kk&pF^50a6a-u_&%_H=R$>3<_8W$v(!$** z=b>PGd`szpr6eakB?r?+0|rRl+`S-(%j&UH=N?v`Q&x12IvASc^R|IS063? zyZD^VK!cwwuOA+rbaLGsK371^*hc# zPNuHo!g!n(3~x&90tTpi_~BRFe!y>iY1}6n0aUSIN1zIoqg#$%dgyeevgce*@HAaw zn%DLXH7}vN3HxJLQ95FD2S?X%+g>!_4ik3KvZ9L6B<%OUsTNdYB(=a7!65#|W#>9o zb0J49g-SMT)8ygv39N8QiJJ1@6RkH5LWIm=f&Dq@-2W|}ahCeJ^(4>HUg?#?xVk^?^=K~2bt%%i$YT_=^O@#8k9poiA1j#;wUI#7&A83TtVJl($G?5LgF0Vj#9O9NVs8b<)zQwS<8_9nqexvJP7n@0TG)4O0GJmu@cbtT-(>VS&m6Kq9=}R3quuR6i*EcrDxwmky`^x+ zn;XA{s*>?dA_sF@d`O^wuGh~ZXh>dwC(~dkpfk*FY_9*CMVtxr7*ClP>vY>-i?5sd z4$WVt1hMz&A?Av`3{bz+|Sdvu)5Fj;~-_ z03;}6%>aom7waj(_7xT9pjiPX-)vEem~(T2_K%zFKuLeZnZ_(MW=yViT^--IPJB}=Xa^7;OjE!S5@ zp?$|!%ReiFlm`2uK=}Q}b%ADleZE@=?M|4J*#n%;OY_C1dDLb$JuDM48~uc8!E!u~CWILqsJQ8jJ;uy-J9@kmsz>bo$Sm9!Zw>V- zA|hw{j~nZAj+@21WorE;o=%RYb)H&wWc372)UGTg8t#MxFu=D~tSKG%V|EgXW>X^# z8+yBDt*R~mbqq=Q%mgizl;IT1$v(c6$Lp=?yNqD_C+N@$R`iwiO@uGc=QPSIKzFwu z?MZltjOT%_J;$UsK;aMVismgEv5syO5QKwcM@)eqFW3cHqY&@*OvPvVERW^7awRTd zh)A)z)w8z&C|(fZ77!_U%O|Zgxy0cw2!>Tk$DoXIf%YLmMSjafTkONSu}oZ$ z+i>>7hgAkEvdyahHTHe3W+G>r~l^{!froPpsoipW- z?FXneJ5*L=L`ze`1lXPU^Nq=)k&8dDNj1KyUYf)@6|S~KN2zV0<`sGM6*;c|{f^P5 zq(j8_$uAxxbIVus0Z_aYiCkpBq59lwoeX4uFF0Eg7#w0!ib6%lUiJCJ2ax^DnDvtW zmpQAah@`s6kHR@!PDQL-2yP#sH(Vhq^AOMzIrRsuV?mDxGT?^@;SBGu#4a}H+^1d} z1}|U>JqdjP%IW61qXyGoCd$HIXkbBmu|8LjO+k=NEX6eQ1RBl>Isw1M)-JmJ&nFr~ zJ!Z-9`b^ovWuN{ONft$X$lzfolNsD$H8l@T@?`t#c%!6wq;SsuLyH1*R?T)!bcz=F5 z;W3az%D&qU>Rl3|BLA)^_;yMLnOSFS45Fljx*sObQXf;MnXr(>d5#)Gc*xH%^4y;( zWq5IQT($}o?8z;}jc%5cS>_jNtP*jC{(wP(+gSdcIb9^sQ^{93;^?xI%hYhT)$QgM z5zdCDO(}rsEiIzgAfV)05u2PTb1ta*X$ABQx~}b=eo z>F(y{t1e|?R5Uv2S6(({cIsy!u3D{FBA;I9M%>dFv~)Z7hEjBQ*goOxzPGWTL78kA zp|RapgL~cH$vcmqMR|hv95~|$F?@Q?&`;Iyd(qu0?3weCvTFJ%m?EJ|wpgmuT?gX& zTlx9#ai`4l%d zkSmC!TBMW8Hb?6140~}W<+VEKMVsEmpp|G|@Q-5W_x_aTo3<XV0~Hm{yXwDRzp9=}jWXUuFA?ys$D)#- zG7+$g6TzjD9sT$oi+Z+*PgMLBWK@=0cxoh=IyfV@ISU%{(v$8FjXnXj;*(7zo9q}~ zDgou!dOw#n0-`eyOOqUD45f41%?q+Dro^ZWuSIvad8 z`|(z$;{22CfP>Bwsmk$9I`EU+5R7H8x{bJwCxhwwZZ~V14?%z^-zwXesM524u`wiV zRG!(hX&Pq{JBkUBc4PohkUrUCY*MNZX*W||NmIsbpRkrFsybB0h-lKqQ1D!J_F$Sj zZt?SIgUMCwJL8w%cftC~vx2bRYd>8%i;)JVnai6{{tmIv?}OFT19jtgc(vobLvJ+_vHxQ8q@7HtX?Z}0T`iizYd6Y22Ulb6Wm!I33Bet~sEN@s* z7Wlbc%eSU^X$VC@Tw8b51w&Rj?EK9t5~g{`D;(`N^NLMM$!IIR!q6_}W!8$#N}TFp zx8ZJ!&gIlA-Sy7DLAY{n_zQXNRZ>LzQ_)77pOf74^E7n}=l(o*9gH97vc@#FGzUV2 z^oYyW#Lu=&6lt;}$rdRsrr3<-`YD~h=-gc@3gACKG>Dyp%rA)^?Wf+yuDHXcV_3gIih5(GomO*UD4s-PF!kZ5*Su751%`7#?6x zf96wW!6oXFypLsFy4VQ0d7c{CrYufnSJ2n_nEp!^k03NWI~XMJkV^1yve2nX8K)Oy zC7?tilv0ZYtphRwX}5}uEt)!DmDU2LuT2t*XAAs_CpO^boPO9mkSAFj}M_1CezmFv|7I0g!7kX@V%2hU-*l&z8 zS`WK_aYfUp;nlDw8lNaenv@~~AQJj(TU+!t{a_9)wK#7eC+k(o`5Y$tRC4@$V7>!_E(ei`)>W&};R+}pb` zK?7amI_?jyo^hr1fa6TWNZXYOI3`wcYL{#t=tRw>>Ul16hV39$*_(~f%@rW_U8HK$+ovTfLv`cAi2$HOLfKJ?{xNu0tVLxc_7 zoCO#;WON@6X|S&M!Rj5~Ovp$$X6iYKH(O!#B9|<|OljJ4)Sa=#z4#I-^mfr} zSOv3sid}-v!DyF$DpjxZ++opcjKXcIp5bps&oV%r9;4WW0f38k1!1w@s&=Tj;2S5W}N7=j7b#1VD!!jB@<M8Xg{t`CI*F;dN>|Ogb^LW*!baf`eGhT|L zf^5vI{vd_UX7LXp%_q^o_2e+kD^us@cXBA*F>HyWage#gpFV#UFQnfPHymK}sIVgD zcZE{}fy9H@#t1|S;Udyubf^0Don{aZQo&O>I?0MW;Ah;wztNyI;Zl*LGj)=!C3Q$* z`jz$yCm!)vL{oTiNoeKlQ(&)Hg!saXUcW{ax1uG?fn7+IjSTR4@{lxD`_uqu8|VD3 z*oLc8!EiSd>^*9QTV}yL`T>ac=&2nmuj1Jg48d7-H?IHn-H&pL-?Tp5H4a3)ESVnX zXwcKeLWM)NJWQ7e5z=0HS#n<5bfRS-TDcHosd6K1SBKnbBn=j0pAOmNc-~d5DEk(* z{5YT-@E(2p31vC>0KgRa7#s}ovuikVh{@8{h=UiLb7U^EIX8r*N8y(R*A$AZ%lQOn z#z+c;*-DY{4IdGz(Qp&U&x+NIgUXS$iz@(TH)b8i(N03_5@D-hFF3Mik zX?M>;ldB$8?T-QcH3!O_^ zbq0r0K$y4#p?)U1d=>9um-&$Z0*_4GUi5qF07DUrO!yAY$O_C_$bD;(;2fRWq^iAJ z0Bo}bWGSZf6jom3FERZ| z|5Eaw-j_IVEb!B!zGX2mkE@A$fS?9EdNv;EY8%w zMo48tf*AR(3L@==9{C=CTD7+lzhp)fPEaB(2LGZ|jr{vUxLA_(ZQujowtD+HMN@9S zhY$Mp^v<#yH+-xe!I|3Pfya|@|3^cstaXb2)H3hJ4zElg~A_DM&&yxn6vg2|1^JV&h43~>5NL`=xb zLFLFF0B_0<0E_4uCYk=jDyrt*)NfF96#ZD(6prGxNdPh2s~$yH#fTLMDTX-tn_dXn zel%=MazENy^aRFTmm8~(O}{$ax5#5x@3JkEbn@L_X|bVs#zZsE?!blWi4}K()s1Zb zrz(Sz_dI1Wt0vg>&{2w%9tN+Ikv!xF5ZXzkYF3uP*LO=8n#L}H6=SDgyO;y%HZ}SD zA`+>g@Us1|hA&v6(QPrRlpf*nP|MhD~efzawVW&^+W>64?_KHYo9+({eY_ zn)k?sungMRWZ$TUG{Rnn$@lhS;qu-VAo$e7LX71o_W3OkOUZXET5|bf+CB%(FXPI`R(_Xw zyM5Iwea8>6F1tLMC>qHYf1BpvGd@6gsoEYF?O&Y1+&lEWO6!cO%+s--|fF6r& zM9?#Tg+DKr5W+hw?&yr0w&RxZ=kMBO|S|qt@R<5HhFUkWK@>p75l9uqI8}+ zXQEdO&fG}e*O|+(aFh6H3cZi3{FEw2`%IM8p8+{L@j0X-7ySI)rJ+#86vWx4BjZm1 z)QFw$+udLA;|I-da&R2RJ_U|FK^GHrXjPHt`6P)sdmj@CCuSWmVf= z+qr23VXj3zOUlM5HJR0g4Ehh@2~Wy5gozr+lr=WcX89~Ye#C9jeWq=b&hIWdnc_8% zzh1T}YN&>(ICYOrC!z|P2h}pW2wEAiX@2MSieeh#2bK7zJ_3xxQ@OmdHoUbOw;?1c z5A{iBj>zF;hf0z|?#XPn{fP-d_H(J(7MH|iFmVe+vqYm}O*mjB5=?btM|~(BHxnxN zS~YN0%L4!ns3EhloOfdOVK~^f4JyAJt0CICj(+g5JYFjt3dB_b%_=GE1fOL%T-x`( z=c==OFKet*LIdtP zo~C5{^P+*j?S7TsALz(c;E&?e&`LDwLGFXQCWX>j$-GdmEX1cu|5&$By&7nlTC8UG zF^@E8TbI>SZSqfmR-XCu6c%tDv5RUii2F)Rq{EaRSGJXkDFkyimPoA^(;lWWv|hn{ z@+H1oBb2cN5^+qki}G)ypXPebrf|+i44O5$qkSksyrqqAxxsJuHR};$W5RgSrT2q4 z{c`ngLU()znO%{^5DsjW7r`ph$!asMZ@T+yyq~g|w$Uj;*%Se#F8G$dxQSR7VZ51K z-E%GsqV#DWVUswTRbCsynosJ)8$@*h<0U%0oF;M0w{hGzObD6B{w^Bqg=v^tFo9 zXD%3zGDPk`m=5f#&HrjaPF3~w&ADqw_`zFVD-wnAth|F#3Z33mNn2-2H*pvhfhRj@>eg)m-T1xZ-!cCmf+{jON;EpM7)iQ|10A zXzIxsX1i=G?QTc;mn_3pci-JKj%9kC#B}0p z%`W1hG+$ILS4%clMen}^3R8Rl_(X-{U=0RFg-e_X3Ay&nbz#B=+90xNgx+gP#+$M% zIUR~1_-Ngv7BcQ_MzMV9Fz$W$7Yg)%(XKoe&{TkF3wl%3lu$X05L?l@FkUL+jY8_A zFuj!3wYF`;1LaYbOu;dL(s!@RCLx}c?fc4OxkF>$KQ?Ky=wAYv$YVr>GWq^yr7QDZ zRFWA=m6l7=NOcbDK7l%y%r8L~3Y)D7_3>Xo-I*&CEBxpewIFue!g-OVvkYnv+h4OC zHe`$azVe;tjgvfqeFdxRi5;O@r44G7OdkMzyh9wmbZ1w8^2st>H07&I!FgXIoLO)R zN-Hh)3Yk6*l)jB&%OL&(fE(RB@0*onh$EPo@hPzHzDO3h_gNQ8B2nkNQ|Gb4Jk|#) z_dhGqyRNhy%#VGc{H|;8Tu*9AmmEcgN}6fnyY=Hw27j9O*CKOcQ?6mR`qr1Q+rr_} zTcT-nPv1p>bIf1x>qnFYT@I*vKc^A;B;j+Ny98!!dg6y@2Yy!>XSpYHuo5YJ0A>W| zO;0QQzZUYpaYu8_yBqJFBsbAgq~xrEOk>pXZT0tOFrzzWj$4!Xa%o$+?n`OetWZ_R z8;qK5|0)XC@Mef#>waM$^JzkIFcK-6wA_v4SD{q@>)yc+~~)k)9HP8xV-cxK+zedb9|00 zOp+{gZKH>T`LJ=ZU6ny0|A{5@7hea&9QEHtG7D<_2`-pX*1(XHkG~6 zH!xN;=P*l{frW(gzwOv;%5KS|qrO_>L4oC21N;>6AFBaq&p-v9XD$iAc!FDJdy& z32EqPDCo#2C@DVH0kSX%2na}sNEpb-7!-KecohF1fc`i8(F?#v2E+nl;b3S0u-Gtg z*f1Z10CE5T1_1^J=0EH6zaYTDBO)Qg!T?Y{1_9Do02o+!csNA3|Caw3_#ZWdfQ^Vl z&5bLqjYMPR8JdX4Q-Dk>1MI`+{aiOo!0(l``U9?47e;%@rxmJ7U z9>gf_ETJRn)`hbKpV#EY^5j*F&(T$e$F?*uBXNYdxps24u=jAn2Aj;iBNdjXouyR| zUw0$Dq^ip{iaddAH9T#h7b|Z&ZfAF$Qmjt{VsqaovdYx3q*(*9HoV{*G#ke8y~(C0 zHQ5nX?Y_w|ftw+2NUC!CXny{zN@W5pm-Tn&XTnR!mszHX-^|JOv0NHqHL)h@#Ytz) zD)bOWB0d1Yhc3f{HQH-WZBq9fMs>FA%Cv$~oNa&OZ971pM=t$WvNJtNHzr^)=lEDe z`Fd0AvC3ndq2gIQy63}t>!Hqu0djGPtKA4CSO?XlNh})UzGRwjHKf(#r454*U+qs_ z8~m6UU!~uEOE%^+ZPxDJse0Ob+}_p?9_r-5?kTL>Db;dI_63Za;53_<7TxgvB#z;T zPxn={ zLM++<7T*s5ZDV+nLT6E!dr*21Fzv~)sU>x+$A6r-%&V&ihtC%WL*baw?2@)G>kwd! zZwyQgOKDq?L9o{H=^m6*&S9oYK<$@4rnF_AHv>_#9U5qutZP~4mm*)e?^w5pD3M6| zp2HvZbi`+Wa(?u?4!fxm{R?lS_iRkWhM&LZ8*Olb6rHNOMGE$S&`hM$W*p=xwOo@e z|8b&Q)Uc&TuU*w!VE1-~w}C#Eu;@Xfvu|T`G+OmrFj|(LrHuTrVB>jDdIq)?)v`+_ z8$m(N7mx0*aS(kvW|l8j4FlcZGZ(u$+Xl}YE=TP>E70}u^m~c6Rqgk0S+32zhxUBup<|a{Gi2JLD)0!y2XoYi z(~?y1rs_-((~6N@kW$n(CxjiXCAs_rj*RHh`{sO}AQJ zJEO$8p%EWC*!h>_1bufHbJ&|7HR5P<{MR4ls{pC0v&v5VtPcQ_?5JZt!={rbf>Bao zmg_iD=bTpd1N`^=&_?4jw&o@(07xU>xtP?Ky^CXgDhnTr0A=fFTXlHMqa_jXi@K?_ zn;kGNDC;(kBe4stIER-GVA^`Bspjv7 z8`-=XEjq+*!|{|#adh=G18h7hDhUxJ|%PR}Kl-;ETs-|{q_v@84IkMx?kTvy5Q?|+oUx@S04wI-3&fLweO>{8g~`m_W9 z@0_OqRzqm?WO*y*1Y{^CvTxxzef((xTSzt` z-0h)Jq@M;I&4?ld4F;lI;RG!meCE2_$NkE9siNsQhuH2dK?6&<0FRAkJ>!Sg<-tJf zc#O-^wnNtAOJ~9LH%3CkQQ4tNl|_*{*oQl>7M;CGt!X$H28Jcog69?vWUZb=_dHY3Wmun z0##ua&~?7{`h=pC9TKQiitYB7;_w<-{;0aXD2k%ehMsTI~bIE$lvCTv|FfN!X)Zh5kKx0B+hJ!A6 z{igsBG{^hS{wJw`Gs8=DdQI0o{acYC#(jM?mM4+*s@cF+AQ(Tm7M3&>)lt=+nRJV` z)j96x&o+bK0rVsQU32)_Bbi)r@@b_JzjC9hYJ(t$vI77DZ~;nkU6YL{ z#qO~1Kp!B48?N2hQME87t?E?9wvIGTX;Li-inx0{2}VUM3S)eMgewGM{AKgaS0#{ z-6uC_6p%N0@?weEY>n27y~6TLO*M@`Y0`QEdz6ys$V|o9&DFZ8z=R@UkbDTDfBARcWe$hgaBYUeZSyUNZGs_6tlK*0EiwRY1&-bG8s#`a4CoL0OOWLswocAni zqn+ukRE{cJiBuama2(CF$$=Y%hgCQgi*<68@fWg@XtX~*|846UF8p=;Dg=bAJriV1 ztcwbsWdhJ`3Nj36yT0x#J%m2E`gvLJ)mHf;8|zCPIo1Ple|K_LFU>K38HNW9H%4bb zbh#?W7xWvOwK)pcLq(2M{XN}INWK+Q(P%EZ1A`J_mLcIeN zYoqi2s21k8xzR?8lx;Y>Q>g)a>^Y{Ud2=|_xjEII6j8I8SwB%>i8kwXCsx`+4l^Ou zxrLt`TnfjJ*rz$oL_W{e`1OLcxwrXD-8`m>Y?H@5ZKRh^jwK8C66vHisA?yhS_KMTap=n1G@IClcO z^HgQS@;ZZ<5}RvxP+A4U3=#=eM2u~JH(oFR^+LtfRZKFp+vn>f6qwwmnQ!qcT?ROW zQ<$dmI0F)$6N9VeoO+rF;4{js?)4le;00gqpnfyAr1JQyci?8y6q7?R^lw-bQ&vK* z?85n{E1H}fFKafhWsrk=CXAu27&%j&Cs zf{%)BqV1=r-WC>K;9SGJYo##P#R4R`Bo#&JUh5$;ZV9-Js`m(-BT?|zS8DaO$qGdG ziLhxRICsrcT~EEpU9HB08rQic^v9|%uzF!C5 zuO!xMJh9XLL2l9#J#!STHyQG&8q!`#p>3zmQjE41fe1cl*`1@W@kuEdA;e)Zw0ZFH zxc<*JPyV2a@Z!gx+rffZ*Q$;qz-8=ZeLoo_UkQdaQ1EAIkrXO7x|>4>j*|QV^C;co1?Gu5vB2ZeWRF}T zt*Hd>aiwPkgOxs?+E^R&>XJNb|J>XfkyKsNNzv!6?aP_DI4AHO(|8(Ss$98iuSvxj z$gfxmvOV5b8;GW#Q|~X7rT=lJIo6u8xUk;AL=ZwKj-Jnmp&2miF8_!AEws+V*f=dy z04RmPx?ODI^99`IAe?T#F+Cdz?OC&3od4Uz*k6sj@@B2+rzlsgo9_6kEK&-Y;UGpq z`6O23I*9+vHncscw|=SAynAl2M#p5R(V$oh)bA59Vy8&mHlOpq+>5X)BTF74* zJv^khEl%>y|8Rvyh@JmHz(8*z#jrS`l3A-<nkQNmAd-KK%iUp)UVp~e+!2fh5eSkGK9yrJr$ENCz* z;R_9F;<}l>lS?{fZFStv<2zq;WAjt&jL6uL;Y|z7EVQC#7^jT&pP4IGe6lIAL&L|EDtjVI5+)s*@d}SZ?u7&Gzpx8X#pPe3iCblQ9p22I{~jX;-3aY` z87OFLrKn3YNy<&Oz|pW;QaS-CF;ho#(~B$e44g8W;DiM?h7k&n?31PokWlvf@>$=# z%FMO?>Y&$=Rv(&{K}zho{UIRas%jr4s*;|nJ=2jw-*|r|rxCSQd61IK$AFWd5Y=^$ z&0^iy?rZpWDOGo^5vbBJGSdeK;wT}@QNv&_4>;22XOaD6WP*x8Etfbz_(pv}@=Mjo z(6qAMq8SRTFW(ZJW0)9$fLzA`R&|4HSejyHbC@GOhn#!{MB*sAyM%0hsKk^z`Z1o0y;Ch4} z{B{|lXeX~M7U&s|NhaKpt-=so2Km=QmPDD%B1j5;8ohEmjF=SX?I-6+MIFQF)mi`G=)fVhdTu0ub$i&rlM32Yxwr7o7(*N%FkF+_FAILx+@nwkwkylKpjpJt+#BW*X zZl^at9-|c2GR$N|@R$JanWx;%3Wj+zWKOOGc@pwN2W8?}U23@Tqb`0vEEJEc^E6*Q zB~)Q@=dTfL`pdo*kvN`PbpEuTtU_k&Jz(6Hr8r)!$ot&Htq|RK3)j>XmS+d2np!y#)A8!*!_IY6lM$tV^sc z`srn;;=8k2M&&e98kyFjP4>pVxL@$k@I(GJ(s8y)A&&4|OAFP1MD1Ka62DlYOvITiA!{?slL0&ISmK>4%U4=?9 z$uq{3SIa%;DRm%R-vmBcxHnQ_q;t}*LF1L_&QVI|MZwdXiwR;|_7-UsGwI`hN)ORL zF?90~9Mfc^=#1PA-)QD8cnYDhJZB3D48)%FsMqIsT|7<|eDgKTjw-6<$n{dKBozNlNo&=ho>8@-)^nmOD+C&XoufzM}G&Zn@xm^Aov z)(>(6HaQ6GEHlTy_;B!a(m_~AXVev{`qI^Z+l*8=*aPAi9j4O+PRI|ice9>DQ8*kD zUz+k%KLE7k%tHX==5DOyaHb6iduzsL<;@CdRBMvMYFtJ|)8`8gHE}bk zQY*F$N-82+Ftq{K(D2ZGPgzur0H-Acls=$Xy)rBJZ;gk=CUwEBxDj(4>77myau6Ou zh5|Q?<9ld&B2hPv@LMtF53vI#br(}7w&aQH~JO#72)Ze3ebD;SxsflPm+TssSG1#&E-GUuxi z`ABX_mIcEzDjU2|Y&l>b@^ z`d-f2&qZxS7N}dW#D@50?tB%S?~-x)qm$|cV)g!wv^XQ&!vg7w)tM{T0F*xuD(D}*${Ua4PoGrKq zPCQ#doO=fas+_;Z-jbGhhH;lU;J;>4+ROX%?^l0aX%n%(WJPHzKfqZ8uIxT2*|*on z6Y}dI<{8~;t~fPEH?!J*iqqaeye;9%HTg{Ta$G^D7JU*8*;rM{`F-M4Y`;)B$jI4|%F5&SvDY4T)Fd`r}m z>~?yU$j#Xh&o!srZrU|~U(TqZZPLTvYWs$qETWF2vKD*LZR8|qx=r6fhY2z@95Rnu z;+CxfCCbT-#l=_$^FVd9eA8GT%2vf7+|Sewbmdrl^|y3lfpA*gEQ4T zMYJHbk29qz?#TE5{f4@AT)VA8-1!jq&${D{*!=y%^%r-XOrTj}8bf=|Waj3v`b-a* z@PM>rraNHK-R3Dy%SLB%!RcoXyM)kU)3vilyBk^+oG?COhq7jp?YN94UHS-r2g>*3 zZ*r)G{=zrl5Gje`*R@>zr)SpJ5y$%QP3s*PW2KldqPWpecgEvvHD$bJ>8l2ky8P?R zw%N+Fr{9KQvOtRw0?paO>q_kDZPj_v6NcDfy7&Tj_t#|**VXelFmH-8y3yBzeL336 zI+J=#_Vg0iJ=vA}kz;(#4hC)Jr$;b^ZU}=z!n|PR1Gdh@e_P>|GlQGz~C8w+v%PZ`0iLL2$-{Do>SN;&C$E#o8=pw8D2;7 z;EVa4oz*xz3NKNc-(Yi_ni$;J0hwu~zfK^kCo*WKwgr1RWT9v!L?(opP3f9zw$ySGU0r z5ofzQNxcOg;%1{|;L^PH_CrW}m{=;5$nv3pcZYJJ+}YO<_Fs`&H_91hN^aj()9i#N zP&#@h<7t+ZdoHKl^(lX4CH^%To;XSEDUv8BH*>%LZlli)U-ixJ9&ud5v*B5^%q};o zRka^)(Cdxz_^w)2iAQK)o0QrxGsOJI!_A9uTemOizoo`8iMg+c97k$&Ovn$2B@0a{ zF&=k$lL;YavkCoMS>{RNCd2UV<&n{gZrx z;;{*PZ5L@(@s&}en{s9=vDj@|I;f&Rm~Fe=uNapT9KWObd(B4nR*s*DId+`TScSzl zVVNX%gm?#%k%aKa$AIs@EL#(&$AhFm0H%Ou!JJlYqoe74>zQ8#*-=MC=v@mh@aS2s z@i*3sUwxU$C(P>TF?uwDW>cWAz%XlG4q5=|TLZV_99UN}txG35dG)$@fva$ChHG46 z+Qw-3L7XzJqiq&wjxkXP?;smLVXF>&=!7;0(*kryxM_IjtwM0-tB#fS*ltoUMZ_tq zB^u>gOU~eSW`%Mtn(8UPe!e}hv!=@N4K$A*tGNCS;)}<}r(9jX!0#)T{W%y~>`6nj z`FBKQ{Z3ja<>*vp>gyq`^Hes6>i35o3o*)Hfg3zUhK`KAw5PIpA6_{mLcb&mp4mba zOEN!Sj{CYoZc4U#rrIk@c5UVbez0tAe8Eh%BA?+zDU=&`(e|ewZX-ek==Gm0Yn-!N z5d_O`%neM6s8rzPdV7&Qjmr;mt1Mo5^Ti4_#+}?qz1o zbh(q(dDKeh5OI6t<`($@T|f=^Quj}c+YfTE5@K)fe2Dpn&E9h zHpn2D`p02esiO{PT@_wTC|&ePk5jvV;tQSmwOw)Tr$boe)&8(Uyo5p0D;k)Uh+`M&x&7bxK?&B6@+)>16&!n&H(*$whgF_P#<}T!pCtZ*Y3&Ih0+H<;9 z!NM3AYp|m`EuDqj1BL3zpgJ-muac1Q_rY>H#j&hvZdJFw${Mg?!ygS#$ zMRWbeslotzCa%BZC)tOx+^6yy)rAD*#7zwS1)Pi$9PQgU)g;u9$F+I*l3?8M)gwz^ z9T^uG51vWq+p=uW>WT>FpVUjvjdy=0_zo%EBh^woK#>y>CcjK8)V^Up(}OkI|$eKT&p29v;UI(UJ`_E4c0jvgK7?l=zEUlSX*h=dyQ6hALT5c>OW4Z zoiF{9#0)dsq^ZK0UZWa{P+h+@9C9IVXlezVyfzbs{4nnI?fxR2B7bZ(J5{^!=dE#* z`q73213O+PV0N4TT`#{L-FaYh+Q%Ssl!8rHrZ9%z8!hV{^fK+g6V@c-5ShxD0(Y@7 zW1A$?tX;sp%%Sgua_|VV4hWcQWvjVzN5ChS=1??-4H<1|kQ*zL2_ovl83>jaUWNnL zKNUBly~PeSFIOq$%@Uexekk4j;kTg#FqG^00=4%Dbv9NiipIjHZ8r3h% zEyaQ0eNpgzd5Wsj3E?+U*0!*tmMaGW7|u4Ey*KU!M8t@PK{ zfmS-uGu5=Bne9ckAAY1ddYWO5cFki#i%!Hv;T*~L>6XeTC?3VcNyN26}YFYEj^MI_b`IHc~&o z`B|>1mB^49a^gH;@;1k(ak6~o{QLzna&9EHI$YgGOgR2ps$b|MelZCH=XxyC-uTy+ z*(R6i=w78;d6(`CIse@J?#eca%32YpG3Fn8|6ha#G~t*;x{Qn{D@QNb*Nk!L>n)o-Dt{e<0Rs-KkRYjk##j>S{jFva_B+&}# zGGwlIMvHV;zpjNA;roH4baM*)?$Dor-z6oMBiG)& zGWI2V{<}(0#ASGB!{iEs&%tsga)o2zMoYPd`~Qp*F<9UV@vj);#HSR!!OC(mJjKpbexH0Vzqe0Z4-Ud^1YSMPWzDISSGcCp;GSX>Iato`CE|Q?)Q>3$ z<$b%QE#d+OXPK7mX-wQiHhtpm)%bD_CyVHqdz=MGoi*C+8+7#}C_N@c8RcYmTlv1u z9<2pi9ir~1J0x3*lU@)9r~@{9?V##_oRmP*4b!e@s9CP;*_E-GNunGYh(0Z} z?URmD@Q%u?f?18+LvKi)sZMjW^Ir6{&qjxMGqj%zGMF2GLI*AF%3L#45}eVOhV4E_ zqg#~>aKk9;YGj8lrLS+OG_Da`!o23^3Rk(ZMtAs==u#7#c<_1))Y_mmFNuq*7CbIr z#{pwFgDvM+db*(+D`?l((MfyCMXfJ_$tokX(d*yr37StT|E7lPaDf0Ap$Y zS4qdAi9YwMbQ(h+jY8viQuHaB*JGsS?380aeYidGIq0paufDZq#Znd^II z=|75USSxeOtEbjt9+Q4zGKC}OP;JA~mjbmRwY8S?XB|mnDET6NDm&t6UyPDK9}&l^ z|98zML`#}#?kPzra4MKMT4eH~$$ci^FH`CkmX^D|nKvR1nX?6TKj(zKDLDNkAfw_e z?$E@W$sF84Q#{ztth?b)1QL^n~v+yee1|vg9n38}2*ddG0>3LX__% z|EXBtMb)zr$KI%(rbk)9dsl}j+gKW8L5mA7NyBPBcuD<$BSbw4h^;-M({Ln66d%!4 z7S`=beFqKqUte?Gpy;9T=(d^q5d^3B6`QQ|G$?#uxCAhWU5BG}0k#lK! z^jTl$E6<Y|pW0>*upk3d>M?zgQ-i+y9B5TZi_IPhwYP$Fvq1={P zW>x%z9|a^KdX*@_fstv^TId`|T-!2kG*JI|63LUy%UGg7S$E@uzv^C&`H7N|-@nm= zeTsGYa^%q|sk`Xa0TR)JAbSZmBwr`}NR2QiPFvb>Om~(7l%La~1XBprZf`AK?42Xn z*tcZa65Q;yy&noDit`vdTcSd@pn6!&>T%iJ=A=8p6^LO{k@EN%2 zH4M^yPnO+2xe6$&^l;vhSD$+td0TQs1d*v!c1oMcEfiet0HHXOd|UdU_`yjbBF`1@ zaisChjS_y6(%imkNgU2O!Of#4w$(geQ5lVhHfE4YWM;?f0t+PH)$YzXGL7HHT zfOpV-5s3#dri{R!p5(~c)U|27?%;A5bX6H=J?>a(YD|C;S#|zHpzM@a9E)MR9|nr{ zGxX+QG`fasf^W-n`Adrnn*esp4X%XitmntGH+plUUkw)u7L_+P0v@iOMbuYbyuq#) z+{9#~jf&BKW1{bRK_b!Exh!<7GVKaq%vw@{Oh-FnmLM84|5+Y{*$S zTGMC8;xy^(JHYYQX?xN}ns3P~#52Ib%=e|hBduc1?k@p|Af3%3b)uXvS*<`0V?2eSm%fLD=#6=R7k^TxW z0UsLV)0rxBgd8%^xpmo%J{mirikg}U`FsvepbGo%pHNwx|BBqlb(@h&dKXPsN%e_9%ozfAinrXS0LE= zYQlsD11Sl$9(%)PNY|#lX{ZoWa)z^brxQCXlIbf85^n*Mf ztp;aFoC&W?Foti76NSK0lJWut-YAvdIb7gX9yFpV^kMi0vCW8YtgTz6hnWRm&vpJD z2*$eAB9#=4nQ|Q@XY>RH0lf3Et@y1*Jjml~D_W&VhmRY3=((e*HahTiYyWEpWZls!k~R#QZ^8QCYu1r`i2N5?#Uz-F%WWGXL+b^ce-6;z-iA zj77O+eG> zl%m(*@EsHpp1?%=!{X-p>2Hl!l=W9s4w;O#wSTczrvJjI94#k#Z;F&Zb`^fhNHYIb zLmijRuzU8LtuGuzF)G#HTLYObbJH4EYx&g2dJ(_gHX-I~sdgrF_cceRPmm?07N349 zp#&C#n&&Mkv($Re&?66crH711a@LF9yn~vgXbdqOsd$FBMXv-F1><^q>>~poBG@-5 zpB7&tZw!e25QYEtE_i-!Ox^M(Z_llJ2jP+=la9AN2P=GqOiIW5Yqr`Y3pkPmFqo$c zrhG&H5m4~;!#1mYTwa;){kO4&-|@j&q>QZag_<%1eVb&ZrS&X;Kss5%gwDv*oQzht ztC(dtC-;jZEehK=W-dsj*GfQWQ3~0((CrvQ&htm}ddy%dl9Dc8;cYyTzi1$KNLly? z^HODh3E#FB^Hg$g&EysO$`Awks;U=6#rAQ>?u%8gDzQQW=A9R+sWWcEZ#O&>Cfxhf zNrQ~%WbErvCPy*=fQlnVzZDod^{2P^aE^Kjm42H zH1t)f zp#o2R4ij6$VN{9UT;)@WKPf(yuz;DAGKyq}xI*y|)tGs&cU6nD$0X~LmJX=4 zWWbY!VUry6s%32n66g;(`^Y>u0*llM-2er>YQ-2M(hlY$nm-49J&ZuZ&fm?MB2ATp zUxipm?;zkh?o^?X2|*(kBX8=yNVbB2R$NErdewhO!v8J!#74EUF$yx5=^g6 z{+;;oIA#eJr%bhSV(WL%sPOx80}N>ZZUf3Uq#Xd}f5|%(B=*0U_#gj!F9;I}vV*Gv*Uy~OTPR%IVrr%?uJx)w zVt)VS`yhxE=07SJnE$7OhlN8xg!zC3LPmKX1PT8)?fzdA;s;pd|CV7)IC!itAZ&I7 z3QpDoT`Bdd3DGxrhU`6c+^~GuK8P+|9|T}I#yJiHWhY`dzg9zyt8`- zZXa(`B%0!LXY~0`-$9GVT1t4cW>eW=Z@~3!w$c60^L-c~dHWvOQOHJioy@Sa3A73p^ z0fV09W4g3K@YLzE;s*91E|=6fSn+8Udxbl9cIWPr?E3b6HT-%MFavUP@1NvbJh~XB z+MExoRLe7;xtZ{(k-rBi#JFlJqiVxp5@3o%i;qvP2O)Nm4gUTTX=x{MBlksvXH-Pp zg|7?lPJdLV?xP4Js%;JHSi;&>1q0r$eVKiohDbO<{Hc-dz;vDthnzrtV(Y{7bf)EN zAUVYc$w%T}dLPC<0mX+;Brn3R-`~jn)pIg#XtIiV(3C3e+SVYA{+~&;w0HbEe@U?> zv9GfK$iqlV>`P}5C7$iKZCF{9IsAiXOQI0i7jaFV)vOyToa;!VD9J3S|A#w+g-y@v z^62ASz?Fa3VJb^e44}klQW?VR<54xfizy>QIteZ+rBgrs2+-O0DnUCNPIC-z9MCIo z(U|QF^N!?O%*eHL10jDtK}w-YjG-Mmc84o06ehS#7TZ*4I{v{c@Vu#5-Lx3Qa58G}IW(nw{j?T+g?khMh$0SOn4i&gRGAaer(r0_!qqJEeYF~24~Hx~8199hB79X?wOK4YoA z#jv$^ZIqp`F`6dv>11?#@u!mufe>1Ap?9#ey|(Vv2eEt~DK zY`Mj&R^T_c(=@ex|985;t_dFXLWH8DH75?i`Xg)#MtHVO%;?|YW_8bG*)*qx>aoh0 z+tp7shf)`UH)%7jV!2`e3_c?l1^I0qk(Jg-9Chh3>2nX!(sNB%4;0!cpO1#sUf4MA zk2qFQ1=5E*wjDc)yRNXZC)~q3G*4T6{@!W(Q36P5OX&F|J*KAcxkY#6kike z$bV)R$JhKQHO1`+0^hRyexb7C8JPCD(*$yF^=Q$#2f#PO8HXokF>zdp-HHNNmNrY6 z;J`S7G#B%%gBEk>$tI_-Fn2oa9=F;Ml&QwYazM|*hI#uV zC$>EiJO!02miT7xo6xI3oe6un4{L7uFfvKnmitFLwzj|oOhV0nWqC}FLzH-xC_p?j zuN@(Is*7{6kJdbiVo&w>k?WAw&ZtJjY=-@{diSPu)WeW$1-AXC#oEN$vach|zq;W+ zpBKJ^Laf?j>r*WLedL|zPk%=G?S3odRMe)gnAO3qI0h2qO=7bA>LIhq)f3_Y4LR3= zo_zFG4Z?gXIO3Yp4baD07>$%S8U!F)kugvW)bi}YILm#Q&J^+?_jk}1#skr8BlrBP z-i;FLv=1+s16+9wDDU4yLCVAR2Z-N_e^LgrK`?iRyC;fNrd8SJ5ETG|Tx;EFoC9Tx zCYrz&$Hl)mmbK??65Mjpz4dEI-y$KXtt}jh3lMA=3e#}}pV?RILn}=(!0O9v3{cdR z;_2MV*tc)9)?6rPXS+W^UGK2KqxV69TA z@4_mc%=d>F3cW6se$)J}u-x{P761D2o60Uu)PkSDx zrVe)|>v#~1QFjYYvrC4j-#8Cm?jK(LCH;wonwI~n3!jcj8O#tKW&h@Zjx|NLgw}TFRwGW7^}Quu!mTRXY$h~DF`q@+x=Ha@YEai!+QLIF{^ zctwQ=NJh4nHp>@phMIV?xW7r2%O|jFTzGa@qe4$JfA@H?pHLF$Z? z5|iUgr@VN*$Cq4oqSsk|@C|+jbbQ?5_=gPf6*8Gq5MAcul@gbL8(LhGcf~tMj%@a& zXR&6|Oerx{gqG+t-HNz$x9PWB&YeM}jCFsTcn)cAZt^gjq>Z~=_HEjmm=dSs?sb3P zp�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 ZhX9cpo3Mx5wRkFrj(k@JBgyaw|Jf!T8tec7 diff --git a/interface/resources/qml/hifi/commerce/wallet/images/04.jpg b/interface/resources/qml/hifi/commerce/wallet/images/04.jpg index e2358b4dbc54c06314f70e2bd77f1b6d30a56360..82e3e64be327a150213a8eea072549e957df0669 100644 GIT binary patch delta 11292 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 YDW8$(0nz0E3JQ}qK(gOAiMN&i11j5GLI3~& delta 48593 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 delta 45376 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 F|JnQm;RXNz diff --git a/interface/resources/qml/hifi/commerce/wallet/images/lockOverlay.png b/interface/resources/qml/hifi/commerce/wallet/images/lockOverlay.png deleted file mode 100644 index 09b2011e58817e9a9f120ef4303c6ea02e597cd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10087 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 aBo%9$ z6Uow<5g+?8Kxw*HERx?^O@BfKc~Obm7Vq)HoO!QIacXC3Cw44GBVDyYKr$mPn;M$) z-x&O#N%ljAe7t#u^S~aq*^)wdE3WY(NpL}zFH3GyIf$%&Ad_*VSIg4b`S8PO;~}sT za@63Vz(jrv0rb&Dxv$Ggf9*{1SjSw3oq{ zG=(<%xd${59ZNW8Hu_QJqiTXCZkx&(Ta0=JZUn9+nfw9Tyx$cHY@&2X5>eGH4 z6-X;CG%tqpSLH6Lwz`AOAFBgiM#YzE&0O-32hxzg84FE<`|mP;ZV^&orrF&Q`l=Lu zr3Y-MSV^XK7IHyrK9FRXR$jdf1eN_vI6?V4GdmaTi_M*p)vRE5nfr8^K!2VOc(f2R zqV`Rb!Cibwd^2E$=4zH(HzyY(MDFBXp;9W_XB=B1sjR}@?F{y^-Ur=~o^!e72oE$W-+ zGY^_Lk6A(kKrZn7>bw4W^zRb8(slz`D(m$Ca;}_gBQm`bdg>OKuI$ol8W~K}$z0ZJ z(=MVQix0fIxunFpAKowANV=;Y^3R`9=Uy88!(~Yj(qP_57-=8^a2>4?u;k6)bCFrp zEbhDnau%McxK54;Ch8$WNgZ(oZ;UAFiM^>m$^Uv2_Rh8s+^p!`@nns(y>yR-*|G$m;9a$J`Df#0{s(!z|6<<9z-Cx;uaZ3`209ddKW+S0 zS8nOm!R{u7$f?_FT|T4oGSdvD!YULn#|hD&uZy%{YGcPeaD6pOnF`Rgv)o>C*jIjM zj6@PydO0riGwJW2Unh6j?!aqpW2fhe(d+DH+(YzH)E_isGf^`-`C?HY$T05#l>8cX zZLg_x0LGFjULzn3Cf!qwdSWB@MsE(GdhlYIs=?dIc>j}?exKGM?Cf}P9yhvA9qz?t z_5(|=>q}bDr`NyP4cd;T?!S2rLUQw!xzPd#_oA{p87<{j2QL1BFeU8)R zw%=ghS~k3srC`7F}A!OAF`gb~R#hoTi_s z^{mqrjkTeIG4N!kDWb)Pg7LRwvvWao93Q$7E)=29%6X;WJ?+auFep0t=y0lM#WxG2 z8)7%|R66$O!@BR+r;oT>?};5eI{ye>M&0^5iajT37+CnSY<@(zlqUi5Ne_OVW58Y| z94RBwXJb*8YSFip7>T@62{|$`bS9V!rt@iu@zmsgd^=gqquJQ+S<#~Hw1$N8X)7gI zpeCC8?rGL*%#9F6gf1=|9A#NrM+7XI0%Bu}P6LJiNTBsedGasAt4~Zed{74T^Vi}* zj!=+ukzFP$`QWa=)i`7zC{+)dpkI$71iEo=FRV_m*J(%5!3v0+uLq#NtfU z>)fc!%t#ozyPkdP*_WbU`7c_6I0qA?xgOr1;u(}!w%QSA@kl1F>(T(1NG#)JVf7!D zjZO6Q>#U^mgbIS;i(Lyr{np3^27GuNnpV@zr)qIab!{0ff7OM>p1F?tD$JJMmYO>A z=SI4ETs!k;yF+?ZSnAXm#Z&4;0Z-7ok@#}jb$w1<_;&oFfqD!1W!DYNWEO7;xfJq{ zRCz>JwYJ>>wJk-QhCeA+f3TsZ}`{8p0~pMhdy<^nO2|LCI|ws z^FHFwHZt6Oa}4M+<=>_QE}CxKVGeB0GJD;oMElX_0NRWRDJ`FsIdVD5GDc9%?I!LH)a`HIqvXHt2~q&nI^*BD(!LQ7* z;S}-7=NPGIB z@kKPR%TC^z8Y5YThhpO*Q4xi68;I8-q(x*)j!+diSR%#dR`puM(|^6Pn<1Rp!0AsA z$oKtGC9P{}v;ARDPy8XcCw6bCpa>%cMP*wobc~xQ)bH?_%fLEBtvi{5ca&qj2A%tEGp+Vc=d{BZT*aI6 zY{)M+k4g~9WRZWTkRPx}HE&=6;;zsz*9F!Zi7oC^4<{FKeds`(QG~+BWVwZ%CuHW* z-fhCcbHAEUcRDCaVEjJp({hKfxm2=%SiF}lb$X7Z&xeMEyAI%!vGxRV?jHee#7GNw zrIRL6p5;(-Rp%viPm2`Ujww$mL@wmccNU(iA|o5FV9Y@_!LALVM&H|rOEzdE0h?~z z&4qR*(uYPNg*OWvJSqfCL7yXMD5LFPXn#v^>?mvt+n#$|I4EZbM>;_;AC7t?EY>}o z&_aI=QPIphqK#2%n5!=Vu)K{bDF06&z)OkO)U{fQ~6{iuvbw?y>AP# z;9~4RAP)%9?_vS!)ip|03MW-AO1R?bfACoMq)>DK(fsWIeW6Jw`F49j@}Y`Mkz z3j1kk66((FcFoJ}&6idUJ<$=qXvn?XL=TW{KB46hWBDwX@vR!i4`;xZroSOa#llj+ zhI3D%&Q`r#F@`f{o?cX~v!zWczZjKsQHK(dE{^O}4Z+bx_wpH(u)892btbfa{dG%) zEC^xw@z=~#FQF<<<{|<@qKAJ5N~A?WeSF0t!O}%%93^927~3D`(66c4sALA}(lU0c z2T%j32r|!+?o4#u_EugNzq3tvrHg#^UkFlBz49OHXEOR&+0;YIi$8g0(sijQ_~;Hy zH8FNF@JQ6{-IDwgc1aKf9hA3PAbMS8H)~wlfJ8kFmy<_hX5NV`D#~}!NWBUFXhXdh zqS=}IaPyY1vm*1ggW~gO&_2e3g;WVRGrTn#ZT_7J#FURvK2ELX0nT1%ee6F6Gr&mS z@^udhX^#E6q#7sz;Ncsho+J{H{V+QXHPN+Hj+ajkCoBSg(wMleCrt$SIZWnZ4mgwz z;+6IoGB+9h{!0$iotdx5M7JCOBFDB~v4G2HCp5YZq4@;ZH1t_aV}```nH>J_<^)HL zTqw1!DW2J1`g{?m*3iZimD!+o$MH9MP8Ds)ygupLeM*bREJZa?erL;;KJKxY({%Vc zNoIjHfvFOciY#U=qNtPP{piE$<~Vkw_MBr_v&Y9Xm*#qnMzYGa6Gmq>j(UpFMcOW4 zGM_2Kddw%TyUp2A#o$Wt=`(H%$5}Q;QaeW9I}6(D6=zH8n^uc7uEi7f*V`S2@@`HQ zf+)OQhxJjb{Xv>4ve5+^{LnFayu}mB8wJ*noA#!&m%(F#j%az5@87zhwN6haIu~AH zeIdf(5kHCl)rnX#7FMCFYi7h$1)-c!#R?`4IUEmbQ5d3G*jro-YemMb$kaCuN74~;!N0lQ?e|Kkgh$2*(QzO7uB___Fzptp`QGX9 zkp+k1^KC}w2Y^FKs;Eoq^zynwsJ>i&p$tvIhLC+jH^SJ`=nw7z$zhbGllB+Z z7zhOO2p5l>Ry;bn)1B~aysMxLJ}fe9t|9TOCO@}CjiX)w0ARY2HbEK>aqJwBXBS^| zNZd%Y)I0PtbIsJjkr<7S7UZO0YIo%6kpcTYfd?Ans*FrbN*Pg))cxxUpv zD`?gC%fKdXnoanMUy(~o6$5s(C(gtxpFZa8?3~=*-4Hms>D)Ut4m{L>PI9uN2S?50 zP5+$5N?(_wx*FCjvGPPS$>EK453hCg{4(dI1RlyoJr{8n;T;93e{pvh0MU zu)OxJ-l<3B#2_?J&%-H~D{@FgtCG^M)~Wcr)gC_h;^vsbnq9TV?+(N;HFl~9Bj?>U z%`(0<&680MTvfP3$LF#+E>w4tX^3eeEnB-aIzzmMh*|t`n`>Q=b%>QY7X#p0<+XL=Vdq*d3| zhmKEqThD16-<2vd(NT7|rN8HB$LKSChGJHb;OV-auq}&tE%TxeH)pa_%fW`OkrU7} zzw3}F@)WdIG7C?ymeilz{T~2bK%&1&5v`?_W+T8Q0+8c%NoAWT5U{SCqXm>)$xnvX z5YBpNle1-_8Exex>oeb`@eBUpNWK@C?@T#98r1+?&VNfQjo2m*TeCC}S)}$EZ9v^e zwxXdb3`lS6rh8NaTF1fJC)==|GtYdE_9#TUS5pl5)go3)D=i-b_C$e9sceIC)RLG| zCG4T<0;l%om~XU?@T)5!idt*gq}JO1TVE;!>QeP>OHLSF#5w9@zhJOn2nYIsRV zFl}kkbg6Tc?*IXGl8W3O$kP6x&TcCr_;V7)SiG-RkxJFRM!y%zQVgxt{909k>@G`Rj^fV=t^6w2)U_Z4&BHLbJx=h6H5 zauKVgm4*Xg4nwvsUhW$!3*yvdnTdR5P|@Oqa*W z1K!cQD_v4 zsgOCVFun-D=Zi=!tu!70TbOOpLU&n!bcKXiR_CFfoq6S0T8=rA0b7O-aTTTt)DLew zZrtZq43H0meHA*Gv1Tn404}XG0RU4>i9^XGWbKHQV$L4?K3cZl<-o)R67Kg$9nVHu zGrHI<LFm9+6fW>nccGvpSX8H>E@6g0IGEG(Q8rSVU@RrC}K`RajKS?;*fG z2bi_>n|e;ou_9QQvWMn;q|J3qVZqYprp&N8y`VXJpbL08>jR~{6aX%*G)9{+gi}bc zAn)?95J`xN2CVi|o@vb-1mGOy0H>VuVrhczif8BUg^+2jG_|OJUn(NCw9>E)7-fRC zDi4^mNh=@qE%?z)cbU<9;QUeYItS&N3Cj0JkGr)uyY{$%>ff3P=Gc_dYhwCAPs^F)pUxL*n`XcgTdCmMW53C6D2(;PW- zDd1<3sxoIhwzN-@_ij7G^bCLx&aO^$YCIA1T)FPN*TK=wdUGYh>Drq+MWmKi8V-Q> z11q)emxR%cGL6m9%=J>Q;AQhNC#x0G{A~&3T?(5g_QxtGHOg<`TCU4oT4^Q#j0%nD zyQ?$H zXaH|W=a<@Nr<6G=_()l=2Bd=FShjgHS8f>b{{LQ$C)Y3}h12DDnOzy@phiS}?R1Z+AQis)%qoYvml{TUZsh|B^7CKF=$=sxEQn-eL}@%4DjV z)P6eVNy(CpB2r5$O~!zmvQKHwD|34SbDY39XL0*Ag=SxU4uo+}{dPzSpG0Y?m}|@L z^R+VX*Lo4DrIp45;I52KKI6tmGB@P2Imtgl#&_e~(<5R zV&v>%q;rD$WO^McBDJ*AaDo&GM$}^CDUc4veckV&KRF9lP}pCpf)?j&)hp*7sVt$a zL#R}*zUOrv?|-m})Y3}BGZ|^Y22TVAgdodNF-G$p>%#69!j=no9`s(;@wnQ)c%t&L z9~8R)lqCg#ODhcsz{onZsk2my245Lz`jQyrKI*9E7UEQ^g{I$#@#Z-EjR0fNRk?_3 zq47>%_`y=Kq$tht&zJ@4rXo^HD@_K#RvxIiKqGYyHv|vT9Hz!x_q9xGi7=sC`xLZ+ zro@ketAqvNJtTeejPD(GB^_mHrLh1wM>Q6T;Gh8LC<8E21u9AiN(8gUls{sw4=yOD z;$O8&QEq2F@0zPNp_0NN5_KTzFeBRP^t!Q#)zV6%3s_3>2-N1L7OC$7F6mhtwQ3Ia zds0SYoh6D>4iGJ@DL{}jv~5T^rIFt0ejwwqyl*B2fJ-Y4$8Z8*bNddEN0=q4px39+ z;${`ID$ec&0Koxo>p0lHsyqe2oOef#J_ih!|ISsmB-Nn$N}_=-mJvOmkJU`Lw9-lg05Hn5*w^`>mQA6U z=6*`ENtrVbnER3Ir`l33rzh>b_hUt}jlzOUD{XkJRO11?{JSoI%T?yhBJ2Z^ltWUa zq3PL@1Q{sve4mO

    e;+#!(c#(gNX$`#Ao6y#R1&rC}KG(6nwVXA@M)L`_nDX(E3V z;BmiSIZP%jD`1w4wPmr2J}Y|VWcU$)j}_5cT4^*vfrwe6Rj1ODioc{3-Ff zA(K$hIw5H?Js|`vHMSR(>>~i**DJ?A_3kNF+t*nzB?L`-&h8yH)W|RFT@Tt@QaRuo z064PW;)qpRX-WpnF(GoN*gQy&oymS!VZhHjyYH^ilKMCj|Yz@=R7@EH7tbkg38K< zND2~+*SZs~A}=hP?QbrLC@DF&)6@HSQ1mVG~6hM zQlOaw%YqD+78}idxW@q6F{G}qT{o+WbaN50rIn@wV1(Hi%^cgznbuA;sQc$+p4>Un zo=>BTVMy=K4K+do^N>m*q7~OvA&Q+EfNv`zwzSgnG2k2wz@l2wvha~IXw5x_z_L8J zFB(wdoaOgb4G1|8oU)xMz@$1&O3ci@;c99zA;%l)KT9jEv;hFHHG{GYC!zFpq-2h$ z-wfq!QUhKnWo=~4%=i(*aPpCThkt@z>=B=xxf_+ z=GFJ~Tg^M?WS&D@f6JjlZS#`L<2?d!^n$*BdaV{wTUu#42AoTfN}YZXsW6eOJ(nl7 zgq?E7lv+Pmas-g0Py=$E`^@`+@*9q)dMb=hyGIu9z3pbuh9jPY>J!F)ig=TroqJ zR$3zfbMw?%m|!_VH^sWN$p}*ao>Ew>LwVY)0bS#1Vd6%DRoi=@oCGcNOy>sTu)usP zp+c0}^V)Khi)-%k^t#;@Z|KrWQwg}#=CewK!4cS6)3gWmO~ZVV{mD5e)OY6AMMjqy zy9FFODl~Bz-^%=R=|?0zNwTABJaaV{b!~8NCmER{H++51hhE{Qf})7MJX9V zqLUP`$2Kof_}1w)DPp&@(yj(zYMoeA9_AeEPkZ*7n??8KRZBUuMuHJ{T!-Y@p0apQ z6{e72)12DLIjlN}`$iW0rs;LGh~3gkqch+k2sz0;5V?PYka;Y6AA1FhGKC_<31i!@?J%_fLH*S)O0UH4N} zCdqqP7qPFH_wxf?(^l-V&G9LfQ{89WKj?K4S@1n>b{1S(X$6288!Ts4nW1DE(y)uR z`=G~AVs=Jknew2u=fKpYRC!ud3F-!&fZa3wKE zX$64cf-UtORP`24RxK3cvdk%*iW?nhBRy+I0`W)U%+JLkVT$j~L&P zIIy6q)C2*0Z2F$|3>FCp(&{->R!yA)?r$uUa*Q0Na6(&Rl)zX^!O_Z0N-Y_d>nv3z zZE-B(QPkS>dY_x6sVuFu!hn^64e7xg5};w>9HQ+n)p;feK%J_5bO}dmhcLuAj~Kx0 zfpd?jFD;#yCMDJy7%kSngwfySX3059EA6@lU{r=9WcU!OPArN&q6M*c*bG*Zb8#xm zLzk@VLrF@3wg=opvX&4IXkM{yw4j};|J=65<}CO}rq_`oj!P>HSjQZ4n^w8uIjC?M zl(5qlgx4?>>KQVXm08D0=XRFT+>@h-dc*#5lr7EtHhDpJ6^~eE{o_lM|;9l8R zjRi{+HWfkm#Ps@6R|++hb{#TcKj6L%6@)!g^Y=uMlbEklj9+5Dtw3XoI9naE0!39U z0^`}Jq&K<7n(Exw#pcJ;>;CEW#v+zWyWRj8t?5!ss<~KS=;^mPh5oYeZex|6@td6I z`5Iuzb==T(|F(h(ly%;9R_ct$A_(u%7G9-M8g{1F(dqT!>2*sH&81y00L>T(#Zk8#lw9>A5 z0L~rm(TCt3GJ8e6dl1|sB)*3T14Cv7L#y{fssS|M+k#lOsH@0MZcreteZ2Grq8)YB+Uo|Uc*hHLfri>T|?0n z9Zk?5bu*K6X{BBB0E`x}sH7gLf73pvPeAt_a}fadrW`AH;TqX*3J|s0kq52OVE7)n z*Lxq~PM2y1{9ZS6&MK|6>jr=k8(C7L)>4oM;Jp^_*Ou%<8d}t6_!LS(%2kcW8}vVQ zKd#e})fz{GFr_r2&$v$KUeWKLUT-a8y0q&7fGvck7BDFePXW+JgaB`=9@Nzzl!;x# zS}5UE*37BRvXNr0S|I8I7f*a2?=WY|6e|VQiyd7BNfI7KsruB_qAB{Yo0+0-Euy-# z!hq9GJ1IdSVbb2E)7s@^ekq}-xqqd6FH4}L%&A{`kNw2rr3z)}P+m|BnF3^1${a^| z;qJ}U=g}+r{22n1MYm(>I zm8DuY5OYWg&!K`ax1T6;yu`7~+5#eZ2gw+Fr*q<)-!JBh{&82z7M6A$Fkq_Y%H8N1 zbFY?3qn&{?XSped7p-zZi7mBg0k~GA{q@HCqV1iBR2V_ah7n~ExlUV@{d;hF{p9qz zvxx1|u4Mp5r$+2sxJ%+jDXbioU_#?a#%1QHF-GLFMy!bdcsFEyU?gh|!hZS8bKx_| zfjAzwx!HMesU2L}HPObC@#?>Jb~&N73?&D{koE~J+|Z#?QdWt`;*^7Zcb%Vm#%HPb z3Py~ds|ae&hOLu+P?Q3cU%=7&&u84sQ@XU$t~mfsC2pr|CTa>pp8;sOLI}y~`iPmf zsLN{!rc!2!DqF}gkp_ct8Da)86quuxvzQ|~h9zB|UQfGW)l^`TEV@KHW#-Su2~P*uiCo9F(m1u)9anr=-Msq z+HX;j{P2h&j3^a)q#G)i(6a1nUui8RwP#WD;lY5^mpMCizvUtje&B1n%TwK|fC#r} zMAL7mecN?2r>viH#WT9J{RzG?&FTk7LS~E9(sMHh2kyI+&7;lAZKiBVh!2Mm%>l}K z+%+ERzJtvoSvATTi|QDLh}qArETWv=;`#5*)9a_F*BgsWFKwT+C@Z7o=EtVj>d0`j zNtjz3U?H;V^l>&PsbP!gj}=4nir)^QAswnxYcgcqOG8mwQ3@-yUy%vlRx{zdi)=4# z;4QA}x7FZ#D9VVG7j!OD=HmBrJ`-_&${8Ed(F_^Z!8NM}N3R_m6!T-r-n!Dt zjy@wsr%cs67m=+kb(BMm{hxp5rs;L#^m=J}ohx#_w1KvGT=KE$b==L;^|An1!j`k8hi9-r1)nOMvX~I6Z>HbnCTE`qi-vM$G0Bd7QNHc!LTvuJiA_76V(E_H% zSubUxmK05}ur_J;{m}x#r46{)-04WQj$Gv7O#Fo2T@3laa90)!kwzHcBw)Q;7@52DHt2LMst$C^SNLD8QeSj$#S z5`Ff{DQJNNa0^9fnF&V_K3g!Lw4T}n)9WXu*BxzpPm_w03MiB_KnnOGyeBK+rltrY zZXtIIrKZoajcxZs)CNY!(*{TLQJ|VjeFr1UZD~odb1TQHHZVl2XNvc|UwZD*`Z_bc zUM*NqTE;dHHTtNVWo>I+vzjwtG%KaDG7cN*BLv8547K@rHD0_!_Dn}ISIwNE>KAjq zBX!O~0vaT8o8bja!@DQD*jy>2*(izX-zH1v5$mwOF6}Cu{aQ zc8uds2?O@R%|OkJWFhoBH3jbTRblA^1 zmIX<&Q3WSUn2=Hga%V`gVuGCPXI4ri^}AL@N~D$%j^#v~PrK=Uu@X(pMid=niidP* z(iWNSgEjcR(WQ^u8rLrcfP?Te>E=#_Z&*8=NS{sBeuk8oUu(gz8Y}HQbHO(UpmIy@ zSr8@DR~F-vxge4aO+-&DO~i=rLN>mveg z3{$$hX2qwc*QJ6jr6pPf+?ZVBBQ+3jEB2;Gq@tv1rJ+H91_nq;K@{BraV)s^ps-9& z6pmeI-kiF^TxTju0Rhi^7tR_83L!3WUZb+~uDM9qyZrGT=`-c^Hz^oTg3H zM=e(-6}Xgl=9=bHKx$Qtg7dy8^L}u7l0taHwVErdm>0ea=be4xGmFRrlw9xz@@H|( zqxo=L1MuDT|G(;rmvm`YTC|iz59e5S`gpxgcLi(pbxjMv8WyQJBeYI$s0lghqb70g zW>IHW+m|3@VO;Ur_%UeuHtjp&GPofr@eW75_b>H6P zW@o)IyVZf(Y7&^%n9eg`6aaG=DaYJrU7Q_#U#f{ZF#1Flyi~KjXE0>sSV_E!twCIR zq!BW|uLa9*6C&I***=ep0!UWO&xP0A?3^#g49CPBXKJBa+V$09Cad?lnaTNNZ6(}DRGcPRRQDfA9NW&O40zIdEU4n z?*R(@{JT{x4<``f2yqtoL1RmPM_NFi|a55C+_5y;zO2PFH`C0Ibb; z6^zHyD)STSEF0-dz){$OqX}x?@s0?PwhU9Ep4)Sr12xZMaZKD1 z^GM%YGoWae-YWQX&9)et_8~Wm?TuB8Ca(RsQ=$+y0x&WcbO7BWXUL02c%n_JT^W-+ zQAl%5A-NSN?K?E}9hL8y0*;b*MfQc-ACAQ(rHFXJIOnp&P~W%9BeiYs&HEeA{-Q}b znj$aNjJU)NUpp=4ZjPC(qLWqha9+zL`;h=l%|$E)Hch;slo?quH*03dy|31hXwiI! z>OUu(i^>{Prr9ATOX++4A(h!M@b~Uw$G&DrG3(LIZf26+EI_=k+e$s35qM)_jUxgJ zjAvM88Ugr|wRy$5EyrjE;61e)7@d=vnj6?cz;F~CWOzvq5AY_&CRH|2XY46OMw*36 z>vtikhs|*WL@8Ei^EP51wZ*J2=97Mdo1G8FELuASi1$^Ca~Ms`AFZ#u67maSt!unz z>I|5((KH#REHzt7!T_nTk|ZgRfCbXj=!J7ed2TS6A1Z4_GBxx%975_x6sUOuBS6lT z1-sS$E_%ZroL=|2E5#k==fnFozy0;Xc%z5={G9hK?n*2;cTaf0-rYN7wpi#W57A(B z|6Iz%(8jfEoDs~>6X%TkE~qsX66-9r?kb~orHy6QjAsy7NK#{&8Y`p~Orq2x#x%ZP zJP*FTzM|*scc$03YA`N9JS^ai#|F`>`OR+TShwTaQ;%9j{k)D)`L&gU57$79Di>I_ zd@m5b)As$gdr>wqwZE=KC{cdjHBPB_uo22Id+L!aW-Z^BoBLhPy_&^Nv=6r3!YJLNckD)Kk;zdo>upRDihG z7ER6Z-2NdqJL^4JuenrQcghhFWl2c_utXSCgbl0(;FK`r3a`KvAWh$ufG`&TD9@w_ z!>)Iht^>;b0zeQ~LcmBplLNq#@!%>=l6A}lj^Jbp;}HS z-=zFl)@mS*Eco#nh$A~L2|esuOqvptr^K_}JKfCeJZHR=oDmlO6rE0(5&-6G*h&ao zlm(9j;5GoFm>a3z3&QtHeO58pVv4~9#{5#1kKptQg=9@qSYH5+9d}(y_nFk<)1~NW z6#@~zi-vwi3J{Rs=*?bJ$zz%{hz@<#^`F6l0NU;FRZdIS)}CkkHO96h|1&Gb;eAZtEAv z92*2{0c9FbjYHcwR)qPNj-zzVNXAOz=tVgb*1iw7Uq}`#RTeqt*`xZ3X3>i^7|($? zUZnz4dz+h>-%wu%YS0}ydMJ@)DBOslXf^^cM*va**tzuC69*i*K*Z^DMciCk&rq)L zk(_4+0T4?292o0G;sD0A&Tu_=DL_~W-Y+QEV@?2;?ho!h)c~2LqZI=5IhK;e80xJW zkfWLUhi-OO9Iw;YIpD^zIbK^0=D-^PH=eJL8`h*{K6MG}O|onY01gC!iw0l>3l2;m zLjkac1xv=5ddA2BfQqqRIO~I}@Hxgia(o&p=6yNNMY+y`Db8-vkUNC6#|nTozqe$< zZ9;mURVzAcJu|(YcC+*1r``2Y7ID1>+!$LO9jQ((0Pdt=FKQDPN#}|BoX+o1!jL#Z zA*E@j%%XiWUi1VEDl4an0P6Q6xY0*Qan-&TN2yMRSr|{#97OMFb)!SsaY3X zP>?u?aV!-eHeRC|Tw`L4Tk9)= z?-6&UEF`6XNKV#EE!|5BU7ED~&~o54+F?ix%C14*JX2XnO-NU!C`+d`?)|6^P6($N zt>c?^?Q)BRgoNNlIvNJ>UPA2!Es$ws0V032RnBy;9INHu%w^kcH5;7Ep;xBYi!~sh zs{ekrmVwp6pM3-E{LFjKU?aFjPvztFbp!Dc@}c@VumrSKLw|?Z zvK9=PWOx}=GI`3TM${wLZg^@z0;y1I6Y44RXDR_dSLjHK6G+F03k=@zleOelDkb{_ zsT668o@nF0y9`P$kiK&WN5s(wzG z8`^_}6ZbxxzFD0!bG{QYjA4pZR^UET6aeRR6@8Y?YymialD<*KoRbWskAx-MJrNiH zof;>kQes^VEGHB8N~?ln&gcBQyM|USZr)_yu~xNfT??AoOx+{bf8!peqZW$G_{?0I zpU?1|zRp>CGZQ5QM$_jb<9>vtz2g&tw`H*xZE`jHj`@?74%f3TM2j$+@f(s?}VIOjh zN)CeSPHj2(Sni!OA)?9_cvb8CtCcladM`j+Ts27A(tnAwiuBrd;btU~DWYBehiExWWnr z{#r)o0c!^(JV$PL%+j@hu%%LhlILAu-Q+|iB^_QccGlJuI0H_Zg*{-ftQe&g^Y?(o zixxD#=P~se=T&N*unt2&GD$1NPkY=k67S=@)Iaie(uYa2H!+3WfgLKW6l*G6}}?VUH>@iU*eZ}Y}m zZ&-i&doMipso#0@@gF>LVKknMJPr>iO1!ona+9}zD#~K{w;Va0ilz1~WK+wirNG}> z5BPgdmJv&mTbPnVR$DW)3y4C(ad|T~hNuFJ9*~q~7uH?Rw?=QEC zIquZ_+SK23*CAI?w^k~*96@8tB zvyM{6lkw^&1*R-SebTN@Qp}1HPsAZi1jm#`bz%$5`*Y1up2;GevrP)Z85iHNW~g%% z82y%bK8p)_C-wtp4SEMRubQlZw!{v;VZL+i^W?Ka2yS<0yE=U6;P~!$z5mFGU;DQo z{L<|o_>ogDK7ZVuKegg6ou9ZDo?fZe4;;Sj#PPG29{&1E7cQKyMx$|GX5zNBAvv$N z?jzUlVlV(aal>mlP0`Y1^j+Vhq-R|GZYAh*mH7Wf@Clj6Tw7XD+D}fH6*r9^(kw4E z?WX#f0^y3-M=!WPCCLVI9d@M_?NUxq&Z4Zo?`&^}mDLq@^Q|XW-tZrpg`jyO&=$wRW;q?)+>9Dot$X{c;8&e6{zQ|Ge*Fo;s2$N@n~=nJAAFE>ZGv}Q5&GjyF|t>8qQ zXKRHVst;NvVx-tp+~->5d~Q0MY%-Z5Dpp<5-gb9(Z#lRYZu#I(-*)SVe&)f?op|4a z7oU6j&|c}@qbH{lY7gCUCA9qC7ySuwnz5Rg)4<7yT zpMArlZ~MFNId$sglbbKRJl=DzT3MR`YakG^Fk{EtlhyJ1`hm&H^oP~--@NxZWr{`i zDT`eBIp0YGx}Dz{Vi;1JIVn|JSPMYbtRV%I8j$HaXVy3bdBHuUnmR=QazCTS@rDkK zSz?}a%sy&?u|kG3ikzexSm*bvDZs9592`CP_P5`-@$>)9d%jg2 zeA~;X&Kz*nl)R3oL%%Yaf$AK5LzPIs@Atf+F9+|B9Y3*gcH`j6nXTP0emw$vz<#r` zRP_O{l(9i86ck3CeEvkyNvR`Fj*yugih;%F8X>QVFHN1jN;hU(0eed;hGUKm7f2{V zoEvYNlO5(>uN2=1?|6O~QpYrnDSLsa1~-}l=`AjG8bx|Yq$!GTT6-0zKg z8aN4ypqj2Enp95LctpL+k>eMnKqj-`%(1qnYp%`h2l6aQ0SG4?Yt(O-e=jxRH6{Je zQMWa{;i|2j&0F7exXDVmYN{iHo|5MaD;w)uEM>10d^vsV{BEwK)ghyB(|(Tv!j=MnlG2QlTtEsk@N*8Og2wsW3rtqjXGv4) zwF#i{f*@@A-N<0H3z+EL<#;&zgKxZi%fvlBIdVLfHd?Tu zg+10gK6GPs`>`WWz2Uom_L*mYJ`#;`J6j+ zk3yKs#%UzKurPOKqCN~2{#TB^{~z4^;-%dqdmhibyL$%Rn{K(~!try@ zK629^{vTie;vfI#$D@Mv;6p!pZ1nDrJ@CR=x8c?nmRK}BuO8U_;Kd(2I(qJh=bu}< z?@qUJpmx@Z;C#{f!v{9}Due-}6ywl9+@#t6`mi*OjK)Rv-BC}IDP^sc#oF^izwTb7^a*!!NrQqAWpMgm>fsYXwJ_EXQ?apvjo ztc|89df~zpd5^jK)-Jtp*SG%UuO|Qg-+pEazE7`A4s0AfbZ9bq*N>lkVt4IMw|U0z zCFBUTv)1`j?%2^IFK_+DA3VJE$_p2x*?R7Pwa0)joO5eocXc#bnS`_%H`jXQnv|`c znR>2w^!mjXvWErKQUDI%4p}&cp!#lAjU;v6;=zm7F*bSNkg#ZD=+Zr;{Vi3AkP6Y){cdGtee}``PhCFn>EHV1gW>cOZ#s7P z>kBYx|^)lE2uWC zjaJ-!FMap%Z+`lZo?9RLh1FP_pr?R*e%r0MjYBJwDHI ztu<4pp5GJ$tEr^awboLFC>&@_ELZGF6_(J(@k%~#GLr}9JZKY4Q1_!YwC%O~j7RI^ z?|kEH=kL4qPrq=-@jDKj{^S4r)ZaY*$c0c+%uLm>ZC`RH@3`aa)@T326PKp{_WY%)az~F(|9$&P_oi(7a^?LUx81(+(%OOX zx$WIBx{{28OL%gp0YzJ>G{nTxpT!Gg$}(p8zRMA!N)$G|pf=1SIC+ARTw^88X*~Zo zN+~vOZ18fpgXLIB$CT2Sk;|LUI0bg8N<)<537R-y%W{y?7=14Q8dYYuDag8W7cN!b z`P6@UDgy4s^XI1MIQCQY^)Xz@@9tC^cRzGAeB_tkdGXaVqrI$Hdp)bejn18SZ$9

    6y9}LRnb%?p?>C#$CP^3 zh1T}H!+OVey+@nLE`awE$3)G3&XkP$OP4Q&3m48;Zn83(v(qUH4o7b|JUaQ_kKF!! zcf*}-YjalPH)g|%qBc5N-F&-y{%c=&=G3##T#kS}+1c7Y?A*mY59oP!qN#c}_|=<_ zZ%o!V#(Nn+^ewb7V~P2;?ToeS4Av7q&mm(_=fh61wqE;uq+F0gJ9k(%jcNMri9l~5 z6giv9E5<_GKW{KM(ha3i-l$+g1w@MwtNDJlawr*r1&*a~<9u#ntr(5Q)Bh${#%zj_ z>jBuT$*Lj zs=GeRrsc!In+~|i8aEjSg>a;lF{)#g2hSC{A@VwK&Ue{LU#^%y)CNNU3g`P&foJ70 zN-g}Zxwoa#HFEh9Tv&l&oA5#d76&Lbjwd4p2x}AFEcZ7U?OX17yV_GWrq8(JP4^!> z@v)zK+mkP!zjZIvXgs+Bs5?^tK77>qKlzVeKKJb}y*wwiYulSUhqgB_T}ff-W@hME z=5YPcdYG(^dk966uG=B?QR|$=0bTK$9SwlhT4XyJfi@?i-w1F{X|P^7R;W1$j&3SN zYSbx{l17V90oDjS@6<80GAECmO~}u($=M>qQ`_2xWG!l+u`ng7Yooh=`seTa^4i@G zhRc_(+$EMJynKF&w1?bBF8%NuXTS32Pnvs2^X|?m_;~5pXLp#=LI21`b_-~%8h0@ z-;^w3en4wUYsQwIqgk2`Ii|WC%D#D+&SX4x@BM{;boW>9`>FSAz4XG0TU$s`d1=$R zl?`|I(bZ?4{{8>`t1mq9*abJ5%+8EM@S{tcyX%+EZtj_?!#wJD6dAMMT-!LXGoD-( zsKl7_(8HSciM81lyWHn79+c4%@AKMNjKGULCzwgi1A)NAPy@~B>&V$vG2yTYYX4J? zG}8Ien0+Yk5e%N$#v1!bX2CtP%GtLrh0MG5SWjesH1Wp`xlEy6V2W7tU_4Tsm`kmd8KTh7fl~DWYy2ULko_AC+>N z7L4SKIRG~Xa{UF+7*a!ZQZeKl#Xu5LL#;jB^&Qk4w9UhE|2=qJQ4~VeeyDbKs@vcE zz@b-u`q$s{u_y%$ePlm9_B|@DA?s z`Lo;M!r6=N4JX57cWZmHzH)h7xvh=r^3Izsy?o(^+nd`LV|50;0^$w!8ene%swPDSk=TMN{Io!ss$+~=1AQu-+C0@% zkavu+)XE9pVy&_Q6Sj=(jU-_30eQx8#o7_uJcz5u~6@RJiEw{3|5gv3e z|M2wh{ZC(e;_1gOuB{wesc-SxKHfZke)pL#fBvb_=Haj0wEgPlEjwo~9=vpZb8Yj? z<)$=DZuNOn~p|n0w+{X%^07>SIB_wsP>{4j`7)*KPXuK)fgCd(^=Du+)d9 z?^eWqrN*NYj!|1*n|>1r&!Wx+rT>924ppMBU7vN0ttPGL@|*|ad{XC#^I1^dJ1Ey) zYF`Dyy5a0yN&h+XL6u9?{m77WK5T7Y3b);KgL~sY`Q(G2yZ_@K-F@YS4Yz*qis?B% zHh6d9(Bzf7zWDop_UIq{*5l_lxBO@_etid~ZFlF=?ux6nr@!*MGZ(DUtV1KeGF}_` ztFvIC3QzbB1xu=I4-s^IhqcAR3!inb3E^g%37{72wl;}SSR=ypi`LY0Ro`($#^?pi zo5On03~RB!)bx}VOqeRrC`=?M*Kb!^)s%HWvc5SU&)k%++WqJ`R<&pdzPx?O9o<+7 zcmC`@ebW~n__+`7o_lq}t*tMd`-Z*i;0=ejZhQO-fBXIa^1Dx-zi=^(SJ!+~G*R@1 zo~&%dq6>STSrb0+bpcCiJi5%UegM`TZW|GB0#O790=|nKPB|<|JJxs17_v+m#NgM&QNMwGL9|J85zXbQn|@XlUM%J4e#7nqh;e)-q$dGvk%;G-T>18|Pm= zwL4l_9Wg?AQ|q;>c5heG4MZK>ROnAmCooP$B;<3bpFtQfYM`8e-5_ePQdt%Sj1i7Z z)O*CuwVU`{%1ER+o~23)mEmcb9rt|~^CL^vSX+MORNm&1ljq>36gSWmR+#rKRXBlC zkV5MG)%6`sVjAjNX>{P*-rfo~txm!NfB&D|{nU^A;*Y&@>eSJ_L>u#D8}VCP7u@8A zo7`Kro`3lC@BHTHA9?ce?Uj{-F^q^KV=3&EqhwMFRA?10R3$f!VrEs7+v0tE8)esq zQRIq6wdz~0a4N*Y55jq~Oh37-jZ4@+H^GEg2pCPc1V@-Rfv~2OV=6g_X8xBM7I&Ql zqrHR{*ktCjv$N|CPr~k7KK_Z@pZm}!KJvt=^C$MaoX0D3ay(U+x1C#EbN7z69>4s% zzwsB3eE++ft1Aa5-afsrU)nz-mp3(NUkrC-0Fv2j8|O+zSt&b?0u!zIF(@{O`nWS?FTQj>=71qN}b=Ob)y*pm`g@5zW7cPg}s%kpTla<*|x|Y_ z^0SHI!-tXpLn|Dl>;jo6#EKOlST@BW2W3IgW^dvsl@`e#LI;RyLgX~_EY0xMrMPpg z2neMRp&;-5HfE?gu(syk@r%ED@53MX)epXO`rNU-atPy^`R?^%`sWmo$9LSndEjfG z`qB^n;6FaLdG>5I1z&HxwtNc%wUnc&3RYU{BogRyI)JIaZqaW8oBhd+}UWT zVK0lVBK%gnjx5rcQq5JBurFGr!#c@|#u6k+K`NOelmf*9x5#?q4{pBq{)6}Y(yzVw zi+8^Nq0Pr0KJ0G3bCzUd4}7QJ*uE60usd@5-DltP!_R#6EC1`i`{spn=XUCBRZWU~ zZcyyo&jnd#bWypbSb2s7z9chrZU(H??xeyaC-dX}P9pqrI>>SQ$F(3x&&DPM$ufFLUfB5Au|JHAO=fe5(yQA@>Ev=(x^wSDB z7x!k)BR{L!yndhczw^E^7xed+fS8&ySHeU>!gNHrPY^tB@&7~1doBw=ZeC|;&f2cr z&w=nA%5#F#EW#Vv_u>0lX+fhq-FGPpC6F zigvDjSE{3flyNM37<;AwL7ZbFz0y&??dq4-naHXVnf7R7nY~(6xawk#0ATX2+THP0 zb!q$9u@h_e{@q`>`>_xGlMg(vG13}AL#U}8+VkKP zz#uB2P@fT)pKB{PQI(B~z$i7EbBZCPD(a-FN`(Yd(y%Vddj;@)gL3XG#(e|x+0s45 z-Pe)|SBO4x#@rLu=4)+h`%-mabivptoB;~C?P zz+3G)x3l9`d~gpOsGeE>+NZzt@E`ov6X(xwM)2jDj1iUPax>OzsT?8d9igFD8~~P1 zIu*<%!c(%@xq^}cOj3oRsUIl;S1C887KEjx8-vrAYpjT$1*7|-)?3qOQgb)uIo!FD z0iG;WhxQJGw~V0 zcy|iEHy+)6$K~gKIQh)~^@Z<#_K#jZfB8~aSy>+`_aH*~;MN6iI|HVAFki2LOZmO{ zo^t@6xOPuh+vb*zdW%n^M3c*WiI)&~!q{5!w==4bDH?}LwD3`cj)zUtiS`Ygsb zGTz91Blu27eEr0&TW>q{@V8I@_W$suXTSZ8bK8D0o=hV1W%CduD0gM_YxCf50$Sg3 zzv~%jm&a*jFiyUS0ed0q<1DNp0C3y?R|00G5tJ$6lTr$_4jvNIE5!^=VPt5hEM!4> zjk#Jor_Qkdi1&AjF+eeL7P#*%w(afe_G3rgT_69IyB~YYPrT#Oc;)2FJ1c8@@x1FB zv-92K0WK2y+*+l?l-fi&z3OU5Lbwqb*PBdOJ!;2BD_N;A6)nL6genW_HAauUv^z4Y5(e(8hP|M(w%=biugzy9&h-~Yv(tH;k> z$#zlYGFWLzfim-b>+iOb>h{(_Nh*_(4Qw7W@P{GjlE;n=t)P=aq~3U|e8!4`Eht2i zw1l`%)u7jdpFW&FSfBPRs4nH~-q# zufF#8|FduX_;>!+Tes5j8+UKrJUTbDXFYuLZ>Dlq-+LB%zWsSh&%c!Z_Pw9~;Ddkq zKYZ`zxBs{IKDzPg>3n=;TD>0aG%f-MFsf82gP%7N;-yY=*m~bo>xQ&+Tu=}6X364~ zluw{bF|yM-A_y%k{KZ-D-&RGBLYr2;Vrw*o`+jYc2TJw?DJ}wRp>44|{^{-0 zZ@%^W&;07&`H#Pm{@{V7&8MZfWl)89+K`paj&^N+v(?mzi&{_?XMpWHv^-{9T%Mp31fLe`vc zeMDF_CtJe?E?TX17R=Z+>_ydu2XK&%F7&*Ixd^ zKYI1`fAin_+S@ODdyaY!BE5GWpy8|6(#tQueDhzw@&EqC zpZ{0?`S;%Y^B>+iw<1KADi5zHoW+5Orfzh}7H<6g2zS*QQjW*&fTiBJHnUHw1S|@W zi@+rrsmQy+7V5~tIYk-ZCFx3QBvA?N*dnriE#+ys;b7}F7um1iEuf6pLFTMG<{==_-XMXi}Z@l}nE9WIeXWfhP#v@MepL021dH%&a zFWtTQi?95%fB3z3|JVQiXE#3kd^viwji3~Oka2Ag#<+!$M=(a3-;SOHrlFxEt^rOC zg4GKJOY2`b<5`%`(kdMdPc#|{=AYoQea50NBq`Y`86|bpsLW(8fyJnI!PY1P7paSlTjR3; zp0kx~)!e7E?nHEYb`^V?uO6S!$;+>ue0KFqCtrE%_g{G9@BSyh{r0Q>%I|#o>93zl zpMUn?$xGUfNIuZ*_wSq=-{WVV$uHfzb?Zx~cYgYn|K-2@?sxzA|GatUfwiVYnW5&h zK_~Sx#1iARwJ+4z%hvZ>cd=92??=jnWeP!~1g@nlGT^yVk_)Xe=OSyaQCt-BY9;cZ z4G{}gEAHDtdKl`U2zW34fQ_7j9qsgM%yzYsSEeN%HLcsC# zD*1^pyd0$4Y<{t2fH~eTLdYy_2Bk`rtEd-+!qK8P?eAL+4=gA+B{?N0r&S>eg|{3x z3wxUt$+irZQ~8WO^3Jax z{QWcO>dDdR%XIJa=a197uYKo#`r$i&`hR@%-j9Ct*=fo&&u74w(8d%EQc&HnkRNV_ zaaJP5Cjs%Y4X|f6v=$**&?I+08oEp_s`utN&Iwl#r=&zWDNsbnn_H&%Sc?{bydgcKhbdDSiBII?ral`s_o)oAO`X^Pc_h>AiTOUXFtHsNG%!2H2xv z1ZO?8hBB)3NK&?g+`|Qcjx9(!AO7j|fsuUq*^~UnZ@qE++*g11x%}m?J%96+ufFh$ zt6zTp)%@ADtIs|6%;zt>{_Ka(y#DOXU*De7hrf8(67VdveC0PtH?L@sFSwh}zH|Ic zO3yu)zWVHZ^Ti+k$xm+o+5i4-{^55&{P~Z*ckA@Q_mAdhjuafu==$D>5cI`wr(~-5Z(d)GgH z7&g9m%>2Nh9-aKgZOsQf&GZ{yqv;`m#%VrB!_U0_+VZWtAH4h7|NYN@cKe6lc|ZN= zJ0E`V%b$II=YeHDp0CaaHN93=Kd(F(E0UHN(%K#GlV*HdT+kHo?(@$K^uV^l6qbeY z3YoNM@I2lf^*l-#I?Pt7!WQdHR*b_U2UBG)z2%VGP#^=81B!Z3D-)c(-u&H?PxEW9 zy?XrazxMCG`ObTvyprzUPU+VVzIyBWgMYp8ux#k$nZFXX3uOBRmirA8MYgS?S(W;)S5eS?})22zReJo*leE8ek(!T~#=WT5>?icyK+|X-YDdEQ^6g zd7;`d9NyXXFv2alAmjY3|@MHX3WO%EXJy^nwS`O)?FZ>7`amGcbcGc@}}=5l_t zZuji(&y4Q9yAP4^XE)DDHI9GhUrpb*^U)_?{^-v?`tCpf@2-FT;a`08!4LoBqnkf_ z=kxdPFX!x4bbRf2ZqNNF$r+>a%7B-g676{=h6B`!P!I#XX_JZeY?LVw*-Gr_7yC5> z6%tC>dn*3y@Jy63j&@wEMs`YVA`FBP%nBKmq8pF>BA`5L$3Yv1-qLed5@C8EAoJ0u zzkK)e`!_zmldimUMm>LC9`skA-=N*IlA<%CdwOPgKTqi^zn!kV@#gK{yYt@7yC41X z=DXkiA8#DJ_k)jr{{3%%eEQ2@-udO-yUTL_-nn5snx?a?W{JZoHJG%L`Py>`@UC2l zB1vm)E%MGa(mjoH4O~qQo>xYo5EyjG{mVn8E$ zT$hYm2G1cg)|$J<9(A6ZituPYrVrlx#hsHIAKjk6{8GNZ+@o}6H_wppS?2P+dk-|w z0~Pez*YcHTul@SfJ2!5>div?@&u_l>tNfjBf1H2##~-}+_IGdG{?%uvf4SVxr}yqI z={JPUbY^%T9mjMq4pdYC4Nf#+Y$`aE1U|Lj3K6?{|&yK=oF}moM zaI^{`NsOq(bFC@Jb$0Y(TqEy&x=?D%0D{X!;w{6IJIH$^QkI~rREQRADNHF!e^_Gi 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 izPp5WyL|qC0R{jQ2O?d8-_&RT0000GQ; literal 0 HcmV?d00001 diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index a091c6d5cf..a0af92dec0 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -138,6 +138,7 @@ Item { id: colorSchemes readonly property int light: 0 readonly property int dark: 1 + readonly property int faintGray: 2 } Item { @@ -178,7 +179,7 @@ Item { readonly property real tableHeading: dimensions.largeScreen ? 12 : 10 readonly property real tableHeadingIcon: dimensions.largeScreen ? 60 : 33 readonly property real tableText: dimensions.largeScreen ? 15 : 12 - readonly property real buttonLabel: dimensions.largeScreen ? 13 : 9 + readonly property real buttonLabel: dimensions.largeScreen ? 14 : 9 readonly property real iconButton: dimensions.largeScreen ? 13 : 9 readonly property real listItem: dimensions.largeScreen ? 15 : 11 readonly property real tabularData: dimensions.largeScreen ? 15 : 11 @@ -209,11 +210,15 @@ Item { readonly property int blue: 1 readonly property int red: 2 readonly property int black: 3 - readonly property var textColor: [ colors.darkGray, colors.white, colors.white, colors.white ] - readonly property var colorStart: [ colors.white, colors.primaryHighlight, "#d42043", "#343434" ] - readonly property var colorFinish: [ colors.lightGrayText, colors.blueAccent, "#94132e", colors.black ] - readonly property var hoveredColor: [ colorStart[white], colorStart[blue], colorStart[red], colorFinish[black] ] - readonly property var pressedColor: [ colorFinish[white], colorFinish[blue], colorFinish[red], colorStart[black] ] + readonly property int none: 4 + readonly property int noneBorderless: 5 + readonly property int noneBorderlessWhite: 6 + readonly property int noneBorderlessGray: 7 + readonly property var textColor: [ colors.darkGray, colors.white, colors.white, colors.white, colors.white, colors.blueAccent, colors.white, colors.darkGray ] + readonly property var colorStart: [ colors.white, colors.primaryHighlight, "#d42043", "#343434", Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0) ] + readonly property var colorFinish: [ colors.lightGrayText, colors.blueAccent, "#94132e", colors.black, Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0) ] + readonly property var hoveredColor: [ colorStart[white], colorStart[blue], colorStart[red], colorFinish[black], colorStart[none], colorStart[noneBorderless], colorStart[noneBorderlessWhite], colorStart[noneBorderlessGray] ] + readonly property var pressedColor: [ colorFinish[white], colorFinish[blue], colorFinish[red], colorStart[black], colorStart[none], colorStart[noneBorderless], colorStart[noneBorderlessWhite], colorStart[noneBorderlessGray] ] readonly property var disabledColorStart: [ colorStart[white], colors.baseGrayHighlight] readonly property var disabledColorFinish: [ colorFinish[white], colors.baseGrayShadow] readonly property var disabledTextColor: [ colors.lightGrayText, colors.baseGrayShadow] @@ -338,6 +343,15 @@ Item { readonly property string stop_square: "\ue01e" readonly property string avatarTPose: "\ue01f" readonly property string lock: "\ue006" - readonly property string check_2_01: "\ue020" + readonly property string checkmark: "\ue020" + readonly property string leftRightArrows: "\ue021" + readonly property string hfc: "\ue022" + readonly property string home2: "\ue023" + readonly property string walletKey: "\ue024" + readonly property string lightning: "\ue025" + readonly property string securityImage: "\ue026" + readonly property string wallet: "\ue027" + readonly property string paperPlane: "\ue028" + readonly property string passphrase: "\ue029" } } diff --git a/interface/resources/qml/styles-uit/RalewayRegular.qml b/interface/resources/qml/styles-uit/RalewayRegular.qml index 2cffeeb59d..aab31ecf33 100644 --- a/interface/resources/qml/styles-uit/RalewayRegular.qml +++ b/interface/resources/qml/styles-uit/RalewayRegular.qml @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.5 +import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index a1488af1fe..d8860192ad 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -17,6 +17,7 @@ #include "Wallet.h" #include "Ledger.h" #include "CommerceLogging.h" +#include // inventory answers {status: 'success', data: {assets: [{id: "guid", title: "name", preview: "url"}....]}} // balance answers {status: 'success', data: {balance: integer}} @@ -59,12 +60,16 @@ void Ledger::send(const QString& endpoint, const QString& success, const QString QJsonDocument(request).toJson()); } -void Ledger::signedSend(const QString& propertyName, const QByteArray& text, const QString& key, const QString& endpoint, const QString& success, const QString& fail) { +void Ledger::signedSend(const QString& propertyName, const QByteArray& text, const QString& key, const QString& endpoint, const QString& success, const QString& fail, const bool controlled_failure) { auto wallet = DependencyManager::get(); QString signature = key.isEmpty() ? "" : wallet->signWithKey(text, key); QJsonObject request; request[propertyName] = QString(text); - request["signature"] = signature; + if (!controlled_failure) { + request["signature"] = signature; + } else { + request["signature"] = QString("controlled failure!"); + } send(endpoint, success, fail, QNetworkAccessManager::PutOperation, request); } @@ -75,16 +80,15 @@ void Ledger::keysQuery(const QString& endpoint, const QString& success, const QS send(endpoint, success, fail, QNetworkAccessManager::PostOperation, request); } -void Ledger::buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const QString& buyerUsername) { +void Ledger::buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const bool controlled_failure) { QJsonObject transaction; transaction["hfc_key"] = hfc_key; transaction["cost"] = cost; transaction["asset_id"] = asset_id; transaction["inventory_key"] = inventory_key; - transaction["inventory_buyer_username"] = buyerUsername; QJsonDocument transactionDoc{ transaction }; auto transactionString = transactionDoc.toJson(QJsonDocument::Compact); - signedSend("transaction", transactionString, hfc_key, "buy", "buySuccess", "buyFailure"); + signedSend("transaction", transactionString, hfc_key, "buy", "buySuccess", "buyFailure", controlled_failure); } bool Ledger::receiveAt(const QString& hfc_key, const QString& old_key) { @@ -110,14 +114,15 @@ void Ledger::inventory(const QStringList& keys) { QString nameFromKey(const QString& key, const QStringList& publicKeys) { if (key.isNull() || key.isEmpty()) { - return "Marketplace"; + return "Marketplace"; } if (publicKeys.contains(key)) { return "You"; } - return "Someone"; + return "Someone"; } +static const QString MARKETPLACE_ITEMS_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL.toString() + "/marketplace/items/"; void Ledger::historySuccess(QNetworkReply& reply) { // here we send a historyResult with some extra stuff in it // Namely, the styled text we'd like to show. The issue is the @@ -135,10 +140,27 @@ void Ledger::historySuccess(QNetworkReply& reply) { QJsonArray newHistoryArray; // TODO: do this with 0 copies if possible - for(auto it = historyArray.begin(); it != historyArray.end(); it++) { + for (auto it = historyArray.begin(); it != historyArray.end(); it++) { auto valueObject = (*it).toObject(); QString from = nameFromKey(valueObject["sender_key"].toString(), keys); QString to = nameFromKey(valueObject["recipient_key"].toString(), keys); + bool isHfc = valueObject["asset_title"].toString() == "HFC"; + bool iAmReceiving = to == "You"; + QString coloredQuantityAndAssetTitle = QString::number(valueObject["quantity"].toInt()) + " " + valueObject["asset_title"].toString(); + if (isHfc) { + if (iAmReceiving) { + coloredQuantityAndAssetTitle = QString("") + coloredQuantityAndAssetTitle + QString(""); + } else { + coloredQuantityAndAssetTitle = QString("") + coloredQuantityAndAssetTitle + QString(""); + } + } else { + coloredQuantityAndAssetTitle = QString("\"") + + coloredQuantityAndAssetTitle + + QString("\""); + } // turns out on my machine, toLocalTime convert to some weird timezone, yet the // systemTimeZone is correct. To avoid a strange bug with other's systems too, lets // be explicit @@ -148,8 +170,8 @@ void Ledger::historySuccess(QNetworkReply& reply) { QDateTime createdAt = QDateTime::fromSecsSinceEpoch(valueObject["created_at"].toInt(), Qt::UTC); #endif QDateTime localCreatedAt = createdAt.toTimeZone(QTimeZone::systemTimeZone()); - valueObject["text"] = QString("%1 sent %2 %3 %4 on %5 with message \"%6\""). - arg(from, to, QString::number(valueObject["quantity"].toInt()), valueObject["asset_title"].toString(), localCreatedAt.toString(Qt::SystemLocaleShortDate), valueObject["message"].toString()); + valueObject["text"] = QString("%1 sent %2 %3 with message \"%4\""). + arg(from, to, coloredQuantityAndAssetTitle, valueObject["message"].toString()); newHistoryArray.push_back(valueObject); } // now copy the rest of the json -- this is inefficient @@ -201,3 +223,18 @@ void Ledger::accountFailure(QNetworkReply& reply) { void Ledger::account() { send("hfc_account", "accountSuccess", "accountFailure", QNetworkAccessManager::PutOperation, QJsonObject()); } + +// The api/failResponse is called just for the side effect of logging. +void Ledger::updateLocationSuccess(QNetworkReply& reply) { apiResponse("reset", reply); } +void Ledger::updateLocationFailure(QNetworkReply& reply) { failResponse("reset", reply); } +void Ledger::updateLocation(const QString& asset_id, const QString location, const bool controlledFailure) { + auto wallet = DependencyManager::get(); + QStringList keys = wallet->listPublicKeys(); + QString key = keys[0]; + QJsonObject transaction; + transaction["asset_id"] = asset_id; + transaction["location"] = location; + QJsonDocument transactionDoc{ transaction }; + auto transactionString = transactionDoc.toJson(QJsonDocument::Compact); + signedSend("transaction", transactionString, key, "location", "updateLocationSuccess", "updateLocationFailure", controlledFailure); +} diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index 6aadf5afb0..da6c67224f 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -24,13 +24,14 @@ class Ledger : public QObject, public Dependency { SINGLETON_DEPENDENCY public: - void buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const QString& buyerUsername = ""); + void buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const bool controlled_failure = false); bool receiveAt(const QString& hfc_key, const QString& old_key); void balance(const QStringList& keys); void inventory(const QStringList& keys); void history(const QStringList& keys); void account(); void reset(); + void updateLocation(const QString& asset_id, const QString location, const bool controlledFailure = false); signals: void buyResult(QJsonObject result); @@ -39,6 +40,7 @@ signals: void inventoryResult(QJsonObject result); void historyResult(QJsonObject result); void accountResult(QJsonObject result); + void locationUpdateResult(QJsonObject result); public slots: void buySuccess(QNetworkReply& reply); @@ -55,13 +57,15 @@ public slots: void resetFailure(QNetworkReply& reply); void accountSuccess(QNetworkReply& reply); void accountFailure(QNetworkReply& reply); + void updateLocationSuccess(QNetworkReply& reply); + void updateLocationFailure(QNetworkReply& reply); private: QJsonObject apiResponse(const QString& label, QNetworkReply& reply); QJsonObject failResponse(const QString& label, QNetworkReply& reply); void send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, QJsonObject request); void keysQuery(const QString& endpoint, const QString& success, const QString& fail); - void signedSend(const QString& propertyName, const QByteArray& text, const QString& key, const QString& endpoint, const QString& success, const QString& fail); + void signedSend(const QString& propertyName, const QByteArray& text, const QString& key, const QString& endpoint, const QString& success, const QString& fail, const bool controlled_failure = false); }; #endif // hifi_Ledger_h diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 9315a2e9c5..93de2ef566 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -54,7 +54,7 @@ void QmlCommerce::chooseSecurityImage(const QString& imageFile) { wallet->chooseSecurityImage(imageFile); } -void QmlCommerce::buy(const QString& assetId, int cost, const QString& buyerUsername) { +void QmlCommerce::buy(const QString& assetId, int cost, const bool controlledFailure) { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); QStringList keys = wallet->listPublicKeys(); @@ -64,7 +64,7 @@ void QmlCommerce::buy(const QString& assetId, int cost, const QString& buyerUser } QString key = keys[0]; // For now, we receive at the same key that pays for it. - ledger->buy(key, cost, assetId, key, buyerUsername); + ledger->buy(key, cost, assetId, key, controlledFailure); } void QmlCommerce::balance() { diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 7b0bab95a6..42f44a3a85 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -50,7 +50,7 @@ protected: Q_INVOKABLE void chooseSecurityImage(const QString& imageFile); Q_INVOKABLE void setPassphrase(const QString& passphrase); - Q_INVOKABLE void buy(const QString& assetId, int cost, const QString& buyerUsername = ""); + Q_INVOKABLE void buy(const QString& assetId, int cost, const bool controlledFailure = false); Q_INVOKABLE void balance(); Q_INVOKABLE void inventory(); Q_INVOKABLE void history(); diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 04cf5a671e..e1e2849a04 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -280,6 +280,13 @@ void initializeAESKeys(unsigned char* ivec, unsigned char* ckey, const QByteArra memcpy(ckey, wallet->getCKey(), 32); } +Wallet::Wallet() { + auto nodeList = DependencyManager::get(); + auto& packetReceiver = nodeList->getPacketReceiver(); + + packetReceiver.registerListener(PacketType::ChallengeOwnership, this, "verifyOwnerChallenge"); +} + Wallet::~Wallet() { if (_securityImage) { delete _securityImage; @@ -645,3 +652,30 @@ bool Wallet::changePassphrase(const QString& newPassphrase) { qCDebug(commerce) << "changing passphrase"; return writeWallet(newPassphrase); } + +void Wallet::handleChallengeOwnershipPacket(QSharedPointer packet, SharedNodePointer sendingNode) { + QString decryptedText; + quint64 encryptedTextSize; + quint64 publicKeySize; + + if (verifyOwnerChallenge(packet->read(packet->readPrimitive(&encryptedTextSize)), packet->read(packet->readPrimitive(&publicKeySize)), decryptedText)) { + auto nodeList = DependencyManager::get(); + // setup the packet + auto decryptedTextPacket = NLPacket::create(PacketType::ChallengeOwnership, NUM_BYTES_RFC4122_UUID + decryptedText.size(), true); + + // write the decrypted text to the packet + decryptedTextPacket->write(decryptedText.toUtf8()); + + qCDebug(commerce) << "Sending ChallengeOwnership Packet containing decrypted text"; + + nodeList->sendPacket(std::move(decryptedTextPacket), *sendingNode); + } else { + qCDebug(commerce) << "verifyOwnerChallenge() returned false"; + } +} + +bool Wallet::verifyOwnerChallenge(const QByteArray& encryptedText, const QString& publicKey, QString& decryptedText) { + // I have no idea how to do this yet, so here's some dummy code that may not even work. + decryptedText = QString("hello"); + return true; +} diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index b8913e9a5e..59812d5222 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -15,6 +15,8 @@ #define hifi_Wallet_h #include +#include +#include #include @@ -23,7 +25,7 @@ class Wallet : public QObject, public Dependency { SINGLETON_DEPENDENCY public: - + Wallet(); ~Wallet(); // These are currently blocking calls, although they might take a moment. bool generateKeyPair(); @@ -52,6 +54,9 @@ signals: void securityImageResult(bool exists); void keyFilePathIfExistsResult(const QString& path); +private slots: + void handleChallengeOwnershipPacket(QSharedPointer packet, SharedNodePointer sendingNode); + private: QStringList _publicKeys{}; QPixmap* _securityImage { nullptr }; @@ -64,6 +69,8 @@ private: void updateImageProvider(); bool writeSecurityImage(const QPixmap* pixmap, const QString& outputFilePath); bool readSecurityImage(const QString& inputFilePath, unsigned char** outputBufferPtr, int* outputBufferLen); + + bool verifyOwnerChallenge(const QByteArray& encryptedText, const QString& publicKey, QString& decryptedText); }; #endif // hifi_Wallet_h diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index 2366b888e7..8e4254a786 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -137,7 +137,7 @@ bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID& boundingBox.findRayIntersection(cameraPosition, direction, distance, face, normal); float offsetAngle = -CONTEXT_OVERLAY_OFFSET_ANGLE; if (event.getID() == LEFT_HAND_HW_ID) { - offsetAngle *= -1; + offsetAngle *= -1.0f; } contextOverlayPosition = (glm::quat(glm::radians(glm::vec3(0.0f, offsetAngle, 0.0f)))) * ((cameraPosition + direction * (distance - CONTEXT_OVERLAY_OFFSET_DISTANCE))); @@ -201,8 +201,12 @@ bool ContextOverlayInterface::destroyContextOverlay(const EntityItemID& entityIt void ContextOverlayInterface::contextOverlays_mousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { if (overlayID == _contextOverlayID && event.getButton() == PointerEvent::PrimaryButton) { qCDebug(context_overlay) << "Clicked Context Overlay. Entity ID:" << _currentEntityWithContextOverlay << "Overlay ID:" << overlayID; - openMarketplace(); - destroyContextOverlay(_currentEntityWithContextOverlay, PointerEvent()); + if (_commerceSettingSwitch.get()) { + openInspectionCertificate(); + } else { + openMarketplace(); + } + emit contextOverlayClicked(_currentEntityWithContextOverlay); _contextOverlayJustClicked = true; } } @@ -239,6 +243,16 @@ void ContextOverlayInterface::contextOverlays_hoverLeaveEntity(const EntityItemI } } +static const QString INSPECTION_CERTIFICATE_QML_PATH = qApp->applicationDirPath() + "../../../qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; +void ContextOverlayInterface::openInspectionCertificate() { + // lets open the tablet to the inspection certificate QML + if (!_currentEntityWithContextOverlay.isNull() && _entityMarketplaceID.length() > 0) { + auto tablet = dynamic_cast(_tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + tablet->loadQMLSource(INSPECTION_CERTIFICATE_QML_PATH); + _hmdScriptingInterface->openTablet(); + } +} + static const QString MARKETPLACE_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL.toString() + "/marketplace/items/"; void ContextOverlayInterface::openMarketplace() { diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h index fddd1fcdb5..ec5913444f 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.h +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -56,6 +56,9 @@ public: bool getIsInMarketplaceInspectionMode() { return _isInMarketplaceInspectionMode; } void setIsInMarketplaceInspectionMode(bool mode) { _isInMarketplaceInspectionMode = mode; } +signals: + void contextOverlayClicked(const QUuid& currentEntityWithContextOverlay); + public slots: bool createOrDestroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event); bool destroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event); @@ -76,6 +79,9 @@ private: bool _isInMarketplaceInspectionMode { false }; + Setting::Handle _commerceSettingSwitch{ "commerce", false }; + + void openInspectionCertificate(); void openMarketplace(); void enableEntityHighlight(const EntityItemID& entityItemID); void disableEntityHighlight(const EntityItemID& entityItemID); diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index a95f7ba316..4eac23c867 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -306,9 +306,6 @@ public: QString getMarketplaceID() const; void setMarketplaceID(const QString& value); - bool getShouldHighlight() const; - void setShouldHighlight(const bool value); - // TODO: get rid of users of getRadius()... float getRadius() const; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index c6b5bc953b..b8396e7357 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -47,6 +47,8 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership connect(nodeList.data(), &NodeList::isAllowedEditorChanged, this, &EntityScriptingInterface::canAdjustLocksChanged); connect(nodeList.data(), &NodeList::canRezChanged, this, &EntityScriptingInterface::canRezChanged); connect(nodeList.data(), &NodeList::canRezTmpChanged, this, &EntityScriptingInterface::canRezTmpChanged); + connect(nodeList.data(), &NodeList::canRezCertifiedChanged, this, &EntityScriptingInterface::canRezCertifiedChanged); + connect(nodeList.data(), &NodeList::canRezTmpCertifiedChanged, this, &EntityScriptingInterface::canRezTmpCertifiedChanged); connect(nodeList.data(), &NodeList::canWriteAssetsChanged, this, &EntityScriptingInterface::canWriteAssetsChanged); } @@ -76,6 +78,16 @@ bool EntityScriptingInterface::canRezTmp() { return nodeList->getThisNodeCanRezTmp(); } +bool EntityScriptingInterface::canRezCertified() { + auto nodeList = DependencyManager::get(); + return nodeList->getThisNodeCanRezCertified(); +} + +bool EntityScriptingInterface::canRezTmpCertified() { + auto nodeList = DependencyManager::get(); + return nodeList->getThisNodeCanRezTmpCertified(); +} + bool EntityScriptingInterface::canWriteAssets() { auto nodeList = DependencyManager::get(); return nodeList->getThisNodeCanWriteAssets(); diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 7248c1f851..e594f555d1 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -126,6 +126,18 @@ public slots: */ Q_INVOKABLE bool canRezTmp(); + /**jsdoc + * @function Entities.canRezCertified + * @return {bool} `true` if the DomainServer will allow this Node/Avatar to rez new certified entities + */ + Q_INVOKABLE bool canRezCertified(); + + /**jsdoc + * @function Entities.canRezTmpCertified + * @return {bool} `true` if the DomainServer will allow this Node/Avatar to rez new temporary certified entities + */ + Q_INVOKABLE bool canRezTmpCertified(); + /**jsdoc * @function Entities.canWriteAsseets * @return {bool} `true` if the DomainServer will allow this Node/Avatar to write to the asset server @@ -380,6 +392,8 @@ signals: void canAdjustLocksChanged(bool canAdjustLocks); void canRezChanged(bool canRez); void canRezTmpChanged(bool canRez); + void canRezCertifiedChanged(bool canRez); + void canRezTmpCertifiedChanged(bool canRez); void canWriteAssetsChanged(bool canWriteAssets); void mousePressOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 08acf9b058..5c5aee97ff 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -350,7 +350,8 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti } if (!properties.getClientOnly() && getIsClient() && - !nodeList->getThisNodeCanRez() && !nodeList->getThisNodeCanRezTmp()) { + !nodeList->getThisNodeCanRez() && !nodeList->getThisNodeCanRezTmp() && + !nodeList->getThisNodeCanRezCertified() && !nodeList->getThisNodeCanRezTmpCertified()) { return nullptr; } @@ -1076,7 +1077,8 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } if ((isAdd || properties.lifetimeChanged()) && - !senderNode->getCanRez() && senderNode->getCanRezTmp()) { + ((!senderNode->getCanRez() && senderNode->getCanRezTmp()) || + (!senderNode->getCanRezCertified() && senderNode->getCanRezTmpCertified()))) { // this node is only allowed to rez temporary entities. if need be, cap the lifetime. if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME || properties.getLifetime() > _maxTmpEntityLifetime) { @@ -1146,8 +1148,11 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } else if (!senderNode->getCanRez() && !senderNode->getCanRezTmp()) { failedAdd = true; qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID() - << "] attempted to add an entity ID:" << entityItemID; - + << "] attempted to add an entity ID:" << entityItemID; + // FIXME after Cert ID property integrated + } else if (/*!properties.getCertificateID().isNull() && */!senderNode->getCanRezCertified() && !senderNode->getCanRezTmpCertified()) { + qCDebug(entities) << "User without 'certified rez rights' [" << senderNode->getUUID() + << "] attempted to add a certified entity with ID:" << entityItemID; } else { // this is a new entity... assign a new entityID properties.setCreated(properties.getLastEdited()); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index d0cb0109c7..574ec7f054 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -159,6 +159,14 @@ void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) { newPermissions.can(NodePermissions::Permission::canRezTemporaryEntities)) { emit canRezTmpChanged(_permissions.can(NodePermissions::Permission::canRezTemporaryEntities)); } + if (originalPermissions.can(NodePermissions::Permission::canRezPermanentCertifiedEntities) != + newPermissions.can(NodePermissions::Permission::canRezPermanentCertifiedEntities)) { + emit canRezCertifiedChanged(_permissions.can(NodePermissions::Permission::canRezPermanentCertifiedEntities)); + } + if (originalPermissions.can(NodePermissions::Permission::canRezTemporaryCertifiedEntities) != + newPermissions.can(NodePermissions::Permission::canRezTemporaryCertifiedEntities)) { + emit canRezTmpCertifiedChanged(_permissions.can(NodePermissions::Permission::canRezTemporaryCertifiedEntities)); + } if (originalPermissions.can(NodePermissions::Permission::canWriteToAssetServer) != newPermissions.can(NodePermissions::Permission::canWriteToAssetServer)) { emit canWriteAssetsChanged(_permissions.can(NodePermissions::Permission::canWriteToAssetServer)); diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index f730bcfa17..994f91db19 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -113,6 +113,8 @@ public: bool isAllowedEditor() const { return _permissions.can(NodePermissions::Permission::canAdjustLocks); } bool getThisNodeCanRez() const { return _permissions.can(NodePermissions::Permission::canRezPermanentEntities); } bool getThisNodeCanRezTmp() const { return _permissions.can(NodePermissions::Permission::canRezTemporaryEntities); } + bool getThisNodeCanRezCertified() const { return _permissions.can(NodePermissions::Permission::canRezPermanentCertifiedEntities); } + bool getThisNodeCanRezTmpCertified() const { return _permissions.can(NodePermissions::Permission::canRezTemporaryCertifiedEntities); } bool getThisNodeCanWriteAssets() const { return _permissions.can(NodePermissions::Permission::canWriteToAssetServer); } bool getThisNodeCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); } bool getThisNodeCanReplaceContent() const { return _permissions.can(NodePermissions::Permission::canReplaceDomainContent); } @@ -330,6 +332,8 @@ signals: void isAllowedEditorChanged(bool isAllowedEditor); void canRezChanged(bool canRez); void canRezTmpChanged(bool canRezTmp); + void canRezCertifiedChanged(bool canRez); + void canRezTmpCertifiedChanged(bool canRezTmp); void canWriteAssetsChanged(bool canWriteAssets); void canKickChanged(bool canKick); void canReplaceContentChanged(bool canReplaceContent); diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 4451ba4abe..00d3c61fd0 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -72,6 +72,8 @@ public: bool isAllowedEditor() const { return _permissions.can(NodePermissions::Permission::canAdjustLocks); } bool getCanRez() const { return _permissions.can(NodePermissions::Permission::canRezPermanentEntities); } bool getCanRezTmp() const { return _permissions.can(NodePermissions::Permission::canRezTemporaryEntities); } + bool getCanRezCertified() const { return _permissions.can(NodePermissions::Permission::canRezPermanentCertifiedEntities); } + bool getCanRezTmpCertified() const { return _permissions.can(NodePermissions::Permission::canRezTemporaryCertifiedEntities); } bool getCanWriteToAssetServer() const { return _permissions.can(NodePermissions::Permission::canWriteToAssetServer); } bool getCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); } bool getCanReplaceContent() const { return _permissions.can(NodePermissions::Permission::canReplaceDomainContent); } diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp index 67359ee862..92ebf1d01e 100644 --- a/libraries/networking/src/NodePermissions.cpp +++ b/libraries/networking/src/NodePermissions.cpp @@ -60,6 +60,8 @@ NodePermissions::NodePermissions(QMap perms) { permissions |= perms["id_can_adjust_locks"].toBool() ? Permission::canAdjustLocks : Permission::none; permissions |= perms["id_can_rez"].toBool() ? Permission::canRezPermanentEntities : Permission::none; permissions |= perms["id_can_rez_tmp"].toBool() ? Permission::canRezTemporaryEntities : Permission::none; + permissions |= perms["id_can_rez_certified"].toBool() ? Permission::canRezPermanentCertifiedEntities : Permission::none; + permissions |= perms["id_can_rez_tmp_certified"].toBool() ? Permission::canRezTemporaryCertifiedEntities : Permission::none; permissions |= perms["id_can_write_to_asset_server"].toBool() ? Permission::canWriteToAssetServer : Permission::none; permissions |= perms["id_can_connect_past_max_capacity"].toBool() ? Permission::canConnectPastMaxCapacity : Permission::none; @@ -86,6 +88,8 @@ QVariant NodePermissions::toVariant(QHash groupRanks) { values["id_can_adjust_locks"] = can(Permission::canAdjustLocks); values["id_can_rez"] = can(Permission::canRezPermanentEntities); values["id_can_rez_tmp"] = can(Permission::canRezTemporaryEntities); + values["id_can_rez_certified"] = can(Permission::canRezPermanentCertifiedEntities); + values["id_can_rez_tmp_certified"] = can(Permission::canRezTemporaryCertifiedEntities); values["id_can_write_to_asset_server"] = can(Permission::canWriteToAssetServer); values["id_can_connect_past_max_capacity"] = can(Permission::canConnectPastMaxCapacity); values["id_can_kick"] = can(Permission::canKick); @@ -144,6 +148,12 @@ QDebug operator<<(QDebug debug, const NodePermissions& perms) { if (perms.can(NodePermissions::Permission::canRezTemporaryEntities)) { debug << " rez-tmp"; } + if (perms.can(NodePermissions::Permission::canRezPermanentCertifiedEntities)) { + debug << " rez-certified"; + } + if (perms.can(NodePermissions::Permission::canRezTemporaryCertifiedEntities)) { + debug << " rez-tmp-certified"; + } if (perms.can(NodePermissions::Permission::canWriteToAssetServer)) { debug << " asset-server"; } diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index 8f7bdaebfe..d0e421a438 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -73,7 +73,9 @@ public: canWriteToAssetServer = 16, canConnectPastMaxCapacity = 32, canKick = 64, - canReplaceDomainContent = 128 + canReplaceDomainContent = 128, + canRezPermanentCertifiedEntities = 256, + canRezTemporaryCertifiedEntities = 512 }; Q_DECLARE_FLAGS(Permissions, Permission) Permissions permissions; diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 4fefc6ab3a..f492e42460 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -122,6 +122,7 @@ public: ReplicatedKillAvatar, ReplicatedBulkAvatarData, OctreeFileReplacementFromUrl, + ChallengeOwnership, NUM_PACKET_TYPE }; diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 107160154a..0e58d1ecd3 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -25,6 +25,7 @@ // -WALLET_QML_SOURCE: The path to the Wallet QML // -onWalletScreen: true/false depending on whether we're looking at the app. var WALLET_QML_SOURCE = Script.resourcesPath() + "qml/hifi/commerce/wallet/Wallet.qml"; + var MARKETPLACE_PURCHASES_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/purchases/Purchases.qml"; var onWalletScreen = false; function onButtonClicked() { if (!tablet) { @@ -54,6 +55,7 @@ // -Called when a message is received from SpectatorCamera.qml. The "message" argument is what is sent from the QML // in the format "{method, params}", like json-rpc. See also sendToQml(). var isHmdPreviewDisabled = true; + var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); function fromQml(message) { switch (message.method) { case 'passphrasePopup_cancelClicked': @@ -79,6 +81,12 @@ onButtonClicked(); onButtonClicked(); break; + case 'transactionHistory_linkClicked': + tablet.gotoWebScreen(message.marketplaceLink, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'goToPurchases': + tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); + break; default: print('Unrecognized message from QML:', JSON.stringify(message)); } diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 138e3a3956..0c82ab4343 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -27,6 +27,7 @@ var isPreparing = false; // Explicitly track download request status. var confirmAllPurchases = false; // Set this to "true" to cause Checkout.qml to popup for all items, even if free + var userIsLoggedIn = false; function injectCommonCode(isDirectoryPage) { @@ -90,23 +91,77 @@ }); } - function addPurchasesButton() { - // Why isn't this an id?! This really shouldn't be a class on the website, but it is. - var navbarBrandElement = document.getElementsByClassName('navbar-brand')[0]; - var purchasesElement = document.createElement('a'); - purchasesElement.classList.add("btn"); - purchasesElement.classList.add("btn-default"); - purchasesElement.id = "purchasesButton"; - purchasesElement.setAttribute('href', "#"); - purchasesElement.innerHTML = "PURCHASES"; - purchasesElement.style = "height:100%;margin-top:0;padding:15px 15px;"; - navbarBrandElement.parentNode.insertAdjacentElement('beforeend', purchasesElement); - $('#purchasesButton').on('click', function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "PURCHASES", - referrerURL: window.location.href - })); - }); + function maybeAddLogInButton() { + if (!userIsLoggedIn) { + var resultsElement = document.getElementById('results'); + var logInElement = document.createElement('div'); + logInElement.classList.add("row"); + logInElement.id = "logInDiv"; + logInElement.style = "height:60px;margin:20px 10px 10px 10px;padding:5px;" + + "background-color:#D6F4D8;border-color:#aee9b2;border-width:2px;border-style:solid;border-radius:5px;"; + + var button = document.createElement('a'); + button.classList.add("btn"); + button.classList.add("btn-default"); + button.id = "logInButton"; + button.setAttribute('href', "#"); + button.innerHTML = "LOG IN"; + button.style = "width:80px;height:100%;margin-top:0;margin-left:10px;padding:13px;font-weight:bold;background:linear-gradient(white, #ccc);"; + button.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "LOGIN" + })); + }; + + var span = document.createElement('span'); + span.style = "margin:10px;color:#1b6420;font-size:15px;"; + span.innerHTML = "to purchase items from the Marketplace."; + + var xButton = document.createElement('a'); + xButton.id = "xButton"; + xButton.setAttribute('href', "#"); + xButton.style = "width:50px;height:100%;margin:0;color:#ccc;font-size:20px;"; + xButton.innerHTML = "X"; + xButton.onclick = function () { + logInElement.remove(); + dummyRow.remove(); + }; + + logInElement.appendChild(button); + logInElement.appendChild(span); + logInElement.appendChild(xButton); + + resultsElement.insertBefore(logInElement, resultsElement.firstChild); + + // Dummy row for padding + var dummyRow = document.createElement('div'); + dummyRow.classList.add("row"); + dummyRow.style = "height:15px;"; + resultsElement.insertBefore(dummyRow, resultsElement.firstChild); + } + } + + function maybeAddPurchasesButton() { + if (userIsLoggedIn) { + // Why isn't this an id?! This really shouldn't be a class on the website, but it is. + var navbarBrandElement = document.getElementsByClassName('navbar-brand')[0]; + var purchasesElement = document.createElement('a'); + var dropDownElement = document.getElementById('user-dropdown'); + purchasesElement.id = "purchasesButton"; + purchasesElement.setAttribute('href', "#"); + purchasesElement.innerHTML = "MY PURCHASES"; + // FRONTEND WEBDEV RANT: The username dropdown should REALLY not be programmed to be on the same + // line as the search bar, overlaid on top of the search bar, floated right, and then relatively bumped up using "top:-50px". + purchasesElement.style = "height:100%;margin-top:18px;font-weight:bold;float:right;margin-right:" + (dropDownElement.offsetWidth + 30) + + "px;position:relative;z-index:999;"; + navbarBrandElement.parentNode.insertAdjacentElement('beforeend', purchasesElement); + $('#purchasesButton').on('click', function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "PURCHASES", + referrerURL: window.location.href + })); + }); + } } function buyButtonClicked(id, name, author, price, href) { @@ -123,6 +178,14 @@ function injectBuyButtonOnMainPage() { var cost; + // Unbind original mouseenter and mouseleave behavior + $('body').off('mouseenter', '#price-or-edit .price'); + $('body').off('mouseleave', '#price-or-edit .price'); + + $('.grid-item').find('#price-or-edit').each(function () { + $(this).css({ "margin-top": "0" }); + }); + $('.grid-item').find('#price-or-edit').find('a').each(function() { $(this).attr('data-href', $(this).attr('href')); $(this).attr('href', '#'); @@ -131,14 +194,36 @@ $(this).closest('.col-xs-3').prev().attr("class", 'col-xs-6'); $(this).closest('.col-xs-3').attr("class", 'col-xs-6'); + var priceElement = $(this).find('.price') + priceElement.css({ + "padding": "3px 5px", + "height": "40px", + "background": "linear-gradient(#00b4ef, #0093C5)", + "color": "#FFF", + "font-weight": "600", + "line-height": "34px" + }); + if (parseInt(cost) > 0) { - var priceElement = $(this).find('.price') - priceElement.css({ "width": "auto", "padding": "3px 5px", "height": "26px" }); - priceElement.text(cost + ' HFC'); - priceElement.css({ "min-width": priceElement.width() + 10 }); + priceElement.css({ "width": "auto" }); + priceElement.html(' ' + cost); + priceElement.css({ "min-width": priceElement.width() + 30 }); } }); + // change pricing to GET on button hover + $('body').on('mouseenter', '#price-or-edit .price', function () { + var $this = $(this); + $this.data('initialHtml', $this.html()); + $this.text('GET'); + }); + + $('body').on('mouseleave', '#price-or-edit .price', function () { + var $this = $(this); + $this.html($this.data('initialHtml')); + }); + $('.grid-item').find('#price-or-edit').find('a').on('click', function () { buyButtonClicked($(this).closest('.grid-item').attr('data-item-id'), @@ -151,6 +236,9 @@ function injectHiFiCode() { if (confirmAllPurchases) { + + maybeAddLogInButton(); + var target = document.getElementById('templated-items'); // MutationObserver is necessary because the DOM is populated after the page is loaded. // We're searching for changes to the element whose ID is '#templated-items' - this is @@ -167,30 +255,41 @@ // Try this here in case it works (it will if the user just pressed the "back" button, // since that doesn't trigger another AJAX request. injectBuyButtonOnMainPage(); - addPurchasesButton(); + maybeAddPurchasesButton(); } } function injectHiFiItemPageCode() { if (confirmAllPurchases) { - var href = $('#side-info').find('.btn').first().attr('href'); - $('#side-info').find('.btn').first().attr('href', '#'); + + maybeAddLogInButton(); + + var purchaseButton = $('#side-info').find('.btn').first(); + + var href = purchaseButton.attr('href'); + purchaseButton.attr('href', '#'); + purchaseButton.css({ + "background": "linear-gradient(#00b4ef, #0093C5)", + "color": "#FFF", + "font-weight": "600", + "padding-bottom": "10px" + }); var cost = $('.item-cost').text(); if (parseInt(cost) > 0 && $('#side-info').find('#buyItemButton').size() === 0) { - $('#side-info').find('.btn').first().html('Own Item: ' + cost + ' HFC'); - + purchaseButton.html('PURCHASE ' + cost); } - $('#side-info').find('.btn').first().on('click', function () { + purchaseButton.on('click', function () { buyButtonClicked(window.location.pathname.split("/")[3], $('#top-center').find('h1').text(), $('#creator').find('.value').text(), cost, href); }); - addPurchasesButton(); + maybeAddPurchasesButton(); } } @@ -451,7 +550,8 @@ if (parsedJsonMessage.type === "marketplaces") { if (parsedJsonMessage.action === "commerceSetting") { - confirmAllPurchases = !!parsedJsonMessage.data; + confirmAllPurchases = !!parsedJsonMessage.data.commerceMode; + userIsLoggedIn = !!parsedJsonMessage.data.userIsLoggedIn injectCode(); } } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 7ae0aa3390..5a00b20441 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -22,6 +22,7 @@ var MARKETPLACE_CHECKOUT_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/checkout/Checkout.qml"; var MARKETPLACE_PURCHASES_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/purchases/Purchases.qml"; var MARKETPLACE_WALLET_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/wallet/Wallet.qml"; + var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "commerce/inspectionCertificate/InspectionCertificate.qml"; var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; // var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; @@ -56,10 +57,26 @@ Window.messageBoxClosed.connect(onMessageBoxClosed); var onMarketplaceScreen = false; + var onCommerceScreen = false; + var debugCheckout = false; function showMarketplace() { - UserActivityLogger.openedMarketplace(); - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + if (!debugCheckout) { + UserActivityLogger.openedMarketplace(); + tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + } else { + tablet.pushOntoStack(MARKETPLACE_CHECKOUT_QML_PATH); + tablet.sendToQml({ + method: 'updateCheckoutQML', params: { + itemId: '0d90d21c-ce7a-4990-ad18-e9d2cf991027', + itemName: 'Test Flaregun', + itemAuthor: 'hifiDave', + itemPrice: 17, + itemHref: 'http://mpassets.highfidelity.com/0d90d21c-ce7a-4990-ad18-e9d2cf991027-v1/flaregun.json', + }, + canRezCertifiedItems: Entities.canRezCertified || Entities.canRezTmpCertified + }); + } } var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); @@ -76,7 +93,7 @@ } function onClick() { - if (onMarketplaceScreen) { + if (onMarketplaceScreen || onCommerceScreen) { // for toolbar-mode: go back to home screen, this will close the window. tablet.gotoHomeScreen(); } else { @@ -86,11 +103,24 @@ } } + var referrerURL; // Used for updating Purchases QML + var filterText; // Used for updating Purchases QML function onScreenChanged(type, url) { onMarketplaceScreen = type === "Web" && url === MARKETPLACE_URL_INITIAL; - wireEventBridge(type === "QML" && (url === MARKETPLACE_CHECKOUT_QML_PATH || url === MARKETPLACE_PURCHASES_QML_PATH)); + onCommerceScreen = type === "QML" && (url === MARKETPLACE_CHECKOUT_QML_PATH || url === MARKETPLACE_PURCHASES_QML_PATH || url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); + wireEventBridge(onCommerceScreen); + + if (url === MARKETPLACE_PURCHASES_QML_PATH) { + tablet.sendToQml({ + method: 'updatePurchases', + canRezCertifiedItems: Entities.canRezCertified || Entities.canRezTmpCertified, + referrerURL: referrerURL, + filterText: filterText + }); + } + // for toolbar mode: change button to active when window is first openend, false otherwise. - marketplaceButton.editProperties({ isActive: onMarketplaceScreen }); + marketplaceButton.editProperties({ isActive: onMarketplaceScreen || onCommerceScreen }); if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { ContextOverlay.isInMarketplaceInspectionMode = true; } else { @@ -98,9 +128,36 @@ } } + function setCertificateInfo(currentEntityWithContextOverlay, itemMarketplaceId, closeGoesToPurchases) { + wireEventBridge(true); + tablet.sendToQml({ + method: 'inspectionCertificate_setMarketplaceId', + marketplaceId: itemMarketplaceId || Entities.getEntityProperties(currentEntityWithContextOverlay, ['marketplaceID']).marketplaceID, + closeGoesToPurchases: closeGoesToPurchases + }); + // ZRF FIXME! Make a call to the endpoint to get item info instead of this silliness + Script.setTimeout(function () { + var randomNumber = Math.floor((Math.random() * 150) + 1); + tablet.sendToQml({ + method: 'inspectionCertificate_setItemInfo', + itemName: "The Greatest Item", + itemOwner: "ABCDEFG1234567", + itemEdition: (Math.floor(Math.random() * randomNumber) + " / " + randomNumber) + }); + }, 500); + } + + function onUsernameChanged() { + if (onMarketplaceScreen) { + tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + } + } + marketplaceButton.clicked.connect(onClick); tablet.screenChanged.connect(onScreenChanged); Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); + ContextOverlay.contextOverlayClicked.connect(setCertificateInfo); + GlobalServices.myUsernameChanged.connect(onUsernameChanged); function onMessage(message) { @@ -133,20 +190,28 @@ } else { var parsedJsonMessage = JSON.parse(message); if (parsedJsonMessage.type === "CHECKOUT") { + wireEventBridge(true); tablet.pushOntoStack(MARKETPLACE_CHECKOUT_QML_PATH); - tablet.sendToQml({ method: 'updateCheckoutQML', params: parsedJsonMessage }); + tablet.sendToQml({ + method: 'updateCheckoutQML', + params: parsedJsonMessage, + canRezCertifiedItems: Entities.canRezCertified || Entities.canRezTmpCertified + }); } else if (parsedJsonMessage.type === "REQUEST_SETTING") { tablet.emitScriptEvent(JSON.stringify({ type: "marketplaces", action: "commerceSetting", - data: Settings.getValue("commerce", false) + data: { + commerceMode: Settings.getValue("commerce", false), + userIsLoggedIn: Account.loggedIn + } })); } else if (parsedJsonMessage.type === "PURCHASES") { + referrerURL = parsedJsonMessage.referrerURL; + filterText = ""; tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); - tablet.sendToQml({ - method: 'updatePurchases', - referrerURL: parsedJsonMessage.referrerURL - }); + } else if (parsedJsonMessage.type === "LOGIN") { + openLoginWindow(); } } } @@ -154,13 +219,15 @@ tablet.webEventReceived.connect(onMessage); Script.scriptEnding.connect(function () { - if (onMarketplaceScreen) { + if (onMarketplaceScreen || onCommerceScreen) { tablet.gotoHomeScreen(); } tablet.removeButton(marketplaceButton); tablet.screenChanged.disconnect(onScreenChanged); + ContextOverlay.contextOverlayClicked.disconnect(setCertificateInfo); tablet.webEventReceived.disconnect(onMessage); Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); + GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); }); @@ -200,22 +267,33 @@ var isHmdPreviewDisabled = true; function fromQml(message) { switch (message.method) { + case 'purchases_openWallet': + case 'checkout_openWallet': case 'checkout_setUpClicked': tablet.pushOntoStack(MARKETPLACE_WALLET_QML_PATH); break; + case 'purchases_walletNotSetUp': + case 'checkout_walletNotSetUp': + wireEventBridge(true); + tablet.sendToQml({ + method: 'updateWalletReferrer', + referrer: "purchases" + }); + tablet.pushOntoStack(MARKETPLACE_WALLET_QML_PATH); + break; case 'checkout_cancelClicked': tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.params, MARKETPLACES_INJECT_SCRIPT_URL); // TODO: Make Marketplace a QML app that's a WebView wrapper so we can use the app stack. // I don't think this is trivial to do since we also want to inject some JS into the DOM. //tablet.popFromStack(); break; + case 'header_goToPurchases': case 'checkout_goToPurchases': + referrerURL = MARKETPLACE_URL_INITIAL; + filterText = message.filterText; tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); - tablet.sendToQml({ - method: 'updatePurchases', - referrerURL: MARKETPLACE_URL_INITIAL - }); break; + case 'checkout_itemLinkClicked': case 'checkout_continueShopping': tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); //tablet.popFromStack(); @@ -226,6 +304,7 @@ tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + itemId, MARKETPLACES_INJECT_SCRIPT_URL); } break; + case 'header_marketplaceImageClicked': case 'purchases_backClicked': tablet.gotoWebScreen(message.referrerURL, MARKETPLACES_INJECT_SCRIPT_URL); break; @@ -246,6 +325,37 @@ case 'maybeEnableHmdPreview': Menu.setIsOptionChecked("Disable Preview", isHmdPreviewDisabled); break; + case 'purchases_getIsFirstUse': + tablet.sendToQml({ + method: 'purchases_getIsFirstUseResult', + isFirstUseOfPurchases: Settings.getValue("isFirstUseOfPurchases", true) + }); + break; + case 'purchases_setIsFirstUse': + Settings.setValue("isFirstUseOfPurchases", false); + break; + case 'purchases_openGoTo': + tablet.loadQMLSource("TabletAddressDialog.qml"); + break; + case 'purchases_itemCertificateClicked': + tablet.loadQMLSource("../commerce/inspectionCertificate/InspectionCertificate.qml"); + setCertificateInfo("", message.itemMarketplaceId, true); + break; + case 'inspectionCertificate_closeClicked': + if (message.closeGoesToPurchases) { + referrerURL = MARKETPLACE_URL_INITIAL; + filterText = ""; + tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); + } else { + tablet.gotoHomeScreen(); + } + break; + case 'inspectionCertificate_showInMarketplaceClicked': + tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'header_myItemsClicked': + tablet.gotoWebScreen(MARKETPLACE_URL + '?view=mine', MARKETPLACES_INJECT_SCRIPT_URL); + break; default: print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message)); } From 3bd6e35e301c828b464d571a4f576da699546395 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 27 Sep 2017 15:19:21 -0700 Subject: [PATCH 469/722] Fixing the broken on hud overlay's render transform --- interface/src/ui/overlays/Base3DOverlay.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 9afab80243..714723e48e 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -270,10 +270,10 @@ void Base3DOverlay::update(float duration) { // then the correct transform used for rendering is computed in the update transaction and assigned. if (_renderTransformDirty) { auto itemID = getRenderItemID(); + // Capture the render transform value in game loop before + auto latestTransform = evalRenderTransform(); + _renderTransformDirty = false; if (render::Item::isValidID(itemID)) { - _renderTransformDirty = false; - // Capture the render transform value in game loop before - auto latestTransform = evalRenderTransform(); render::ScenePointer scene = qApp->getMain3DScene(); render::Transaction transaction; transaction.updateItem(itemID, [latestTransform](Overlay& data) { @@ -282,7 +282,9 @@ void Base3DOverlay::update(float duration) { overlay3D->setRenderTransform(latestTransform); } }); - scene->enqueueTransaction(transaction); + scene->enqueueTransaction(transaction); + } else { + setRenderTransform(latestTransform); } } } From eb06c33187533d5bb8a822a32cfc621e9424640f Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 27 Sep 2017 15:31:29 -0700 Subject: [PATCH 470/722] Naming coding standard fix --- interface/resources/qml/hifi/AssetServer.qml | 6 +++--- .../resources/qml/hifi/dialogs/TabletAssetServer.qml | 6 +++--- .../scripting/AssetMappingsScriptingInterface.cpp | 12 ++++++------ .../src/scripting/AssetMappingsScriptingInterface.h | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index 9ec61e0057..46df421d08 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -786,11 +786,11 @@ ScrollingWindow { anchors.verticalCenter: parent.verticalCenter function makeText() { - var pendingBakes = assetMappingsModel.bakesPendingCount; - if (selectedItems > 1 || pendingBakes === 0) { + var numPendingBakes = assetMappingsModel.numPendingBakes; + if (selectedItems > 1 || numPendingBakes === 0) { return selectedItems + " items selected"; } else { - return pendingBakes + " bakes pending" + return numPendingBakes + " bakes pending" } } diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 2018433be6..95ccc61d27 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -785,11 +785,11 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter function makeText() { - var pendingBakes = assetMappingsModel.bakesPendingCount; - if (selectedItems > 1 || pendingBakes === 0) { + var numPendingBakes = assetMappingsModel.numPendingBakes; + if (selectedItems > 1 || numPendingBakes === 0) { return selectedItems + " items selected"; } else { - return pendingBakes + " bakes pending" + return numPendingBakes + " bakes pending" } } diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.cpp b/interface/src/scripting/AssetMappingsScriptingInterface.cpp index 6c6f6dc244..5308be59bf 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.cpp +++ b/interface/src/scripting/AssetMappingsScriptingInterface.cpp @@ -239,7 +239,7 @@ void AssetMappingModel::refresh() { connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) mutable { if (request->getError() == MappingRequest::NoError) { - int bakesPendingCount = 0; + int numPendingBakes = 0; auto mappings = request->getMappings(); auto existingPaths = _pathToItemMap.keys(); for (auto& mapping : mappings) { @@ -289,7 +289,7 @@ void AssetMappingModel::refresh() { lastItem->setData(statusString, Qt::UserRole + 5); lastItem->setData(mapping.second.bakingErrors, Qt::UserRole + 6); if (mapping.second.status == Pending) { - ++bakesPendingCount; + ++numPendingBakes; } } @@ -339,9 +339,9 @@ void AssetMappingModel::refresh() { } } - if (bakesPendingCount != _bakesPendingCount) { - _bakesPendingCount = bakesPendingCount; - emit bakesPendingCountChanged(_bakesPendingCount); + if (numPendingBakes != _numPendingBakes) { + _numPendingBakes = numPendingBakes; + emit numPendingBakesChanged(_numPendingBakes); } } else { emit errorGettingMappings(request->getErrorString()); @@ -373,4 +373,4 @@ void AssetMappingModel::setupRoles() { roleNames[Qt::DisplayRole] = "name"; roleNames[Qt::UserRole + 5] = "baked"; setItemRoleNames(roleNames); -} \ No newline at end of file +} diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.h b/interface/src/scripting/AssetMappingsScriptingInterface.h index 49d92ec070..1a4c7dae48 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.h +++ b/interface/src/scripting/AssetMappingsScriptingInterface.h @@ -26,7 +26,7 @@ class AssetMappingModel : public QStandardItemModel { Q_OBJECT Q_PROPERTY(bool autoRefreshEnabled READ isAutoRefreshEnabled WRITE setAutoRefreshEnabled) - Q_PROPERTY(int bakesPendingCount READ getBakesPendingCount NOTIFY bakesPendingCountChanged) + Q_PROPERTY(int numPendingBakes READ getNumPendingBakes NOTIFY numPendingBakesChanged) public: AssetMappingModel(); @@ -39,13 +39,13 @@ public: bool isKnownMapping(QString path) const { return _pathToItemMap.contains(path); } bool isKnownFolder(QString path) const; - int getBakesPendingCount() const { return _bakesPendingCount; } + int getNumPendingBakes() const { return _numPendingBakes; } public slots: void clear(); signals: - void bakesPendingCountChanged(int newCount); + void numPendingBakesChanged(int newCount); void errorGettingMappings(QString errorString); void updated(); @@ -54,7 +54,7 @@ private: QHash _pathToItemMap; QTimer _autoRefreshTimer; - int _bakesPendingCount{ 0 }; + int _numPendingBakes{ 0 }; }; Q_DECLARE_METATYPE(AssetMappingModel*) From 57cec5058395501cd64725f65bd597ed08572797 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 28 Sep 2017 12:18:09 +1300 Subject: [PATCH 471/722] Fix UI disappearing when inside complex model --- scripts/vr-edit/vr-edit.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 81db666727..63c16b1ed0 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -455,7 +455,7 @@ return rootEntityID; } - function isCameraOutsideEntity(entityID, entityPosition) { + function isCameraOutsideEntity(entityID, testPosition) { var cameraPosition, pickRay, PRECISION_PICKING = true, @@ -466,12 +466,11 @@ cameraPosition = Camera.position; pickRay = { origin: cameraPosition, - direction: Vec3.normalize(Vec3.subtract(entityPosition, cameraPosition)), - length: Vec3.distance(entityPosition, cameraPosition) + direction: Vec3.normalize(Vec3.subtract(testPosition, cameraPosition)), + length: Vec3.distance(testPosition, cameraPosition) }; intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, [entityID], NO_EXCLUDE_IDS, VISIBLE_ONLY); - - return intersection.distance < pickRay.length; + return !intersection.intersects || intersection.distance < pickRay.length; } @@ -927,10 +926,10 @@ isGripClicked = hand.gripClicked(); isTriggerPressed = hand.triggerPressed(); - // Hide UI if hand is intersecting entity and camera is outside entity, or it hand is intersecting stretch handle. - if (dominantHand !== side) { + // Hide UI if hand is intersecting entity and camera is outside entity, or if hand is intersecting stretch handle. + if (side !== dominantHand) { showUI = !intersection.handIntersected || (intersection.entityID !== null - && !isCameraOutsideEntity(intersection.entityID, intersection.intersection)); + && !isCameraOutsideEntity(intersection.entityID, hand.palmPosition())); if (showUI !== isUIVisible) { isUIVisible = !isUIVisible; ui.setVisible(isUIVisible); @@ -952,7 +951,7 @@ && otherEditor.isHandle(intersection.overlayID)) && !(intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab - && (isCameraOutsideEntity(intersection.entityID, intersection.intersection) || isTriggerPressed)) + && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, hand.palmPosition()))) && !(intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (!wasTriggerClicked || isAutoGrab) && isTriggerClicked)) { // No transition. @@ -969,7 +968,7 @@ setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab - && (isCameraOutsideEntity(intersection.entityID, intersection.intersection) || isTriggerPressed)) { + && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, hand.palmPosition()))) { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); @@ -1020,7 +1019,7 @@ case EDITOR_HIGHLIGHTING: if (hand.valid() && intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) - && (isCameraOutsideEntity(intersection.entityID, intersection.intersection) || isTriggerPressed) + && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, hand.palmPosition())) && !(!wasTriggerClicked && isTriggerClicked && (!otherEditor.isEditing(rootEntityID) || toolSelected !== TOOL_SCALE)) && !(!wasTriggerClicked && isTriggerClicked && intersection.overlayID @@ -1101,7 +1100,7 @@ setState(EDITOR_GRABBING); } } else if (!intersection.entityID || !intersection.editableEntity - || (!isCameraOutsideEntity(intersection.entityID, intersection.intersection) && !isTriggerPressed)) { + || (!isTriggerPressed && !isCameraOutsideEntity(intersection.entityID, hand.palmPosition()))) { setState(EDITOR_SEARCHING); } else { log(side, "ERROR: Editor: Unexpected condition B in EDITOR_HIGHLIGHTING!"); From 9a51ce4b29754cec5be2efb00f0d7c97fbb5f7d7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 28 Sep 2017 12:18:36 +1300 Subject: [PATCH 472/722] Simplification --- scripts/vr-edit/vr-edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 63c16b1ed0..d78b66f60f 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -492,7 +492,7 @@ function getScaleTargetPosition() { if (isScalingWithHand) { - return side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); + return hand.palmPosition(); } return Vec3.sum(Vec3.sum(hand.position(), Vec3.multiplyQbyV(hand.orientation(), laserOffset)), Vec3.multiply(laser.length(), Quat.getUp(hand.orientation()))); From 7b7e0bc78b518404796b9e187d8d6fbe0154eb34 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 27 Sep 2017 16:35:35 -0700 Subject: [PATCH 473/722] Updated Recent Activity --- .../qml/hifi/commerce/wallet/WalletHome.qml | 119 +++++++++++------- 1 file changed, 77 insertions(+), 42 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 7083592c1d..a277d643d6 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -26,6 +26,7 @@ Item { id: root; property bool historyReceived: false; + property int pendingCount: 0; Hifi.QmlCommerce { id: commerce; @@ -39,6 +40,8 @@ Item { if (result.status === 'success') { transactionHistoryModel.clear(); transactionHistoryModel.append(result.data.history); + + calculatePendingAndInvalidated(); } } } @@ -200,55 +203,74 @@ Item { model: transactionHistoryModel; delegate: Item { width: parent.width; - height: transactionText.height + 30; + height: (model.transaction_type === "pendingCount" && root.pendingCount !== 0) ? 40 : ((model.status === "confirmed" || model.status === "invalidated") ? transactionText.height + 30 : 0); - HifiControlsUit.Separator { - visible: index === 0; - colorScheme: 1; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: parent.top; - } - - AnonymousProRegular { - id: dateText; - text: getFormattedDate(model.created_at * 1000); - // Style - size: 18; + Item { + visible: model.transaction_type === "pendingCount" && root.pendingCount !== 0; + anchors.top: parent.top; anchors.left: parent.left; - anchors.top: parent.top; - anchors.topMargin: 15; - width: 118; - height: paintedHeight; - color: hifi.colors.blueAccent; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignRight; - } + width: parent.width; + height: visible ? parent.height : 0; - AnonymousProRegular { - id: transactionText; - text: model.text; - size: 18; - anchors.top: parent.top; - anchors.topMargin: 15; - anchors.left: dateText.right; - anchors.leftMargin: 20; - anchors.right: parent.right; - height: paintedHeight; - color: hifi.colors.baseGrayHighlight; - wrapMode: Text.WordWrap; - - onLinkActivated: { - sendSignalToWallet({method: 'transactionHistory_linkClicked', marketplaceLink: link}); + AnonymousProRegular { + id: pendingCountText; + anchors.fill: parent; + text: root.pendingCount + ' Transactions Pending'; + size: 18; + color: hifi.colors.blueAccent; + verticalAlignment: Text.AlignVCenter; + horizontalAlignment: Text.AlignHCenter; } } - HifiControlsUit.Separator { - colorScheme: 1; + Item { + visible: model.transaction_type !== "pendingCount" && (model.status === "confirmed" || model.status === "invalidated"); + anchors.top: parent.top; anchors.left: parent.left; - anchors.right: parent.right; - anchors.bottom: parent.bottom; + width: parent.width; + height: visible ? parent.height : 0; + + AnonymousProRegular { + id: dateText; + text: model.created_at ? getFormattedDate(model.created_at * 1000) : ""; + // Style + size: 18; + anchors.left: parent.left; + anchors.top: parent.top; + anchors.topMargin: 15; + width: 118; + height: paintedHeight; + color: hifi.colors.blueAccent; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignRight; + } + + AnonymousProRegular { + id: transactionText; + text: model.text ? (model.status === "invalidated" ? ("INVALIDATED: " + model.text) : model.text) : ""; + size: 18; + anchors.top: parent.top; + anchors.topMargin: 15; + anchors.left: dateText.right; + anchors.leftMargin: 20; + anchors.right: parent.right; + height: paintedHeight; + color: model.status === "invalidated" ? hifi.colors.redAccent : hifi.colors.baseGrayHighlight; + wrapMode: Text.WordWrap; + font.strikeout: model.status === "invalidated"; + + onLinkActivated: { + sendSignalToWallet({method: 'transactionHistory_linkClicked', marketplaceLink: link}); + } + } + + HifiControlsUit.Separator { + colorScheme: 1; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + } } } onAtYEndChanged: { @@ -299,6 +321,19 @@ Item { return year + '-' + month + '-' + day + '
    ' + drawnHour + ':' + min + amOrPm; } + + function calculatePendingAndInvalidated(startingPendingCount) { + var pendingCount = startingPendingCount ? startingPendingCount : 0; + for (var i = 0; i < transactionHistoryModel.count; i++) { + if (transactionHistoryModel.get(i).status === "pending") { + pendingCount++; + } + } + + root.pendingCount = pendingCount; + transactionHistoryModel.insert(0, {"transaction_type": "pendingCount"}); + } + // // Function Name: fromScript() // From a74678a24d2086454f7c069684bac38d77947e5f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 28 Sep 2017 12:57:30 +1300 Subject: [PATCH 474/722] Improve size of scale handles for distant entities --- scripts/vr-edit/modules/handles.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/vr-edit/modules/handles.js b/scripts/vr-edit/modules/handles.js index f7400c6561..0318b21efb 100644 --- a/scripts/vr-edit/modules/handles.js +++ b/scripts/vr-edit/modules/handles.js @@ -38,7 +38,7 @@ Handles = function (side) { FACE_HANDLE_OVERLAY_OFFSETS, FACE_HANDLE_OVERLAY_ROTATIONS, FACE_HANDLE_OVERLAY_SCALE_AXES, - DISTANCE_MULTIPLIER_MULTIPLIER = 0.5, + DISTANCE_MULTIPLIER_MULTIPLIER = 0.25, hoveredOverlayID = null, isVisible = false, @@ -179,9 +179,9 @@ Handles = function (side) { // display smaller in order to give comfortable depth cue. cameraPosition = Camera.position; boundingBoxVector = Vec3.subtract(boundingBox.center, Camera.position); - distanceMultiplier = DISTANCE_MULTIPLIER_MULTIPLIER * Math.sqrt( - Math.max(Vec3.length(boundingBoxVector, Vec3.length(boundingBox.dimensions) / 2)) - ); + distanceMultiplier = Vec3.length(boundingBoxVector); + distanceMultiplier = DISTANCE_MULTIPLIER_MULTIPLIER + * (distanceMultiplier + (1 - Math.LOG10E * Math.log(distanceMultiplier + 1))); // Corner scale handles. // At right-most and opposite corners of bounding box. From 60823903cf3b9609ceb43a5fbb6dddda13350a92 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 27 Sep 2017 17:05:07 -0700 Subject: [PATCH 475/722] You own X others --- .../hifi/commerce/purchases/PurchasedItem.qml | 4 ++-- .../qml/hifi/commerce/purchases/Purchases.qml | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index 1186687a82..af93288bde 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -35,7 +35,7 @@ Item { property string itemPreviewImageUrl; property string itemHref; property int ownedItemCount; - property int itemEdition; + property int itemInstanceNumber; height: 110; width: parent.width; @@ -196,7 +196,7 @@ Item { } else if (root.purchaseStatus === "invalidated") { "INVALIDATED" } else if (root.ownedItemCount > 1) { - "(#" + root.itemEdition + ") You own " + root.ownedItemCount + " others" + "(#" + root.itemInstanceNumber + ") You own " + (root.ownedItemCount - 1) + " others" } else { "" } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 1002fb881b..a073a21e77 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -417,6 +417,8 @@ Rectangle { itemHref: root_file_url; purchaseStatus: status; purchaseStatusChanged: statusChanged; + itemInstanceNumber: model.itemInstanceNumber; + ownedItemCount: model.ownedItemCount; anchors.topMargin: 12; anchors.bottomMargin: 12; @@ -541,6 +543,22 @@ Rectangle { } } } + + var itemCountDictionary = {}; + var currentItemId; + for (var i = 0; i < filteredPurchasesModel.count; i++) { + currentItemId = filteredPurchasesModel.get(i).id; + if (itemCountDictionary[currentItemId] === undefined) { + itemCountDictionary[currentItemId] = 1; + } else { + itemCountDictionary[currentItemId]++; + } + filteredPurchasesModel.setProperty(i, "itemInstanceNumber", itemCountDictionary[currentItemId]); + } + + for (var i = 0; i < filteredPurchasesModel.count; i++) { + filteredPurchasesModel.setProperty(i, "ownedItemCount", itemCountDictionary[currentItemId]); + } } function checkIfAnyItemStatusChanged() { From 40ca98214bb9a4908a346b765f373d4d065a0740 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 27 Sep 2017 17:58:43 -0700 Subject: [PATCH 476/722] Moving all of the camera and avatar eval to game loop --- interface/src/Application.cpp | 221 ++++++++----------------- interface/src/Application.h | 5 +- interface/src/ui/overlays/Overlays.cpp | 2 +- 3 files changed, 70 insertions(+), 158 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 2dba980fb1..465c066bec 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2374,11 +2374,7 @@ void Application::initializeUi() { } void Application::updateCamera(RenderArgs& renderArgs) { - // load the view frustum - { - QMutexLocker viewLocker(&_viewMutex); - _myCamera.loadViewFrustum(_displayViewFrustum); - } + glm::vec3 boomOffset; { @@ -2481,12 +2477,7 @@ void Application::updateCamera(RenderArgs& renderArgs) { } } - renderArgs._cameraMode = (int8_t)_myCamera.getMode(); // HACK - - { - QMutexLocker viewLocker(&_viewMutex); - renderArgs.setViewFrustum(_displayViewFrustum); - } + renderArgs._cameraMode = (int8_t)_myCamera.getMode(); } void Application::editRenderArgs(RenderArgsEditor editor) { @@ -2539,42 +2530,46 @@ void Application::paintGL() { RenderArgs renderArgs; float sensorToWorldScale; glm::mat4 HMDSensorPose; + glm::mat4 eyeToWorld; + glm::mat4 sensorToWorld; { QMutexLocker viewLocker(&_renderArgsMutex); renderArgs = _appRenderArgs._renderArgs; - HMDSensorPose = _appRenderArgs._eyeToWorld; + HMDSensorPose = _appRenderArgs._headPose; + eyeToWorld = _appRenderArgs._eyeToWorld; + sensorToWorld = _appRenderArgs._sensorToWorld; sensorToWorldScale = _appRenderArgs._sensorToWorldScale; } -/* - float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); - { - PROFILE_RANGE(render, "/buildFrustrumAndArgs"); - { - QMutexLocker viewLocker(&_viewMutex); - // adjust near clip plane to account for sensor scaling. - auto adjustedProjection = glm::perspective(_viewFrustum.getFieldOfView(), - _viewFrustum.getAspectRatio(), - DEFAULT_NEAR_CLIP * sensorToWorldScale, - _viewFrustum.getFarClip()); - _viewFrustum.setProjection(adjustedProjection); - _viewFrustum.calculate(); - } - renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(), - lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, - RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); - { - QMutexLocker viewLocker(&_viewMutex); - renderArgs.setViewFrustum(_viewFrustum); - } - } -*/ - { - PROFILE_RANGE(render, "/resizeGL"); - PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::paintGL()"); - resizeGL(); - } + + //float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); + //{ + // PROFILE_RANGE(render, "/buildFrustrumAndArgs"); + // { + // QMutexLocker viewLocker(&_viewMutex); + // // adjust near clip plane to account for sensor scaling. + // auto adjustedProjection = glm::perspective(_viewFrustum.getFieldOfView(), + // _viewFrustum.getAspectRatio(), + // DEFAULT_NEAR_CLIP * sensorToWorldScale, + // _viewFrustum.getFarClip()); + // _viewFrustum.setProjection(adjustedProjection); + // _viewFrustum.calculate(); + // } + // renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(), + // lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, + // RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); + // { + // QMutexLocker viewLocker(&_viewMutex); + // renderArgs.setViewFrustum(_viewFrustum); + // } + //} + + //{ + // PROFILE_RANGE(render, "/resizeGL"); + // PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); + // bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + // PerformanceWarning warn(showWarnings, "Application::paintGL()"); + // resizeGL(); + //} { PROFILE_RANGE(render, "/gpuContextReset"); @@ -2599,103 +2594,10 @@ void Application::paintGL() { } // updateCamera(renderArgs); - - /* glm::vec3 boomOffset; - { - PROFILE_RANGE(render, "/updateCamera"); - { - PerformanceTimer perfTimer("CameraUpdates"); - - auto myAvatar = getMyAvatar(); - boomOffset = myAvatar->getModelScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; - - // The render mode is default or mirror if the camera is in mirror mode, assigned further below - renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - - // Always use the default eye position, not the actual head eye position. - // Using the latter will cause the camera to wobble with idle animations, - // or with changes from the face tracker - if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { - if (isHMDMode()) { - mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - _myCamera.setPosition(extractTranslation(camMat)); - _myCamera.setOrientation(glmExtractRotation(camMat)); - } else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition()); - _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); - } - } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { - if (isHMDMode()) { - auto hmdWorldMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - _myCamera.setOrientation(glm::normalize(glmExtractRotation(hmdWorldMat))); - _myCamera.setPosition(extractTranslation(hmdWorldMat) + - myAvatar->getOrientation() * boomOffset); - } else { - _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); - if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + _myCamera.getOrientation() * boomOffset); - } else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + myAvatar->getOrientation() * boomOffset); - } - } - } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - if (isHMDMode()) { - auto mirrorBodyOrientation = myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)); - - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD yaw and roll - glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); - mirrorHmdEulers.y = -mirrorHmdEulers.y; - mirrorHmdEulers.z = -mirrorHmdEulers.z; - glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); - - glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; - - _myCamera.setOrientation(worldMirrorRotation); - - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD lateral offsets - hmdOffset.x = -hmdOffset.x; - - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) - + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror - + mirrorBodyOrientation * hmdOffset); - } else { - _myCamera.setOrientation(myAvatar->getOrientation() - * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) - + (myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * - glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); - } - renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; - } else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { - EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); - if (cameraEntity != nullptr) { - if (isHMDMode()) { - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - _myCamera.setOrientation(cameraEntity->getRotation() * hmdRotation); - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - _myCamera.setPosition(cameraEntity->getPosition() + (hmdRotation * hmdOffset)); - } else { - _myCamera.setOrientation(cameraEntity->getRotation()); - _myCamera.setPosition(cameraEntity->getPosition()); - } - } - } - // Update camera position - if (!isHMDMode()) { - _myCamera.update(1.0f / _frameCounter.rate()); - } - } - } - */ { PROFILE_RANGE(render, "/updateCompositor"); - getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform(), getMyAvatar()->getSensorToWorldMatrix()); + // getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform(), getMyAvatar()->getSensorToWorldMatrix()); + getApplicationCompositor().setFrameInfo(_frameCount, eyeToWorld, sensorToWorld); } gpu::FramebufferPointer finalFramebuffer; @@ -2761,7 +2663,8 @@ void Application::paintGL() { renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); } renderArgs._blitFramebuffer = finalFramebuffer; - displaySide(&renderArgs, _myCamera); + // displaySide(&renderArgs, _myCamera); + runRenderFrame(&renderArgs); } gpu::Batch postCompositeBatch; @@ -5367,7 +5270,7 @@ void Application::update(float deltaTime) { editRenderArgs([this](AppRenderArgs& appRenderArgs) { - appRenderArgs._eyeToWorld = getHMDSensorPose(); + appRenderArgs._headPose= getHMDSensorPose(); auto myAvatar = getMyAvatar(); @@ -5375,7 +5278,7 @@ void Application::update(float deltaTime) { // update the avatar with a fresh HMD pose { PROFILE_RANGE(render, "/updateAvatar"); - myAvatar->updateFromHMDSensorMatrix(appRenderArgs._eyeToWorld); + myAvatar->updateFromHMDSensorMatrix(appRenderArgs._headPose); } auto lodManager = DependencyManager::get(); @@ -5402,13 +5305,31 @@ void Application::update(float deltaTime) { appRenderArgs._renderArgs.setViewFrustum(_viewFrustum); } } + { + PROFILE_RANGE(render, "/resizeGL"); + PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::paintGL()"); + resizeGL(); + } this->updateCamera(appRenderArgs._renderArgs); + + // HACK + // load the view frustum // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. // Then we can move this logic into the Avatar::simulate call. myAvatar->preDisplaySide(&appRenderArgs._renderArgs); + { + QMutexLocker viewLocker(&_viewMutex); + _myCamera.loadViewFrustum(_displayViewFrustum); + } + { + QMutexLocker viewLocker(&_viewMutex); + appRenderArgs._renderArgs.setViewFrustum(_displayViewFrustum); + } }); AnimDebugDraw::getInstance().update(); @@ -5740,7 +5661,7 @@ namespace render { } } -void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool selfAvatarOnly) { +void Application::runRenderFrame(RenderArgs* renderArgs) { // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. // Then we can move this logic into the Avatar::simulate call. @@ -5749,7 +5670,7 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se PROFILE_RANGE(render, __FUNCTION__); PerformanceTimer perfTimer("display"); - PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide()"); + PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::runRenderFrame()"); // load the view frustum // { @@ -5761,12 +5682,13 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se render::Transaction transaction; // Assuming nothing gets rendered through that - if (!selfAvatarOnly) { + //if (!selfAvatarOnly) { + { if (DependencyManager::get()->shouldRenderEntities()) { // render models... PerformanceTimer perfTimer("entities"); PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), - "Application::displaySide() ... entities..."); + "Application::runRenderFrame() ... entities..."); RenderArgs::DebugFlags renderDebugFlags = RenderArgs::RENDER_DEBUG_NONE; @@ -5775,7 +5697,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se static_cast(RenderArgs::RENDER_DEBUG_HULLS)); } renderArgs->_debugFlags = renderDebugFlags; - //ViveControllerManager::getInstance().updateRendering(renderArgs, _main3DScene, transaction); } } @@ -5788,17 +5709,10 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se WorldBoxRenderData::_item = _main3DScene->allocateID(); transaction.resetItem(WorldBoxRenderData::_item, worldBoxRenderPayload); - } else { - transaction.updateItem(WorldBoxRenderData::_item, - [](WorldBoxRenderData& payload) { - payload._val++; - }); - } - - { _main3DScene->enqueueTransaction(transaction); } + // For now every frame pass the renderContext { PerformanceTimer perfTimer("EngineRun"); @@ -7861,5 +7775,4 @@ void Application::setAvatarOverrideUrl(const QUrl& url, bool save) { _avatarOverrideUrl = url; _saveAvatarOverrideUrl = save; } - #include "Application.moc" diff --git a/interface/src/Application.h b/interface/src/Application.h index 3cd0ea07bc..edb615794c 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -470,8 +470,6 @@ private: void queryOctree(NodeType_t serverType, PacketType packetType, NodeToJurisdictionMap& jurisdictions, bool forceResend = false); - void renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed); - int sendNackPackets(); void sendAvatarViewFrustum(); @@ -481,7 +479,7 @@ private: void initializeAcceptedFiles(); - void displaySide(RenderArgs* renderArgs, Camera& whichCamera, bool selfAvatarOnly = false); + void runRenderFrame(RenderArgs* renderArgs/*, Camera& whichCamera, bool selfAvatarOnly = false*/); bool importJSONFromURL(const QString& urlString); bool importSVOFromURL(const QString& urlString); @@ -628,6 +626,7 @@ private: struct AppRenderArgs { render::Args _renderArgs; glm::mat4 _eyeToWorld; + glm::mat4 _headPose; glm::mat4 _sensorToWorld; float _sensorToWorldScale { 1.0f }; }; diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index c93d225718..e9cb1f2973 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -152,7 +152,7 @@ void Overlays::render3DHUDOverlays(RenderArgs* renderArgs) { foreach(Overlay::Pointer thisOverlay, _overlays3DHUD) { // Reset necessary batch pipeline settings between overlays batch.setResourceTexture(0, textureCache->getWhiteTexture()); // FIXME - do we really need to do this?? - batch.setModelTransform(Transform()); + // batch.setModelTransform(Transform()); renderArgs->_shapePipeline = _shapePlumber->pickPipeline(renderArgs, thisOverlay->getShapeKey()); thisOverlay->render(renderArgs); From 47ab9d322179b73f53e5cb5033b9b1f73d3df585 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 28 Sep 2017 16:04:22 +1300 Subject: [PATCH 477/722] Fix flash of color from laser target when turn on or jump distance --- scripts/vr-edit/modules/laser.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/laser.js b/scripts/vr-edit/modules/laser.js index afd67cf364..39ce0b713b 100644 --- a/scripts/vr-edit/modules/laser.js +++ b/scripts/vr-edit/modules/laser.js @@ -44,6 +44,7 @@ Laser = function (side) { laserLength, specifiedLaserLength = null, + laserSphereSize = 0, LEFT_HAND = 0, @@ -132,12 +133,16 @@ Laser = function (side) { } else { Overlays.editOverlay(laserLine, { visible: false }); } - updateSphere(searchTarget, sphereSize, color, brightColor); + // Avoid flash from large laser sphere when turn on or suddenly increase distance. Rendering seems to update overlay + // position one frame behind so use sphere size from preceding frame. + updateSphere(searchTarget, laserSphereSize, color, brightColor); + laserSphereSize = sphereSize; } function hide() { Overlays.editOverlay(laserLine, { visible: false }); Overlays.editOverlay(laserSphere, { visible: false }); + laserSphereSize = 0; } function setUIOverlays(overlayIDs) { From 1021aa1a0efaa46508781d7df0632b1f76007fb8 Mon Sep 17 00:00:00 2001 From: beholder Date: Thu, 28 Sep 2017 12:53:10 +0300 Subject: [PATCH 478/722] re-center focused eleemnt even if keyboard was already visible --- .../resources/html/raiseAndLowerKeyboard.js | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/interface/resources/html/raiseAndLowerKeyboard.js b/interface/resources/html/raiseAndLowerKeyboard.js index f87312b838..bbf88ca277 100644 --- a/interface/resources/html/raiseAndLowerKeyboard.js +++ b/interface/resources/html/raiseAndLowerKeyboard.js @@ -37,6 +37,19 @@ return document.activeElement.type === "number"; }; + function scheduleBringToView(timeout) { + + var timer = setTimeout(function () { + clearTimeout(timer); + + var elementRect = document.activeElement.getBoundingClientRect(); + var absoluteElementTop = elementRect.top + window.scrollY; + var middle = absoluteElementTop - (window.innerHeight / 2); + + window.scrollTo(0, middle); + }, timeout); + } + setInterval(function () { var keyboardRaised = shouldRaiseKeyboard(); var numericKeyboard = shouldSetNumeric(); @@ -55,15 +68,7 @@ } if (!isKeyboardRaised) { - var timeout = setTimeout(function () { - clearTimeout(timeout); - - var elementRect = document.activeElement.getBoundingClientRect(); - var absoluteElementTop = elementRect.top + window.scrollY; - var middle = absoluteElementTop - (window.innerHeight / 2); - - window.scrollTo(0, middle); - }, 500); // Allow time for keyboard to be raised in QML. + scheduleBringToView(500); // Allow time for keyboard to be raised in QML. } isKeyboardRaised = keyboardRaised; @@ -71,6 +76,13 @@ } }, POLL_FREQUENCY); + window.addEventListener("click", function () { + var keyboardRaised = shouldRaiseKeyboard(); + if(keyboardRaised && isKeyboardRaised) { + scheduleBringToView(150); + } + }); + window.addEventListener("focus", function () { isWindowFocused = true; }); From 338a230de970010fa28f4c98d4b4c934af324435 Mon Sep 17 00:00:00 2001 From: beholder Date: Thu, 28 Sep 2017 13:01:07 +0300 Subject: [PATCH 479/722] reduced delay to improve user experience --- interface/resources/html/raiseAndLowerKeyboard.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/resources/html/raiseAndLowerKeyboard.js b/interface/resources/html/raiseAndLowerKeyboard.js index bbf88ca277..ad1d889556 100644 --- a/interface/resources/html/raiseAndLowerKeyboard.js +++ b/interface/resources/html/raiseAndLowerKeyboard.js @@ -68,7 +68,8 @@ } if (!isKeyboardRaised) { - scheduleBringToView(500); // Allow time for keyboard to be raised in QML. + scheduleBringToView(250); // Allow time for keyboard to be raised in QML. + // 2DO: should it be rather done from 'client area height changed' event? } isKeyboardRaised = keyboardRaised; From 4f907aba1e1100ccc02ed15eb0f4accdfcebd734 Mon Sep 17 00:00:00 2001 From: vladest Date: Thu, 28 Sep 2017 14:49:48 +0200 Subject: [PATCH 480/722] sign in reworked --- interface/resources/qml/LoginDialog.qml | 4 + .../qml/LoginDialog/LinkAccountBody.qml | 194 ++++++------ .../resources/qml/LoginDialog/SignInBody.qml | 6 +- .../resources/qml/LoginDialog/SignUpBody.qml | 5 +- .../TabletLoginDialog/CompleteProfileBody.qml | 124 -------- .../qml/TabletLoginDialog/LinkAccountBody.qml | 296 ------------------ .../qml/TabletLoginDialog/SignInBody.qml | 109 ------- .../qml/TabletLoginDialog/SignUpBody.qml | 276 ---------------- .../UsernameCollisionBody.qml | 157 ---------- .../qml/TabletLoginDialog/WelcomeBody.qml | 79 ----- .../resources/qml/controls-uit/TextField.qml | 1 + .../qml/dialogs/TabletLoginDialog.qml | 195 ++++++++---- 12 files changed, 248 insertions(+), 1198 deletions(-) delete mode 100644 interface/resources/qml/TabletLoginDialog/CompleteProfileBody.qml delete mode 100644 interface/resources/qml/TabletLoginDialog/LinkAccountBody.qml delete mode 100644 interface/resources/qml/TabletLoginDialog/SignInBody.qml delete mode 100644 interface/resources/qml/TabletLoginDialog/SignUpBody.qml delete mode 100644 interface/resources/qml/TabletLoginDialog/UsernameCollisionBody.qml delete mode 100644 interface/resources/qml/TabletLoginDialog/WelcomeBody.qml diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index 2e7ff39ed6..315cda3551 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -35,6 +35,10 @@ ModalWindow { keyboardOverride: true // Disable ModalWindow's keyboard. + function tryDestroy() { + root.destroy() + } + LoginDialog { id: loginDialog diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index e27635dbbd..7f69e41958 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -9,7 +9,7 @@ // import Hifi 1.0 -import QtQuick 2.4 +import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 as OriginalStyles @@ -56,6 +56,7 @@ Item { parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)); parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : hifi.dimensions.contentSpacing.y); + console.log("sign in h:", targetHeight, parent.height) } } @@ -108,30 +109,27 @@ Item { Column { id: form + width: parent.width + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + anchors { top: mainTextContainer.bottom - left: parent.left - margins: 0 topMargin: 2 * hifi.dimensions.contentSpacing.y } spacing: 2 * hifi.dimensions.contentSpacing.y - Row { - spacing: hifi.dimensions.contentSpacing.x - TextField { - id: usernameField - anchors { - verticalCenter: parent.verticalCenter - } - width: 350 + TextField { + id: usernameField + width: parent.width - label: "Username or Email" - } + label: "Username or Email" ShortcutText { anchors { - verticalCenter: parent.verticalCenter + verticalCenter: usernameField.textFieldLabel.verticalCenter + left: usernameField.textFieldLabel.right + leftMargin: 10 } text: "Forgot Username?" @@ -143,23 +141,19 @@ Item { onLinkActivated: loginDialog.openUrl(link) } } - Row { - spacing: hifi.dimensions.contentSpacing.x - TextField { - id: passwordField - anchors { - verticalCenter: parent.verticalCenter - } - width: 350 + TextField { + id: passwordField + width: parent.width - label: "Password" - echoMode: TextInput.Password - } + label: "Password" + echoMode: showPassword.checked ? TextInput.Normal : TextInput.Password ShortcutText { anchors { - verticalCenter: parent.verticalCenter + verticalCenter: passwordField.textFieldLabel.verticalCenter + left: passwordField.textFieldLabel.right + leftMargin: 10 } text: "Forgot Password?" @@ -172,25 +166,86 @@ Item { } } - } - - InfoItem { - id: additionalInformation - anchors { - top: form.bottom - left: parent.left - margins: 0 - topMargin: hifi.dimensions.contentSpacing.y + CheckBoxQQC2 { + id: showPassword + text: "Show password" } - visible: loginDialog.isSteamRunning() + InfoItem { + id: additionalInformation + anchors { + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } - text: qsTr("Your steam account informations will not be exposed to other users.") - wrapMode: Text.WordWrap - color: hifi.colors.baseGrayHighlight - lineHeight: 1 - lineHeightMode: Text.ProportionalHeight - horizontalAlignment: Text.AlignHCenter + visible: loginDialog.isSteamRunning() + + text: qsTr("Your steam account informations will not be exposed to other users.") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Column { + //width: parent.width + spacing: hifi.dimensions.contentSpacing.y*2 + anchors.horizontalCenter: parent.horizontalCenter + //padding: 10 + + Row { + id: buttons + spacing: hifi.dimensions.contentSpacing.y*2 + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + anchors.horizontalCenter: parent.horizontalCenter + + Button { + id: linkAccountButton + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr(loginDialog.isSteamRunning() ? "Link Account" : "Login") + color: hifi.buttons.blue + + onClicked: linkAccountBody.login() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Cancel") + onClicked: root.tryDestroy() + } + } + + Row { + id: leftButton + + anchors.horizontalCenter: parent.horizontalCenter + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + RalewaySemiBold { + size: hifi.fontSizes.inputLabel + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Don't have an account?") + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Sign Up") + visible: !loginDialog.isSteamRunning() + + onClicked: { + bodyLoader.setSource("SignUpBody.qml") + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + } + } + } } // Override ScrollingWindow's keyboard that would be at very bottom of dialog. @@ -200,65 +255,12 @@ Item { anchors { left: parent.left right: parent.right - bottom: buttons.top + bottom: parent.bottom bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 } } - Row { - id: leftButton - anchors { - left: parent.left - bottom: parent.bottom - bottomMargin: hifi.dimensions.contentSpacing.y - } - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: qsTr("Sign Up") - visible: !loginDialog.isSteamRunning() - - onClicked: { - bodyLoader.setSource("SignUpBody.qml") - bodyLoader.item.width = root.pane.width - bodyLoader.item.height = root.pane.height - } - } - } - - Row { - id: buttons - anchors { - right: parent.right - bottom: parent.bottom - bottomMargin: hifi.dimensions.contentSpacing.y - } - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - id: linkAccountButton - anchors.verticalCenter: parent.verticalCenter - width: 200 - - text: qsTr(loginDialog.isSteamRunning() ? "Link Account" : "Login") - color: hifi.buttons.blue - - onClicked: linkAccountBody.login() - } - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: qsTr("Cancel") - - onClicked: root.destroy() - } - } Component.onCompleted: { root.title = qsTr("Sign Into High Fidelity") diff --git a/interface/resources/qml/LoginDialog/SignInBody.qml b/interface/resources/qml/LoginDialog/SignInBody.qml index 167ed1640a..71ec03f7ff 100644 --- a/interface/resources/qml/LoginDialog/SignInBody.qml +++ b/interface/resources/qml/LoginDialog/SignInBody.qml @@ -9,7 +9,7 @@ // import Hifi 1.0 -import QtQuick 2.4 +import QtQuick 2.7 import QtQuick.Controls.Styles 1.4 as OriginalStyles import "../controls-uit" @@ -18,8 +18,8 @@ import "../styles-uit" Item { id: signInBody clip: true - width: root.pane.width height: root.pane.height + width: root.pane.width property bool required: false @@ -29,7 +29,7 @@ Item { } function cancel() { - root.destroy() + root.tryDestroy() } QtObject { diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index c0ff2d77cb..c7bfa8cfcd 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -9,7 +9,7 @@ // import Hifi 1.0 -import QtQuick 2.4 +import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 as OriginalStyles @@ -50,6 +50,7 @@ Item { parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)); parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : 0); + //console.log("sign up h:", parent.height) } } @@ -237,7 +238,7 @@ Item { text: qsTr("Cancel") - onClicked: root.destroy() + onClicked: root.tryDestroy() } } diff --git a/interface/resources/qml/TabletLoginDialog/CompleteProfileBody.qml b/interface/resources/qml/TabletLoginDialog/CompleteProfileBody.qml deleted file mode 100644 index 6024563bcf..0000000000 --- a/interface/resources/qml/TabletLoginDialog/CompleteProfileBody.qml +++ /dev/null @@ -1,124 +0,0 @@ -// -// CompleteProfileBody.qml -// -// Created by Clement on 7/18/16 -// 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 -// - -import Hifi 1.0 -import QtQuick 2.4 -import QtQuick.Controls.Styles 1.4 as OriginalStyles - -import "../controls-uit" -import "../styles-uit" - -Item { - id: completeProfileBody - clip: true - - QtObject { - id: d - function resize() {} - } - - Row { - id: buttons - anchors { - top: parent.top - horizontalCenter: parent.horizontalCenter - margins: 0 - topMargin: 2 * hifi.dimensions.contentSpacing.y - } - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - anchors.verticalCenter: parent.verticalCenter - width: 200 - - text: qsTr("Create your profile") - color: hifi.buttons.blue - - onClicked: loginDialog.createAccountFromStream() - } - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: qsTr("Cancel") - - onClicked: bodyLoader.popup() - } - } - - ShortcutText { - id: additionalTextContainer - anchors { - top: buttons.bottom - horizontalCenter: parent.horizontalCenter - margins: 0 - topMargin: hifi.dimensions.contentSpacing.y - } - - text: "Already have a High Fidelity profile? Link to an existing profile here." - - wrapMode: Text.WordWrap - lineHeight: 2 - lineHeightMode: Text.ProportionalHeight - horizontalAlignment: Text.AlignHCenter - - onLinkActivated: { - bodyLoader.setSource("LinkAccountBody.qml") - } - } - - InfoItem { - id: termsContainer - anchors { - top: additionalTextContainer.bottom - left: parent.left - margins: 0 - topMargin: 2 * hifi.dimensions.contentSpacing.y - } - - text: qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") - wrapMode: Text.WordWrap - color: hifi.colors.baseGrayHighlight - lineHeight: 1 - lineHeightMode: Text.ProportionalHeight - horizontalAlignment: Text.AlignHCenter - - onLinkActivated: loginDialog.openUrl(link) - } - - Component.onCompleted: { - loginDialogRoot.title = qsTr("Complete Your Profile") - loginDialogRoot.iconText = "<" - d.resize(); - } - - Connections { - target: loginDialog - onHandleCreateCompleted: { - console.log("Create Succeeded") - - loginDialog.loginThroughSteam() - } - onHandleCreateFailed: { - console.log("Create Failed: " + error) - - bodyLoadersetSource("UsernameCollisionBody.qml") - } - onHandleLoginCompleted: { - console.log("Login Succeeded") - - bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : false }) - } - onHandleLoginFailed: { - console.log("Login Failed") - } - } -} diff --git a/interface/resources/qml/TabletLoginDialog/LinkAccountBody.qml b/interface/resources/qml/TabletLoginDialog/LinkAccountBody.qml deleted file mode 100644 index 8010a34250..0000000000 --- a/interface/resources/qml/TabletLoginDialog/LinkAccountBody.qml +++ /dev/null @@ -1,296 +0,0 @@ -// -// LinkAccountBody.qml -// -// Created by Clement on 7/18/16 -// 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 -// - -import Hifi 1.0 -import QtQuick 2.4 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 as OriginalStyles - -import "../controls-uit" -import "../styles-uit" - -Item { - id: linkAccountBody - clip: true - height: parent.height - width: parent.width - property bool failAfterSignUp: false - - function login() { - mainTextContainer.visible = false - toggleLoading(true) - loginDialog.login(usernameField.text, passwordField.text) - } - - property bool keyboardEnabled: false - property bool keyboardRaised: false - property bool punctuationMode: false - - onKeyboardRaisedChanged: d.resize(); - - QtObject { - id: d - function resize() {} - } - - function toggleLoading(isLoading) { - linkAccountSpinner.visible = isLoading - form.visible = !isLoading - - if (loginDialog.isSteamRunning()) { - additionalInformation.visible = !isLoading - } - - leftButton.visible = !isLoading - buttons.visible = !isLoading - } - - BusyIndicator { - id: linkAccountSpinner - - anchors { - top: parent.top - horizontalCenter: parent.horizontalCenter - topMargin: hifi.dimensions.contentSpacing.y - } - - visible: false - running: true - - width: 48 - height: 48 - } - - ShortcutText { - id: mainTextContainer - anchors { - top: parent.top - left: parent.left - margins: 0 - topMargin: hifi.dimensions.contentSpacing.y - } - - visible: false - - text: qsTr("Username or password incorrect.") - wrapMode: Text.WordWrap - color: hifi.colors.redAccent - lineHeight: 1 - lineHeightMode: Text.ProportionalHeight - horizontalAlignment: Text.AlignHCenter - } - - Column { - id: form - anchors { - top: mainTextContainer.bottom - left: parent.left - margins: 0 - topMargin: 2 * hifi.dimensions.contentSpacing.y - } - spacing: 2 * hifi.dimensions.contentSpacing.y - - Row { - spacing: hifi.dimensions.contentSpacing.x - - TextField { - id: usernameField - anchors { - verticalCenter: parent.verticalCenter - } - width: 350 - - label: "Username or Email" - } - - ShortcutText { - anchors { - verticalCenter: parent.verticalCenter - } - - text: "Forgot Username?" - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - linkColor: hifi.colors.blueAccent - - onLinkActivated: loginDialog.openUrl(link) - } - } - Row { - spacing: hifi.dimensions.contentSpacing.x - - TextField { - id: passwordField - anchors { - verticalCenter: parent.verticalCenter - } - width: 350 - - label: "Password" - echoMode: TextInput.Password - } - - ShortcutText { - anchors { - verticalCenter: parent.verticalCenter - } - - text: "Forgot Password?" - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - linkColor: hifi.colors.blueAccent - - onLinkActivated: loginDialog.openUrl(link) - } - } - - } - - InfoItem { - id: additionalInformation - anchors { - top: form.bottom - left: parent.left - margins: 0 - topMargin: hifi.dimensions.contentSpacing.y - } - - visible: loginDialog.isSteamRunning() - - text: qsTr("Your steam account informations will not be exposed to other users.") - wrapMode: Text.WordWrap - color: hifi.colors.baseGrayHighlight - lineHeight: 1 - lineHeightMode: Text.ProportionalHeight - horizontalAlignment: Text.AlignHCenter - } - - // Override ScrollingWindow's keyboard that would be at very bottom of dialog. - Keyboard { - raised: keyboardEnabled && keyboardRaised - numeric: punctuationMode - anchors { - left: parent.left - right: parent.right - bottom: buttons.top - bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 - } - } - - Row { - id: leftButton - anchors { - left: parent.left - bottom: parent.bottom - bottomMargin: hifi.dimensions.contentSpacing.y - } - - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: qsTr("Sign Up") - visible: !loginDialog.isSteamRunning() - - onClicked: { - bodyLoader.setSource("SignUpBody.qml") - } - } - } - - Row { - id: buttons - anchors { - right: parent.right - bottom: parent.bottom - bottomMargin: hifi.dimensions.contentSpacing.y - } - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - id: linkAccountButton - anchors.verticalCenter: parent.verticalCenter - width: 200 - - text: qsTr(loginDialog.isSteamRunning() ? "Link Account" : "Login") - color: hifi.buttons.blue - - onClicked: linkAccountBody.login() - } - - Button { - anchors.verticalCenter: parent.verticalCenter - text: qsTr("Cancel") - onClicked: { - bodyLoader.popup() - } - } - } - - Component.onCompleted: { - loginDialogRoot.title = qsTr("Sign Into High Fidelity") - loginDialogRoot.iconText = "<" - keyboardEnabled = HMD.active; - d.resize(); - - if (failAfterSignUp) { - mainTextContainer.text = "Account created successfully." - mainTextContainer.visible = true - } - - usernameField.forceActiveFocus(); - } - - Connections { - target: loginDialog - onHandleLoginCompleted: { - console.log("Login Succeeded, linking steam account") - - if (loginDialog.isSteamRunning()) { - loginDialog.linkSteam() - } else { - bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) - } - } - onHandleLoginFailed: { - console.log("Login Failed") - mainTextContainer.visible = true - toggleLoading(false) - } - onHandleLinkCompleted: { - console.log("Link Succeeded") - - bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) - } - onHandleLinkFailed: { - console.log("Link Failed") - toggleLoading(false) - } - } - - Keys.onPressed: { - if (!visible) { - return - } - - switch (event.key) { - case Qt.Key_Enter: - case Qt.Key_Return: - event.accepted = true - linkAccountBody.login() - break - } - } -} diff --git a/interface/resources/qml/TabletLoginDialog/SignInBody.qml b/interface/resources/qml/TabletLoginDialog/SignInBody.qml deleted file mode 100644 index 9cdf69c7bc..0000000000 --- a/interface/resources/qml/TabletLoginDialog/SignInBody.qml +++ /dev/null @@ -1,109 +0,0 @@ -// -// SignInBody.qml -// -// Created by Clement on 7/18/16 -// 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 -// - -import Hifi 1.0 -import QtQuick 2.4 -import QtQuick.Controls.Styles 1.4 as OriginalStyles - -import "../controls-uit" -import "../styles-uit" - -Item { - id: signInBody - clip: true - - property bool required: false - - function login() { - console.log("Trying to log in") - loginDialog.loginThroughSteam() - } - - function cancel() { - bodyLoader.popup() - } - - QtObject { - id: d - function resize() {} - } - - InfoItem { - id: mainTextContainer - anchors { - top: parent.top - horizontalCenter: parent.horizontalCenter - margins: 0 - topMargin: hifi.dimensions.contentSpacing.y - } - - text: required ? qsTr("This domain's owner requires that you sign in:") - : qsTr("Sign in to access your user account:") - wrapMode: Text.WordWrap - color: hifi.colors.baseGrayHighlight - lineHeight: 2 - lineHeightMode: Text.ProportionalHeight - horizontalAlignment: Text.AlignHCenter - } - - Row { - id: buttons - anchors { - top: mainTextContainer.bottom - horizontalCenter: parent.horizontalCenter - margins: 0 - topMargin: 2 * hifi.dimensions.contentSpacing.y - } - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - anchors.verticalCenter: parent.verticalCenter - - width: undefined // invalidate so that the image's size sets the width - height: undefined // invalidate so that the image's size sets the height - focus: true - - style: OriginalStyles.ButtonStyle { - background: Image { - id: buttonImage - source: "../../images/steam-sign-in.png" - } - } - onClicked: signInBody.login() - } - Button { - anchors.verticalCenter: parent.verticalCenter - - text: qsTr("Cancel"); - - onClicked: signInBody.cancel() - } - } - - Component.onCompleted: { - loginDialogRoot.title = required ? qsTr("Sign In Required") - : qsTr("Sign In") - loginDialogRoot.iconText = "" - d.resize(); - } - - Connections { - target: loginDialog - onHandleLoginCompleted: { - console.log("Login Succeeded") - bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) - } - onHandleLoginFailed: { - console.log("Login Failed") - bodyLoader.setSource("CompleteProfileBody.qml") - } - } -} diff --git a/interface/resources/qml/TabletLoginDialog/SignUpBody.qml b/interface/resources/qml/TabletLoginDialog/SignUpBody.qml deleted file mode 100644 index 2cfc0e736a..0000000000 --- a/interface/resources/qml/TabletLoginDialog/SignUpBody.qml +++ /dev/null @@ -1,276 +0,0 @@ -// -// SignUpBody.qml -// -// Created by Stephen Birarda on 7 Dec 2016 -// Copyright 2016 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 -// - -import Hifi 1.0 -import QtQuick 2.4 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 as OriginalStyles - -import "../controls-uit" -import "../styles-uit" - -Item { - id: signupBody - clip: true -// height: parent.height -// width: parent.width - - function signup() { - mainTextContainer.visible = false - toggleLoading(true) - loginDialog.signup(emailField.text, usernameField.text, passwordField.text) - } - - property bool keyboardEnabled: false - property bool keyboardRaised: false - property bool punctuationMode: false - - onKeyboardRaisedChanged: d.resize(); - - QtObject { - id: d - function resize() {} - } - - function toggleLoading(isLoading) { - linkAccountSpinner.visible = isLoading - form.visible = !isLoading - - leftButton.visible = !isLoading - buttons.visible = !isLoading - } - - BusyIndicator { - id: linkAccountSpinner - - anchors { - top: parent.top - horizontalCenter: parent.horizontalCenter - topMargin: hifi.dimensions.contentSpacing.y - } - - visible: false - running: true - - width: 48 - height: 48 - } - - ShortcutText { - id: mainTextContainer - anchors { - top: parent.top - left: parent.left - margins: 0 - topMargin: hifi.dimensions.contentSpacing.y - } - - visible: false - - text: qsTr("There was an unknown error while creating your account.") - wrapMode: Text.WordWrap - color: hifi.colors.redAccent - horizontalAlignment: Text.AlignLeft - } - - Column { - id: form - anchors { - top: mainTextContainer.bottom - left: parent.left - margins: 0 - topMargin: 2 * hifi.dimensions.contentSpacing.y - } - spacing: 2 * hifi.dimensions.contentSpacing.y - - Row { - spacing: hifi.dimensions.contentSpacing.x - - TextField { - id: emailField - anchors { - verticalCenter: parent.verticalCenter - } - width: 300 - - label: "Email" - } - } - - Row { - spacing: hifi.dimensions.contentSpacing.x - - TextField { - id: usernameField - anchors { - verticalCenter: parent.verticalCenter - } - width: 300 - - label: "Username" - } - - ShortcutText { - anchors { - verticalCenter: parent.verticalCenter - } - - text: qsTr("No spaces / special chars.") - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - - color: hifi.colors.blueAccent - } - } - - Row { - spacing: hifi.dimensions.contentSpacing.x - - TextField { - id: passwordField - anchors { - verticalCenter: parent.verticalCenter - } - width: 300 - - label: "Password" - echoMode: TextInput.Password - } - - ShortcutText { - anchors { - verticalCenter: parent.verticalCenter - } - - text: qsTr("At least 6 characters") - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - - color: hifi.colors.blueAccent - } - } - - } - - // Override ScrollingWindow's keyboard that would be at very bottom of dialog. - Keyboard { - raised: keyboardEnabled && keyboardRaised - numeric: punctuationMode - anchors { - left: parent.left - right: parent.right - bottom: buttons.top - bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 - } - } - - Row { - id: leftButton - anchors { - left: parent.left - bottom: parent.bottom - bottomMargin: hifi.dimensions.contentSpacing.y - } - - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: qsTr("Existing User") - - onClicked: { - bodyLoader.setSource("LinkAccountBody.qml") - } - } - } - - Row { - id: buttons - anchors { - right: parent.right - bottom: parent.bottom - bottomMargin: hifi.dimensions.contentSpacing.y - } - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - id: linkAccountButton - anchors.verticalCenter: parent.verticalCenter - width: 200 - - text: qsTr("Sign Up") - color: hifi.buttons.blue - - onClicked: signupBody.signup() - } - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: qsTr("Cancel") - - onClicked: bodyLoader.popup() - } - } - - Component.onCompleted: { - loginDialogRoot.title = qsTr("Create an Account") - loginDialogRoot.iconText = "<" - keyboardEnabled = HMD.active; - d.resize(); - - emailField.forceActiveFocus(); - } - - Connections { - target: loginDialog - onHandleSignupCompleted: { - console.log("Sign Up Succeeded"); - - // now that we have an account, login with that username and password - loginDialog.login(usernameField.text, passwordField.text) - } - onHandleSignupFailed: { - console.log("Sign Up Failed") - toggleLoading(false) - - mainTextContainer.text = errorString - mainTextContainer.visible = true - - d.resize(); - } - onHandleLoginCompleted: { - bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack": false }) - } - onHandleLoginFailed: { - // we failed to login, show the LoginDialog so the user will try again - bodyLoader.setSource("LinkAccountBody.qml", { "failAfterSignUp": true }) - } - } - - Keys.onPressed: { - if (!visible) { - return - } - - switch (event.key) { - case Qt.Key_Enter: - case Qt.Key_Return: - event.accepted = true - signupBody.signup() - break - } - } -} diff --git a/interface/resources/qml/TabletLoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/TabletLoginDialog/UsernameCollisionBody.qml deleted file mode 100644 index 9e5b01d339..0000000000 --- a/interface/resources/qml/TabletLoginDialog/UsernameCollisionBody.qml +++ /dev/null @@ -1,157 +0,0 @@ -// -// UsernameCollisionBody.qml -// -// Created by Clement on 7/18/16 -// 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 -// - -import Hifi 1.0 -import QtQuick 2.4 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 as OriginalStyles - -import "../controls-uit" -import "../styles-uit" - -Item { - id: usernameCollisionBody - clip: true - width: parent.width - height: parent.height - - function create() { - mainTextContainer.visible = false - loginDialog.createAccountFromStream(textField.text) - } - - - property bool keyboardEnabled: false - property bool keyboardRaised: false - property bool punctuationMode: false - - onKeyboardRaisedChanged: d.resize(); - - QtObject { - id: d - function resize() {} - } - - ShortcutText { - id: mainTextContainer - anchors { - top: parent.top - left: parent.left - margins: 0 - topMargin: hifi.dimensions.contentSpacing.y - } - - text: qsTr("Your Steam username is not available.") - wrapMode: Text.WordWrap - color: hifi.colors.redAccent - lineHeight: 1 - lineHeightMode: Text.ProportionalHeight - horizontalAlignment: Text.AlignHCenter - } - - - TextField { - id: textField - anchors { - top: mainTextContainer.bottom - left: parent.left - margins: 0 - topMargin: hifi.dimensions.contentSpacing.y - } - width: 250 - - placeholderText: "Choose your own" - } - - // Override ScrollingWindow's keyboard that would be at very bottom of dialog. - Keyboard { - raised: keyboardEnabled && keyboardRaised - numeric: punctuationMode - anchors { - left: parent.left - right: parent.right - bottom: buttons.top - bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 - } - } - - Row { - id: buttons - anchors { - bottom: parent.bottom - right: parent.right - margins: 0 - topMargin: hifi.dimensions.contentSpacing.y - } - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - anchors.verticalCenter: parent.verticalCenter - width: 200 - - text: qsTr("Create your profile") - color: hifi.buttons.blue - - onClicked: usernameCollisionBody.create() - } - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: qsTr("Cancel") - onClicked: bodyLoader.popup() - } - } - - Component.onCompleted: { - loginDialogRoot.title = qsTr("Complete Your Profile") - loginDialogRoot.iconText = "<" - keyboardEnabled = HMD.active; - d.resize(); - } - - Connections { - target: loginDialog - onHandleCreateCompleted: { - console.log("Create Succeeded") - - loginDialog.loginThroughSteam() - } - onHandleCreateFailed: { - console.log("Create Failed: " + error) - - mainTextContainer.visible = true - mainTextContainer.text = "\"" + textField.text + qsTr("\" is invalid or already taken.") - } - onHandleLoginCompleted: { - console.log("Login Succeeded") - - bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : false }) - } - onHandleLoginFailed: { - console.log("Login Failed") - } - } - - Keys.onPressed: { - if (!visible) { - return - } - - switch (event.key) { - case Qt.Key_Enter: - case Qt.Key_Return: - event.accepted = true - usernameCollisionBody.create() - break - } - } -} diff --git a/interface/resources/qml/TabletLoginDialog/WelcomeBody.qml b/interface/resources/qml/TabletLoginDialog/WelcomeBody.qml deleted file mode 100644 index 5ec259ca96..0000000000 --- a/interface/resources/qml/TabletLoginDialog/WelcomeBody.qml +++ /dev/null @@ -1,79 +0,0 @@ -// -// WelcomeBody.qml -// -// Created by Clement on 7/18/16 -// 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 -// - -import Hifi 1.0 -import QtQuick 2.4 - -import "../controls-uit" -import "../styles-uit" - -Item { - id: welcomeBody - clip: true - - property bool welcomeBack: false - - function setTitle() { - loginDialogRoot.title = (welcomeBack ? qsTr("Welcome back ") : qsTr("Welcome ")) + Account.username + qsTr("!") - loginDialogRoot.iconText = "" - d.resize(); - } - - QtObject { - id: d - function resize() {} - } - - InfoItem { - id: mainTextContainer - anchors { - top: parent.top - horizontalCenter: parent.horizontalCenter - margins: 0 - topMargin: hifi.dimensions.contentSpacing.y - } - - text: qsTr("You are now signed into High Fidelity") - wrapMode: Text.WordWrap - color: hifi.colors.baseGrayHighlight - lineHeight: 2 - lineHeightMode: Text.ProportionalHeight - horizontalAlignment: Text.AlignHCenter - } - - Row { - id: buttons - anchors { - top: mainTextContainer.bottom - horizontalCenter: parent.horizontalCenter - margins: 0 - topMargin: 2 * hifi.dimensions.contentSpacing.y - } - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: qsTr("Close"); - - onClicked: bodyLoader.popup() - } - } - - Component.onCompleted: { - welcomeBody.setTitle() - } - - Connections { - target: Account - onUsernameChanged: welcomeBody.setTitle() - } -} diff --git a/interface/resources/qml/controls-uit/TextField.qml b/interface/resources/qml/controls-uit/TextField.qml index 65fab00700..a1c98b54d4 100644 --- a/interface/resources/qml/controls-uit/TextField.qml +++ b/interface/resources/qml/controls-uit/TextField.qml @@ -31,6 +31,7 @@ TextField { font.pixelSize: hifi.fontSizes.textFieldInput font.italic: textField.text == "" height: implicitHeight + 3 // Make surrounding box higher so that highlight is vertically centered. + property alias textFieldLabel: textFieldLabel y: textFieldLabel.visible ? textFieldLabel.height + textFieldLabel.anchors.bottomMargin : 0 diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index 36ca480b24..ae00cb3d40 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -16,48 +16,63 @@ import "../controls-uit" import "../styles-uit" import "../windows" +import "../LoginDialog" + TabletModalWindow { - id: loginDialogRoot + id: realRoot objectName: "LoginDialog" signal sendToScript(var message); property bool isHMD: false property bool gotoPreviousApp: false; color: hifi.colors.baseGray + title: qsTr("Sign in to High Fidelity") + property alias titleWidth: root.titleWidth + + //fake root for shared components expecting root here + property var root: QtObject { + id: root + property alias title: realRoot.title + + property real width: realRoot.width + property real height: realRoot.height + + property int titleWidth: 0 + property string iconText: hifi.glyphs.avatar + property int iconSize: 35 + + property var pane: QtObject { + property real width: root.width + property real height: root.height + } + + function tryDestroy() { + canceled() + } + } + + //property int colorScheme: hifi.colorSchemes.dark - property int colorScheme: hifi.colorSchemes.dark - property int titleWidth: 0 - property string iconText: "" - property int icon: hifi.icons.none - property int iconSize: 35 MouseArea { - width: parent.width - height: parent.height + width: realRoot.width + height: realRoot.height } property bool keyboardOverride: true - onIconChanged: updateIcon(); property var items; property string label: "" - onTitleWidthChanged: d.resize(); + //onTitleWidthChanged: d.resize(); property bool keyboardEnabled: false property bool keyboardRaised: false property bool punctuationMode: false - onKeyboardRaisedChanged: d.resize(); + //onKeyboardRaisedChanged: d.resize(); signal canceled(); - function updateIcon() { - if (!root) { - return; - } - iconText = hifi.glyphForIcon(root.icon); - } - property alias bodyLoader: bodyLoader property alias loginDialog: loginDialog property alias hifi: hifi @@ -65,9 +80,10 @@ TabletModalWindow { HifiConstants { id: hifi } onCanceled: { - if (loginDialogRoot.Stack.view) { - loginDialogRoot.Stack.view.pop(); - } else if (gotoPreviousApp) { + if (bodyLoader.active === true) { + bodyLoader.active = false + } + if (gotoPreviousApp) { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); tablet.returnToPreviousApp(); } else { @@ -75,45 +91,112 @@ TabletModalWindow { } } - LoginDialog { - id: loginDialog - width: parent.width - height: parent.height - StackView { - id: bodyLoader - property var item: currentItem - property var props - property string source: "" - onCurrentItemChanged: { - //cleanup source for future usage - source = "" + TabletModalFrame { + id: mfRoot + + width: root.width + height: root.height + frameMarginTop + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + onHeightChanged: console.log("tablet mf h:", height) + + LoginDialog { + id: loginDialog + + anchors { + fill: parent + topMargin: parent.frameMarginTop + leftMargin: hifi.dimensions.contentMargin.x + rightMargin: hifi.dimensions.contentMargin.x + horizontalCenter: parent.horizontalCenter } - function setSource(src, props) { - source = "../TabletLoginDialog/" + src - bodyLoader.props = props - } - function popup() { - bodyLoader.pop() - - //check if last screen, if yes, dialog is popped out - if (depth === 1) - loginDialogRoot.canceled() - } - - anchors.fill: parent - anchors.margins: 10 - onSourceChanged: { - if (source !== "") { - bodyLoader.push(Qt.resolvedUrl(source), props) - } - } - Component.onCompleted: { - setSource(loginDialog.isSteamRunning() ? - "SignInBody.qml" : - "LinkAccountBody.qml") + Loader { + id: bodyLoader + anchors.fill: parent + anchors.horizontalCenter: parent.horizontalCenter + source: loginDialog.isSteamRunning() ? "../LoginDialog/SignInBody.qml" : "../LoginDialog/LinkAccountBody.qml" } } } + + Keys.onPressed: { + if (!visible) { + return + } + + if (event.modifiers === Qt.ControlModifier) + switch (event.key) { + case Qt.Key_A: + event.accepted = true + detailedText.selectAll() + break + case Qt.Key_C: + event.accepted = true + detailedText.copy() + break + case Qt.Key_Period: + if (Qt.platform.os === "osx") { + event.accepted = true + content.reject() + } + break + } else switch (event.key) { + case Qt.Key_Escape: + case Qt.Key_Back: + event.accepted = true + destroy() + break + + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + break + } + } +// LoginDialog { +// id: loginDialog +// width: parent.width +// height: parent.height +// StackView { +// id: bodyLoader +// property var item: currentItem +// property var props +// property string source: "" + +// onCurrentItemChanged: { +// //cleanup source for future usage +// source = "" +// } + +// function setSource(src, props) { +// source = "../TabletLoginDialog/" + src +// bodyLoader.props = props +// } +// function popup() { +// bodyLoader.pop() + +// //check if last screen, if yes, dialog is popped out +// if (depth === 1) +// loginDialogRoot.canceled() +// } + +// anchors.fill: parent +// anchors.margins: 10 +// onSourceChanged: { +// if (source !== "") { +// bodyLoader.push(Qt.resolvedUrl(source), props) +// } +// } +// Component.onCompleted: { +// setSource(loginDialog.isSteamRunning() ? +// "SignInBody.qml" : +// "LinkAccountBody.qml") +// } +// } +// } } From 0a9bebeefc52c53577b4705de147f8d596caa3fd Mon Sep 17 00:00:00 2001 From: vladest Date: Thu, 28 Sep 2017 15:34:56 +0200 Subject: [PATCH 481/722] Added workaround to enable menu bar under Linux --- interface/src/main.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/src/main.cpp b/interface/src/main.cpp index cb90160cfe..33053dd294 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -49,6 +49,10 @@ int main(int argc, const char* argv[]) { CrashReporter crashReporter { BUG_SPLAT_DATABASE, BUG_SPLAT_APPLICATION_NAME, BuildInfo::VERSION }; #endif +#ifdef Q_OS_UNIX + QApplication::setAttribute(Qt::AA_DontUseNativeMenuBar); +#endif + disableQtBearerPoll(); // Fixes wifi ping spikes QElapsedTimer startupTime; From 65a63ea8c37e5d1ff2bb31b5374cb5c351783324 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 28 Sep 2017 09:38:22 -0700 Subject: [PATCH 482/722] Remove dead OS X code --- interface/src/commerce/Ledger.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index d8860192ad..a68a6fe929 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -164,11 +164,7 @@ void Ledger::historySuccess(QNetworkReply& reply) { // turns out on my machine, toLocalTime convert to some weird timezone, yet the // systemTimeZone is correct. To avoid a strange bug with other's systems too, lets // be explicit -#ifdef Q_OS_MAC - QDateTime createdAt = QDateTime::fromTime_t(valueObject["created_at"].toInt(), Qt::UTC); -#else QDateTime createdAt = QDateTime::fromSecsSinceEpoch(valueObject["created_at"].toInt(), Qt::UTC); -#endif QDateTime localCreatedAt = createdAt.toTimeZone(QTimeZone::systemTimeZone()); valueObject["text"] = QString("%1 sent %2 %3 with message \"%4\""). arg(from, to, coloredQuantityAndAssetTitle, valueObject["message"].toString()); From 8d7448739348eb8bb99cff22802a65625cbe31d8 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 28 Sep 2017 10:25:05 -0700 Subject: [PATCH 483/722] Use edition number; fix CONFIRMED timer --- .../qml/hifi/commerce/purchases/PurchasedItem.qml | 12 +++++++++--- .../qml/hifi/commerce/purchases/Purchases.qml | 3 +-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index af93288bde..97ca5f15b5 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -35,13 +35,18 @@ Item { property string itemPreviewImageUrl; property string itemHref; property int ownedItemCount; - property int itemInstanceNumber; + property int itemEdition; + + property string originalStatusText; + property string originalStatusColor; height: 110; width: parent.width; onPurchaseStatusChangedChanged: { if (root.purchaseStatusChanged === true && root.purchaseStatus === "confirmed") { + root.originalStatusText = statusText.text; + root.originalStatusColor = statusText.color; statusText.text = "CONFIRMED!"; statusText.color = hifi.colors.blueAccent; confirmedTimer.start(); @@ -53,7 +58,8 @@ Item { id: confirmedTimer; interval: 3000; onTriggered: { - root.purchaseStatus = ""; + statusText.text = root.originalStatusText; + statusText.color = root.originalStatusColor; } } @@ -196,7 +202,7 @@ Item { } else if (root.purchaseStatus === "invalidated") { "INVALIDATED" } else if (root.ownedItemCount > 1) { - "(#" + root.itemInstanceNumber + ") You own " + (root.ownedItemCount - 1) + " others" + "(#" + root.itemEdition + ") You own " + (root.ownedItemCount - 1) + " others" } else { "" } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index a073a21e77..786e8daad0 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -417,7 +417,7 @@ Rectangle { itemHref: root_file_url; purchaseStatus: status; purchaseStatusChanged: statusChanged; - itemInstanceNumber: model.itemInstanceNumber; + itemEdition: model.edition_number; ownedItemCount: model.ownedItemCount; anchors.topMargin: 12; anchors.bottomMargin: 12; @@ -553,7 +553,6 @@ Rectangle { } else { itemCountDictionary[currentItemId]++; } - filteredPurchasesModel.setProperty(i, "itemInstanceNumber", itemCountDictionary[currentItemId]); } for (var i = 0; i < filteredPurchasesModel.count; i++) { From b5dc6b791b06d3415b8d288ca421f5b307691ad1 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 28 Sep 2017 10:42:45 -0700 Subject: [PATCH 484/722] Fix commerce setting handle --- interface/src/ui/overlays/ContextOverlayInterface.cpp | 3 ++- interface/src/ui/overlays/ContextOverlayInterface.h | 2 -- libraries/ui/src/ui/types/RequestFilters.cpp | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index 8e4254a786..39fd4f9377 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -201,7 +201,8 @@ bool ContextOverlayInterface::destroyContextOverlay(const EntityItemID& entityIt void ContextOverlayInterface::contextOverlays_mousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { if (overlayID == _contextOverlayID && event.getButton() == PointerEvent::PrimaryButton) { qCDebug(context_overlay) << "Clicked Context Overlay. Entity ID:" << _currentEntityWithContextOverlay << "Overlay ID:" << overlayID; - if (_commerceSettingSwitch.get()) { + Setting::Handle _settingSwitch{ "commerce", false }; + if (_settingSwitch.get()) { openInspectionCertificate(); } else { openMarketplace(); diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h index ec5913444f..b4d3ddc0c2 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.h +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -79,8 +79,6 @@ private: bool _isInMarketplaceInspectionMode { false }; - Setting::Handle _commerceSettingSwitch{ "commerce", false }; - void openInspectionCertificate(); void openMarketplace(); void enableEntityHighlight(const EntityItemID& entityItemID); diff --git a/libraries/ui/src/ui/types/RequestFilters.cpp b/libraries/ui/src/ui/types/RequestFilters.cpp index 233a9458fe..f9686d2311 100644 --- a/libraries/ui/src/ui/types/RequestFilters.cpp +++ b/libraries/ui/src/ui/types/RequestFilters.cpp @@ -61,7 +61,7 @@ void RequestFilters::interceptHFWebEngineRequest(QWebEngineUrlRequestInfo& info) // During the period in which we have HFC commerce in the system, but not applied everywhere: const QString tokenStringCommerce{ "Chrome/48.0 (HighFidelityInterface WithHFC)" }; - static Setting::Handle _settingSwitch{ "commerce", false }; + Setting::Handle _settingSwitch{ "commerce", false }; bool isMoney = _settingSwitch.get(); const QString tokenString = !isAuthable ? tokenStringMobile : (isMoney ? tokenStringCommerce : tokenStringMetaverse); From f0d668c5c326cb5a65fab040938cc38c42bb52b9 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 28 Sep 2017 11:14:44 -0700 Subject: [PATCH 485/722] Fix marketplace injection problems - yay! --- scripts/system/html/js/marketplacesInject.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 0c82ab4343..bdee322381 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -235,7 +235,9 @@ } function injectHiFiCode() { - if (confirmAllPurchases) { + if (!$('body').hasClass("code-injected") && confirmAllPurchases) { + + $('body').addClass("code-injected"); maybeAddLogInButton(); @@ -260,7 +262,9 @@ } function injectHiFiItemPageCode() { - if (confirmAllPurchases) { + if (!$('body').hasClass("code-injected") && confirmAllPurchases) { + + $('body').addClass("code-injected"); maybeAddLogInButton(); @@ -567,4 +571,5 @@ // Load / unload. window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready(). + window.addEventListener("page:change", onLoad); // Triggered after Marketplace HTML is changed }()); From 3b05317db67af35cffc6e46ca205a8fddea92fa0 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Thu, 28 Sep 2017 11:18:40 -0700 Subject: [PATCH 486/722] fixing grabbing and tablet bugs --- .../controllerModules/equipEntity.js | 16 +++++++++++++--- .../controllerModules/farActionGrabEntity.js | 4 ++-- .../controllerModules/tabletStylusInput.js | 17 ++++++++++++++++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index 29db02c6de..4978f225ce 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -255,6 +255,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa this.messageGrabEntity = false; this.grabEntityProps = null; this.shouldSendStart = false; + this.equipedWithSecondary = false; this.parameters = makeDispatcherModuleParameters( 300, @@ -370,6 +371,10 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa return this.rawSecondaryValue < BUMPER_ON_VALUE; }; + this.secondarySmoothedSqueezed = function() { + return this.rawSecondaryValue > BUMPER_ON_VALUE; + }; + this.chooseNearEquipHotspots = function(candidateEntityProps, controllerData) { var _this = this; var collectedHotspots = flatten(candidateEntityProps.map(function(props) { @@ -592,11 +597,13 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa // if the potentialHotspot os not cloneable and locked return null if (potentialEquipHotspot && - ((this.triggerSmoothedSqueezed() && !this.waitForTriggerRelease) || this.messageGrabEntity)) { + (((this.triggerSmoothedSqueezed() || this.secondarySmoothedSqueezed()) && !this.waitForTriggerRelease) || + this.messageGrabEntity)) { this.grabbedHotspot = potentialEquipHotspot; this.targetEntityID = this.grabbedHotspot.entityID; this.startEquipEntity(controllerData); this.messageGrabEnity = false; + this.equipedWithSecondary = this.secondarySmoothedSqueezed(); return makeRunningValues(true, [potentialEquipHotspot.entityID], []); } else { return makeRunningValues(false, [], []); @@ -627,7 +634,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa return this.checkNearbyHotspots(controllerData, deltaTime, timestamp); } - if (controllerData.secondaryValues[this.hand]) { + if (controllerData.secondaryValues[this.hand] && !this.equipedWithSecondary) { // this.secondaryReleased() will always be true when not depressed // so we cannot simply rely on that for release - ensure that the // trigger was first "prepared" by being pushed in before the release @@ -644,7 +651,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var dropDetected = this.dropGestureProcess(deltaTime); - if (this.triggerSmoothedReleased()) { + if (this.triggerSmoothedReleased() || this.secondaryReleased()) { if (this.shouldSendStart) { // we don't want to send startEquip message until the trigger is released. otherwise, // guns etc will fire right as they are equipped. @@ -653,6 +660,9 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa this.shouldSendStart = false; } this.waitForTriggerRelease = false; + if (this.secondaryReleased() && this.equipedWithSecondary) { + this.equipedWithSecondary = false; + } } if (dropDetected && this.prevDropDetected !== dropDetected) { diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index 5c31c859e9..c5b82f75f0 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -132,7 +132,7 @@ Script.include("/~/system/libraries/controllers.js"); this.updateLaserPointer = function(controllerData) { var SEARCH_SPHERE_SIZE = 0.011; var MIN_SPHERE_SIZE = 0.0005; - var radius = Math.max(1.2 * SEARCH_SPHERE_SIZE * this.intersectionDistance, MIN_SPHERE_SIZE); + var radius = Math.max(1.2 * SEARCH_SPHERE_SIZE * this.intersectionDistance, MIN_SPHERE_SIZE) * MyAvatar.sensorToWorldScale; var dim = {x: radius, y: radius, z: radius}; var mode = "hold"; if (!this.distanceHolding && !this.distanceRotating) { @@ -424,7 +424,7 @@ Script.include("/~/system/libraries/controllers.js"); this.laserPointerOff(); return makeRunningValues(false, [], []); } - + this.intersectionDistance = controllerData.rayPicks[this.hand].distance; this.updateLaserPointer(controllerData); var otherModuleName =this.hand === RIGHT_HAND ? "LeftFarActionGrabEntity" : "RightFarActionGrabEntity"; diff --git a/scripts/system/controllers/controllerModules/tabletStylusInput.js b/scripts/system/controllers/controllerModules/tabletStylusInput.js index def958b223..0a3b2b8adc 100644 --- a/scripts/system/controllers/controllerModules/tabletStylusInput.js +++ b/scripts/system/controllers/controllerModules/tabletStylusInput.js @@ -152,6 +152,20 @@ Script.include("/~/system/libraries/controllers.js"); } }; + this.updateStylus = function() { + if (this.stylus) { + var X_ROT_NEG_90 = { x: -0.70710678, y: 0, z: 0, w: 0.70710678 }; + var modelOrientation = Quat.multiply(this.stylusTip.orientation, X_ROT_NEG_90); + var modelPositionOffset = Vec3.multiplyQbyV(modelOrientation, { x: 0, y: 0, z: MyAvatar.sensorToWorldScale * -WEB_STYLUS_LENGTH / 2 }); + + var stylusProps = { + position: Vec3.sum(this.stylusTip.position, modelPositionOffset), + rotation: modelOrientation + }; + Overlays.editOverlay(this.stylus, stylusProps); + } + }; + this.showStylus = function() { if (this.stylus) { return; @@ -320,6 +334,7 @@ Script.include("/~/system/libraries/controllers.js"); if (this.isNearStylusTarget) { if (!this.useFingerInsteadOfStylus) { this.showStylus(); + this.updateStylus(); } else { this.pointFinger(true); } @@ -335,7 +350,7 @@ Script.include("/~/system/libraries/controllers.js"); var SCALED_TABLET_MAX_HOVER_DISTANCE = TABLET_MAX_HOVER_DISTANCE * sensorScaleFactor; if (nearestStylusTarget && nearestStylusTarget.distance > SCALED_TABLET_MIN_TOUCH_DISTANCE && - nearestStylusTarget.distance < SCALED_TABLET_MAX_HOVER_DISTANCE) { + nearestStylusTarget.distance < SCALED_TABLET_MAX_HOVER_DISTANCE && !this.getOtherHandController().stylusTouchingTarget) { this.requestTouchFocus(nearestStylusTarget); From 769c57208c3307ad88761e14b3efab6f91abb557 Mon Sep 17 00:00:00 2001 From: vladest Date: Thu, 28 Sep 2017 21:35:41 +0200 Subject: [PATCH 487/722] Signup ready --- .../qml/LoginDialog/LinkAccountBody.qml | 104 +++++------ .../resources/qml/LoginDialog/SignUpBody.qml | 170 ++++++++---------- .../qml/dialogs/TabletLoginDialog.qml | 2 +- interface/src/main.cpp | 4 +- 4 files changed, 120 insertions(+), 160 deletions(-) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 7f69e41958..26e90eb9a6 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -45,8 +45,8 @@ Item { function resize() { var targetWidth = Math.max(titleWidth, form.contentWidth); var targetHeight = hifi.dimensions.contentSpacing.y + mainTextContainer.height + - 4 * hifi.dimensions.contentSpacing.y + form.height + - hifi.dimensions.contentSpacing.y + buttons.height; + 4 * hifi.dimensions.contentSpacing.y + form.height/* + + hifi.dimensions.contentSpacing.y + buttons.height*/; if (additionalInformation.visible) { targetWidth = Math.max(targetWidth, additionalInformation.width); @@ -56,7 +56,7 @@ Item { parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)); parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : hifi.dimensions.contentSpacing.y); - console.log("sign in h:", targetHeight, parent.height) + console.log("sign in h:", targetHeight, parent.height, form.height) } } @@ -67,9 +67,6 @@ Item { if (loginDialog.isSteamRunning()) { additionalInformation.visible = !isLoading } - - leftButton.visible = !isLoading - buttons.visible = !isLoading } BusyIndicator { @@ -173,11 +170,6 @@ Item { InfoItem { id: additionalInformation - anchors { - left: parent.left - margins: 0 - topMargin: hifi.dimensions.contentSpacing.y - } visible: loginDialog.isSteamRunning() @@ -189,65 +181,59 @@ Item { horizontalAlignment: Text.AlignHCenter } - Column { - //width: parent.width + Row { + id: buttons spacing: hifi.dimensions.contentSpacing.y*2 + onHeightChanged: d.resize(); onWidthChanged: d.resize(); anchors.horizontalCenter: parent.horizontalCenter - //padding: 10 - Row { - id: buttons - spacing: hifi.dimensions.contentSpacing.y*2 - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - anchors.horizontalCenter: parent.horizontalCenter + Button { + id: linkAccountButton + anchors.verticalCenter: parent.verticalCenter + width: 200 - Button { - id: linkAccountButton - anchors.verticalCenter: parent.verticalCenter - width: 200 + text: qsTr(loginDialog.isSteamRunning() ? "Link Account" : "Login") + color: hifi.buttons.blue - text: qsTr(loginDialog.isSteamRunning() ? "Link Account" : "Login") - color: hifi.buttons.blue - - onClicked: linkAccountBody.login() - } - - Button { - anchors.verticalCenter: parent.verticalCenter - text: qsTr("Cancel") - onClicked: root.tryDestroy() - } + onClicked: linkAccountBody.login() } - Row { - id: leftButton + Button { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Cancel") + onClicked: root.tryDestroy() + } + } - anchors.horizontalCenter: parent.horizontalCenter - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); + Row { + id: leftButton - RalewaySemiBold { - size: hifi.fontSizes.inputLabel - anchors.verticalCenter: parent.verticalCenter - text: qsTr("Don't have an account?") - } + anchors.horizontalCenter: parent.horizontalCenter + spacing: hifi.dimensions.contentSpacing.y*2 + onHeightChanged: d.resize(); onWidthChanged: d.resize(); - Button { - anchors.verticalCenter: parent.verticalCenter + RalewaySemiBold { + size: hifi.fontSizes.inputLabel + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Don't have an account?") + } - text: qsTr("Sign Up") - visible: !loginDialog.isSteamRunning() + Button { + anchors.verticalCenter: parent.verticalCenter - onClicked: { - bodyLoader.setSource("SignUpBody.qml") - bodyLoader.item.width = root.pane.width - bodyLoader.item.height = root.pane.height - } + text: qsTr("Sign Up") + visible: !loginDialog.isSteamRunning() + + onClicked: { + bodyLoader.setSource("SignUpBody.qml") + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height } } } } + // Override ScrollingWindow's keyboard that would be at very bottom of dialog. Keyboard { raised: keyboardEnabled && keyboardRaised @@ -266,7 +252,7 @@ Item { root.title = qsTr("Sign Into High Fidelity") root.iconText = "<" keyboardEnabled = HMD.active; - d.resize(); + //d.resize(); if (failAfterSignUp) { mainTextContainer.text = "Account created successfully." @@ -313,11 +299,11 @@ Item { } switch (event.key) { - case Qt.Key_Enter: - case Qt.Key_Return: - event.accepted = true - linkAccountBody.login() - break + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + linkAccountBody.login() + break } } } diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index c7bfa8cfcd..aef20c7a61 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -44,13 +44,13 @@ Item { function resize() { var targetWidth = Math.max(titleWidth, form.contentWidth); var targetHeight = hifi.dimensions.contentSpacing.y + mainTextContainer.height + - 4 * hifi.dimensions.contentSpacing.y + form.height + - hifi.dimensions.contentSpacing.y + buttons.height; + 4 * hifi.dimensions.contentSpacing.y + form.height/* + + hifi.dimensions.contentSpacing.y + buttons.height*/; parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)); parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : 0); - //console.log("sign up h:", parent.height) + console.log("sign up h:", parent.height, targetHeight, form.height) } } @@ -97,44 +97,31 @@ Item { Column { id: form + width: parent.width + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + anchors { top: mainTextContainer.bottom - left: parent.left - margins: 0 topMargin: 2 * hifi.dimensions.contentSpacing.y } spacing: 2 * hifi.dimensions.contentSpacing.y - Row { - spacing: hifi.dimensions.contentSpacing.x - - TextField { - id: emailField - anchors { - verticalCenter: parent.verticalCenter - } - width: 350 - - label: "Email" - } + TextField { + id: emailField + width: parent.width + label: "Email" } - Row { - spacing: hifi.dimensions.contentSpacing.x - - TextField { - id: usernameField - anchors { - verticalCenter: parent.verticalCenter - } - width: 350 - - label: "Username" - } + TextField { + id: usernameField + width: parent.width + label: "Username" ShortcutText { anchors { - verticalCenter: parent.verticalCenter + verticalCenter: parent.textFieldLabel.verticalCenter + left: parent.textFieldLabel.right + leftMargin: 10 } text: qsTr("No spaces / special chars.") @@ -146,23 +133,17 @@ Item { } } - Row { - spacing: hifi.dimensions.contentSpacing.x - - TextField { - id: passwordField - anchors { - verticalCenter: parent.verticalCenter - } - width: 350 - - label: "Password" - echoMode: TextInput.Password - } + TextField { + id: passwordField + width: parent.width + label: "Password" + echoMode: TextInput.Password ShortcutText { anchors { - verticalCenter: parent.verticalCenter + verticalCenter: parent.textFieldLabel.verticalCenter + left: parent.textFieldLabel.right + leftMargin: 10 } text: qsTr("At least 6 characters") @@ -174,6 +155,51 @@ Item { } } + Row { + id: leftButton + anchors.horizontalCenter: parent.horizontalCenter + + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Existing User") + + onClicked: { + bodyLoader.setSource("LinkAccountBody.qml") + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + } + } + + Row { + id: buttons + anchors.horizontalCenter: parent.horizontalCenter + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + id: linkAccountButton + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Sign Up") + color: hifi.buttons.blue + + onClicked: signupBody.signup() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + + onClicked: root.tryDestroy() + } + } } // Override ScrollingWindow's keyboard that would be at very bottom of dialog. @@ -183,65 +209,11 @@ Item { anchors { left: parent.left right: parent.right - bottom: buttons.top + bottom: parent.bottom bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 } } - Row { - id: leftButton - anchors { - left: parent.left - bottom: parent.bottom - bottomMargin: hifi.dimensions.contentSpacing.y - } - - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: qsTr("Existing User") - - onClicked: { - bodyLoader.setSource("LinkAccountBody.qml") - bodyLoader.item.width = root.pane.width - bodyLoader.item.height = root.pane.height - } - } - } - - Row { - id: buttons - anchors { - right: parent.right - bottom: parent.bottom - bottomMargin: hifi.dimensions.contentSpacing.y - } - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - id: linkAccountButton - anchors.verticalCenter: parent.verticalCenter - width: 200 - - text: qsTr("Sign Up") - color: hifi.buttons.blue - - onClicked: signupBody.signup() - } - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: qsTr("Cancel") - - onClicked: root.tryDestroy() - } - } - Component.onCompleted: { root.title = qsTr("Create an Account") root.iconText = "<" diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index ae00cb3d40..78221ab6cb 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -96,7 +96,7 @@ TabletModalWindow { id: mfRoot width: root.width - height: root.height + frameMarginTop + height: root.height //+ frameMarginTop anchors { horizontalCenter: parent.horizontalCenter diff --git a/interface/src/main.cpp b/interface/src/main.cpp index cb90160cfe..334715ef04 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -48,7 +48,9 @@ int main(int argc, const char* argv[]) { static QString BUG_SPLAT_APPLICATION_NAME = "Interface"; CrashReporter crashReporter { BUG_SPLAT_DATABASE, BUG_SPLAT_APPLICATION_NAME, BuildInfo::VERSION }; #endif - +#ifdef Q_OS_UNIX + QApplication::setAttribute(Qt::AA_DontUseNativeMenuBar); +#endif disableQtBearerPoll(); // Fixes wifi ping spikes QElapsedTimer startupTime; From c3225c2c761d52b5ade6a8e361189bc7f1829140 Mon Sep 17 00:00:00 2001 From: vladest Date: Thu, 28 Sep 2017 21:38:03 +0200 Subject: [PATCH 488/722] Make fix precisely for Linux --- interface/src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 33053dd294..5c07bebc23 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -49,7 +49,7 @@ int main(int argc, const char* argv[]) { CrashReporter crashReporter { BUG_SPLAT_DATABASE, BUG_SPLAT_APPLICATION_NAME, BuildInfo::VERSION }; #endif -#ifdef Q_OS_UNIX +#ifdef Q_OS_LINUX QApplication::setAttribute(Qt::AA_DontUseNativeMenuBar); #endif From 68c08969aef501db8104ab9cdc0b3cb535a6a8e9 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 28 Sep 2017 12:49:38 -0700 Subject: [PATCH 489/722] Update Purchases --- .../commerce/common/SortableListModel.qml | 68 +++++++++++++++++++ .../InspectionCertificate.qml | 19 ++++-- .../hifi/commerce/purchases/PurchasedItem.qml | 22 ++---- .../qml/hifi/commerce/purchases/Purchases.qml | 61 +++++++++++++---- scripts/system/marketplaces/marketplaces.js | 17 ++--- 5 files changed, 140 insertions(+), 47 deletions(-) create mode 100644 interface/resources/qml/hifi/commerce/common/SortableListModel.qml diff --git a/interface/resources/qml/hifi/commerce/common/SortableListModel.qml b/interface/resources/qml/hifi/commerce/common/SortableListModel.qml new file mode 100644 index 0000000000..2d82e42ddb --- /dev/null +++ b/interface/resources/qml/hifi/commerce/common/SortableListModel.qml @@ -0,0 +1,68 @@ +// +// SortableListModel.qml +// qml/hifi/commerce/common +// +// SortableListModel +// +// Created by Zach Fox on 2017-09-28 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 + +ListModel { + id: root; + property string sortColumnName: ""; + property bool isSortingDescending: true; + + function swap(a, b) { + if (a < b) { + move(a, b, 1); + move (b - 1, a, 1); + } else if (a > b) { + move(b, a, 1); + move(a - 1, b, 1); + } + } + + function partition(begin, end, pivot) { + var piv = get(pivot)[sortColumnName]; + swap(pivot, end - 1); + var store = begin; + + for (var i = begin; i < end - 1; ++i) { + if (isSortingDescending) { + if (get(i)[sortColumnName] < piv) { + swap(store, i); + ++store; + } + } else { + if (get(i)[sortColumnName] > piv) { + swap(store, i); + ++store; + } + } + } + swap(end - 1, store); + + return store; + } + + function qsort(begin, end) { + if (end - 1 > begin) { + var pivot = begin + Math.floor(Math.random() * (end - begin)); + + pivot = partition(begin, end, pivot); + + qsort(begin, pivot); + qsort(pivot + 1, end); + } + } + + function quickSort() { + qsort(0, count) + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml index 65bfcfc4b3..19728daa82 100644 --- a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml +++ b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml @@ -30,12 +30,20 @@ Rectangle { property string itemOwner: "--"; property string itemEdition: "--"; property string dateOfPurchase: ""; - property bool closeGoesToPurchases: false; + property bool isLightbox: false; // Style color: hifi.colors.faintGray; Hifi.QmlCommerce { id: commerce; - } + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + } Image { anchors.fill: parent; @@ -262,7 +270,11 @@ Rectangle { height: 50; text: "close"; onClicked: { - sendToScript({method: 'inspectionCertificate_closeClicked', closeGoesToPurchases: root.closeGoesToPurchases}); + if (root.isLightbox) { + root.visible = false; + } else { + sendToScript({method: 'inspectionCertificate_closeClicked', closeGoesToPurchases: root.closeGoesToPurchases}); + } } } @@ -303,7 +315,6 @@ Rectangle { switch (message.method) { case 'inspectionCertificate_setMarketplaceId': root.marketplaceId = message.marketplaceId; - root.closeGoesToPurchases = message.closeGoesToPurchases; break; case 'inspectionCertificate_setItemInfo': root.itemName = message.itemName; diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index 97ca5f15b5..6f23b7a1a5 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -34,7 +34,7 @@ Item { property string itemId; property string itemPreviewImageUrl; property string itemHref; - property int ownedItemCount; + property int displayedItemCount; property int itemEdition; property string originalStatusText; @@ -182,7 +182,7 @@ Item { Item { id: statusContainer; - visible: root.purchaseStatus || root.ownedItemCount > 1; + visible: root.purchaseStatus || root.displayedItemCount > 1; anchors.left: itemName.left; anchors.top: certificateContainer.bottom; anchors.topMargin: 8; @@ -201,8 +201,8 @@ Item { "PENDING..." } else if (root.purchaseStatus === "invalidated") { "INVALIDATED" - } else if (root.ownedItemCount > 1) { - "(#" + root.itemEdition + ") You own " + (root.ownedItemCount - 1) + " others" + } else if (root.displayedItemCount > 1) { + ("#" + root.itemEdition) } else { "" } @@ -213,8 +213,8 @@ Item { hifi.colors.blueAccent } else if (root.purchaseStatus === "invalidated") { hifi.colors.redAccent - } else if (root.ownedItemCount > 1) { - hifi.colors.blueAccent + } else if (root.displayedItemCount > 1) { + hifi.colors.lightGray } else { hifi.colors.baseGray } @@ -246,8 +246,6 @@ Item { hifi.colors.blueAccent } else if (root.purchaseStatus === "invalidated") { hifi.colors.redAccent - } else if (root.ownedItemCount > 1) { - hifi.colors.blueAccent } else { hifi.colors.baseGray } @@ -263,8 +261,6 @@ Item { sendToPurchases({method: 'showPendingLightbox'}); } else if (root.purchaseStatus === "invalidated") { sendToPurchases({method: 'showInvalidatedLightbox'}); - } else if (root.ownedItemCount > 1) { - sendToPurchases({method: 'setFilterText', filterText: root.itemName}); } } onEntered: { @@ -274,9 +270,6 @@ Item { } else if (root.purchaseStatus === "invalidated") { statusText.color = hifi.colors.redAccent; statusIcon.color = hifi.colors.redAccent; - } else if (root.ownedItemCount > 1) { - statusText.color = hifi.colors.blueHighlight; - statusIcon.color = hifi.colors.blueHighlight; } } onExited: { @@ -286,9 +279,6 @@ Item { } else if (root.purchaseStatus === "invalidated") { statusText.color = hifi.colors.redHighlight; statusIcon.color = hifi.colors.redHighlight; - } else if (root.ownedItemCount > 1) { - statusText.color = hifi.colors.blueAccent; - statusIcon.color = hifi.colors.blueAccent; } } } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 786e8daad0..5e0c7b9e99 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -19,6 +19,7 @@ import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls import "../wallet" as HifiWallet import "../common" as HifiCommerceCommon +import "../inspectionCertificate" as HifiInspectionCertificate // references XXX from root context @@ -121,6 +122,19 @@ Rectangle { } } + HifiInspectionCertificate.InspectionCertificate { + id: inspectionCertificate; + z: 999; + visible: false; + anchors.fill: parent; + + Connections { + onSendToScript: { + sendToScript(message); + } + } + } + HifiCommerceCommon.CommerceLightbox { id: lightboxPopup; visible: false; @@ -331,7 +345,7 @@ Rectangle { ListModel { id: previousPurchasesModel; } - ListModel { + HifiCommerceCommon.SortableListModel { id: filteredPurchasesModel; } @@ -418,7 +432,7 @@ Rectangle { purchaseStatus: status; purchaseStatusChanged: statusChanged; itemEdition: model.edition_number; - ownedItemCount: model.ownedItemCount; + displayedItemCount: model.displayedItemCount; anchors.topMargin: 12; anchors.bottomMargin: 12; @@ -427,6 +441,8 @@ Rectangle { if (msg.method === 'purchases_itemInfoClicked') { sendToScript({method: 'purchases_itemInfoClicked', itemId: itemId}); } else if (msg.method === 'purchases_itemCertificateClicked') { + inspectionCertificate.visible = true; + inspectionCertificate.isLightbox = true; sendToScript(msg); } else if (msg.method === "showInvalidatedLightbox") { lightboxPopup.titleText = "Item Invalidated"; @@ -532,18 +548,7 @@ Rectangle { // FUNCTION DEFINITIONS START // - function buildFilteredPurchasesModel() { - filteredPurchasesModel.clear(); - for (var i = 0; i < purchasesModel.count; i++) { - if (purchasesModel.get(i).title.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) { - if (purchasesModel.get(i).status !== "confirmed") { - filteredPurchasesModel.insert(0, purchasesModel.get(i)); - } else { - filteredPurchasesModel.append(purchasesModel.get(i)); - } - } - } - + function populateDisplayedItemCounts() { var itemCountDictionary = {}; var currentItemId; for (var i = 0; i < filteredPurchasesModel.count; i++) { @@ -556,10 +561,32 @@ Rectangle { } for (var i = 0; i < filteredPurchasesModel.count; i++) { - filteredPurchasesModel.setProperty(i, "ownedItemCount", itemCountDictionary[currentItemId]); + filteredPurchasesModel.setProperty(i, "displayedItemCount", itemCountDictionary[filteredPurchasesModel.get(i).id]); } } + function sortByDate() { + filteredPurchasesModel.sortColumnName = "purchase_date"; + filteredPurchasesModel.isSortingDescending = false; + filteredPurchasesModel.quickSort(); + } + + function buildFilteredPurchasesModel() { + filteredPurchasesModel.clear(); + for (var i = 0; i < purchasesModel.count; i++) { + if (purchasesModel.get(i).title.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) { + if (purchasesModel.get(i).status !== "confirmed") { + filteredPurchasesModel.insert(0, purchasesModel.get(i)); + } else { + filteredPurchasesModel.append(purchasesModel.get(i)); + } + } + } + + populateDisplayedItemCounts(); + sortByDate(); + } + function checkIfAnyItemStatusChanged() { var currentPurchasesModelId, currentPurchasesModelEdition, currentPurchasesModelStatus; var previousPurchasesModelStatus; @@ -609,6 +636,10 @@ Rectangle { commerce.inventory(); } break; + case 'inspectionCertificate_setMarketplaceId': + case 'inspectionCertificate_setItemInfo': + inspectionCertificate.fromScript(message); + break; default: console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 5a00b20441..73008ef4ce 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -128,12 +128,11 @@ } } - function setCertificateInfo(currentEntityWithContextOverlay, itemMarketplaceId, closeGoesToPurchases) { + function setCertificateInfo(currentEntityWithContextOverlay, itemMarketplaceId) { wireEventBridge(true); tablet.sendToQml({ method: 'inspectionCertificate_setMarketplaceId', - marketplaceId: itemMarketplaceId || Entities.getEntityProperties(currentEntityWithContextOverlay, ['marketplaceID']).marketplaceID, - closeGoesToPurchases: closeGoesToPurchases + marketplaceId: itemMarketplaceId || Entities.getEntityProperties(currentEntityWithContextOverlay, ['marketplaceID']).marketplaceID }); // ZRF FIXME! Make a call to the endpoint to get item info instead of this silliness Script.setTimeout(function () { @@ -338,17 +337,11 @@ tablet.loadQMLSource("TabletAddressDialog.qml"); break; case 'purchases_itemCertificateClicked': - tablet.loadQMLSource("../commerce/inspectionCertificate/InspectionCertificate.qml"); - setCertificateInfo("", message.itemMarketplaceId, true); + console.log("ZRFJIOSE FJSOPIEFJSE OIFJSOPEI FJSIOEFJ ") + setCertificateInfo("", message.itemMarketplaceId); break; case 'inspectionCertificate_closeClicked': - if (message.closeGoesToPurchases) { - referrerURL = MARKETPLACE_URL_INITIAL; - filterText = ""; - tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); - } else { - tablet.gotoHomeScreen(); - } + tablet.gotoHomeScreen(); break; case 'inspectionCertificate_showInMarketplaceClicked': tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); From 751dca0761d92d44ec845dcd8a9c3844d96756c7 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 28 Sep 2017 13:06:37 -0700 Subject: [PATCH 490/722] styling updates for purchases --- .../hifi/commerce/purchases/PurchasedItem.qml | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index 6f23b7a1a5..a026a818c0 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -180,9 +180,30 @@ Item { } Item { - id: statusContainer; + id: editionContainer; + visible: root.displayedItemCount > 1 && !statusContainer.visible; + anchors.left: itemName.left; + anchors.top: certificateContainer.bottom; + anchors.topMargin: 8; + anchors.bottom: parent.bottom; + anchors.right: buttonContainer.left; + anchors.rightMargin: 2; - visible: root.purchaseStatus || root.displayedItemCount > 1; + FiraSansRegular { + anchors.left: parent.left; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + width: paintedWidth; + text: "#" + root.itemEdition; + size: 15; + color: "#cc6a6a6a"; + verticalAlignment: Text.AlignTop; + } + } + + Item { + id: statusContainer; + visible: root.purchaseStatus === "pending" || root.purchaseStatus === "invalidated"; anchors.left: itemName.left; anchors.top: certificateContainer.bottom; anchors.topMargin: 8; @@ -201,8 +222,6 @@ Item { "PENDING..." } else if (root.purchaseStatus === "invalidated") { "INVALIDATED" - } else if (root.displayedItemCount > 1) { - ("#" + root.itemEdition) } else { "" } @@ -213,8 +232,6 @@ Item { hifi.colors.blueAccent } else if (root.purchaseStatus === "invalidated") { hifi.colors.redAccent - } else if (root.displayedItemCount > 1) { - hifi.colors.lightGray } else { hifi.colors.baseGray } From 3742ccbdd3a8e08d429a7a8a59175c03e0f91a8b Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 28 Sep 2017 13:12:42 -0700 Subject: [PATCH 491/722] Fix keyboard focus during setup while tip is up --- .../hifi/commerce/wallet/PassphraseSelection.qml | 15 +++++++++++---- .../qml/hifi/commerce/wallet/WalletSetup.qml | 2 ++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml index e7ff8489d1..4f2ae0f9b2 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -26,6 +26,7 @@ Item { id: root; property bool isChangingPassphrase: false; property bool isShowingTip: false; + property bool shouldImmediatelyFocus: true; // This object is always used in a popup. // This MouseArea is used to prevent a user from being @@ -53,10 +54,8 @@ Item { // TODO: Fix this unlikely bug onVisibleChanged: { if (visible) { - if (root.isChangingPassphrase) { - currentPassphraseField.focus = true; - } else { - passphraseField.focus = true; + if (root.shouldImmediatelyFocus) { + focusFirstTextField(); } sendMessageToLightbox({method: 'disableHmdPreview'}); } else { @@ -320,5 +319,13 @@ Item { setErrorText(""); } + function focusFirstTextField() { + if (root.isChangingPassphrase) { + currentPassphraseField.focus = true; + } else { + passphraseField.focus = true; + } + } + signal sendMessageToLightbox(var msg); } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml index c2b20a37ef..b90dc925a6 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml @@ -422,6 +422,7 @@ Item { onClicked: { root.hasShownSecurityImageTip = true; securityImageTip.visible = false; + passphraseSelection.focusFirstTextField(); } } } @@ -466,6 +467,7 @@ Item { PassphraseSelection { id: passphraseSelection; + shouldImmediatelyFocus: root.hasShownSecurityImageTip; isShowingTip: securityImageTip.visible; anchors.top: passphraseTitleHelper.bottom; anchors.topMargin: 30; From 3a537cdcc32067673234604cceec410b5b9562b8 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 28 Sep 2017 13:21:55 -0700 Subject: [PATCH 492/722] Make usernameDropdown collapse when clicking anywhere outside it --- .../resources/qml/hifi/commerce/checkout/Checkout.qml | 7 +++++++ .../qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml | 1 + .../resources/qml/hifi/commerce/purchases/Purchases.qml | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 182a8df055..fe3e9fd78f 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -176,6 +176,13 @@ Rectangle { } } } + MouseArea { + enabled: titleBarContainer.usernameDropdownVisible; + anchors.fill: parent; + onClicked: { + titleBarContainer.usernameDropdownVisible = false; + } + } // // TITLE BAR END // diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index 5786350721..9e52b2c50a 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -27,6 +27,7 @@ Item { id: root; property string referrerURL: "https://metaverse.highfidelity.com/marketplace?"; readonly property int additionalDropdownHeight: usernameDropdown.height - myUsernameButton.anchors.bottomMargin; + property alias usernameDropdownVisible: usernameDropdown.visible; height: mainContainer.height + additionalDropdownHeight; diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 5e0c7b9e99..7983ab5211 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -179,6 +179,13 @@ Rectangle { } } } + MouseArea { + enabled: titleBarContainer.usernameDropdownVisible; + anchors.fill: parent; + onClicked: { + titleBarContainer.usernameDropdownVisible = false; + } + } // // TITLE BAR END // From 1947f2ba9980b2d77834d166ccd07657ad7bf32c Mon Sep 17 00:00:00 2001 From: druiz17 Date: Thu, 28 Sep 2017 13:23:28 -0700 Subject: [PATCH 493/722] remove update stylus --- .../controllerModules/tabletStylusInput.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/scripts/system/controllers/controllerModules/tabletStylusInput.js b/scripts/system/controllers/controllerModules/tabletStylusInput.js index 0a3b2b8adc..36ed7920dd 100644 --- a/scripts/system/controllers/controllerModules/tabletStylusInput.js +++ b/scripts/system/controllers/controllerModules/tabletStylusInput.js @@ -152,20 +152,6 @@ Script.include("/~/system/libraries/controllers.js"); } }; - this.updateStylus = function() { - if (this.stylus) { - var X_ROT_NEG_90 = { x: -0.70710678, y: 0, z: 0, w: 0.70710678 }; - var modelOrientation = Quat.multiply(this.stylusTip.orientation, X_ROT_NEG_90); - var modelPositionOffset = Vec3.multiplyQbyV(modelOrientation, { x: 0, y: 0, z: MyAvatar.sensorToWorldScale * -WEB_STYLUS_LENGTH / 2 }); - - var stylusProps = { - position: Vec3.sum(this.stylusTip.position, modelPositionOffset), - rotation: modelOrientation - }; - Overlays.editOverlay(this.stylus, stylusProps); - } - }; - this.showStylus = function() { if (this.stylus) { return; @@ -334,7 +320,6 @@ Script.include("/~/system/libraries/controllers.js"); if (this.isNearStylusTarget) { if (!this.useFingerInsteadOfStylus) { this.showStylus(); - this.updateStylus(); } else { this.pointFinger(true); } From 4023c4014957f9163543aaea9744a2ff513b7f57 Mon Sep 17 00:00:00 2001 From: vladest Date: Thu, 28 Sep 2017 22:55:01 +0200 Subject: [PATCH 494/722] Cleanup --- .../qml/LoginDialog/CompleteProfileBody.qml | 2 +- .../qml/LoginDialog/LinkAccountBody.qml | 2 - .../resources/qml/LoginDialog/SignUpBody.qml | 1 - .../qml/LoginDialog/UsernameCollisionBody.qml | 2 +- .../resources/qml/LoginDialog/WelcomeBody.qml | 2 +- .../qml/dialogs/TabletLoginDialog.qml | 48 ++----------------- 6 files changed, 7 insertions(+), 50 deletions(-) diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index e06ce239ab..2c6bc1082a 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -65,7 +65,7 @@ Item { text: qsTr("Cancel") - onClicked: root.destroy() + onClicked: root.tryDestroy() } } diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 26e90eb9a6..13462804c3 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -56,7 +56,6 @@ Item { parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)); parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : hifi.dimensions.contentSpacing.y); - console.log("sign in h:", targetHeight, parent.height, form.height) } } @@ -233,7 +232,6 @@ Item { } } - // Override ScrollingWindow's keyboard that would be at very bottom of dialog. Keyboard { raised: keyboardEnabled && keyboardRaised diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index aef20c7a61..c4056422dd 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -50,7 +50,6 @@ Item { parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)); parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : 0); - console.log("sign up h:", parent.height, targetHeight, form.height) } } diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index 18c831b3a9..b049d7f8bb 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -122,7 +122,7 @@ Item { text: qsTr("Cancel") - onClicked: root.destroy() + onClicked: root.tryDestroy() } } diff --git a/interface/resources/qml/LoginDialog/WelcomeBody.qml b/interface/resources/qml/LoginDialog/WelcomeBody.qml index eb91956532..551ec263b7 100644 --- a/interface/resources/qml/LoginDialog/WelcomeBody.qml +++ b/interface/resources/qml/LoginDialog/WelcomeBody.qml @@ -77,7 +77,7 @@ Item { text: qsTr("Close"); - onClicked: root.destroy() + onClicked: root.tryDestroy() } } diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index 78221ab6cb..6cf7394cf0 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -47,8 +47,10 @@ TabletModalWindow { } function tryDestroy() { + console.log("tryDestroy") canceled() } + Component.onDestruction: console.log("root dying") } //property int colorScheme: hifi.colorSchemes.dark @@ -81,7 +83,7 @@ TabletModalWindow { onCanceled: { if (bodyLoader.active === true) { - bodyLoader.active = false + //bodyLoader.active = false } if (gotoPreviousApp) { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); @@ -96,13 +98,12 @@ TabletModalWindow { id: mfRoot width: root.width - height: root.height //+ frameMarginTop + height: root.height + frameMarginTop + hifi.dimensions.contentMargin.x anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } - onHeightChanged: console.log("tablet mf h:", height) LoginDialog { id: loginDialog @@ -158,45 +159,4 @@ TabletModalWindow { break } } -// LoginDialog { -// id: loginDialog -// width: parent.width -// height: parent.height -// StackView { -// id: bodyLoader -// property var item: currentItem -// property var props -// property string source: "" - -// onCurrentItemChanged: { -// //cleanup source for future usage -// source = "" -// } - -// function setSource(src, props) { -// source = "../TabletLoginDialog/" + src -// bodyLoader.props = props -// } -// function popup() { -// bodyLoader.pop() - -// //check if last screen, if yes, dialog is popped out -// if (depth === 1) -// loginDialogRoot.canceled() -// } - -// anchors.fill: parent -// anchors.margins: 10 -// onSourceChanged: { -// if (source !== "") { -// bodyLoader.push(Qt.resolvedUrl(source), props) -// } -// } -// Component.onCompleted: { -// setSource(loginDialog.isSteamRunning() ? -// "SignInBody.qml" : -// "LinkAccountBody.qml") -// } -// } -// } } From 66be558a04c22a11f8f48eb06ecf0a65711b974b Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 28 Sep 2017 13:53:43 -0700 Subject: [PATCH 495/722] My Items Interface --- .../qml/hifi/commerce/checkout/Checkout.qml | 3 +- .../qml/hifi/commerce/purchases/Purchases.qml | 69 ++++++++++++++++--- scripts/system/marketplaces/marketplaces.js | 9 ++- 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index fe3e9fd78f..7859333b21 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -39,7 +39,7 @@ Rectangle { property bool itemIsJson: true; property bool shouldBuyWithControlledFailure: false; property bool debugCheckoutSuccess: false; - property bool canRezCertifiedItems: false; + property bool canRezCertifiedItems: Entities.canRezCertified || Entities.canRezTmpCertified; // Style color: hifi.colors.white; Hifi.QmlCommerce { @@ -829,7 +829,6 @@ Rectangle { if (itemHref.indexOf('.json') === -1) { root.itemIsJson = false; } - root.canRezCertifiedItems = message.canRezCertifiedItems; setBuyText(); break; default: diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 7983ab5211..ef6cfcbe6e 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -32,8 +32,9 @@ Rectangle { property bool securityImageResultReceived: false; property bool purchasesReceived: false; property bool punctuationMode: false; - property bool canRezCertifiedItems: false; + property bool canRezCertifiedItems: Entities.canRezCertified || Entities.canRezTmpCertified; property bool pendingInventoryReply: true; + property bool isShowingMyItems: false; // Style color: hifi.colors.white; Hifi.QmlCommerce { @@ -299,7 +300,7 @@ Rectangle { anchors.topMargin: 4; RalewayRegular { - id: myPurchasesText; + id: myText; anchors.top: parent.top; anchors.topMargin: 10; anchors.bottom: parent.bottom; @@ -307,7 +308,7 @@ Rectangle { anchors.left: parent.left; anchors.leftMargin: 4; width: paintedWidth; - text: "My Purchases"; + text: isShowingMyItems ? "My Items" : "My Purchases"; color: hifi.colors.baseGray; size: 28; } @@ -317,7 +318,7 @@ Rectangle { colorScheme: hifi.colorSchemes.faintGray; hasClearButton: true; hasRoundedBorder: true; - anchors.left: myPurchasesText.right; + anchors.left: myText.right; anchors.leftMargin: 16; anchors.top: parent.top; anchors.bottom: parent.bottom; @@ -421,7 +422,7 @@ Rectangle { ListView { id: purchasesContentsList; - visible: purchasesModel.count !== 0; + visible: (root.isShowingMyItems && filteredPurchasesModel.count !== 0) || (!root.isShowingMyItems && filteredPurchasesModel.count !== 0); clip: true; model: filteredPurchasesModel; // Anchors @@ -473,9 +474,55 @@ Rectangle { } } + Item { + id: noItemsAlertContainer; + visible: !purchasesContentsList.visible && root.purchasesReceived && root.isShowingMyItems && filterBar.text === ""; + anchors.top: filterBarContainer.bottom; + anchors.topMargin: 12; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: parent.width; + + // Explanitory text + RalewayRegular { + id: noItemsYet; + text: "You haven't submitted anything to the Marketplace yet!

    Submit an item to the Marketplace to add it to My Items."; + // Text size + size: 22; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 150; + anchors.left: parent.left; + anchors.leftMargin: 24; + anchors.right: parent.right; + anchors.rightMargin: 24; + height: paintedHeight; + // Style + color: hifi.colors.baseGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + } + + // "Go To Marketplace" button + HifiControlsUit.Button { + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: noItemsYet.bottom; + anchors.topMargin: 20; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width * 2 / 3; + height: 50; + text: "Visit Marketplace"; + onClicked: { + sendToScript({method: 'purchases_goToMarketplaceClicked'}); + } + } + } + Item { id: noPurchasesAlertContainer; - visible: !purchasesContentsList.visible && root.purchasesReceived; + visible: !purchasesContentsList.visible && root.purchasesReceived && !root.isShowingMyItems && filterBar.text === ""; anchors.top: filterBarContainer.bottom; anchors.topMargin: 12; anchors.left: parent.left; @@ -503,7 +550,7 @@ Rectangle { horizontalAlignment: Text.AlignHCenter; } - // "Set Up" button + // "Go To Marketplace" button HifiControlsUit.Button { color: hifi.buttons.blue; colorScheme: hifi.colorSchemes.dark; @@ -582,9 +629,9 @@ Rectangle { filteredPurchasesModel.clear(); for (var i = 0; i < purchasesModel.count; i++) { if (purchasesModel.get(i).title.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) { - if (purchasesModel.get(i).status !== "confirmed") { + if (purchasesModel.get(i).status !== "confirmed" && !root.isShowingMyItems) { filteredPurchasesModel.insert(0, purchasesModel.get(i)); - } else { + } else if ((root.isShowingMyItems && purchasesModel.get(i).edition_number === -1) || !root.isShowingMyItems) { filteredPurchasesModel.append(purchasesModel.get(i)); } } @@ -632,7 +679,6 @@ Rectangle { case 'updatePurchases': referrerURL = message.referrerURL; titleBarContainer.referrerURL = message.referrerURL; - root.canRezCertifiedItems = message.canRezCertifiedItems; filterBar.text = message.filterText ? message.filterText : ""; break; case 'purchases_getIsFirstUseResult': @@ -647,6 +693,9 @@ Rectangle { case 'inspectionCertificate_setItemInfo': inspectionCertificate.fromScript(message); break; + case 'purchases_showMyItems': + root.isShowingMyItems = true; + break; default: console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 73008ef4ce..7fc5e22554 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -337,7 +337,6 @@ tablet.loadQMLSource("TabletAddressDialog.qml"); break; case 'purchases_itemCertificateClicked': - console.log("ZRFJIOSE FJSOPIEFJSE OIFJSOPEI FJSIOEFJ ") setCertificateInfo("", message.itemMarketplaceId); break; case 'inspectionCertificate_closeClicked': @@ -347,7 +346,13 @@ tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); break; case 'header_myItemsClicked': - tablet.gotoWebScreen(MARKETPLACE_URL + '?view=mine', MARKETPLACES_INJECT_SCRIPT_URL); + referrerURL = MARKETPLACE_URL_INITIAL; + filterText = ""; + tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); + wireEventBridge(true); + tablet.sendToQml({ + method: 'purchases_showMyItems' + }); break; default: print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message)); From c112a3baf16f3be42fd940798ed29eaa4a762d6b Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 28 Sep 2017 15:09:49 -0700 Subject: [PATCH 496/722] Seting up of camera and avatar correctly in game loop, not in render loop --- interface/src/Application.cpp | 159 ++++++++++++++++++++++++---------- interface/src/Application.h | 3 + 2 files changed, 116 insertions(+), 46 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 596917da28..284cc5056a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2536,6 +2536,11 @@ void Application::paintGL() { glm::mat4 HMDSensorPose; glm::mat4 eyeToWorld; glm::mat4 sensorToWorld; + + bool isStereo; + glm::mat4 stereoEyeOffsets[2]; + glm::mat4 stereoEyeProjections[2]; + { QMutexLocker viewLocker(&_renderArgsMutex); renderArgs = _appRenderArgs._renderArgs; @@ -2543,6 +2548,11 @@ void Application::paintGL() { eyeToWorld = _appRenderArgs._eyeToWorld; sensorToWorld = _appRenderArgs._sensorToWorld; sensorToWorldScale = _appRenderArgs._sensorToWorldScale; + isStereo = _appRenderArgs._isStereo; + for_each_eye([&](Eye eye) { + stereoEyeOffsets[eye] = _appRenderArgs._eyeOffsets[eye]; + stereoEyeProjections[eye] = _appRenderArgs._eyeProjections[eye]; + }); } //float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); @@ -2615,57 +2625,65 @@ void Application::paintGL() { finalFramebuffer = framebufferCache->getFramebuffer(); } - auto hmdInterface = DependencyManager::get(); - float ipdScale = hmdInterface->getIPDScale(); + //auto hmdInterface = DependencyManager::get(); + //float ipdScale = hmdInterface->getIPDScale(); - // scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. - ipdScale *= sensorToWorldScale; + //// scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. + //ipdScale *= sensorToWorldScale; { - PROFILE_RANGE(render, "/mainRender"); - PerformanceTimer perfTimer("mainRender"); - // FIXME is this ever going to be different from the size previously set in the render args - // in the overlay render? - // Viewport is assigned to the size of the framebuffer - renderArgs._viewport = ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height()); - auto baseProjection = renderArgs.getViewFrustum().getProjection(); - if (displayPlugin->isStereo()) { - // Stereo modes will typically have a larger projection matrix overall, - // so we ask for the 'mono' projection matrix, which for stereo and HMD - // plugins will imply the combined projection for both eyes. - // - // This is properly implemented for the Oculus plugins, but for OpenVR - // and Stereo displays I'm not sure how to get / calculate it, so we're - // just relying on the left FOV in each case and hoping that the - // overall culling margin of error doesn't cause popping in the - // right eye. There are FIXMEs in the relevant plugins - _myCamera.setProjection(displayPlugin->getCullingProjection(baseProjection)); + //PROFILE_RANGE(render, "/mainRender"); + //PerformanceTimer perfTimer("mainRender"); + //// FIXME is this ever going to be different from the size previously set in the render args + //// in the overlay render? + //// Viewport is assigned to the size of the framebuffer + //renderArgs._viewport = ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height()); + //auto baseProjection = renderArgs.getViewFrustum().getProjection(); + //if (displayPlugin->isStereo()) { + // // Stereo modes will typically have a larger projection matrix overall, + // // so we ask for the 'mono' projection matrix, which for stereo and HMD + // // plugins will imply the combined projection for both eyes. + // // + // // This is properly implemented for the Oculus plugins, but for OpenVR + // // and Stereo displays I'm not sure how to get / calculate it, so we're + // // just relying on the left FOV in each case and hoping that the + // // overall culling margin of error doesn't cause popping in the + // // right eye. There are FIXMEs in the relevant plugins + // _myCamera.setProjection(displayPlugin->getCullingProjection(baseProjection)); + // renderArgs._context->enableStereo(true); + // mat4 eyeOffsets[2]; + // mat4 eyeProjections[2]; + + // // FIXME we probably don't need to set the projection matrix every frame, + // // only when the display plugin changes (or in non-HMD modes when the user + // // changes the FOV manually, which right now I don't think they can. + // for_each_eye([&](Eye eye) { + // // For providing the stereo eye views, the HMD head pose has already been + // // applied to the avatar, so we need to get the difference between the head + // // pose applied to the avatar and the per eye pose, and use THAT as + // // the per-eye stereo matrix adjustment. + // mat4 eyeToHead = displayPlugin->getEyeToHeadTransform(eye); + // // Grab the translation + // vec3 eyeOffset = glm::vec3(eyeToHead[3]); + // // Apply IPD scaling + // mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); + // eyeOffsets[eye] = eyeOffsetTransform; + // eyeProjections[eye] = displayPlugin->getEyeProjection(eye, baseProjection); + // }); + // renderArgs._context->setStereoProjections(eyeProjections); + // renderArgs._context->setStereoViews(eyeOffsets); + + // // Configure the type of display / stereo + // renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); + //} + + if (isStereo) { renderArgs._context->enableStereo(true); - mat4 eyeOffsets[2]; - mat4 eyeProjections[2]; - - // FIXME we probably don't need to set the projection matrix every frame, - // only when the display plugin changes (or in non-HMD modes when the user - // changes the FOV manually, which right now I don't think they can. - for_each_eye([&](Eye eye) { - // For providing the stereo eye views, the HMD head pose has already been - // applied to the avatar, so we need to get the difference between the head - // pose applied to the avatar and the per eye pose, and use THAT as - // the per-eye stereo matrix adjustment. - mat4 eyeToHead = displayPlugin->getEyeToHeadTransform(eye); - // Grab the translation - vec3 eyeOffset = glm::vec3(eyeToHead[3]); - // Apply IPD scaling - mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); - eyeOffsets[eye] = eyeOffsetTransform; - eyeProjections[eye] = displayPlugin->getEyeProjection(eye, baseProjection); - }); - renderArgs._context->setStereoProjections(eyeProjections); - renderArgs._context->setStereoViews(eyeOffsets); - - // Configure the type of display / stereo - renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); + renderArgs._context->setStereoProjections(stereoEyeProjections); + renderArgs._context->setStereoViews(stereoEyeOffsets); + // renderArgs._displayMode } + renderArgs._blitFramebuffer = finalFramebuffer; // displaySide(&renderArgs, _myCamera); runRenderFrame(&renderArgs); @@ -5320,7 +5338,55 @@ void Application::update(float deltaTime) { } this->updateCamera(appRenderArgs._renderArgs); + appRenderArgs._isStereo = false; + { + auto hmdInterface = DependencyManager::get(); + float ipdScale = hmdInterface->getIPDScale(); + + // scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. + ipdScale *= sensorToWorldScale; + + auto baseProjection = appRenderArgs._renderArgs.getViewFrustum().getProjection(); + if (getActiveDisplayPlugin()->isStereo()) { + // Stereo modes will typically have a larger projection matrix overall, + // so we ask for the 'mono' projection matrix, which for stereo and HMD + // plugins will imply the combined projection for both eyes. + // + // This is properly implemented for the Oculus plugins, but for OpenVR + // and Stereo displays I'm not sure how to get / calculate it, so we're + // just relying on the left FOV in each case and hoping that the + // overall culling margin of error doesn't cause popping in the + // right eye. There are FIXMEs in the relevant plugins + _myCamera.setProjection(getActiveDisplayPlugin()->getCullingProjection(baseProjection)); + appRenderArgs._isStereo = true; + + auto& eyeOffsets = appRenderArgs._eyeOffsets; + auto& eyeProjections = appRenderArgs._eyeProjections; + + // FIXME we probably don't need to set the projection matrix every frame, + // only when the display plugin changes (or in non-HMD modes when the user + // changes the FOV manually, which right now I don't think they can. + for_each_eye([&](Eye eye) { + // For providing the stereo eye views, the HMD head pose has already been + // applied to the avatar, so we need to get the difference between the head + // pose applied to the avatar and the per eye pose, and use THAT as + // the per-eye stereo matrix adjustment. + mat4 eyeToHead = getActiveDisplayPlugin()->getEyeToHeadTransform(eye); + // Grab the translation + vec3 eyeOffset = glm::vec3(eyeToHead[3]); + // Apply IPD scaling + mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); + eyeOffsets[eye] = eyeOffsetTransform; + eyeProjections[eye] = getActiveDisplayPlugin()->getEyeProjection(eye, baseProjection); + }); + //renderArgs._context->setStereoProjections(eyeProjections); + //renderArgs._context->setStereoViews(eyeOffsets); + + // Configure the type of display / stereo + appRenderArgs._renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); + } + } // HACK // load the view frustum @@ -5332,6 +5398,7 @@ void Application::update(float deltaTime) { QMutexLocker viewLocker(&_viewMutex); _myCamera.loadViewFrustum(_displayViewFrustum); } + { QMutexLocker viewLocker(&_viewMutex); appRenderArgs._renderArgs.setViewFrustum(_displayViewFrustum); diff --git a/interface/src/Application.h b/interface/src/Application.h index 377a31fb2c..3f4c55999e 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -626,9 +626,12 @@ private: struct AppRenderArgs { render::Args _renderArgs; glm::mat4 _eyeToWorld; + glm::mat4 _eyeOffsets[2]; + glm::mat4 _eyeProjections[2]; glm::mat4 _headPose; glm::mat4 _sensorToWorld; float _sensorToWorldScale { 1.0f }; + bool _isStereo{ false }; }; AppRenderArgs _appRenderArgs; From 2cff5c1fa6f3dae42e7b08a54e332e90769ffc8d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 28 Sep 2017 15:12:55 -0700 Subject: [PATCH 497/722] First time Purchases tutorial --- .../commerce/purchases/FirstUseTutorial.qml | 223 +++++++++--------- .../qml/hifi/commerce/purchases/Purchases.qml | 10 +- .../purchases/images/Purchase-First-Run-1.jpg | Bin 0 -> 73498 bytes .../purchases/images/Purchase-First-Run-2.jpg | Bin 0 -> 83399 bytes scripts/system/commerce/wallet.js | 1 + 5 files changed, 111 insertions(+), 123 deletions(-) create mode 100644 interface/resources/qml/hifi/commerce/purchases/images/Purchase-First-Run-1.jpg create mode 100644 interface/resources/qml/hifi/commerce/purchases/images/Purchase-First-Run-2.jpg diff --git a/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml b/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml index b5d7efe818..34800f1ec5 100644 --- a/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml +++ b/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml @@ -13,6 +13,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 +import QtGraphicalEffects 1.0 import QtQuick.Controls 1.4 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit @@ -24,40 +25,103 @@ Rectangle { HifiConstants { id: hifi; } id: root; - property string activeView: "step_1"; - // Style - color: hifi.colors.baseGray; + property int activeView: 1; + + Image { + anchors.fill: parent; + source: "images/Purchase-First-Run-" + root.activeView + ".jpg"; + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + } + + Item { + id: header; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: childrenRect.height; + + Image { + id: marketplaceHeaderImage; + source: "../common/images/marketplaceHeaderImage.png"; + anchors.top: parent.top; + anchors.topMargin: 2; + anchors.left: parent.left; + anchors.leftMargin: 8; + width: 140; + height: 58; + fillMode: Image.PreserveAspectFit; + visible: false; + } + ColorOverlay { + anchors.fill: marketplaceHeaderImage; + source: marketplaceHeaderImage; + color: "#FFFFFF" + } + RalewayRegular { + id: introText1; + text: "INTRODUCTION TO
    My Purchases"; + // Text size + size: 28; + // Anchors + anchors.top: marketplaceHeaderImage.bottom; + anchors.topMargin: -8; + anchors.left: parent.left; + anchors.leftMargin: 12; + anchors.right: parent.right; + height: paintedHeight; + // Style + color: hifi.colors.white; + } + } // // "STEP 1" START // Item { id: step_1; - visible: root.activeView === "step_1"; - anchors.top: parent.top; + visible: root.activeView === 1; + anchors.top: header.bottom; + anchors.topMargin: 100; anchors.left: parent.left; + anchors.leftMargin: 30; anchors.right: parent.right; - anchors.bottom: tutorialActionButtonsContainer.top; + anchors.bottom: parent.bottom; RalewayRegular { id: step1text; - text: "This is the first-time Purchases tutorial.

    Here is some bold text " + - "inside Step 1."; + text: "The 'REZ IT' button makes your purchase appear in front of you."; // Text size - size: 24; + size: 20; // Anchors anchors.top: parent.top; - anchors.bottom: parent.bottom; anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; + width: 180; + height: paintedHeight; // Style - color: hifi.colors.faintGray; + color: hifi.colors.white; wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + } + + // "Next" button + HifiControlsUit.Button { + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: step1text.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + width: 150; + height: 40; + text: "Next"; + onClicked: { + root.activeView++; + } } } // @@ -69,127 +133,52 @@ Rectangle { // Item { id: step_2; - visible: root.activeView === "step_2"; - anchors.top: parent.top; + visible: root.activeView === 2; + anchors.top: header.bottom; + anchors.topMargin: 45; anchors.left: parent.left; + anchors.leftMargin: 30; anchors.right: parent.right; - anchors.bottom: tutorialActionButtonsContainer.top; + anchors.bottom: parent.bottom; RalewayRegular { id: step2text; - text: "STEP TWOOO!!!"; + text: "If you rez an item twice, the first one will disappear."; // Text size - size: 24; + size: 20; // Anchors anchors.top: parent.top; - anchors.bottom: parent.bottom; anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; + width: 180; + height: paintedHeight; // Style - color: hifi.colors.faintGray; + color: hifi.colors.white; wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + } + + // "GOT IT" button + HifiControlsUit.Button { + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: step2text.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + width: 150; + height: 40; + text: "GOT IT"; + onClicked: { + sendSignalToParent({method: 'tutorial_finished'}); + } } } // // "STEP 2" END // - Item { - id: tutorialActionButtonsContainer; - // Size - width: root.width; - height: 70; - // Anchors - anchors.left: parent.left; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 24; - - // "Skip" or "Back" button - HifiControlsUit.Button { - id: skipOrBackButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.left: parent.left; - anchors.leftMargin: 20; - width: parent.width/2 - anchors.leftMargin*2; - text: root.activeView === "step_1" ? "Skip" : "Back"; - onClicked: { - if (root.activeView === "step_1") { - sendSignalToParent({method: 'tutorial_skipClicked'}); - } else { - root.activeView = "step_" + (parseInt(root.activeView.split("_")[1]) - 1); - } - } - } - - // "Next" or "Finish" button - HifiControlsUit.Button { - id: nextButton; - color: hifi.buttons.blue; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: parent.width/2 - anchors.rightMargin*2; - text: root.activeView === "step_2" ? "Finish" : "Next"; - onClicked: { - // If this is the final step... - if (root.activeView === "step_2") { - sendSignalToParent({method: 'tutorial_finished'}); - } else { - root.activeView = "step_" + (parseInt(root.activeView.split("_")[1]) + 1); - } - } - } - } - // // FUNCTION DEFINITIONS START // - // - // Function Name: fromScript() - // - // Relevant Variables: - // None - // - // Arguments: - // message: The message sent from the JavaScript, in this case the Marketplaces JavaScript. - // Messages are in format "{method, params}", like json-rpc. - // - // Description: - // Called when a message is received from a script. - // - function fromScript(message) { - switch (message.method) { - case 'updatePurchases': - referrerURL = message.referrerURL; - break; - case 'purchases_getIsFirstUseResult': - if (message.isFirstUseOfPurchases && root.activeView !== "firstUseTutorial") { - root.activeView = "firstUseTutorial"; - } else if (!message.isFirstUseOfPurchases && root.activeView === "initialize") { - root.activeView = "purchasesMain"; - commerce.inventory(); - } - break; - default: - console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); - } - } signal sendSignalToParent(var message); - // // FUNCTION DEFINITIONS END // diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index ef6cfcbe6e..abb5f19732 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -35,6 +35,7 @@ Rectangle { property bool canRezCertifiedItems: Entities.canRezCertified || Entities.canRezTmpCertified; property bool pendingInventoryReply: true; property bool isShowingMyItems: false; + property bool isDebuggingFirstUseTutorial: false; // Style color: hifi.colors.white; Hifi.QmlCommerce { @@ -250,12 +251,9 @@ Rectangle { FirstUseTutorial { id: firstUseTutorial; + z: 999; visible: root.activeView === "firstUseTutorial"; - anchors.top: titleBarContainer.bottom; - anchors.topMargin: -titleBarContainer.additionalDropdownHeight; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.right: parent.right; + anchors.fill: parent; Connections { onSendSignalToParent: { @@ -682,7 +680,7 @@ Rectangle { filterBar.text = message.filterText ? message.filterText : ""; break; case 'purchases_getIsFirstUseResult': - if (message.isFirstUseOfPurchases && root.activeView !== "firstUseTutorial") { + if ((message.isFirstUseOfPurchases || root.isDebuggingFirstUseTutorial) && root.activeView !== "firstUseTutorial") { root.activeView = "firstUseTutorial"; } else if (!message.isFirstUseOfPurchases && root.activeView === "initialize") { root.activeView = "purchasesMain"; diff --git a/interface/resources/qml/hifi/commerce/purchases/images/Purchase-First-Run-1.jpg b/interface/resources/qml/hifi/commerce/purchases/images/Purchase-First-Run-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ce0d87363f225fd7805782b53dc2ce54523068fe GIT binary patch literal 73498 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 z\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 literal 0 HcmV?d00001 diff --git a/interface/resources/qml/hifi/commerce/purchases/images/Purchase-First-Run-2.jpg b/interface/resources/qml/hifi/commerce/purchases/images/Purchase-First-Run-2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..40bed974aff24cf603d04601ff5e84cdc09dc02d GIT binary patch literal 83399 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 Date: Thu, 28 Sep 2017 12:54:26 -0700 Subject: [PATCH 498/722] support animation of model overlays --- interface/src/ui/overlays/ModelOverlay.cpp | 210 +++++++++++++++++++++ interface/src/ui/overlays/ModelOverlay.h | 31 +++ 2 files changed, 241 insertions(+) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index ca5ca54144..713115bffc 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -9,6 +9,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include +#include + #include "ModelOverlay.h" #include @@ -60,6 +63,15 @@ void ModelOverlay::update(float deltatime) { _model->simulate(deltatime); } _isLoaded = _model->isActive(); + + + if (isAnimatingSomething()) { + if (!jointsMapped()) { + mapAnimationJoints(_model->getJointNames()); + } + animate(); + } + } bool ModelOverlay::addToScene(Overlay::Pointer overlay, const render::ScenePointer& scene, render::Transaction& transaction) { @@ -172,6 +184,51 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { } _updateModel = true; } + + auto animationSettings = properties["animationSettings"]; + if (animationSettings.canConvert(QVariant::Map)) { + QVariantMap animationSettingsMap = animationSettings.toMap(); + + auto animationURL = animationSettingsMap["url"]; + auto animationFPS = animationSettingsMap["fps"]; + auto animationCurrentFrame = animationSettingsMap["currentFrame"]; + auto animationFirstFrame = animationSettingsMap["firstFrame"]; + auto animationLastFrame = animationSettingsMap["lastFrame"]; + auto animationRunning = animationSettingsMap["running"]; + auto animationLoop = animationSettingsMap["loop"]; + auto animationHold = animationSettingsMap["hold"]; + auto animationAllowTranslation = animationSettingsMap["allowTranslation"]; + + if (animationURL.canConvert(QVariant::Url)) { + _animationURL = animationURL.toUrl(); + } + if (animationFPS.isValid()) { + _animationFPS = animationFPS.toFloat(); + } + if (animationCurrentFrame.isValid()) { + _animationCurrentFrame = animationCurrentFrame.toFloat(); + } + if (animationFirstFrame.isValid()) { + _animationFirstFrame = animationFirstFrame.toFloat(); + } + if (animationLastFrame.isValid()) { + _animationLastFrame = animationLastFrame.toFloat(); + } + + if (animationRunning.canConvert(QVariant::Bool)) { + _animationRunning = animationRunning.toBool(); + } + if (animationLoop.canConvert(QVariant::Bool)) { + _animationLoop = animationLoop.toBool(); + } + if (animationHold.canConvert(QVariant::Bool)) { + _animationHold = animationHold.toBool(); + } + if (animationAllowTranslation.canConvert(QVariant::Bool)) { + _animationAllowTranslation = animationAllowTranslation.toBool(); + } + + } } template @@ -259,6 +316,24 @@ QVariant ModelOverlay::getProperty(const QString& property) { }); } + // animation properties + if (property == "animationSettings") { + QVariantMap animationSettingsMap; + + animationSettingsMap["url"] = _animationURL; + animationSettingsMap["fps"] = _animationFPS; + animationSettingsMap["currentFrame"] = _animationCurrentFrame; + animationSettingsMap["firstFrame"] = _animationFirstFrame; + animationSettingsMap["lastFrame"] = _animationLastFrame; + animationSettingsMap["running"] = _animationRunning; + animationSettingsMap["loop"] = _animationLoop; + animationSettingsMap["hold"]= _animationHold; + animationSettingsMap["allowTranslation"] = _animationAllowTranslation; + + return animationSettingsMap; + } + + return Volume3DOverlay::getProperty(property); } @@ -301,3 +376,138 @@ QString ModelOverlay::getName() const { } return QString("Overlay:") + getType() + ":" + _url.toString(); } + + +void ModelOverlay::animate() { + + if (!_animation || !_animation->isLoaded() || !_model || !_model->isLoaded()) { + return; + } + + + QVector jointsData; + + const QVector& frames = _animation->getFramesReference(); // NOTE: getFrames() is too heavy + int frameCount = frames.size(); + if (frameCount <= 0) { + return; + } + + if (!_lastAnimated) { + _lastAnimated = usecTimestampNow(); + return; + } + + auto now = usecTimestampNow(); + auto interval = now - _lastAnimated; + _lastAnimated = now; + float deltaTime = (float)interval / (float)USECS_PER_SECOND; + _animationCurrentFrame += (deltaTime * _animationFPS); + + { + int animationCurrentFrame = (int)(glm::floor(_animationCurrentFrame)) % frameCount; + if (animationCurrentFrame < 0 || animationCurrentFrame > frameCount) { + animationCurrentFrame = 0; + } + + if (animationCurrentFrame == _lastKnownCurrentFrame) { + return; + } + _lastKnownCurrentFrame = animationCurrentFrame; + } + + if (_jointMapping.size() != _model->getJointStateCount()) { + return; + } + + QStringList animationJointNames = _animation->getGeometry().getJointNames(); + auto& fbxJoints = _animation->getGeometry().joints; + + auto& originalFbxJoints = _model->getFBXGeometry().joints; + auto& originalFbxIndices = _model->getFBXGeometry().jointIndices; + + const QVector& rotations = frames[_lastKnownCurrentFrame].rotations; + const QVector& translations = frames[_lastKnownCurrentFrame].translations; + + jointsData.resize(_jointMapping.size()); + for (int j = 0; j < _jointMapping.size(); j++) { + int index = _jointMapping[j]; + + if (index >= 0) { + glm::mat4 translationMat; + + if (_animationAllowTranslation) { + if (index < translations.size()) { + translationMat = glm::translate(translations[index]); + } + } + else if (index < animationJointNames.size()) { + QString jointName = fbxJoints[index].name; + + if (originalFbxIndices.contains(jointName)) { + // Making sure the joint names exist in the original model the animation is trying to apply onto. If they do, then remap and get it's translation. + int remappedIndex = originalFbxIndices[jointName] - 1; // JointIndeces seem to always start from 1 and the found index is always 1 higher than actual. + translationMat = glm::translate(originalFbxJoints[remappedIndex].translation); + } + } + glm::mat4 rotationMat; + if (index < rotations.size()) { + rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * rotations[index] * fbxJoints[index].postRotation); + } + else { + rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * fbxJoints[index].postRotation); + } + + glm::mat4 finalMat = (translationMat * fbxJoints[index].preTransform * + rotationMat * fbxJoints[index].postTransform); + auto& jointData = jointsData[j]; + jointData.translation = extractTranslation(finalMat); + jointData.translationSet = true; + jointData.rotation = glmExtractRotation(finalMat); + jointData.rotationSet = true; + } + } + // Set the data in the model + copyAnimationJointDataToModel(jointsData); +} + + +void ModelOverlay::mapAnimationJoints(const QStringList& modelJointNames) { + + // if we don't have animation, or we're already joint mapped then bail early + if (!hasAnimation() || jointsMapped()) { + return; + } + + if (!_animation || _animation->getURL() != _animationURL) { + _animation = DependencyManager::get()->getAnimation(_animationURL); + } + + if (_animation && _animation->isLoaded()) { + QStringList animationJointNames = _animation->getJointNames(); + + if (modelJointNames.size() > 0 && animationJointNames.size() > 0) { + _jointMapping.resize(modelJointNames.size()); + for (int i = 0; i < modelJointNames.size(); i++) { + _jointMapping[i] = animationJointNames.indexOf(modelJointNames[i]); + } + _jointMappingCompleted = true; + _jointMappingURL = _animationURL; + } + } +} + +void ModelOverlay::copyAnimationJointDataToModel(QVector jointsData) { + if (!_model || !_model->isLoaded()) { + return; + } + + // relay any inbound joint changes from scripts/animation/network to the model/rig + for (int index = 0; index < jointsData.size(); ++index) { + auto& jointData = jointsData[index]; + _model->setJointRotation(index, true, jointData.rotation, 1.0f); + _model->setJointTranslation(index, true, jointData.translation, 1.0f); + } + _updateModel = true; +} + diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index 8d8429b29e..edee4f7ac6 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -13,6 +13,7 @@ #define hifi_ModelOverlay_h #include +#include #include "Volume3DOverlay.h" @@ -45,6 +46,9 @@ public: float getLoadPriority() const { return _loadPriority; } + bool hasAnimation() const { return !_animationURL.isEmpty(); } + bool jointsMapped() const { return _jointMappingURL == _animationURL && _jointMappingCompleted; } + protected: Transform evalRenderTransform() override; @@ -53,6 +57,14 @@ protected: template vectorType mapJoints(mapFunction function) const; + void animate(); + void mapAnimationJoints(const QStringList& modelJointNames); + bool isAnimatingSomething() const { + return !_animationURL.isEmpty() && _animationRunning && _animationFPS != 0.0f; + } + void copyAnimationJointDataToModel(QVector jointsData); + + private: ModelPointer _model; @@ -62,6 +74,25 @@ private: bool _updateModel = { false }; bool _scaleToFit = { false }; float _loadPriority { 0.0f }; + + AnimationPointer _animation; + + QUrl _animationURL; + float _animationFPS { 0.0f }; + float _animationCurrentFrame { 0.0f }; + bool _animationRunning { false }; + bool _animationLoop { false }; + float _animationFirstFrame { 0.0f }; + float _animationLastFrame = { 0.0f }; + bool _animationHold { false }; + bool _animationAllowTranslation { false }; + uint64_t _lastAnimated { 0 }; + int _lastKnownCurrentFrame { -1 }; + + QUrl _jointMappingURL; + bool _jointMappingCompleted { false }; + QVector _jointMapping; // domain is index into model-joints, range is index into animation-joints + }; #endif // hifi_ModelOverlay_h From cabd68a63a72fddf31fd0175ba8f282e6dba5723 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 28 Sep 2017 16:03:09 -0700 Subject: [PATCH 499/722] Every time a dialog opened in edit.js a new callback is created, which wasn't disconnected after the event happened, this caused lots of entities to be created after each next import. --- scripts/system/edit.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 5d29d8103b..346f3626b9 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1474,6 +1474,8 @@ function onFileSaveChanged(filename) { } function onFileOpenChanged(filename) { + // disconnect the event, otherwise the requests will stack up + Window.openFileChanged.disconnect(onFileOpenChanged); var importURL = null; if (filename !== "") { importURL = "file:///" + filename; From 0f66fb41fd3ff2b58c4fcdef8f5145830559e3bf Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 28 Sep 2017 16:07:15 -0700 Subject: [PATCH 500/722] Fix entity add after incomplete rezCertified implementation --- libraries/entities/src/EntityTree.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 5c5aee97ff..c8675bdcba 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1148,11 +1148,8 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } else if (!senderNode->getCanRez() && !senderNode->getCanRezTmp()) { failedAdd = true; qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID() - << "] attempted to add an entity ID:" << entityItemID; - // FIXME after Cert ID property integrated - } else if (/*!properties.getCertificateID().isNull() && */!senderNode->getCanRezCertified() && !senderNode->getCanRezTmpCertified()) { - qCDebug(entities) << "User without 'certified rez rights' [" << senderNode->getUUID() - << "] attempted to add a certified entity with ID:" << entityItemID; + << "] attempted to add an entity ID:" << entityItemID; + } else { // this is a new entity... assign a new entityID properties.setCreated(properties.getLastEdited()); From 45b8bfdb1f7522fb3128ea991bbf8edd0676e8c6 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Thu, 28 Sep 2017 16:17:39 -0700 Subject: [PATCH 501/722] disbale grab.js in HMD --- scripts/system/controllers/grab.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index 2844940d2b..0e9b8569ae 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -306,7 +306,7 @@ Grabber.prototype.computeNewGrabPlane = function() { }; Grabber.prototype.pressEvent = function(event) { - if (isInEditMode()) { + if (isInEditMode() || HMD.active) { return; } @@ -401,7 +401,7 @@ Grabber.prototype.pressEvent = function(event) { }; Grabber.prototype.releaseEvent = function(event) { - if (event.isLeftButton!==true || event.isRightButton===true || event.isMiddleButton===true) { + if ((event.isLeftButton!==true || event.isRightButton===true || event.isMiddleButton===true) && !HMD.active) { return; } @@ -447,7 +447,7 @@ Grabber.prototype.moveEvent = function(event) { // during the handling of the event, do as little as possible. We save the updated mouse position, // and start a timer to react to the change. If more changes arrive before the timer fires, only // the last update will be considered. This is done to avoid backing-up Qt's event queue. - if (!this.isGrabbing) { + if (!this.isGrabbing || HMD.active) { return; } mouse.updateDrag(event); @@ -458,7 +458,7 @@ Grabber.prototype.moveEventProcess = function() { this.moveEventTimer = null; // see if something added/restored gravity var entityProperties = Entities.getEntityProperties(this.entityID); - if (!entityProperties || !entityProperties.gravity) { + if (!entityProperties || !entityProperties.gravity || HMD.active) { return; } From d8e2cbf871fd1b10a7507b979e832f65e13f8c44 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 28 Sep 2017 16:20:09 -0700 Subject: [PATCH 502/722] Oculus: Bug fix for head offset on large/small scaled avatars. --- plugins/oculus/src/OculusControllerManager.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 6f7be26554..d0c717bd20 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -334,10 +334,8 @@ void OculusControllerManager::TouchDevice::handleHeadPose(float deltaTime, glm::mat4 defaultHeadOffset = glm::inverse(inputCalibrationData.defaultCenterEyeMat) * inputCalibrationData.defaultHeadMat; - controller::Pose hmdHeadPose = pose.transform(sensorToAvatar); - pose.valid = true; - _poseStateMap[controller::HEAD] = hmdHeadPose.postTransform(defaultHeadOffset); + _poseStateMap[controller::HEAD] = pose.postTransform(defaultHeadOffset).transform(sensorToAvatar); } void OculusControllerManager::TouchDevice::handleRotationForUntrackedHand(const controller::InputCalibrationData& inputCalibrationData, From a1ae13489ed850b2774a19be2f8faea48894b32d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 28 Sep 2017 16:35:16 -0700 Subject: [PATCH 503/722] Slight changes to tutorial --- .../commerce/purchases/FirstUseTutorial.qml | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml b/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml index 34800f1ec5..2e8ad6db65 100644 --- a/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml +++ b/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml @@ -66,12 +66,26 @@ Rectangle { } RalewayRegular { id: introText1; - text: "INTRODUCTION TO
    My Purchases"; + text: "INTRODUCTION TO"; + // Text size + size: 15; + // Anchors + anchors.top: marketplaceHeaderImage.bottom; + anchors.topMargin: 8; + anchors.left: parent.left; + anchors.leftMargin: 12; + anchors.right: parent.right; + height: paintedHeight; + // Style + color: hifi.colors.white; + } + RalewayRegular { + id: introText2; + text: "My Purchases"; // Text size size: 28; // Anchors - anchors.top: marketplaceHeaderImage.bottom; - anchors.topMargin: -8; + anchors.top: introText1.bottom; anchors.left: parent.left; anchors.leftMargin: 12; anchors.right: parent.right; @@ -123,6 +137,22 @@ Rectangle { root.activeView++; } } + + // "SKIP" button + HifiControlsUit.Button { + color: hifi.buttons.noneBorderlessGray; + colorScheme: hifi.colorSchemes.dark; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 32; + anchors.right: parent.right; + anchors.rightMargin: 16; + width: 150; + height: 40; + text: "SKIP"; + onClicked: { + sendSignalToParent({method: 'tutorial_finished'}); + } + } } // // "STEP 1" END From d1350a03c2bf81cab25c3c44eb138239df25296c Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 28 Sep 2017 17:06:07 -0700 Subject: [PATCH 504/722] working checkpoint, but with a lot of debug/development stuff --- libraries/entities/src/EntityItem.cpp | 100 +++++++++++++++++- libraries/entities/src/EntityItem.h | 4 +- .../controllerModules/hudOverlayPointer.js | 2 +- 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 5a205742ae..5b9d10a759 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1569,15 +1569,45 @@ float EntityItem::getRadius() const { } // Checking Certifiable Properties -QString EntityItem::getStaticCertificateJSON() const { +#include // fixme +#include +#include +#include +#include // fixme +#define ADD_STRING_PROPERTY(n, N) if (!propertySet.get##N().isEmpty()) json[#n] = propertySet.get##N() +#define ADD_ENUM_PROPERTY(n, N) json[#n] = propertySet.get##N##AsString() +#define ADD_INT_PROPERTY(n, N) if (propertySet.get##N() != 0) json[#n] = (propertySet.get##N() == -1) ? -1.0 : ((double) propertySet.get##N()) +QByteArray EntityItem::getStaticCertificateJSON() const { // Produce a compact json of every non-default static certificate property, with the property names in alphabetical order. // The static certificate properties include all an only those properties that cannot be changed without altering the identity // of the entity as reviewed during the certification submission. - return "FIXME"; + + QJsonObject json; + EntityItemProperties propertySet = getProperties(); // Note: neither EntityItem nor EntityitemProperties "properties" are QObject "properties"! + // It is important that this be reproducible in the same order each time. Since we also generate these on the server, we do it alphabetically + // to help maintainence in two different code bases. + // animation + ADD_STRING_PROPERTY(collisionSoundURL, CollisionSoundURL); + ADD_STRING_PROPERTY(compoundShapeURL, CompoundShapeURL); + ADD_INT_PROPERTY(editionNumber, EditionNumber); + ADD_INT_PROPERTY(entityInstanceNumber, EntityInstanceNumber); + ADD_STRING_PROPERTY(itemArtist, ItemArtist); + ADD_STRING_PROPERTY(itemCategories, ItemCategories); + ADD_STRING_PROPERTY(itemDescription, ItemDescription); + ADD_STRING_PROPERTY(itemLicense, ItemLicense); + ADD_STRING_PROPERTY(itemName, ItemName); + ADD_INT_PROPERTY(limitedRun, LimitedRun); + ADD_STRING_PROPERTY(marketplaceID, MarketplaceID); + ADD_STRING_PROPERTY(modelURL, ModelURL); + ADD_STRING_PROPERTY(script, Script); + ADD_ENUM_PROPERTY(shapeType, ShapeType); + json["type"] = EntityTypes::getEntityTypeName(propertySet.getType()); + + return QJsonDocument(json).toJson(QJsonDocument::Compact); } -QString EntityItem::getStaticCertificateHash() const { +QByteArray EntityItem::getStaticCertificateHash() const { // The base64 encoded, sha224 hash of static certificate json. - return "FIXME"; + return QCryptographicHash::hash(getStaticCertificateJSON(), QCryptographicHash::Sha256); } bool EntityItem::verifyStaticCertificateProperties() const { // True IIF a non-empty certificateID matches the static certificate json. @@ -1585,7 +1615,67 @@ bool EntityItem::verifyStaticCertificateProperties() const { if (_certificateID.isEmpty()) { return false; } - return false; // fixme + // FIXME: really verify(hifi-pub-key, certificateID-as-signature-for-getStaticCertifcateHash) + + //const char text[] = "{\"collisionSoundURL\":\"colSound02\",\"compoundShapeURL\":\"http://mpassets.highfidelity.com/31479af7-94b0-45f2-84ba-478d27e5af90-v1/gnome_phys.obj\",\"entityInstanceNumber\":2,\"itemName\":\"Explosive Garden Nomex\",\"limitedRun\":-1,\"marketplaceID\":\"31479af7-94b0-45f2-84ba-478d27e5af90\",\"modelURL\":\"http://mpassets.highfidelity.com/31479af7-94b0-45f2-84ba-478d27e5af90-v1/gnome_green.fbx\",\"script\":\"http://mpassets.highfidelity.com/31479af7-94b0-45f2-84ba-478d27e5af90-v1/explodingGnomeEntity.js\",\"shapeType\":\"compound\",\"type\":\"Model\"}"; + // auto textLength = sizeof(text); + auto hash = getStaticCertificateHash(); + const char* text = hash.constData(); + auto textLength = hash.length(); + qDebug() << "HRS FIXME text" << getStaticCertificateJSON() << "hash base64" << hash.toBase64(); + + //const char signatureBase64[] = "QpRnN7XGeVFnF/a+FVjZDWhdbHM3P5Cu69rL0/X2DMnqQEGwhx/oBs/7guTs6aNuO+ahmbTTc0+Nqdcqv36KGA=="; + //auto signatureBytes = QByteArray::fromBase64(signatureBase64); + //const char* signature = signatureBytes.constData(); + //auto signatureLength = signatureBytes.length(); + + const char key[] = "-----BEGIN RSA PRIVATE KEY-----\n\ +MIIBOQIBAAJBALCoBiDAZOClO26tC5pd7JikBL61WIgpAqbcNnrV/TcG6LPI7Zbi\n\ +MjdUixmTNvYMRZH3Wlqtl2IKG1W68y3stKECAwEAAQJABvOlwhYwIhL+gr12jm2R\n\ +yPPzZ9nVEQ6kFxLlZfIT09119fd6OU1X5d4sHWfMfSIEgjwQIDS3ZU1kY3XKo87X\n\ +zQIhAOPHlYa1OC7BLhaTouy68qIU2vCKLP8mt4S31/TT0UOnAiEAxor6gU6yupTQ\n\ +yuyV3yHvr5LkZKBGqhjmOTmDfgtX7ncCIChGbgX3nQuHVOLhD/nTxHssPNozVGl5\n\ +KxHof+LmYSYZAiB4U+yEh9SsXdq40W/3fpLMPuNq1PRezJ5jGidGMcvF+wIgUNec\n\ +3Kg2U+CVZr8/bDT/vXRrsKj1zfobYuvbfVH02QY=\n\ +-----END RSA PRIVATE KEY-----"; + BIO* vbio = BIO_new(BIO_s_mem()); + int vlen = BIO_write(vbio, key, sizeof(key)); + RSA* vrsa = PEM_read_bio_RSAPrivateKey(vbio, NULL, NULL, NULL); + qDebug() << "HRS FIXME private key bufio" << !!vbio << vlen << !!vrsa << key; + + QByteArray signature(RSA_size(vrsa), 0); + unsigned int signatureLength = 0; + int signOK = RSA_sign(NID_sha256, reinterpret_cast(text), textLength, reinterpret_cast(signature.data()), &signatureLength, vrsa); + QByteArray signature64 = signature.toBase64(); + qDebug() << "HRS FIXME signature" << signature64.length() << signature64 << "ok:" << signOK; + + ///* + const char publicKey[] = "-----BEGIN PUBLIC KEY-----\n\ +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALCoBiDAZOClO26tC5pd7JikBL61WIgp\n\ +AqbcNnrV/TcG6LPI7ZbiMjdUixmTNvYMRZH3Wlqtl2IKG1W68y3stKECAwEAAQ==\n\ +-----END PUBLIC KEY-----"; + //*/ + // const char publicKey[] = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALCoBiDAZOClO26tC5pd7JikBL61WIgpAqbcNnrV/TcG6LPI7ZbiMjdUixmTNvYMRZH3Wlqtl2IKG1W68y3stKECAwEAAQ=="; + //const unsigned char* publicKeyData = reinterpret_cast(publicKey); + //RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, sizeof(publicKey)); + qDebug() << "HRS FIXME key:" << sizeof(publicKey) << QString(publicKey) << "text:" << textLength << QString(text) << "signature:" << signatureLength << QString(signature); + + + //BIO *bio = BIO_new_mem_buf((void*)publicKey, sizeof(publicKey)); + BIO* bio = BIO_new(BIO_s_mem()); + int len = BIO_write(bio, publicKey, sizeof(publicKey)); + EVP_PKEY* evp_key = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); + qDebug() << "HRS FIXME bufio" << !!bio << len << !!evp_key; + //RSA *rsa; + //PEM_read_bio_RSA_PUBKEY(bio, &rsa, 0, NULL); + RSA* rsa = EVP_PKEY_get1_RSA(evp_key); + //RSA* rsa = PEM_read_bio_RSAPublicKey(bio, NULL, NULL, NULL); + qDebug() << "HRS FIXME rsa" << !!rsa; + + bool answer = RSA_verify(NID_sha256, reinterpret_cast(text), textLength, reinterpret_cast(signature.constData()), signatureLength, rsa); + qDebug() << "HRS FIXME key:" << sizeof(publicKey) << QString(publicKey) << "text:" << textLength << QString(text) << "signature:" << signatureLength << QString(signature) << "verified:" << answer; + //return _certificateID == getStaticCertificateHash(); + return answer; } diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index a6153c0234..b92f6120f0 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -324,8 +324,8 @@ public: void setEntityInstanceNumber(const quint32&); QString getCertificateID() const; void setCertificateID(const QString& value); - QString getStaticCertificateJSON() const; - QString getStaticCertificateHash() const; + QByteArray getStaticCertificateJSON() const; + QByteArray getStaticCertificateHash() const; bool verifyStaticCertificateProperties() const; // TODO: get rid of users of getRadius()... diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js index 487e491201..b953ddf002 100644 --- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js +++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js @@ -239,7 +239,7 @@ function cleanup() { ControllerDispatcherUtils.disableDispatcherModule("LeftHudOverlayPointer"); - ControllerDispatcherUtils.disbaleDispatcherModule("RightHudOverlayPointer"); + ControllerDispatcherUtils.disableDispatcherModule("RightHudOverlayPointer"); } Script.scriptEnding.connect(cleanup); From e637842f8afbf126c6fb1113d1c1976551e2210c Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 28 Sep 2017 17:45:50 -0700 Subject: [PATCH 505/722] Separating some of the code sepecific to render in its own cpp --- interface/src/Application_render.cpp | 320 +++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 interface/src/Application_render.cpp diff --git a/interface/src/Application_render.cpp b/interface/src/Application_render.cpp new file mode 100644 index 0000000000..ee3267284a --- /dev/null +++ b/interface/src/Application_render.cpp @@ -0,0 +1,320 @@ +// +// Application_render.cpp +// interface/src +// +// 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 +// + +#ifdef tryingSOmething + +void Application::paintGL() { + // Some plugins process message events, allowing paintGL to be called reentrantly. + if (_aboutToQuit || _window->isMinimized()) { + return; + } + + _frameCount++; + _lastTimeRendered.start(); + + auto lastPaintBegin = usecTimestampNow(); + PROFILE_RANGE_EX(render, __FUNCTION__, 0xff0000ff, (uint64_t)_frameCount); + PerformanceTimer perfTimer("paintGL"); + + if (nullptr == _displayPlugin) { + return; + } + + DisplayPluginPointer displayPlugin; + { + PROFILE_RANGE(render, "/getActiveDisplayPlugin"); + displayPlugin = getActiveDisplayPlugin(); + } + + { + PROFILE_RANGE(render, "/pluginBeginFrameRender"); + // If a display plugin loses it's underlying support, it + // needs to be able to signal us to not use it + if (!displayPlugin->beginFrameRender(_frameCount)) { + updateDisplayMode(); + return; + } + } + + // update the avatar with a fresh HMD pose + // { + // PROFILE_RANGE(render, "/updateAvatar"); + // getMyAvatar()->updateFromHMDSensorMatrix(getHMDSensorPose()); + // } + + // auto lodManager = DependencyManager::get(); + + RenderArgs renderArgs; + float sensorToWorldScale; + glm::mat4 HMDSensorPose; + { + QMutexLocker viewLocker(&_renderArgsMutex); + renderArgs = _appRenderArgs._renderArgs; + HMDSensorPose = _appRenderArgs._eyeToWorld; + sensorToWorldScale = _appRenderArgs._sensorToWorldScale; + } + /* + float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); + { + PROFILE_RANGE(render, "/buildFrustrumAndArgs"); + { + QMutexLocker viewLocker(&_viewMutex); + // adjust near clip plane to account for sensor scaling. + auto adjustedProjection = glm::perspective(_viewFrustum.getFieldOfView(), + _viewFrustum.getAspectRatio(), + DEFAULT_NEAR_CLIP * sensorToWorldScale, + _viewFrustum.getFarClip()); + _viewFrustum.setProjection(adjustedProjection); + _viewFrustum.calculate(); + } + renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(), + lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, + RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); + { + QMutexLocker viewLocker(&_viewMutex); + renderArgs.setViewFrustum(_viewFrustum); + } + } + */ + { + PROFILE_RANGE(render, "/resizeGL"); + PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::paintGL()"); + resizeGL(); + } + + { + PROFILE_RANGE(render, "/gpuContextReset"); + // _gpuContext->beginFrame(getHMDSensorPose()); + _gpuContext->beginFrame(HMDSensorPose); + // Reset the gpu::Context Stages + // Back to the default framebuffer; + gpu::doInBatch(_gpuContext, [&](gpu::Batch& batch) { + batch.resetStages(); + }); + } + + + { + PROFILE_RANGE(render, "/renderOverlay"); + PerformanceTimer perfTimer("renderOverlay"); + // NOTE: There is no batch associated with this renderArgs + // the ApplicationOverlay class assumes it's viewport is setup to be the device size + QSize size = getDeviceSize(); + renderArgs._viewport = glm::ivec4(0, 0, size.width(), size.height()); + _applicationOverlay.renderOverlay(&renderArgs); + } + + // updateCamera(renderArgs); + + /* glm::vec3 boomOffset; + { + PROFILE_RANGE(render, "/updateCamera"); + { + PerformanceTimer perfTimer("CameraUpdates"); + + auto myAvatar = getMyAvatar(); + boomOffset = myAvatar->getModelScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; + + // The render mode is default or mirror if the camera is in mirror mode, assigned further below + renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; + + // Always use the default eye position, not the actual head eye position. + // Using the latter will cause the camera to wobble with idle animations, + // or with changes from the face tracker + if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { + if (isHMDMode()) { + mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); + _myCamera.setPosition(extractTranslation(camMat)); + _myCamera.setOrientation(glmExtractRotation(camMat)); + } else { + _myCamera.setPosition(myAvatar->getDefaultEyePosition()); + _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); + } + } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { + if (isHMDMode()) { + auto hmdWorldMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); + _myCamera.setOrientation(glm::normalize(glmExtractRotation(hmdWorldMat))); + _myCamera.setPosition(extractTranslation(hmdWorldMat) + + myAvatar->getOrientation() * boomOffset); + } else { + _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); + if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + _myCamera.getOrientation() * boomOffset); + } else { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + myAvatar->getOrientation() * boomOffset); + } + } + } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { + if (isHMDMode()) { + auto mirrorBodyOrientation = myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)); + + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD yaw and roll + glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); + mirrorHmdEulers.y = -mirrorHmdEulers.y; + mirrorHmdEulers.z = -mirrorHmdEulers.z; + glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); + + glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; + + _myCamera.setOrientation(worldMirrorRotation); + + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD lateral offsets + hmdOffset.x = -hmdOffset.x; + + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) + + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + + mirrorBodyOrientation * hmdOffset); + } else { + _myCamera.setOrientation(myAvatar->getOrientation() + * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) + + (myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * + glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); + } + renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; + } else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { + EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); + if (cameraEntity != nullptr) { + if (isHMDMode()) { + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + _myCamera.setOrientation(cameraEntity->getRotation() * hmdRotation); + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + _myCamera.setPosition(cameraEntity->getPosition() + (hmdRotation * hmdOffset)); + } else { + _myCamera.setOrientation(cameraEntity->getRotation()); + _myCamera.setPosition(cameraEntity->getPosition()); + } + } + } + // Update camera position + if (!isHMDMode()) { + _myCamera.update(1.0f / _frameCounter.rate()); + } + } + } + */ + { + PROFILE_RANGE(render, "/updateCompositor"); + getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform(), getMyAvatar()->getSensorToWorldMatrix()); + } + + gpu::FramebufferPointer finalFramebuffer; + QSize finalFramebufferSize; + { + PROFILE_RANGE(render, "/getOutputFramebuffer"); + // Primary rendering pass + auto framebufferCache = DependencyManager::get(); + finalFramebufferSize = framebufferCache->getFrameBufferSize(); + // Final framebuffer that will be handled to the display-plugin + finalFramebuffer = framebufferCache->getFramebuffer(); + } + + auto hmdInterface = DependencyManager::get(); + float ipdScale = hmdInterface->getIPDScale(); + + // scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. + ipdScale *= sensorToWorldScale; + + { + PROFILE_RANGE(render, "/mainRender"); + PerformanceTimer perfTimer("mainRender"); + // FIXME is this ever going to be different from the size previously set in the render args + // in the overlay render? + // Viewport is assigned to the size of the framebuffer + renderArgs._viewport = ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height()); + auto baseProjection = renderArgs.getViewFrustum().getProjection(); + if (displayPlugin->isStereo()) { + // Stereo modes will typically have a larger projection matrix overall, + // so we ask for the 'mono' projection matrix, which for stereo and HMD + // plugins will imply the combined projection for both eyes. + // + // This is properly implemented for the Oculus plugins, but for OpenVR + // and Stereo displays I'm not sure how to get / calculate it, so we're + // just relying on the left FOV in each case and hoping that the + // overall culling margin of error doesn't cause popping in the + // right eye. There are FIXMEs in the relevant plugins + _myCamera.setProjection(displayPlugin->getCullingProjection(baseProjection)); + renderArgs._context->enableStereo(true); + mat4 eyeOffsets[2]; + mat4 eyeProjections[2]; + + // FIXME we probably don't need to set the projection matrix every frame, + // only when the display plugin changes (or in non-HMD modes when the user + // changes the FOV manually, which right now I don't think they can. + for_each_eye([&](Eye eye) { + // For providing the stereo eye views, the HMD head pose has already been + // applied to the avatar, so we need to get the difference between the head + // pose applied to the avatar and the per eye pose, and use THAT as + // the per-eye stereo matrix adjustment. + mat4 eyeToHead = displayPlugin->getEyeToHeadTransform(eye); + // Grab the translation + vec3 eyeOffset = glm::vec3(eyeToHead[3]); + // Apply IPD scaling + mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); + eyeOffsets[eye] = eyeOffsetTransform; + eyeProjections[eye] = displayPlugin->getEyeProjection(eye, baseProjection); + }); + renderArgs._context->setStereoProjections(eyeProjections); + renderArgs._context->setStereoViews(eyeOffsets); + + // Configure the type of display / stereo + renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); + } + renderArgs._blitFramebuffer = finalFramebuffer; + // displaySide(&renderArgs, _myCamera); + runRenderFrame(&renderArgs); + } + + gpu::Batch postCompositeBatch; + { + PROFILE_RANGE(render, "/postComposite"); + PerformanceTimer perfTimer("postComposite"); + renderArgs._batch = &postCompositeBatch; + renderArgs._batch->setViewportTransform(ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height())); + renderArgs._batch->setViewTransform(renderArgs.getViewFrustum().getView()); + _overlays.render3DHUDOverlays(&renderArgs); + } + + auto frame = _gpuContext->endFrame(); + frame->frameIndex = _frameCount; + frame->framebuffer = finalFramebuffer; + frame->framebufferRecycler = [](const gpu::FramebufferPointer& framebuffer) { + DependencyManager::get()->releaseFramebuffer(framebuffer); + }; + frame->overlay = _applicationOverlay.getOverlayTexture(); + frame->postCompositeBatch = postCompositeBatch; + // deliver final scene rendering commands to the display plugin + { + PROFILE_RANGE(render, "/pluginOutput"); + PerformanceTimer perfTimer("pluginOutput"); + _frameCounter.increment(); + displayPlugin->submitFrame(frame); + } + + // Reset the framebuffer and stereo state + renderArgs._blitFramebuffer.reset(); + renderArgs._context->enableStereo(false); + + { + Stats::getInstance()->setRenderDetails(renderArgs._details); + } + + uint64_t lastPaintDuration = usecTimestampNow() - lastPaintBegin; + _frameTimingsScriptingInterface.addValue(lastPaintDuration); +} +#endif \ No newline at end of file From af69222fdb236d041db696660ba49dadd34d2463 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 28 Sep 2017 18:34:42 -0700 Subject: [PATCH 506/722] Fix icon name change --- interface/resources/qml/hifi/AssetServer.qml | 2 +- interface/resources/qml/hifi/dialogs/TabletAssetServer.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index 46df421d08..6b60cbff7b 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -567,7 +567,7 @@ ScrollingWindow { case "Not Baked": return hifi.glyphs.circleSlash; case "Baked": - return hifi.glyphs.check_2_01; + return hifi.glyphs.checkmark; case "Error": return hifi.glyphs.alert; default: diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 95ccc61d27..44cd700eac 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -566,7 +566,7 @@ Rectangle { case "Not Baked": return hifi.glyphs.circleSlash; case "Baked": - return hifi.glyphs.check_2_01; + return hifi.glyphs.checkmark; case "Error": return hifi.glyphs.alert; default: From 72cacc4cef489fa12749ea6ba531cfbc8fd46cc9 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 28 Sep 2017 19:56:15 -0700 Subject: [PATCH 507/722] server only deletes and entity if it's still the child of an avatar, not if it has ever been --- libraries/entities/src/EntityItem.cpp | 8 ++++++-- libraries/entities/src/EntityTree.cpp | 7 +++++++ libraries/entities/src/EntityTree.h | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 107af837fe..643145942a 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1570,13 +1570,17 @@ void EntityItem::updatePosition(const glm::vec3& value) { } void EntityItem::updateParentID(const QUuid& value) { - if (getParentID() != value) { + QUuid oldParentID = getParentID(); + if (oldParentID != value) { + EntityTreePointer tree = getTree(); + if (!oldParentID.isNull()) { + tree->removeFromChildrenOfAvatars(getThisPointer()); + } setParentID(value); // children are forced to be kinematic // may need to not collide with own avatar markDirtyFlags(Simulation::DIRTY_MOTION_TYPE | Simulation::DIRTY_COLLISION_GROUP); - EntityTreePointer tree = getTree(); if (tree) { tree->addToNeedsParentFixupList(getThisPointer()); } diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index c8675bdcba..bcb73f352c 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1326,6 +1326,13 @@ void EntityTree::deleteDescendantsOfAvatar(QUuid avatarID) { } } +void EntityTree::removeFromChildrenOfAvatars(EntityItemPointer entity) { + QUuid avatarID = entity->getParentID(); + if (_childrenOfAvatars.contains(avatarID)) { + _childrenOfAvatars[avatarID].remove(entity->getID()); + } +} + void EntityTree::addToNeedsParentFixupList(EntityItemPointer entity) { QWriteLocker locker(&_needsParentFixupLock); _needsParentFixup.append(entity); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index c0183d7ec2..cb16f2fac1 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -254,6 +254,7 @@ public: void knowAvatarID(QUuid avatarID) { _avatarIDs += avatarID; } void forgetAvatarID(QUuid avatarID) { _avatarIDs -= avatarID; } void deleteDescendantsOfAvatar(QUuid avatarID); + void removeFromChildrenOfAvatars(EntityItemPointer entity); void addToNeedsParentFixupList(EntityItemPointer entity); From d3cd034d614cc9285baf4ca7bb975c721ba07ae9 Mon Sep 17 00:00:00 2001 From: vladest Date: Fri, 29 Sep 2017 16:33:52 +0200 Subject: [PATCH 508/722] Cleanup --- interface/resources/qml/dialogs/TabletLoginDialog.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index 6cf7394cf0..f4f7a5848c 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -47,10 +47,8 @@ TabletModalWindow { } function tryDestroy() { - console.log("tryDestroy") canceled() } - Component.onDestruction: console.log("root dying") } //property int colorScheme: hifi.colorSchemes.dark From 01a0b26b90e4146857797f49f173ab9b2f7bc9b8 Mon Sep 17 00:00:00 2001 From: vladest Date: Fri, 29 Sep 2017 17:14:35 +0200 Subject: [PATCH 509/722] Remove mouse area for text fields. use activeFocusOnPress property instead --- .../commerce/wallet/PassphraseSelection.qml | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml index e7ff8489d1..653f81501c 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -76,6 +76,8 @@ Item { height: 50; echoMode: TextInput.Password; placeholderText: "enter current passphrase"; + activeFocusOnPress: true + activeFocusOnTab: true onFocusChanged: { if (focus) { @@ -85,14 +87,6 @@ Item { } } - MouseArea { - anchors.fill: parent; - onClicked: { - parent.focus = true; - sendSignalToWallet({method: 'walletSetup_raiseKeyboard'}); - } - } - onAccepted: { passphraseField.focus = true; } @@ -109,6 +103,8 @@ Item { height: 50; echoMode: TextInput.Password; placeholderText: root.isShowingTip ? "" : "enter new passphrase"; + activeFocusOnPress: true + activeFocusOnTab: true onFocusChanged: { if (focus) { @@ -118,14 +114,6 @@ Item { } } - MouseArea { - anchors.fill: parent; - onClicked: { - parent.focus = true; - sendMessageToLightbox({method: 'walletSetup_raiseKeyboard'}); - } - } - onAccepted: { passphraseFieldAgain.focus = true; } @@ -140,6 +128,8 @@ Item { height: 50; echoMode: TextInput.Password; placeholderText: root.isShowingTip ? "" : "re-enter new passphrase"; + activeFocusOnPress: true + activeFocusOnTab: true onFocusChanged: { if (focus) { @@ -149,14 +139,6 @@ Item { } } - MouseArea { - anchors.fill: parent; - onClicked: { - parent.focus = true; - sendMessageToLightbox({method: 'walletSetup_raiseKeyboard'}); - } - } - onAccepted: { focus = false; } From 9c81bc5479464e81889128d13dbdd8a20e7eff78 Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Fri, 29 Sep 2017 09:52:30 -0700 Subject: [PATCH 510/722] CR fixes --- interface/src/ui/overlays/ModelOverlay.cpp | 26 +++++++++------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 713115bffc..c857ad97ab 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -404,18 +404,16 @@ void ModelOverlay::animate() { float deltaTime = (float)interval / (float)USECS_PER_SECOND; _animationCurrentFrame += (deltaTime * _animationFPS); - { - int animationCurrentFrame = (int)(glm::floor(_animationCurrentFrame)) % frameCount; - if (animationCurrentFrame < 0 || animationCurrentFrame > frameCount) { - animationCurrentFrame = 0; - } - - if (animationCurrentFrame == _lastKnownCurrentFrame) { - return; - } - _lastKnownCurrentFrame = animationCurrentFrame; + int animationCurrentFrame = (int)(glm::floor(_animationCurrentFrame)) % frameCount; + if (animationCurrentFrame < 0 || animationCurrentFrame > frameCount) { + animationCurrentFrame = 0; } + if (animationCurrentFrame == _lastKnownCurrentFrame) { + return; + } + _lastKnownCurrentFrame = animationCurrentFrame; + if (_jointMapping.size() != _model->getJointStateCount()) { return; } @@ -440,12 +438,11 @@ void ModelOverlay::animate() { if (index < translations.size()) { translationMat = glm::translate(translations[index]); } - } - else if (index < animationJointNames.size()) { + } else if (index < animationJointNames.size()) { QString jointName = fbxJoints[index].name; if (originalFbxIndices.contains(jointName)) { - // Making sure the joint names exist in the original model the animation is trying to apply onto. If they do, then remap and get it's translation. + // Making sure the joint names exist in the original model the animation is trying to apply onto. If they do, then remap and get its translation. int remappedIndex = originalFbxIndices[jointName] - 1; // JointIndeces seem to always start from 1 and the found index is always 1 higher than actual. translationMat = glm::translate(originalFbxJoints[remappedIndex].translation); } @@ -453,8 +450,7 @@ void ModelOverlay::animate() { glm::mat4 rotationMat; if (index < rotations.size()) { rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * rotations[index] * fbxJoints[index].postRotation); - } - else { + } else { rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * fbxJoints[index].postRotation); } From a7d507e9fd51a37e99f2fff1e429f1933aea7c39 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 29 Sep 2017 10:37:21 -0700 Subject: [PATCH 511/722] Disable watchdog with environment variable --- interface/src/Application.cpp | 11 ++++++++--- interface/src/Application.h | 2 -- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 85172fc73f..fcae7430a8 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -289,6 +289,10 @@ static QTimer locationUpdateTimer; static QTimer identityPacketTimer; static QTimer pingTimer; +static const QString DISABLE_WATCHDOG_FLAG("HIFI_DISABLE_WATCHDOG"); +static bool DISABLE_WATCHDOG = QProcessEnvironment::systemEnvironment().contains(DISABLE_WATCHDOG_FLAG); + + static const int MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; // For processing on QThreadPool, we target a number of threads after reserving some @@ -805,8 +809,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo nodeList->startThread(); // Set up a watchdog thread to intentionally crash the application on deadlocks - _deadlockWatchdogThread = new DeadlockWatchdogThread(); - _deadlockWatchdogThread->start(); + if (!DISABLE_WATCHDOG) { + (new DeadlockWatchdogThread())->start(); + } if (steamClient) { qCDebug(interfaceapp) << "[VERSION] SteamVR buildID:" << steamClient->getSteamVRBuildID(); @@ -1933,7 +1938,7 @@ void Application::showCursor(const Cursor::Icon& cursor) { } void Application::updateHeartbeat() const { - static_cast(_deadlockWatchdogThread)->updateHeartbeat(); + DeadlockWatchdogThread::updateHeartbeat(); } void Application::onAboutToQuit() { diff --git a/interface/src/Application.h b/interface/src/Application.h index 74e84ae92c..0819555584 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -650,8 +650,6 @@ private: Qt::CursorShape _desiredCursor{ Qt::BlankCursor }; bool _cursorNeedsChanging { false }; - QThread* _deadlockWatchdogThread; - std::map> _postUpdateLambdas; std::mutex _postUpdateLambdasLock; From fcfac9efc0b4787872eda616165cc70f43be093c Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 29 Sep 2017 11:14:29 -0700 Subject: [PATCH 512/722] no tpose when switching avatars --- libraries/animation/src/Rig.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 86a1e629b4..712c728dcb 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -249,6 +249,7 @@ void Rig::reset(const FBXGeometry& geometry) { _rightShoulderJointIndex = _rightElbowJointIndex >= 0 ? geometry.joints.at(_rightElbowJointIndex).parentIndex : -1; if (!_animGraphURL.isEmpty()) { + _animNode.reset(); initAnimGraph(_animGraphURL); } } @@ -1619,7 +1620,7 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo } void Rig::initAnimGraph(const QUrl& url) { - if (_animGraphURL != url) { + if (_animGraphURL != url || !_animNode) { _animGraphURL = url; _animNode.reset(); From 4fa60f51085ac36b9badda7a78640b5156731dc4 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 16 Aug 2017 11:26:29 -0700 Subject: [PATCH 513/722] cleanup AddEntityOperator --- libraries/entities/src/AddEntityOperator.cpp | 9 ++++----- libraries/entities/src/AddEntityOperator.h | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/libraries/entities/src/AddEntityOperator.cpp b/libraries/entities/src/AddEntityOperator.cpp index 78d986f538..2ff1c6f622 100644 --- a/libraries/entities/src/AddEntityOperator.cpp +++ b/libraries/entities/src/AddEntityOperator.cpp @@ -9,18 +9,17 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "AddEntityOperator.h" + #include "EntityItem.h" #include "EntityTree.h" #include "EntityTreeElement.h" -#include "AddEntityOperator.h" - AddEntityOperator::AddEntityOperator(EntityTreePointer tree, EntityItemPointer newEntity) : _tree(tree), _newEntity(newEntity), - _foundNew(false), - _changeTime(usecTimestampNow()), - _newEntityBox() + _newEntityBox(), + _foundNew(false) { // caller must have verified existence of newEntity assert(_newEntity); diff --git a/libraries/entities/src/AddEntityOperator.h b/libraries/entities/src/AddEntityOperator.h index 48ee49f4d1..0c36797e24 100644 --- a/libraries/entities/src/AddEntityOperator.h +++ b/libraries/entities/src/AddEntityOperator.h @@ -12,20 +12,28 @@ #ifndef hifi_AddEntityOperator_h #define hifi_AddEntityOperator_h +#include + +#include +#include + +#include "EntityTypes.h" + +class EntityTree; +using EntityTreePointer = std::shared_ptr; + class AddEntityOperator : public RecurseOctreeOperator { public: AddEntityOperator(EntityTreePointer tree, EntityItemPointer newEntity); - + virtual bool preRecursion(const OctreeElementPointer& element) override; virtual bool postRecursion(const OctreeElementPointer& element) override; virtual OctreeElementPointer possiblyCreateChildAt(const OctreeElementPointer& element, int childIndex) override; private: EntityTreePointer _tree; EntityItemPointer _newEntity; - bool _foundNew; - quint64 _changeTime; - AABox _newEntityBox; + bool _foundNew; }; From 55e9ced5c387c74b579d1b1c3028308c8c8ecfbf Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 16 Aug 2017 11:27:41 -0700 Subject: [PATCH 514/722] cleanup MovingEntitiesOperator --- libraries/entities/src/EntitySimulation.cpp | 2 +- .../entities/src/MovingEntitiesOperator.cpp | 21 ++++++++---------- .../entities/src/MovingEntitiesOperator.h | 22 +++++++++++-------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/libraries/entities/src/EntitySimulation.cpp b/libraries/entities/src/EntitySimulation.cpp index 6a1c359b5a..2e330fdcc5 100644 --- a/libraries/entities/src/EntitySimulation.cpp +++ b/libraries/entities/src/EntitySimulation.cpp @@ -141,7 +141,7 @@ void EntitySimulation::callUpdateOnEntitiesThatNeedIt(const quint64& now) { void EntitySimulation::sortEntitiesThatMoved() { // NOTE: this is only for entities that have been moved by THIS EntitySimulation. // External changes to entity position/shape are expected to be sorted outside of the EntitySimulation. - MovingEntitiesOperator moveOperator(_entityTree); + MovingEntitiesOperator moveOperator; AACube domainBounds(glm::vec3((float)-HALF_TREE_SCALE), (float)TREE_SCALE); SetOfEntities::iterator itemItr = _entitiesToSort.begin(); while (itemItr != _entitiesToSort.end()) { diff --git a/libraries/entities/src/MovingEntitiesOperator.cpp b/libraries/entities/src/MovingEntitiesOperator.cpp index 42e5a2ece5..ea30ce85f2 100644 --- a/libraries/entities/src/MovingEntitiesOperator.cpp +++ b/libraries/entities/src/MovingEntitiesOperator.cpp @@ -16,15 +16,7 @@ #include "MovingEntitiesOperator.h" -MovingEntitiesOperator::MovingEntitiesOperator(EntityTreePointer tree) : - _tree(tree), - _changeTime(usecTimestampNow()), - _foundOldCount(0), - _foundNewCount(0), - _lookingCount(0), - _wantDebug(false) -{ -} +MovingEntitiesOperator::MovingEntitiesOperator() { } MovingEntitiesOperator::~MovingEntitiesOperator() { if (_wantDebug) { @@ -146,7 +138,7 @@ bool MovingEntitiesOperator::preRecursion(const OctreeElementPointer& element) { // In Pre-recursion, we're generally deciding whether or not we want to recurse this // path of the tree. For this operation, we want to recurse the branch of the tree if - // and of the following are true: + // any of the following are true: // * We have not yet found the old entity, and this branch contains our old entity // * We have not yet found the new entity, and this branch contains our new entity // @@ -230,8 +222,6 @@ bool MovingEntitiesOperator::postRecursion(const OctreeElementPointer& element) if ((shouldRecurseSubTree(element))) { element->markWithChangedTime(); } - - // It's not OK to prune if we have the potential of deleting the original containing element // because if we prune the containing element then new might end up reallocating the same memory later @@ -286,3 +276,10 @@ OctreeElementPointer MovingEntitiesOperator::possiblyCreateChildAt(const OctreeE } return NULL; } + +void MovingEntitiesOperator::reset() { + _entitiesToMove.clear(); + _foundOldCount = 0; + _foundNewCount = 0; + _lookingCount = 0; +} diff --git a/libraries/entities/src/MovingEntitiesOperator.h b/libraries/entities/src/MovingEntitiesOperator.h index fc6ccf2513..d93efa60f2 100644 --- a/libraries/entities/src/MovingEntitiesOperator.h +++ b/libraries/entities/src/MovingEntitiesOperator.h @@ -12,6 +12,11 @@ #ifndef hifi_MovingEntitiesOperator_h #define hifi_MovingEntitiesOperator_h +#include + +#include "EntityTypes.h" +#include "EntityTreeElement.h" + class EntityToMoveDetails { public: EntityItemPointer entity; @@ -34,7 +39,7 @@ inline bool operator==(const EntityToMoveDetails& a, const EntityToMoveDetails& class MovingEntitiesOperator : public RecurseOctreeOperator { public: - MovingEntitiesOperator(EntityTreePointer tree); + MovingEntitiesOperator(); ~MovingEntitiesOperator(); void addEntityToMoveList(EntityItemPointer entity, const AACube& newCube); @@ -42,16 +47,15 @@ public: virtual bool postRecursion(const OctreeElementPointer& element) override; virtual OctreeElementPointer possiblyCreateChildAt(const OctreeElementPointer& element, int childIndex) override; bool hasMovingEntities() const { return _entitiesToMove.size() > 0; } + void reset(); private: - EntityTreePointer _tree; - QSet _entitiesToMove; - quint64 _changeTime; - int _foundOldCount; - int _foundNewCount; - int _lookingCount; bool shouldRecurseSubTree(const OctreeElementPointer& element); - - bool _wantDebug; + + QSet _entitiesToMove; + int _foundOldCount { 0 }; + int _foundNewCount { 0 }; + int _lookingCount { 0 }; + bool _wantDebug { false }; }; #endif // hifi_MovingEntitiesOperator_h From 56bc48b31a3c09dd84e0e99a77018cdc96b79768 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 16 Aug 2017 11:31:02 -0700 Subject: [PATCH 515/722] don't use octcode data in entity update packets --- libraries/entities/src/EntityTree.cpp | 120 ++++++++++++++++- libraries/entities/src/EntityTree.h | 12 +- libraries/entities/src/EntityTreeElement.cpp | 135 +------------------ 3 files changed, 130 insertions(+), 137 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index bcb73f352c..3198ad4344 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -22,7 +22,6 @@ #include "VariantMapToScriptValue.h" #include "AddEntityOperator.h" -#include "MovingEntitiesOperator.h" #include "UpdateEntityOperator.h" #include "QVariantGLM.h" #include "EntitiesLogging.h" @@ -107,6 +106,121 @@ void EntityTree::eraseAllOctreeElements(bool createNewRoot) { clearDeletedEntities(); } +void EntityTree::readBitstreamToTree(const unsigned char* bitstream, + unsigned long int bufferSizeBytes, ReadBitstreamToTreeParams& args) { + Octree::readBitstreamToTree(bitstream, bufferSizeBytes, args); + + // add entities + QHash::const_iterator itr; + for (itr = _entitiesToAdd.constBegin(); itr != _entitiesToAdd.constEnd(); ++itr) { + EntityItemPointer entityItem = itr.value(); + AddEntityOperator theOperator(getThisPointer(), entityItem); + recurseTreeWithOperator(&theOperator); + if (!entityItem->getParentID().isNull()) { + addToNeedsParentFixupList(entityItem); + } + postAddEntity(entityItem); + } + _entitiesToAdd.clear(); + + // move entities + if (_entityMover.hasMovingEntities()) { + PerformanceTimer perfTimer("recurseTreeWithOperator"); + recurseTreeWithOperator(&_entityMover); + _entityMover.reset(); + } +} + +int EntityTree::readEntityDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args) { + const unsigned char* dataAt = data; + int bytesRead = 0; + uint16_t numberOfEntities = 0; + int expectedBytesPerEntity = EntityItem::expectedBytes(); + + args.elementsPerPacket++; + + if (bytesLeftToRead >= (int)sizeof(numberOfEntities)) { + // read our entities in.... + numberOfEntities = *(uint16_t*)dataAt; + + dataAt += sizeof(numberOfEntities); + bytesLeftToRead -= (int)sizeof(numberOfEntities); + bytesRead += sizeof(numberOfEntities); + + if (bytesLeftToRead >= (int)(numberOfEntities * expectedBytesPerEntity)) { + for (uint16_t i = 0; i < numberOfEntities; i++) { + int bytesForThisEntity = 0; + EntityItemID entityItemID = EntityItemID::readEntityItemIDFromBuffer(dataAt, bytesLeftToRead); + EntityItemPointer entity = findEntityByEntityItemID(entityItemID); + + if (entity) { + QString entityScriptBefore = entity->getScript(); + QUuid parentIDBefore = entity->getParentID(); + QString entityServerScriptsBefore = entity->getServerScripts(); + quint64 entityScriptTimestampBefore = entity->getScriptTimestamp(); + + bytesForThisEntity = entity->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args); + if (entity->getDirtyFlags()) { + entityChanged(entity); + } + _entityMover.addEntityToMoveList(entity, entity->getQueryAACube()); + + QString entityScriptAfter = entity->getScript(); + QString entityServerScriptsAfter = entity->getServerScripts(); + quint64 entityScriptTimestampAfter = entity->getScriptTimestamp(); + bool reload = entityScriptTimestampBefore != entityScriptTimestampAfter; + + // If the script value has changed on us, or it's timestamp has changed to force + // a reload then we want to send out a script changing signal... + if (reload || entityScriptBefore != entityScriptAfter) { + emitEntityScriptChanging(entityItemID, reload); // the entity script has changed + } + if (reload || entityServerScriptsBefore != entityServerScriptsAfter) { + emitEntityServerScriptChanging(entityItemID, reload); // the entity server script has changed + } + + QUuid parentIDAfter = entity->getParentID(); + if (parentIDBefore != parentIDAfter) { + addToNeedsParentFixupList(entity); + } + } else { + entity = EntityTypes::constructEntityItem(dataAt, bytesLeftToRead, args); + if (entity) { + bytesForThisEntity = entity->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args); + + // don't add if we've recently deleted.... + if (!isDeletedEntity(entityItemID)) { + _entitiesToAdd.insert(entityItemID, entity); + + /* + addEntityMapEntry(entity); + oldElement->addEntityItem(entity); // add this new entity to this elements entities + entityItemID = entity->getEntityItemID(); + postAddEntity(entity); + */ + + if (entity->getCreated() == UNKNOWN_CREATED_TIME) { + entity->recordCreationTime(); + } + } else { + #ifdef WANT_DEBUG + qCDebug(entities) << "Received packet for previously deleted entity [" << + entityItemID << "] ignoring. (inside " << __FUNCTION__ << ")"; + #endif + } + } + } + // Move the buffer forward to read more entities + dataAt += bytesForThisEntity; + bytesLeftToRead -= bytesForThisEntity; + bytesRead += bytesForThisEntity; + } + } + } + + return bytesRead; +} + bool EntityTree::handlesEditPacketType(PacketType packetType) const { // we handle these types of "edit" packets switch (packetType) { @@ -1250,7 +1364,7 @@ void EntityTree::entityChanged(EntityItemPointer entity) { void EntityTree::fixupNeedsParentFixups() { - MovingEntitiesOperator moveOperator(getThisPointer()); + MovingEntitiesOperator moveOperator; QWriteLocker locker(&_needsParentFixupLock); @@ -1674,7 +1788,7 @@ QVector EntityTree::sendEntities(EntityEditPacketSender* packetSen // add-entity packet to the server. // fix the queryAACubes of any children that were read in before their parents, get them into the correct element - MovingEntitiesOperator moveOperator(localTree); + MovingEntitiesOperator moveOperator; QHash::iterator i = map.begin(); while (i != map.end()) { EntityItemID newID = i.value(); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index cb16f2fac1..7dff2985fc 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -19,11 +19,12 @@ #include class EntityTree; -typedef std::shared_ptr EntityTreePointer; - +using EntityTreePointer = std::shared_ptr; +#include "AddEntityOperator.h" #include "EntityTreeElement.h" #include "DeleteEntityOperator.h" +#include "MovingEntitiesOperator.h" class EntityEditFilters; class Model; @@ -80,6 +81,10 @@ public: virtual void eraseAllOctreeElements(bool createNewRoot = true) override; + virtual void readBitstreamToTree(const unsigned char* bitstream, + unsigned long int bufferSizeBytes, ReadBitstreamToTreeParams& args) override; + int readEntityDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args); + // These methods will allow the OctreeServer to send your tree inbound edit packets of your // own definition. Implement these to allow your octree based server to support editing virtual bool getWantSVOfileVersions() const override { return true; } @@ -347,6 +352,9 @@ protected: bool filterProperties(EntityItemPointer& existingEntity, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType); bool _hasEntityEditFilter{ false }; QStringList _entityScriptSourceWhitelist; + + MovingEntitiesOperator _entityMover; + QHash _entitiesToAdd; }; #endif // hifi_EntityTree_h diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 487bf60f61..4056bbd0b7 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -107,7 +107,7 @@ bool EntityTreeElement::shouldIncludeChildData(int childIndex, EncodeBitstreamPa OctreeElementExtraEncodeData* extraEncodeData = &entityNodeData->extraEncodeData; assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes - + if (extraEncodeData->contains(this)) { EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData = std::static_pointer_cast((*extraEncodeData)[this]); @@ -305,7 +305,7 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData int numberOfEntitiesOffset = 0; withReadLock([&] { QVector indexesOfEntitiesToInclude; - + // It's possible that our element has been previous completed. In this case we'll simply not include any of our // entities for encoding. This is needed because we encode the element data at the "parent" level, and so we // need to handle the case where our sibling elements need encoding but we don't. @@ -928,138 +928,9 @@ bool EntityTreeElement::removeEntityItem(EntityItemPointer entity) { } -// Things we want to accomplish as we read these entities from the data buffer. -// -// 1) correctly update the properties of the entity -// 2) add any new entities that didn't previously exist -// -// TODO: Do we also need to do this? -// 3) mark our tree as dirty down to the path of the previous location of the entity -// 4) mark our tree as dirty down to the path of the new location of the entity -// -// Since we're potentially reading several entities, we'd prefer to do all the moving around -// and dirty path marking in one pass. int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args) { - // If we're the root, but this bitstream doesn't support root elements with data, then - // return without reading any bytes - if (this == _myTree->getRoot().get() && args.bitstreamVersion < VERSION_ROOT_ELEMENT_HAS_DATA) { - return 0; - } - - const unsigned char* dataAt = data; - int bytesRead = 0; - uint16_t numberOfEntities = 0; - int expectedBytesPerEntity = EntityItem::expectedBytes(); - - args.elementsPerPacket++; - - if (bytesLeftToRead >= (int)sizeof(numberOfEntities)) { - // read our entities in.... - numberOfEntities = *(uint16_t*)dataAt; - - dataAt += sizeof(numberOfEntities); - bytesLeftToRead -= (int)sizeof(numberOfEntities); - bytesRead += sizeof(numberOfEntities); - - if (bytesLeftToRead >= (int)(numberOfEntities * expectedBytesPerEntity)) { - for (uint16_t i = 0; i < numberOfEntities; i++) { - int bytesForThisEntity = 0; - EntityItemID entityItemID; - EntityItemPointer entityItem = NULL; - - // Old model files don't have UUIDs in them. So we don't want to try to read those IDs from the stream. - // Since this can only happen on loading an old file, we can safely treat these as new entity cases, - // which will correctly handle the case of creating models and letting them parse the old format. - if (args.bitstreamVersion >= VERSION_ENTITIES_SUPPORT_SPLIT_MTU) { - entityItemID = EntityItemID::readEntityItemIDFromBuffer(dataAt, bytesLeftToRead); - entityItem = _myTree->findEntityByEntityItemID(entityItemID); - } - - // If the item already exists in our tree, we want do the following... - // 1) allow the existing item to read from the databuffer - // 2) check to see if after reading the item, the containing element is still correct, fix it if needed - // - // TODO: Do we need to also do this? - // 3) remember the old cube for the entity so we can mark it as dirty - if (entityItem) { - QString entityScriptBefore = entityItem->getScript(); - QUuid parentIDBefore = entityItem->getParentID(); - QString entityServerScriptsBefore = entityItem->getServerScripts(); - quint64 entityScriptTimestampBefore = entityItem->getScriptTimestamp(); - bool bestFitBefore = bestFitEntityBounds(entityItem); - EntityTreeElementPointer currentContainingElement = _myTree->getContainingElement(entityItemID); - - bytesForThisEntity = entityItem->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args); - if (entityItem->getDirtyFlags()) { - _myTree->entityChanged(entityItem); - } - bool bestFitAfter = bestFitEntityBounds(entityItem); - - if (bestFitBefore != bestFitAfter) { - // This is the case where the entity existed, and is in some element in our tree... - if (!bestFitBefore && bestFitAfter) { - // This is the case where the entity existed, and is in some element in our tree... - if (currentContainingElement.get() != this) { - // if the currentContainingElement is non-null, remove the entity from it - if (currentContainingElement) { - currentContainingElement->removeEntityItem(entityItem); - } - addEntityItem(entityItem); - } - } - } - - QString entityScriptAfter = entityItem->getScript(); - QString entityServerScriptsAfter = entityItem->getServerScripts(); - quint64 entityScriptTimestampAfter = entityItem->getScriptTimestamp(); - bool reload = entityScriptTimestampBefore != entityScriptTimestampAfter; - - // If the script value has changed on us, or it's timestamp has changed to force - // a reload then we want to send out a script changing signal... - if (entityScriptBefore != entityScriptAfter || reload) { - _myTree->emitEntityScriptChanging(entityItemID, reload); // the entity script has changed - } - if (entityServerScriptsBefore != entityServerScriptsAfter || reload) { - _myTree->emitEntityServerScriptChanging(entityItemID, reload); // the entity server script has changed - } - - QUuid parentIDAfter = entityItem->getParentID(); - if (parentIDBefore != parentIDAfter) { - _myTree->addToNeedsParentFixupList(entityItem); - } - - } else { - entityItem = EntityTypes::constructEntityItem(dataAt, bytesLeftToRead, args); - if (entityItem) { - bytesForThisEntity = entityItem->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args); - - // don't add if we've recently deleted.... - if (!_myTree->isDeletedEntity(entityItem->getID())) { - _myTree->addEntityMapEntry(entityItem); - addEntityItem(entityItem); // add this new entity to this elements entities - entityItemID = entityItem->getEntityItemID(); - _myTree->postAddEntity(entityItem); - if (entityItem->getCreated() == UNKNOWN_CREATED_TIME) { - entityItem->recordCreationTime(); - } - } else { - #ifdef WANT_DEBUG - qCDebug(entities) << "Received packet for previously deleted entity [" << - entityItem->getID() << "] ignoring. (inside " << __FUNCTION__ << ")"; - #endif - } - } - } - // Move the buffer forward to read more entities - dataAt += bytesForThisEntity; - bytesLeftToRead -= bytesForThisEntity; - bytesRead += bytesForThisEntity; - } - } - } - - return bytesRead; + return _myTree->readEntityDataFromBuffer(data, bytesLeftToRead, args); } void EntityTreeElement::addEntityItem(EntityItemPointer entity) { From 171151b92ac47570364b226784793bb2260f6e97 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 16 Aug 2017 11:31:56 -0700 Subject: [PATCH 516/722] use new form of MovingEntitiesOperator ctor --- interface/src/avatar/MyAvatar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 19179d613d..10e2202553 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -584,7 +584,7 @@ void MyAvatar::simulate(float deltaTime) { } auto now = usecTimestampNow(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); - MovingEntitiesOperator moveOperator(entityTree); + MovingEntitiesOperator moveOperator; forEachDescendant([&](SpatiallyNestablePointer object) { // if the queryBox has changed, tell the entity-server if (object->getNestableType() == NestableType::Entity && object->checkAndMaybeUpdateQueryAACube()) { From 82ed19386f02d06c9be8fe8533a06c65776124ff Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 16 Aug 2017 11:45:18 -0700 Subject: [PATCH 517/722] make Octree::readBitstreamToTree() virtual --- libraries/entities/src/EntityTree.cpp | 2 +- libraries/entities/src/EntityTree.h | 2 +- libraries/octree/src/Octree.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 3198ad4344..518d3bd883 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -107,7 +107,7 @@ void EntityTree::eraseAllOctreeElements(bool createNewRoot) { } void EntityTree::readBitstreamToTree(const unsigned char* bitstream, - unsigned long int bufferSizeBytes, ReadBitstreamToTreeParams& args) { + uint64_t bufferSizeBytes, ReadBitstreamToTreeParams& args) { Octree::readBitstreamToTree(bitstream, bufferSizeBytes, args); // add entities diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 7dff2985fc..17dda32b53 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -82,7 +82,7 @@ public: virtual void eraseAllOctreeElements(bool createNewRoot = true) override; virtual void readBitstreamToTree(const unsigned char* bitstream, - unsigned long int bufferSizeBytes, ReadBitstreamToTreeParams& args) override; + uint64_t bufferSizeBytes, ReadBitstreamToTreeParams& args) override; int readEntityDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args); // These methods will allow the OctreeServer to send your tree inbound edit packets of your diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index 3b84618a56..2794ca85f0 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -232,7 +232,7 @@ public: virtual void eraseAllOctreeElements(bool createNewRoot = true); - void readBitstreamToTree(const unsigned char* bitstream, uint64_t bufferSizeBytes, ReadBitstreamToTreeParams& args); + virtual void readBitstreamToTree(const unsigned char* bitstream, uint64_t bufferSizeBytes, ReadBitstreamToTreeParams& args); void deleteOctalCodeFromTree(const unsigned char* codeBuffer, bool collapseEmptyTrees = DONT_COLLAPSE); void reaverageOctreeElements(OctreeElementPointer startElement = OctreeElementPointer()); From 3ae5c215ba4dd1cbb565f31702a65d92dfded885 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 12 Jul 2017 16:03:48 -0700 Subject: [PATCH 518/722] stub EntityTreeSendThread::traverseTreeAndSendContents() --- assignment-client/src/entities/EntityTreeSendThread.cpp | 5 +++++ assignment-client/src/entities/EntityTreeSendThread.h | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 7febdc67e1..8144bf2e83 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -80,6 +80,11 @@ void EntityTreeSendThread::preDistributionProcessing() { } } +void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, + bool viewFrustumChanged, bool isFullScene) { + OctreeSendThread::traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); +} + bool EntityTreeSendThread::addAncestorsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData) { // check if this entity has a parent that is also an entity diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index bfb4c743f1..8bb5c3915d 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -23,7 +23,9 @@ public: EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) : OctreeSendThread(myServer, node) {}; protected: - virtual void preDistributionProcessing() override; + void preDistributionProcessing() override; + void traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, + bool viewFrustumChanged, bool isFullScene) override; private: // the following two methods return booleans to indicate if any extra flagged entities were new additions to set From 7edd99ca0b597ce83f9029619bd2375c88227dc8 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 26 Jul 2017 08:00:07 -0700 Subject: [PATCH 519/722] add basics for send queue and tree traversal --- .../src/entities/EntityTreeSendThread.cpp | 261 ++++++++++++++++++ .../src/entities/EntityTreeSendThread.h | 70 +++++ libraries/entities/src/EntityTreeElement.h | 8 +- 3 files changed, 335 insertions(+), 4 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 8144bf2e83..b971634d7e 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -10,12 +10,214 @@ // #include "EntityTreeSendThread.h" +#include // adebug #include #include #include "EntityServer.h" +const float INVALID_ENTITY_SEND_PRIORITY = -1.0e-6f; + +void PrioritizedEntity::updatePriority(const ViewFrustum& view) { + EntityItemPointer entity = _weakEntity.lock(); + if (entity) { + bool success; + AACube cube = entity->getQueryAACube(success); + if (success) { + glm::vec3 center = cube.calcCenter() - view.getPosition(); + const float MIN_DISTANCE = 0.001f; + float distanceToCenter = glm::length(center) + MIN_DISTANCE; + float distance = distanceToCenter; //- 0.5f * cube.getScale(); + if (distance < MIN_DISTANCE) { + // this object's bounding box overlaps the camera --> give it a big priority + _priority = cube.getScale(); + } else { + // NOTE: we assume view.aspectRatio < 1.0 (view width greater than height) + // so we only check against the larger (horizontal) view angle + float front = glm::dot(center, view.getDirection()) / distanceToCenter; + if (front > cosf(view.getFieldOfView()) || distance < view.getCenterRadius()) { + _priority = cube.getScale() / distance; // + front; + } else { + _priority = INVALID_ENTITY_SEND_PRIORITY; + } + } + } else { + // when in doubt just it something positive + _priority = 1.0f; + } + } else { + _priority = INVALID_ENTITY_SEND_PRIORITY; + } +} + +TreeTraversalPath::Fork::Fork(EntityTreeElementPointer& element) : _nextIndex(0) { + assert(element); + _weakElement = element; +} + +EntityTreeElementPointer TreeTraversalPath::Fork::getNextElement(const ViewFrustum& view) { + if (_nextIndex == -1) { + // only get here for the TreeTraversalPath's root Fork at the very beginning of traversal + // safe to assume this element is in view + ++_nextIndex; + return _weakElement.lock(); + } else if (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer element = _weakElement.lock(); + if (element) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && ViewFrustum::OUTSIDE != nextElement->computeViewIntersection(view)) { + return nextElement; + } + } + } + } + return EntityTreeElementPointer(); +} + +EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementAgain(const ViewFrustum& view, uint64_t oldTime) { + if (_nextIndex == -1) { + // only get here for the TreeTraversalPath's root Fork at the very beginning of traversal + // safe to assume this element is in view + ++_nextIndex; + EntityTreeElementPointer element = _weakElement.lock(); + assert(element); // should never lose root element + if (element->getLastChanged() < oldTime) { + _nextIndex = NUMBER_OF_CHILDREN; + return EntityTreeElementPointer(); + } + if (element->getLastChanged() > oldTime) { + return element; + } + } + if (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer element = _weakElement.lock(); + if (element) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && nextElement->getLastChanged() > oldTime && + ViewFrustum::OUTSIDE != nextElement->computeViewIntersection(view)) { + return nextElement; + } + } + } + } + return EntityTreeElementPointer(); +} + +EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementDelta(const ViewFrustum& newView, const ViewFrustum& oldView, uint64_t oldTime) { + if (_nextIndex == -1) { + // only get here for the TreeTraversalPath's root Fork at the very beginning of traversal + // safe to assume this element is in newView + ++_nextIndex; + EntityTreeElementPointer element = _weakElement.lock(); + assert(element); // should never lose root element + if (element->getLastChanged() < oldTime) { + _nextIndex = NUMBER_OF_CHILDREN; + return EntityTreeElementPointer(); + } + return element; + } else if (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer element = _weakElement.lock(); + if (element) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && + !(nextElement->getLastChanged() < oldTime && + ViewFrustum::INSIDE == nextElement->computeViewIntersection(oldView)) && + ViewFrustum::OUTSIDE != nextElement->computeViewIntersection(newView)) { + return nextElement; + } + } + } + } + return EntityTreeElementPointer(); +} + +TreeTraversalPath::TreeTraversalPath() { + const int32_t MIN_PATH_DEPTH = 16; + _forks.reserve(MIN_PATH_DEPTH); + _traversalCallback = std::bind(&TreeTraversalPath::traverseFirstTime, this); +} + +void TreeTraversalPath::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root) { + if (_startOfLastCompletedTraversal == 0) { + _traversalCallback = std::bind(&TreeTraversalPath::traverseFirstTime, this); + _currentView = view; + } else if (_currentView.isVerySimilar(view)) { + _traversalCallback = std::bind(&TreeTraversalPath::traverseAgain, this); + } else { + _currentView = view; + _traversalCallback = std::bind(&TreeTraversalPath::traverseDelta, this); + } + _forks.clear(); + if (root) { + _forks.push_back(Fork(root)); + // set root fork's index such that root element returned at getNextElement() + _forks.back().initRootNextIndex(); + } + _startOfCurrentTraversal = usecTimestampNow(); +} + +EntityTreeElementPointer TreeTraversalPath::traverseFirstTime() { + return _forks.back().getNextElement(_currentView); +} + +EntityTreeElementPointer TreeTraversalPath::traverseAgain() { + return _forks.back().getNextElementAgain(_currentView, _startOfLastCompletedTraversal); +} + +EntityTreeElementPointer TreeTraversalPath::traverseDelta() { + return _forks.back().getNextElementDelta(_currentView, _lastCompletedView, _startOfLastCompletedTraversal); +} + +EntityTreeElementPointer TreeTraversalPath::getNextElement() { + if (_forks.empty() || !_traversalCallback) { + return EntityTreeElementPointer(); + } + EntityTreeElementPointer nextElement = _traversalCallback(); + if (nextElement) { + int8_t nextIndex = _forks.back().getNextIndex(); + if (nextIndex > 0) { + // nextElement needs to be added to the path + _forks.push_back(Fork(nextElement)); + } + } else { + // we're done at this level + while (!nextElement) { + // pop one level + _forks.pop_back(); + if (_forks.empty()) { + // we've traversed the entire tree + onCompleteTraversal(); + return nextElement; + } + // keep looking for nextElement + nextElement = _traversalCallback(); + if (nextElement) { + // we've descended one level so add it to the path + _forks.push_back(Fork(nextElement)); + } + } + } + return nextElement; +} + +void TreeTraversalPath::dump() const { + for (size_t i = 0; i < _forks.size(); ++i) { + std::cout << (int)(_forks[i].getNextIndex()) << "-->"; + } +} + +void TreeTraversalPath::onCompleteTraversal() { + _lastCompletedView = _currentView; + _startOfLastCompletedTraversal = _startOfCurrentTraversal; +} + void EntityTreeSendThread::preDistributionProcessing() { auto node = _node.toStrongRef(); auto nodeData = static_cast(node->getLinkedData()); @@ -80,8 +282,67 @@ void EntityTreeSendThread::preDistributionProcessing() { } } +static size_t adebug = 0; + void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) { + if (viewFrustumChanged) { + ViewFrustum view; + nodeData->copyCurrentViewFrustum(view); + EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); + _path.startNewTraversal(view, root); + + std::cout << "adebug reset view" << std::endl; // adebug + adebug = 0; + } + if (!_path.empty()) { + int32_t numElements = 0; + uint64_t t0 = usecTimestampNow(); + uint64_t now = t0; + + QVector entities; + EntityTreeElementPointer nextElement = _path.getNextElement(); + while (nextElement) { + nextElement->getEntities(_path.getView(), entities); + ++numElements; + + now = usecTimestampNow(); + const uint64_t PARTIAL_TRAVERSAL_TIME_BUDGET = 80; + if (now - t0 > PARTIAL_TRAVERSAL_TIME_BUDGET) { + break; + } + nextElement = _path.getNextElement(); + } + uint64_t dt1 = now - t0; + for (EntityItemPointer& entity : entities) { + PrioritizedEntity entry(entity); + entry.updatePriority(_path.getView()); + if (entry.getPriority() > INVALID_ENTITY_SEND_PRIORITY) { + _sendQueue.push(entry); + } + } + adebug += entities.size(); + std::cout << "adebug traverseTreeAndSendContents totalEntities = " << adebug + << " numElements = " << numElements + << " numEntities = " << entities.size() + << " dt = " << dt1 << std::endl; // adebug + } else if (!_sendQueue.empty()) { + + while (!_sendQueue.empty()) { + PrioritizedEntity entry = _sendQueue.top(); + EntityItemPointer entity = entry.getEntity(); + if (entity) { + std::cout << "adebug traverseTreeAndSendContents() " << entry.getPriority() + << " '" << entity->getName().toStdString() << "'" + << std::endl; // adebug + } + _sendQueue.pop(); + } + // std::priority_queue doesn't have a clear method, + // so we "clear" _sendQueue by setting it equal to an empty queue + _sendQueue = EntityPriorityQueue(); + } + OctreeSendThread::traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); } diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 8bb5c3915d..e81105c977 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -12,11 +12,79 @@ #ifndef hifi_EntityTreeSendThread_h #define hifi_EntityTreeSendThread_h +#include + #include "../octree/OctreeSendThread.h" +#include "EntityTreeElement.h" + class EntityNodeData; class EntityItem; +class PrioritizedEntity { +public: + PrioritizedEntity(EntityItemPointer entity) : _weakEntity(entity) { } + void updatePriority(const ViewFrustum& view); + EntityItemPointer getEntity() const { return _weakEntity.lock(); } + float getPriority() const { return _priority; } + + class Compare { + public: + bool operator() (const PrioritizedEntity& A, const PrioritizedEntity& B) { return A._priority < B._priority; } + }; + + friend class Compare; + +private: + EntityItemWeakPointer _weakEntity; + float _priority { 0.0f }; +}; +using EntityPriorityQueue = std::priority_queue< PrioritizedEntity, std::vector, PrioritizedEntity::Compare >; + +class TreeTraversalPath { +public: + TreeTraversalPath(); + + void startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root); + + EntityTreeElementPointer getNextElement(); + + const ViewFrustum& getView() const { return _currentView; } + + bool empty() const { return _forks.empty(); } + size_t size() const { return _forks.size(); } // adebug + void dump() const; + + class Fork { + public: + Fork(EntityTreeElementPointer& element); + + EntityTreeElementPointer getNextElement(const ViewFrustum& view); + EntityTreeElementPointer getNextElementAgain(const ViewFrustum& view, uint64_t oldTime); + EntityTreeElementPointer getNextElementDelta(const ViewFrustum& newView, const ViewFrustum& oldView, uint64_t oldTime); + int8_t getNextIndex() const { return _nextIndex; } + void initRootNextIndex() { _nextIndex = -1; } + + protected: + EntityTreeElementWeakPointer _weakElement; + int8_t _nextIndex; + }; + +protected: + EntityTreeElementPointer traverseFirstTime(); + EntityTreeElementPointer traverseAgain(); + EntityTreeElementPointer traverseDelta(); + void onCompleteTraversal(); + + ViewFrustum _currentView; + ViewFrustum _lastCompletedView; + std::vector _forks; + std::function _traversalCallback { nullptr }; + uint64_t _startOfLastCompletedTraversal { 0 }; + uint64_t _startOfCurrentTraversal { 0 }; +}; + + class EntityTreeSendThread : public OctreeSendThread { public: @@ -32,6 +100,8 @@ private: bool addAncestorsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); bool addDescendantsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); + TreeTraversalPath _path; + EntityPriorityQueue _sendQueue; }; #endif // hifi_EntityTreeSendThread_h diff --git a/libraries/entities/src/EntityTreeElement.h b/libraries/entities/src/EntityTreeElement.h index aee8c7cfd6..3460a893fb 100644 --- a/libraries/entities/src/EntityTreeElement.h +++ b/libraries/entities/src/EntityTreeElement.h @@ -21,11 +21,12 @@ #include "EntityItem.h" #include "EntityTree.h" -typedef QVector EntityItems; - class EntityTree; class EntityTreeElement; -typedef std::shared_ptr EntityTreeElementPointer; + +using EntityItems = QVector; +using EntityTreeElementWeakPointer = std::weak_ptr; +using EntityTreeElementPointer = std::shared_ptr; class EntityTreeUpdateArgs { public: @@ -173,7 +174,6 @@ public: void setTree(EntityTreePointer tree) { _myTree = tree; } EntityTreePointer getTree() const { return _myTree; } - bool updateEntity(const EntityItem& entity); void addEntityItem(EntityItemPointer entity); EntityItemPointer getClosestEntity(glm::vec3 position) const; From ca470d67b449644fee2a1763761a4fc58e676947 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 26 Jul 2017 08:46:03 -0700 Subject: [PATCH 520/722] fix indentation --- libraries/entities/src/EntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 643145942a..71b119f415 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -2024,7 +2024,7 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPoi _previouslyDeletedActions.insert(actionID, usecTimestampNow()); if (_objectActions.contains(actionID)) { if (!simulation) { - EntityTreeElementPointer element = _element; // use local copy of _element for logic below + EntityTreeElementPointer element = _element; // use local copy of _element for logic below EntityTreePointer entityTree = element ? element->getTree() : nullptr; simulation = entityTree ? entityTree->getSimulation() : nullptr; } From 2b31a746e34082f81dcc4b80a4c2468049203bec Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 26 Jul 2017 08:48:54 -0700 Subject: [PATCH 521/722] add content timestamp for EntityTreeElement --- libraries/entities/src/EntityTreeElement.cpp | 4 ++++ libraries/entities/src/EntityTreeElement.h | 4 ++++ libraries/entities/src/MovingEntitiesOperator.cpp | 2 ++ libraries/entities/src/UpdateEntityOperator.cpp | 2 ++ 4 files changed, 12 insertions(+) diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 4056bbd0b7..bf5780e7cb 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -893,6 +893,7 @@ void EntityTreeElement::cleanupEntities() { } _entityItems.clear(); }); + _lastChangedContent = usecTimestampNow(); } bool EntityTreeElement::removeEntityWithEntityItemID(const EntityItemID& id) { @@ -906,6 +907,7 @@ bool EntityTreeElement::removeEntityWithEntityItemID(const EntityItemID& id) { // NOTE: only EntityTreeElement should ever be changing the value of entity->_element entity->_element = NULL; _entityItems.removeAt(i); + _lastChangedContent = usecTimestampNow(); break; } } @@ -922,6 +924,7 @@ bool EntityTreeElement::removeEntityItem(EntityItemPointer entity) { // NOTE: only EntityTreeElement should ever be changing the value of entity->_element assert(entity->_element.get() == this); entity->_element = NULL; + _lastChangedContent = usecTimestampNow(); return true; } return false; @@ -939,6 +942,7 @@ void EntityTreeElement::addEntityItem(EntityItemPointer entity) { withWriteLock([&] { _entityItems.push_back(entity); }); + _lastChangedContent = usecTimestampNow(); entity->_element = getThisPointer(); } diff --git a/libraries/entities/src/EntityTreeElement.h b/libraries/entities/src/EntityTreeElement.h index 3460a893fb..c7fb80c330 100644 --- a/libraries/entities/src/EntityTreeElement.h +++ b/libraries/entities/src/EntityTreeElement.h @@ -238,10 +238,14 @@ public: return std::static_pointer_cast(shared_from_this()); } + void bumpChangedContent() { _lastChangedContent = usecTimestampNow(); } + uint64_t getLastChangedContent() const { return _lastChangedContent; } + protected: virtual void init(unsigned char * octalCode) override; EntityTreePointer _myTree; EntityItems _entityItems; + uint64_t _lastChangedContent { 0 }; }; #endif // hifi_EntityTreeElement_h diff --git a/libraries/entities/src/MovingEntitiesOperator.cpp b/libraries/entities/src/MovingEntitiesOperator.cpp index ea30ce85f2..cf043dd93e 100644 --- a/libraries/entities/src/MovingEntitiesOperator.cpp +++ b/libraries/entities/src/MovingEntitiesOperator.cpp @@ -192,6 +192,8 @@ bool MovingEntitiesOperator::preRecursion(const OctreeElementPointer& element) { oldElement->removeEntityItem(details.entity); } entityTreeElement->addEntityItem(details.entity); + } else { + entityTreeElement->bumpChangedContent(); } _foundNewCount++; //details.newFound = true; // TODO: would be nice to add this optimization diff --git a/libraries/entities/src/UpdateEntityOperator.cpp b/libraries/entities/src/UpdateEntityOperator.cpp index 7a5c87187a..ea284e6d5b 100644 --- a/libraries/entities/src/UpdateEntityOperator.cpp +++ b/libraries/entities/src/UpdateEntityOperator.cpp @@ -173,6 +173,8 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { if (oldElement != _containingElement) { qCDebug(entities) << "WARNING entity moved during UpdateEntityOperator recursion"; _containingElement->removeEntityItem(_existingEntity); + } else { + _containingElement->bumpChangedContent(); } if (_wantDebug) { From 929d52276e6fa8a108a134dd675a96c3675227f2 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 26 Jul 2017 09:03:57 -0700 Subject: [PATCH 522/722] minor cleanup --- .../src/entities/EntityTreeSendThread.cpp | 13 ++++--------- .../src/entities/EntityTreeSendThread.h | 1 - 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index b971634d7e..34b1c6e123 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -43,7 +43,7 @@ void PrioritizedEntity::updatePriority(const ViewFrustum& view) { } } } else { - // when in doubt just it something positive + // when in doubt give it something positive _priority = 1.0f; } } else { @@ -193,7 +193,8 @@ EntityTreeElementPointer TreeTraversalPath::getNextElement() { _forks.pop_back(); if (_forks.empty()) { // we've traversed the entire tree - onCompleteTraversal(); + _lastCompletedView = _currentView; + _startOfLastCompletedTraversal = _startOfCurrentTraversal; return nextElement; } // keep looking for nextElement @@ -213,11 +214,6 @@ void TreeTraversalPath::dump() const { } } -void TreeTraversalPath::onCompleteTraversal() { - _lastCompletedView = _currentView; - _startOfLastCompletedTraversal = _startOfCurrentTraversal; -} - void EntityTreeSendThread::preDistributionProcessing() { auto node = _node.toStrongRef(); auto nodeData = static_cast(node->getLinkedData()); @@ -307,7 +303,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O ++numElements; now = usecTimestampNow(); - const uint64_t PARTIAL_TRAVERSAL_TIME_BUDGET = 80; + const uint64_t PARTIAL_TRAVERSAL_TIME_BUDGET = 80; // usec if (now - t0 > PARTIAL_TRAVERSAL_TIME_BUDGET) { break; } @@ -327,7 +323,6 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O << " numEntities = " << entities.size() << " dt = " << dt1 << std::endl; // adebug } else if (!_sendQueue.empty()) { - while (!_sendQueue.empty()) { PrioritizedEntity entry = _sendQueue.top(); EntityItemPointer entity = entry.getEntity(); diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index e81105c977..dd072454b9 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -74,7 +74,6 @@ protected: EntityTreeElementPointer traverseFirstTime(); EntityTreeElementPointer traverseAgain(); EntityTreeElementPointer traverseDelta(); - void onCompleteTraversal(); ViewFrustum _currentView; ViewFrustum _lastCompletedView; From 648b8ff0546ecb96e265d2c335bd29bd1bc9edbe Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 26 Jul 2017 13:31:15 -0700 Subject: [PATCH 523/722] fix repeated and differential traversals --- .../src/entities/EntityTreeSendThread.cpp | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 34b1c6e123..ba66c85be3 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -25,9 +25,9 @@ void PrioritizedEntity::updatePriority(const ViewFrustum& view) { bool success; AACube cube = entity->getQueryAACube(success); if (success) { - glm::vec3 center = cube.calcCenter() - view.getPosition(); + glm::vec3 offset = cube.calcCenter() - view.getPosition(); const float MIN_DISTANCE = 0.001f; - float distanceToCenter = glm::length(center) + MIN_DISTANCE; + float distanceToCenter = glm::length(offset) + MIN_DISTANCE; float distance = distanceToCenter; //- 0.5f * cube.getScale(); if (distance < MIN_DISTANCE) { // this object's bounding box overlaps the camera --> give it a big priority @@ -35,8 +35,8 @@ void PrioritizedEntity::updatePriority(const ViewFrustum& view) { } else { // NOTE: we assume view.aspectRatio < 1.0 (view width greater than height) // so we only check against the larger (horizontal) view angle - float front = glm::dot(center, view.getDirection()) / distanceToCenter; - if (front > cosf(view.getFieldOfView()) || distance < view.getCenterRadius()) { + float front = glm::dot(offset, view.getDirection()) / distanceToCenter; + if (front > cosf(glm::radians(view.getFieldOfView())) || distance < view.getCenterRadius()) { _priority = cube.getScale() / distance; // + front; } else { _priority = INVALID_ENTITY_SEND_PRIORITY; @@ -84,15 +84,8 @@ EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementAgain(const View ++_nextIndex; EntityTreeElementPointer element = _weakElement.lock(); assert(element); // should never lose root element - if (element->getLastChanged() < oldTime) { - _nextIndex = NUMBER_OF_CHILDREN; - return EntityTreeElementPointer(); - } - if (element->getLastChanged() > oldTime) { - return element; - } - } - if (_nextIndex < NUMBER_OF_CHILDREN) { + return element; + } else if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); if (element) { while (_nextIndex < NUMBER_OF_CHILDREN) { @@ -115,10 +108,6 @@ EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementDelta(const View ++_nextIndex; EntityTreeElementPointer element = _weakElement.lock(); assert(element); // should never lose root element - if (element->getLastChanged() < oldTime) { - _nextIndex = NUMBER_OF_CHILDREN; - return EntityTreeElementPointer(); - } return element; } else if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); @@ -282,13 +271,14 @@ static size_t adebug = 0; void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) { - if (viewFrustumChanged) { + static int foo=0;++foo;bool verbose=(0==(foo%800)); // adebug + if (viewFrustumChanged || verbose) { ViewFrustum view; nodeData->copyCurrentViewFrustum(view); + std::cout << "adebug reset view" << std::endl; // adebug EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); _path.startNewTraversal(view, root); - std::cout << "adebug reset view" << std::endl; // adebug adebug = 0; } if (!_path.empty()) { @@ -321,6 +311,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O std::cout << "adebug traverseTreeAndSendContents totalEntities = " << adebug << " numElements = " << numElements << " numEntities = " << entities.size() + << " queueSize = " << _sendQueue.size() << " dt = " << dt1 << std::endl; // adebug } else if (!_sendQueue.empty()) { while (!_sendQueue.empty()) { From 481df4938660a447d7a15115742d51144b14139e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 7 Aug 2017 11:13:48 -0700 Subject: [PATCH 524/722] on server: note time of entity edit by remote --- libraries/entities/src/EntityTree.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 518d3bd883..f5fa7f4bdc 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -383,6 +383,9 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti UpdateEntityOperator theOperator(getThisPointer(), containingElement, entity, newQueryAACube); recurseTreeWithOperator(&theOperator); entity->setProperties(properties); + if (getIsServer()) { + entity->updateLastEditedFromRemote(); + } // if the entity has children, run UpdateEntityOperator on them. If the children have children, recurse QQueue toProcess; From 64fa3ec88f0b8967275805e24a9c7c331742b154 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 7 Aug 2017 11:14:40 -0700 Subject: [PATCH 525/722] repeated and differential view traversals work --- .../src/entities/EntityTreeSendThread.cpp | 228 ++++++++++-------- .../src/entities/EntityTreeSendThread.h | 104 ++++---- 2 files changed, 190 insertions(+), 142 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index ba66c85be3..2ecd1c6d7b 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -17,38 +17,60 @@ #include "EntityServer.h" -const float INVALID_ENTITY_SEND_PRIORITY = -1.0e-6f; +const float DO_NOT_SEND = -1.0e-6f; -void PrioritizedEntity::updatePriority(const ViewFrustum& view) { +void TreeTraversalPath::ConicalView::set(const ViewFrustum& viewFrustum) { + // The ConicalView has two parts: a central sphere (same as ViewFrustm) and a circular cone that bounds the frustum part. + // Why? Because approximate intersection tests are much faster to compute for a cone than for a frustum. + _position = viewFrustum.getPosition(); + _direction = viewFrustum.getDirection(); + + // We cache the sin and cos of the half angle of the cone that bounds the frustum. + // (the math here is left as an exercise for the reader) + float A = viewFrustum.getAspectRatio(); + float t = tanf(0.5f * viewFrustum.getFieldOfView()); + _cosAngle = 1.0f / sqrtf(1.0f + (A * A + 1.0f) * (t * t)); + _sinAngle = sqrtf(1.0f - _cosAngle * _cosAngle); + + _radius = viewFrustum.getCenterRadius(); +} + +float TreeTraversalPath::ConicalView::computePriority(const AACube& cube) const { + glm::vec3 p = cube.calcCenter() - _position; // position of bounding sphere in view-frame + float d = glm::length(p); // distance to center of bounding sphere + float r = 0.5f * cube.getScale(); // radius of bounding sphere + if (d < _radius + r) { + return r; + } + if (glm::dot(p, _direction) > sqrtf(d * d - r * r) * _cosAngle - r * _sinAngle) { + const float AVOID_DIVIDE_BY_ZERO = 0.001f; + return r / (d + AVOID_DIVIDE_BY_ZERO); + } + return DO_NOT_SEND; +} + + +// static +float TreeTraversalPath::ConicalView::computePriority(const EntityItemPointer& entity) const { + assert(entity); + bool success; + AACube cube = entity->getQueryAACube(success); + if (success) { + return computePriority(cube); + } else { + // when in doubt give it something positive + return 1.0f; + } +} + +float TreeTraversalPath::PrioritizedEntity::updatePriority(const TreeTraversalPath::ConicalView& conicalView) { EntityItemPointer entity = _weakEntity.lock(); if (entity) { - bool success; - AACube cube = entity->getQueryAACube(success); - if (success) { - glm::vec3 offset = cube.calcCenter() - view.getPosition(); - const float MIN_DISTANCE = 0.001f; - float distanceToCenter = glm::length(offset) + MIN_DISTANCE; - float distance = distanceToCenter; //- 0.5f * cube.getScale(); - if (distance < MIN_DISTANCE) { - // this object's bounding box overlaps the camera --> give it a big priority - _priority = cube.getScale(); - } else { - // NOTE: we assume view.aspectRatio < 1.0 (view width greater than height) - // so we only check against the larger (horizontal) view angle - float front = glm::dot(offset, view.getDirection()) / distanceToCenter; - if (front > cosf(glm::radians(view.getFieldOfView())) || distance < view.getCenterRadius()) { - _priority = cube.getScale() / distance; // + front; - } else { - _priority = INVALID_ENTITY_SEND_PRIORITY; - } - } - } else { - // when in doubt give it something positive - _priority = 1.0f; - } + _priority = conicalView.computePriority(entity); } else { - _priority = INVALID_ENTITY_SEND_PRIORITY; + _priority = DO_NOT_SEND; } + return _priority; } TreeTraversalPath::Fork::Fork(EntityTreeElementPointer& element) : _nextIndex(0) { @@ -56,7 +78,7 @@ TreeTraversalPath::Fork::Fork(EntityTreeElementPointer& element) : _nextIndex(0) _weakElement = element; } -EntityTreeElementPointer TreeTraversalPath::Fork::getNextElement(const ViewFrustum& view) { +EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementFirstTime(const ViewFrustum& view) { if (_nextIndex == -1) { // only get here for the TreeTraversalPath's root Fork at the very beginning of traversal // safe to assume this element is in view @@ -68,7 +90,7 @@ EntityTreeElementPointer TreeTraversalPath::Fork::getNextElement(const ViewFrust while (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); ++_nextIndex; - if (nextElement && ViewFrustum::OUTSIDE != nextElement->computeViewIntersection(view)) { + if (nextElement && view.cubeIntersectsKeyhole(nextElement->getAACube())) { return nextElement; } } @@ -77,7 +99,7 @@ EntityTreeElementPointer TreeTraversalPath::Fork::getNextElement(const ViewFrust return EntityTreeElementPointer(); } -EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementAgain(const ViewFrustum& view, uint64_t oldTime) { +EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementAgain(const ViewFrustum& view, uint64_t lastTime) { if (_nextIndex == -1) { // only get here for the TreeTraversalPath's root Fork at the very beginning of traversal // safe to assume this element is in view @@ -91,8 +113,8 @@ EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementAgain(const View while (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); ++_nextIndex; - if (nextElement && nextElement->getLastChanged() > oldTime && - ViewFrustum::OUTSIDE != nextElement->computeViewIntersection(view)) { + if (nextElement && nextElement->getLastChanged() > lastTime && + nextElement && view.cubeIntersectsKeyhole(nextElement->getAACube())) { return nextElement; } } @@ -101,10 +123,10 @@ EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementAgain(const View return EntityTreeElementPointer(); } -EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementDelta(const ViewFrustum& newView, const ViewFrustum& oldView, uint64_t oldTime) { +EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementDifferential(const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime) { if (_nextIndex == -1) { // only get here for the TreeTraversalPath's root Fork at the very beginning of traversal - // safe to assume this element is in newView + // safe to assume this element is in view ++_nextIndex; EntityTreeElementPointer element = _weakElement.lock(); assert(element); // should never lose root element @@ -116,9 +138,9 @@ EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementDelta(const View EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); ++_nextIndex; if (nextElement && - !(nextElement->getLastChanged() < oldTime && - ViewFrustum::INSIDE == nextElement->computeViewIntersection(oldView)) && - ViewFrustum::OUTSIDE != nextElement->computeViewIntersection(newView)) { + (!(nextElement->getLastChanged() < lastTime && + ViewFrustum::INSIDE == lastView.calculateCubeKeyholeIntersection(nextElement->getAACube()))) && + ViewFrustum::OUTSIDE != view.calculateCubeKeyholeIntersection(nextElement->getAACube())) { return nextElement; } } @@ -130,45 +152,41 @@ EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementDelta(const View TreeTraversalPath::TreeTraversalPath() { const int32_t MIN_PATH_DEPTH = 16; _forks.reserve(MIN_PATH_DEPTH); - _traversalCallback = std::bind(&TreeTraversalPath::traverseFirstTime, this); } -void TreeTraversalPath::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root) { - if (_startOfLastCompletedTraversal == 0) { - _traversalCallback = std::bind(&TreeTraversalPath::traverseFirstTime, this); - _currentView = view; - } else if (_currentView.isVerySimilar(view)) { - _traversalCallback = std::bind(&TreeTraversalPath::traverseAgain, this); +void TreeTraversalPath::startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root) { + if (_startOfCompletedTraversal == 0) { + _currentView = viewFrustum; + _getNextElementCallback = [&]() { + return _forks.back().getNextElementFirstTime(_currentView); + }; + + } else if (_currentView.isVerySimilar(viewFrustum)) { + _getNextElementCallback = [&]() { + return _forks.back().getNextElementAgain(_currentView, _startOfCompletedTraversal); + }; } else { - _currentView = view; - _traversalCallback = std::bind(&TreeTraversalPath::traverseDelta, this); + _currentView = viewFrustum; + + _getNextElementCallback = [&]() { + return _forks.back().getNextElementDifferential(_currentView, _completedView, _startOfCompletedTraversal); + }; } + _forks.clear(); - if (root) { - _forks.push_back(Fork(root)); - // set root fork's index such that root element returned at getNextElement() - _forks.back().initRootNextIndex(); - } + assert(root); + _forks.push_back(Fork(root)); + // set root fork's index such that root element returned at getNextElement() + _forks.back().initRootNextIndex(); + _startOfCurrentTraversal = usecTimestampNow(); } -EntityTreeElementPointer TreeTraversalPath::traverseFirstTime() { - return _forks.back().getNextElement(_currentView); -} - -EntityTreeElementPointer TreeTraversalPath::traverseAgain() { - return _forks.back().getNextElementAgain(_currentView, _startOfLastCompletedTraversal); -} - -EntityTreeElementPointer TreeTraversalPath::traverseDelta() { - return _forks.back().getNextElementDelta(_currentView, _lastCompletedView, _startOfLastCompletedTraversal); -} - EntityTreeElementPointer TreeTraversalPath::getNextElement() { - if (_forks.empty() || !_traversalCallback) { + if (_forks.empty()) { return EntityTreeElementPointer(); } - EntityTreeElementPointer nextElement = _traversalCallback(); + EntityTreeElementPointer nextElement = _getNextElementCallback(); if (nextElement) { int8_t nextIndex = _forks.back().getNextIndex(); if (nextIndex > 0) { @@ -182,12 +200,12 @@ EntityTreeElementPointer TreeTraversalPath::getNextElement() { _forks.pop_back(); if (_forks.empty()) { // we've traversed the entire tree - _lastCompletedView = _currentView; - _startOfLastCompletedTraversal = _startOfCurrentTraversal; + _completedView = _currentView; + _startOfCompletedTraversal = _startOfCurrentTraversal; return nextElement; } // keep looking for nextElement - nextElement = _traversalCallback(); + nextElement = _getNextElementCallback(); if (nextElement) { // we've descended one level so add it to the path _forks.push_back(Fork(nextElement)); @@ -199,7 +217,7 @@ EntityTreeElementPointer TreeTraversalPath::getNextElement() { void TreeTraversalPath::dump() const { for (size_t i = 0; i < _forks.size(); ++i) { - std::cout << (int)(_forks[i].getNextIndex()) << "-->"; + std::cout << (int32_t)(_forks[i].getNextIndex()) << "-->"; } } @@ -267,66 +285,74 @@ void EntityTreeSendThread::preDistributionProcessing() { } } -static size_t adebug = 0; - void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) { - static int foo=0;++foo;bool verbose=(0==(foo%800)); // adebug - if (viewFrustumChanged || verbose) { - ViewFrustum view; - nodeData->copyCurrentViewFrustum(view); - std::cout << "adebug reset view" << std::endl; // adebug - EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); - _path.startNewTraversal(view, root); - - adebug = 0; + if (nodeData->getUsesFrustum()) { + if (viewFrustumChanged) { + ViewFrustum viewFrustum; + nodeData->copyCurrentViewFrustum(viewFrustum); + EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); + _path.startNewTraversal(viewFrustum, root); + } } if (!_path.empty()) { int32_t numElements = 0; uint64_t t0 = usecTimestampNow(); uint64_t now = t0; - QVector entities; + uint32_t numEntities = 0; EntityTreeElementPointer nextElement = _path.getNextElement(); + const ViewFrustum& currentView = _path.getCurrentView(); + TreeTraversalPath::ConicalView conicalView(currentView); while (nextElement) { - nextElement->getEntities(_path.getView(), entities); + nextElement->forEachEntity([&](EntityItemPointer entity) { + ++numEntities; + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (currentView.cubeIntersectsKeyhole(cube)) { + float priority = conicalView.computePriority(cube); + _sendQueue.push(TreeTraversalPath::PrioritizedEntity(entity, priority)); + std::cout << "adebug '" << entity->getName().toStdString() << "' send = " << (priority != DO_NOT_SEND) << std::endl; // adebug + } else { + std::cout << "adebug '" << entity->getName().toStdString() << "' out of view" << std::endl; // adebug + } + } else { + const float WHEN_IN_DOUBT_PRIORITY = 1.0f; + _sendQueue.push(TreeTraversalPath::PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + } + }); ++numElements; now = usecTimestampNow(); - const uint64_t PARTIAL_TRAVERSAL_TIME_BUDGET = 80; // usec + const uint64_t PARTIAL_TRAVERSAL_TIME_BUDGET = 100000; // usec if (now - t0 > PARTIAL_TRAVERSAL_TIME_BUDGET) { break; } nextElement = _path.getNextElement(); } uint64_t dt1 = now - t0; - for (EntityItemPointer& entity : entities) { - PrioritizedEntity entry(entity); - entry.updatePriority(_path.getView()); - if (entry.getPriority() > INVALID_ENTITY_SEND_PRIORITY) { - _sendQueue.push(entry); - } - } - adebug += entities.size(); - std::cout << "adebug traverseTreeAndSendContents totalEntities = " << adebug - << " numElements = " << numElements - << " numEntities = " << entities.size() - << " queueSize = " << _sendQueue.size() - << " dt = " << dt1 << std::endl; // adebug - } else if (!_sendQueue.empty()) { + + //} else if (!_sendQueue.empty()) { + size_t sendQueueSize = _sendQueue.size(); while (!_sendQueue.empty()) { - PrioritizedEntity entry = _sendQueue.top(); + TreeTraversalPath::PrioritizedEntity entry = _sendQueue.top(); EntityItemPointer entity = entry.getEntity(); if (entity) { - std::cout << "adebug traverseTreeAndSendContents() " << entry.getPriority() - << " '" << entity->getName().toStdString() << "'" - << std::endl; // adebug + std::cout << "adebug '" << entity->getName().toStdString() << "'" + << " : " << entry.getPriority() << std::endl; // adebug } _sendQueue.pop(); } // std::priority_queue doesn't have a clear method, // so we "clear" _sendQueue by setting it equal to an empty queue _sendQueue = EntityPriorityQueue(); + std::cout << "adebug -end" + << " E = " << numElements + << " e = " << numEntities + << " Q = " << sendQueueSize + << " dt = " << dt1 << std::endl; // adebug + std::cout << "adebug" << std::endl; // adebug } OctreeSendThread::traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index dd072454b9..a7ddf7daa1 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -16,36 +16,71 @@ #include "../octree/OctreeSendThread.h" +#include + #include "EntityTreeElement.h" +const float SQRT_TWO_OVER_TWO = 0.7071067811865; +const float DEFAULT_VIEW_RADIUS = 10.0f; + class EntityNodeData; class EntityItem; -class PrioritizedEntity { -public: - PrioritizedEntity(EntityItemPointer entity) : _weakEntity(entity) { } - void updatePriority(const ViewFrustum& view); - EntityItemPointer getEntity() const { return _weakEntity.lock(); } - float getPriority() const { return _priority; } - - class Compare { - public: - bool operator() (const PrioritizedEntity& A, const PrioritizedEntity& B) { return A._priority < B._priority; } - }; - - friend class Compare; - -private: - EntityItemWeakPointer _weakEntity; - float _priority { 0.0f }; -}; -using EntityPriorityQueue = std::priority_queue< PrioritizedEntity, std::vector, PrioritizedEntity::Compare >; class TreeTraversalPath { public: + class ConicalView { + public: + ConicalView() {} + ConicalView(const ViewFrustum& viewFrustum) { set(viewFrustum); } + void set(const ViewFrustum& viewFrustum); + float computePriority(const AACube& cube) const; + float computePriority(const EntityItemPointer& entity) const; + private: + glm::vec3 _position { 0.0f, 0.0f, 0.0f }; + glm::vec3 _direction { 0.0f, 0.0f, 1.0f }; + float _sinAngle { SQRT_TWO_OVER_TWO }; + float _cosAngle { SQRT_TWO_OVER_TWO }; + float _radius { DEFAULT_VIEW_RADIUS }; + }; + + class PrioritizedEntity { + public: + PrioritizedEntity(EntityItemPointer entity, float priority) : _weakEntity(entity), _priority(priority) { } + float updatePriority(const ConicalView& view); + EntityItemPointer getEntity() const { return _weakEntity.lock(); } + float getPriority() const { return _priority; } + + class Compare { + public: + bool operator() (const PrioritizedEntity& A, const PrioritizedEntity& B) { return A._priority < B._priority; } + }; + friend class Compare; + + private: + EntityItemWeakPointer _weakEntity; + float _priority; + }; + + class Fork { + public: + Fork(EntityTreeElementPointer& element); + + EntityTreeElementPointer getNextElementFirstTime(const ViewFrustum& view); + EntityTreeElementPointer getNextElementAgain(const ViewFrustum& view, uint64_t lastTime); + EntityTreeElementPointer getNextElementDifferential(const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime); + + int8_t getNextIndex() const { return _nextIndex; } + void initRootNextIndex() { _nextIndex = -1; } + + protected: + EntityTreeElementWeakPointer _weakElement; + int8_t _nextIndex; + }; + TreeTraversalPath(); - void startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root); + void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root); EntityTreeElementPointer getNextElement(); @@ -55,34 +90,21 @@ public: size_t size() const { return _forks.size(); } // adebug void dump() const; - class Fork { - public: - Fork(EntityTreeElementPointer& element); - - EntityTreeElementPointer getNextElement(const ViewFrustum& view); - EntityTreeElementPointer getNextElementAgain(const ViewFrustum& view, uint64_t oldTime); - EntityTreeElementPointer getNextElementDelta(const ViewFrustum& newView, const ViewFrustum& oldView, uint64_t oldTime); - int8_t getNextIndex() const { return _nextIndex; } - void initRootNextIndex() { _nextIndex = -1; } - - protected: - EntityTreeElementWeakPointer _weakElement; - int8_t _nextIndex; - }; + const ViewFrustum& getCurrentView() const { return _currentView; } + //float computePriority(EntityItemPointer& entity) const { return _computePriorityCallback(entity); } protected: - EntityTreeElementPointer traverseFirstTime(); - EntityTreeElementPointer traverseAgain(); - EntityTreeElementPointer traverseDelta(); - ViewFrustum _currentView; - ViewFrustum _lastCompletedView; + ViewFrustum _completedView; std::vector _forks; - std::function _traversalCallback { nullptr }; - uint64_t _startOfLastCompletedTraversal { 0 }; + std::function _getNextElementCallback { nullptr }; + //std::function _computePriorityCallback { nullptr }; + uint64_t _startOfCompletedTraversal { 0 }; uint64_t _startOfCurrentTraversal { 0 }; }; +using EntityPriorityQueue = std::priority_queue< TreeTraversalPath::PrioritizedEntity, std::vector, TreeTraversalPath::PrioritizedEntity::Compare >; + class EntityTreeSendThread : public OctreeSendThread { From bf27412091f81d5e4c3d6fbfc0ae2608d5c83e53 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 7 Aug 2017 12:00:10 -0700 Subject: [PATCH 526/722] cleanup --- .../src/entities/EntityTreeSendThread.cpp | 11 ++--------- assignment-client/src/entities/EntityTreeSendThread.h | 2 -- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 2ecd1c6d7b..3a206dae27 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -167,7 +167,6 @@ void TreeTraversalPath::startNewTraversal(const ViewFrustum& viewFrustum, Entity }; } else { _currentView = viewFrustum; - _getNextElementCallback = [&]() { return _forks.back().getNextElementDifferential(_currentView, _completedView, _startOfCompletedTraversal); }; @@ -296,17 +295,14 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O } } if (!_path.empty()) { - int32_t numElements = 0; uint64_t t0 = usecTimestampNow(); uint64_t now = t0; - uint32_t numEntities = 0; - EntityTreeElementPointer nextElement = _path.getNextElement(); const ViewFrustum& currentView = _path.getCurrentView(); TreeTraversalPath::ConicalView conicalView(currentView); + EntityTreeElementPointer nextElement = _path.getNextElement(); while (nextElement) { nextElement->forEachEntity([&](EntityItemPointer entity) { - ++numEntities; bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { @@ -322,7 +318,6 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O _sendQueue.push(TreeTraversalPath::PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); } }); - ++numElements; now = usecTimestampNow(); const uint64_t PARTIAL_TRAVERSAL_TIME_BUDGET = 100000; // usec @@ -348,9 +343,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O // so we "clear" _sendQueue by setting it equal to an empty queue _sendQueue = EntityPriorityQueue(); std::cout << "adebug -end" - << " E = " << numElements - << " e = " << numEntities - << " Q = " << sendQueueSize + << " Q.size = " << sendQueueSize << " dt = " << dt1 << std::endl; // adebug std::cout << "adebug" << std::endl; // adebug } diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index a7ddf7daa1..0e41f032a9 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -91,14 +91,12 @@ public: void dump() const; const ViewFrustum& getCurrentView() const { return _currentView; } - //float computePriority(EntityItemPointer& entity) const { return _computePriorityCallback(entity); } protected: ViewFrustum _currentView; ViewFrustum _completedView; std::vector _forks; std::function _getNextElementCallback { nullptr }; - //std::function _computePriorityCallback { nullptr }; uint64_t _startOfCompletedTraversal { 0 }; uint64_t _startOfCurrentTraversal { 0 }; }; From 91908ca3da759b0f691e4788149099b7e311cb31 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 7 Aug 2017 13:33:28 -0700 Subject: [PATCH 527/722] moved TreePathTraversal logic into EntityTreeSendThread --- .../src/entities/EntityTreeSendThread.cpp | 174 +++++++++--------- .../src/entities/EntityTreeSendThread.h | 133 ++++++------- 2 files changed, 144 insertions(+), 163 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 3a206dae27..8d2d620620 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -19,7 +19,7 @@ const float DO_NOT_SEND = -1.0e-6f; -void TreeTraversalPath::ConicalView::set(const ViewFrustum& viewFrustum) { +void ConicalView::set(const ViewFrustum& viewFrustum) { // The ConicalView has two parts: a central sphere (same as ViewFrustm) and a circular cone that bounds the frustum part. // Why? Because approximate intersection tests are much faster to compute for a cone than for a frustum. _position = viewFrustum.getPosition(); @@ -35,7 +35,7 @@ void TreeTraversalPath::ConicalView::set(const ViewFrustum& viewFrustum) { _radius = viewFrustum.getCenterRadius(); } -float TreeTraversalPath::ConicalView::computePriority(const AACube& cube) const { +float ConicalView::computePriority(const AACube& cube) const { glm::vec3 p = cube.calcCenter() - _position; // position of bounding sphere in view-frame float d = glm::length(p); // distance to center of bounding sphere float r = 0.5f * cube.getScale(); // radius of bounding sphere @@ -51,7 +51,7 @@ float TreeTraversalPath::ConicalView::computePriority(const AACube& cube) const // static -float TreeTraversalPath::ConicalView::computePriority(const EntityItemPointer& entity) const { +float ConicalView::computePriority(const EntityItemPointer& entity) const { assert(entity); bool success; AACube cube = entity->getQueryAACube(success); @@ -63,7 +63,7 @@ float TreeTraversalPath::ConicalView::computePriority(const EntityItemPointer& e } } -float TreeTraversalPath::PrioritizedEntity::updatePriority(const TreeTraversalPath::ConicalView& conicalView) { +float PrioritizedEntity::updatePriority(const ConicalView& conicalView) { EntityItemPointer entity = _weakEntity.lock(); if (entity) { _priority = conicalView.computePriority(entity); @@ -73,14 +73,14 @@ float TreeTraversalPath::PrioritizedEntity::updatePriority(const TreeTraversalPa return _priority; } -TreeTraversalPath::Fork::Fork(EntityTreeElementPointer& element) : _nextIndex(0) { +Fork::Fork(EntityTreeElementPointer& element) : _nextIndex(0) { assert(element); _weakElement = element; } -EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementFirstTime(const ViewFrustum& view) { +EntityTreeElementPointer Fork::getNextElementFirstTime(const ViewFrustum& view) { if (_nextIndex == -1) { - // only get here for the TreeTraversalPath's root Fork at the very beginning of traversal + // only get here for the root Fork at the very beginning of traversal // safe to assume this element is in view ++_nextIndex; return _weakElement.lock(); @@ -99,9 +99,9 @@ EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementFirstTime(const return EntityTreeElementPointer(); } -EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementAgain(const ViewFrustum& view, uint64_t lastTime) { +EntityTreeElementPointer Fork::getNextElementAgain(const ViewFrustum& view, uint64_t lastTime) { if (_nextIndex == -1) { - // only get here for the TreeTraversalPath's root Fork at the very beginning of traversal + // only get here for the root Fork at the very beginning of traversal // safe to assume this element is in view ++_nextIndex; EntityTreeElementPointer element = _weakElement.lock(); @@ -123,9 +123,9 @@ EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementAgain(const View return EntityTreeElementPointer(); } -EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementDifferential(const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime) { +EntityTreeElementPointer Fork::getNextElementDifferential(const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime) { if (_nextIndex == -1) { - // only get here for the TreeTraversalPath's root Fork at the very beginning of traversal + // only get here for the root Fork at the very beginning of traversal // safe to assume this element is in view ++_nextIndex; EntityTreeElementPointer element = _weakElement.lock(); @@ -149,77 +149,12 @@ EntityTreeElementPointer TreeTraversalPath::Fork::getNextElementDifferential(con return EntityTreeElementPointer(); } -TreeTraversalPath::TreeTraversalPath() { +EntityTreeSendThread::EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) + : OctreeSendThread(myServer, node) { const int32_t MIN_PATH_DEPTH = 16; _forks.reserve(MIN_PATH_DEPTH); } -void TreeTraversalPath::startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root) { - if (_startOfCompletedTraversal == 0) { - _currentView = viewFrustum; - _getNextElementCallback = [&]() { - return _forks.back().getNextElementFirstTime(_currentView); - }; - - } else if (_currentView.isVerySimilar(viewFrustum)) { - _getNextElementCallback = [&]() { - return _forks.back().getNextElementAgain(_currentView, _startOfCompletedTraversal); - }; - } else { - _currentView = viewFrustum; - _getNextElementCallback = [&]() { - return _forks.back().getNextElementDifferential(_currentView, _completedView, _startOfCompletedTraversal); - }; - } - - _forks.clear(); - assert(root); - _forks.push_back(Fork(root)); - // set root fork's index such that root element returned at getNextElement() - _forks.back().initRootNextIndex(); - - _startOfCurrentTraversal = usecTimestampNow(); -} - -EntityTreeElementPointer TreeTraversalPath::getNextElement() { - if (_forks.empty()) { - return EntityTreeElementPointer(); - } - EntityTreeElementPointer nextElement = _getNextElementCallback(); - if (nextElement) { - int8_t nextIndex = _forks.back().getNextIndex(); - if (nextIndex > 0) { - // nextElement needs to be added to the path - _forks.push_back(Fork(nextElement)); - } - } else { - // we're done at this level - while (!nextElement) { - // pop one level - _forks.pop_back(); - if (_forks.empty()) { - // we've traversed the entire tree - _completedView = _currentView; - _startOfCompletedTraversal = _startOfCurrentTraversal; - return nextElement; - } - // keep looking for nextElement - nextElement = _getNextElementCallback(); - if (nextElement) { - // we've descended one level so add it to the path - _forks.push_back(Fork(nextElement)); - } - } - } - return nextElement; -} - -void TreeTraversalPath::dump() const { - for (size_t i = 0; i < _forks.size(); ++i) { - std::cout << (int32_t)(_forks[i].getNextIndex()) << "-->"; - } -} - void EntityTreeSendThread::preDistributionProcessing() { auto node = _node.toStrongRef(); auto nodeData = static_cast(node->getLinkedData()); @@ -291,31 +226,30 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O ViewFrustum viewFrustum; nodeData->copyCurrentViewFrustum(viewFrustum); EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); - _path.startNewTraversal(viewFrustum, root); + startNewTraversal(viewFrustum, root); } } - if (!_path.empty()) { + if (!_forks.empty()) { uint64_t t0 = usecTimestampNow(); uint64_t now = t0; - const ViewFrustum& currentView = _path.getCurrentView(); - TreeTraversalPath::ConicalView conicalView(currentView); - EntityTreeElementPointer nextElement = _path.getNextElement(); + ConicalView conicalView(_currentView); + EntityTreeElementPointer nextElement = getNextElement(); while (nextElement) { nextElement->forEachEntity([&](EntityItemPointer entity) { bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { - if (currentView.cubeIntersectsKeyhole(cube)) { + if (_currentView.cubeIntersectsKeyhole(cube)) { float priority = conicalView.computePriority(cube); - _sendQueue.push(TreeTraversalPath::PrioritizedEntity(entity, priority)); + _sendQueue.push(PrioritizedEntity(entity, priority)); std::cout << "adebug '" << entity->getName().toStdString() << "' send = " << (priority != DO_NOT_SEND) << std::endl; // adebug } else { std::cout << "adebug '" << entity->getName().toStdString() << "' out of view" << std::endl; // adebug } } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; - _sendQueue.push(TreeTraversalPath::PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); } }); @@ -324,14 +258,14 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O if (now - t0 > PARTIAL_TRAVERSAL_TIME_BUDGET) { break; } - nextElement = _path.getNextElement(); + nextElement = getNextElement(); } uint64_t dt1 = now - t0; //} else if (!_sendQueue.empty()) { size_t sendQueueSize = _sendQueue.size(); while (!_sendQueue.empty()) { - TreeTraversalPath::PrioritizedEntity entry = _sendQueue.top(); + PrioritizedEntity entry = _sendQueue.top(); EntityItemPointer entity = entry.getEntity(); if (entity) { std::cout << "adebug '" << entity->getName().toStdString() << "'" @@ -400,4 +334,68 @@ bool EntityTreeSendThread::addDescendantsToExtraFlaggedEntities(const QUuid& fil return hasNewChild || hasNewDescendants; } +void EntityTreeSendThread::startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root) { + if (_startOfCompletedTraversal == 0) { + _currentView = viewFrustum; + _getNextElementCallback = [&]() { + return _forks.back().getNextElementFirstTime(_currentView); + }; + } else if (_currentView.isVerySimilar(viewFrustum)) { + _getNextElementCallback = [&]() { + return _forks.back().getNextElementAgain(_currentView, _startOfCompletedTraversal); + }; + } else { + _currentView = viewFrustum; + _getNextElementCallback = [&]() { + return _forks.back().getNextElementDifferential(_currentView, _completedView, _startOfCompletedTraversal); + }; + } + + _forks.clear(); + assert(root); + _forks.push_back(Fork(root)); + // set root fork's index such that root element returned at getNextElement() + _forks.back().initRootNextIndex(); + + _startOfCurrentTraversal = usecTimestampNow(); +} + +EntityTreeElementPointer EntityTreeSendThread::getNextElement() { + if (_forks.empty()) { + return EntityTreeElementPointer(); + } + EntityTreeElementPointer nextElement = _getNextElementCallback(); + if (nextElement) { + int8_t nextIndex = _forks.back().getNextIndex(); + if (nextIndex > 0) { + // nextElement needs to be added to the path + _forks.push_back(Fork(nextElement)); + } + } else { + // we're done at this level + while (!nextElement) { + // pop one level + _forks.pop_back(); + if (_forks.empty()) { + // we've traversed the entire tree + _completedView = _currentView; + _startOfCompletedTraversal = _startOfCurrentTraversal; + return nextElement; + } + // keep looking for nextElement + nextElement = _getNextElementCallback(); + if (nextElement) { + // we've descended one level so add it to the path + _forks.push_back(Fork(nextElement)); + } + } + } + return nextElement; +} + +void EntityTreeSendThread::dump() const { + for (size_t i = 0; i < _forks.size(); ++i) { + std::cout << (int32_t)(_forks[i].getNextIndex()) << "-->"; + } +} diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 0e41f032a9..e9dfa5513d 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -27,87 +27,61 @@ class EntityNodeData; class EntityItem; -class TreeTraversalPath { +class ConicalView { public: - class ConicalView { - public: - ConicalView() {} - ConicalView(const ViewFrustum& viewFrustum) { set(viewFrustum); } - void set(const ViewFrustum& viewFrustum); - float computePriority(const AACube& cube) const; - float computePriority(const EntityItemPointer& entity) const; - private: - glm::vec3 _position { 0.0f, 0.0f, 0.0f }; - glm::vec3 _direction { 0.0f, 0.0f, 1.0f }; - float _sinAngle { SQRT_TWO_OVER_TWO }; - float _cosAngle { SQRT_TWO_OVER_TWO }; - float _radius { DEFAULT_VIEW_RADIUS }; - }; - - class PrioritizedEntity { - public: - PrioritizedEntity(EntityItemPointer entity, float priority) : _weakEntity(entity), _priority(priority) { } - float updatePriority(const ConicalView& view); - EntityItemPointer getEntity() const { return _weakEntity.lock(); } - float getPriority() const { return _priority; } - - class Compare { - public: - bool operator() (const PrioritizedEntity& A, const PrioritizedEntity& B) { return A._priority < B._priority; } - }; - friend class Compare; - - private: - EntityItemWeakPointer _weakEntity; - float _priority; - }; - - class Fork { - public: - Fork(EntityTreeElementPointer& element); - - EntityTreeElementPointer getNextElementFirstTime(const ViewFrustum& view); - EntityTreeElementPointer getNextElementAgain(const ViewFrustum& view, uint64_t lastTime); - EntityTreeElementPointer getNextElementDifferential(const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime); - - int8_t getNextIndex() const { return _nextIndex; } - void initRootNextIndex() { _nextIndex = -1; } - - protected: - EntityTreeElementWeakPointer _weakElement; - int8_t _nextIndex; - }; - - TreeTraversalPath(); - - void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root); - - EntityTreeElementPointer getNextElement(); - - const ViewFrustum& getView() const { return _currentView; } - - bool empty() const { return _forks.empty(); } - size_t size() const { return _forks.size(); } // adebug - void dump() const; - - const ViewFrustum& getCurrentView() const { return _currentView; } - -protected: - ViewFrustum _currentView; - ViewFrustum _completedView; - std::vector _forks; - std::function _getNextElementCallback { nullptr }; - uint64_t _startOfCompletedTraversal { 0 }; - uint64_t _startOfCurrentTraversal { 0 }; + ConicalView() {} + ConicalView(const ViewFrustum& viewFrustum) { set(viewFrustum); } + void set(const ViewFrustum& viewFrustum); + float computePriority(const AACube& cube) const; + float computePriority(const EntityItemPointer& entity) const; +private: + glm::vec3 _position { 0.0f, 0.0f, 0.0f }; + glm::vec3 _direction { 0.0f, 0.0f, 1.0f }; + float _sinAngle { SQRT_TWO_OVER_TWO }; + float _cosAngle { SQRT_TWO_OVER_TWO }; + float _radius { DEFAULT_VIEW_RADIUS }; }; -using EntityPriorityQueue = std::priority_queue< TreeTraversalPath::PrioritizedEntity, std::vector, TreeTraversalPath::PrioritizedEntity::Compare >; +class PrioritizedEntity { +public: + PrioritizedEntity(EntityItemPointer entity, float priority) : _weakEntity(entity), _priority(priority) { } + float updatePriority(const ConicalView& view); + EntityItemPointer getEntity() const { return _weakEntity.lock(); } + float getPriority() const { return _priority; } + + class Compare { + public: + bool operator() (const PrioritizedEntity& A, const PrioritizedEntity& B) { return A._priority < B._priority; } + }; + friend class Compare; + +private: + EntityItemWeakPointer _weakEntity; + float _priority; +}; + +class Fork { +public: + Fork(EntityTreeElementPointer& element); + + EntityTreeElementPointer getNextElementFirstTime(const ViewFrustum& view); + EntityTreeElementPointer getNextElementAgain(const ViewFrustum& view, uint64_t lastTime); + EntityTreeElementPointer getNextElementDifferential(const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime); + + int8_t getNextIndex() const { return _nextIndex; } + void initRootNextIndex() { _nextIndex = -1; } + +protected: + EntityTreeElementWeakPointer _weakElement; + int8_t _nextIndex; +}; + +using EntityPriorityQueue = std::priority_queue< PrioritizedEntity, std::vector, PrioritizedEntity::Compare >; class EntityTreeSendThread : public OctreeSendThread { - public: - EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) : OctreeSendThread(myServer, node) {}; + EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node); protected: void preDistributionProcessing() override; @@ -119,8 +93,17 @@ private: bool addAncestorsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); bool addDescendantsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); - TreeTraversalPath _path; + void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root); + EntityTreeElementPointer getNextElement(); + void dump() const; + EntityPriorityQueue _sendQueue; + ViewFrustum _currentView; + ViewFrustum _completedView; + std::vector _forks; + std::function _getNextElementCallback { nullptr }; + uint64_t _startOfCompletedTraversal { 0 }; + uint64_t _startOfCurrentTraversal { 0 }; }; #endif // hifi_EntityTreeSendThread_h From dd1febba2f33b5ce5ea6bc0ce78fac4b6d597699 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 8 Aug 2017 12:18:37 -0700 Subject: [PATCH 528/722] add missing bump to element changed content --- libraries/entities/src/UpdateEntityOperator.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/entities/src/UpdateEntityOperator.cpp b/libraries/entities/src/UpdateEntityOperator.cpp index ea284e6d5b..fa7e5ca38f 100644 --- a/libraries/entities/src/UpdateEntityOperator.cpp +++ b/libraries/entities/src/UpdateEntityOperator.cpp @@ -213,6 +213,7 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { if (_wantDebug) { qCDebug(entities) << " *** This is the same OLD ELEMENT ***"; } + _containingElement->bumpChangedContent(); } else { // otherwise, this is an add case. if (oldElement) { From 3665a3fbee844ae1c3f064d0c597c465a9eb3d76 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 8 Aug 2017 12:18:53 -0700 Subject: [PATCH 529/722] libraries/entities/src/EntityTreeElement.cpp --- libraries/entities/src/EntityTreeElement.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index bf5780e7cb..f6d27bcc87 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -893,7 +893,7 @@ void EntityTreeElement::cleanupEntities() { } _entityItems.clear(); }); - _lastChangedContent = usecTimestampNow(); + bumpChangedContent(); } bool EntityTreeElement::removeEntityWithEntityItemID(const EntityItemID& id) { @@ -907,7 +907,7 @@ bool EntityTreeElement::removeEntityWithEntityItemID(const EntityItemID& id) { // NOTE: only EntityTreeElement should ever be changing the value of entity->_element entity->_element = NULL; _entityItems.removeAt(i); - _lastChangedContent = usecTimestampNow(); + bumpChangedContent(); break; } } @@ -924,7 +924,7 @@ bool EntityTreeElement::removeEntityItem(EntityItemPointer entity) { // NOTE: only EntityTreeElement should ever be changing the value of entity->_element assert(entity->_element.get() == this); entity->_element = NULL; - _lastChangedContent = usecTimestampNow(); + bumpChangedContent(); return true; } return false; @@ -942,7 +942,7 @@ void EntityTreeElement::addEntityItem(EntityItemPointer entity) { withWriteLock([&] { _entityItems.push_back(entity); }); - _lastChangedContent = usecTimestampNow(); + bumpChangedContent(); entity->_element = getThisPointer(); } From 8d535f9c5a0a9cbcb2430a4b45df7467ec2c03f5 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 8 Aug 2017 12:19:12 -0700 Subject: [PATCH 530/722] remove bump to changeFromRemote for server case (revert) --- libraries/entities/src/EntityTree.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index f5fa7f4bdc..518d3bd883 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -383,9 +383,6 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti UpdateEntityOperator theOperator(getThisPointer(), containingElement, entity, newQueryAACube); recurseTreeWithOperator(&theOperator); entity->setProperties(properties); - if (getIsServer()) { - entity->updateLastEditedFromRemote(); - } // if the entity has children, run UpdateEntityOperator on them. If the children have children, recurse QQueue toProcess; From a4564f89d79efb5e8408e193250f142c2c5d45b2 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 8 Aug 2017 12:20:19 -0700 Subject: [PATCH 531/722] traversals work and cull checks of unchanged content --- .../src/entities/EntityTreeSendThread.cpp | 271 ++++++++++++------ .../src/entities/EntityTreeSendThread.h | 22 +- 2 files changed, 197 insertions(+), 96 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 8d2d620620..54f2288491 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -78,12 +78,14 @@ Fork::Fork(EntityTreeElementPointer& element) : _nextIndex(0) { _weakElement = element; } -EntityTreeElementPointer Fork::getNextElementFirstTime(const ViewFrustum& view) { +void Fork::getNextVisibleElementFirstTime(VisibleElement& next, const ViewFrustum& view) { + // NOTE: no need to set next.intersection in the "FirstTime" context if (_nextIndex == -1) { // only get here for the root Fork at the very beginning of traversal - // safe to assume this element is in view + // safe to assume this element intersects view ++_nextIndex; - return _weakElement.lock(); + next.element = _weakElement.lock(); + return; } else if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); if (element) { @@ -91,68 +93,93 @@ EntityTreeElementPointer Fork::getNextElementFirstTime(const ViewFrustum& view) EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); ++_nextIndex; if (nextElement && view.cubeIntersectsKeyhole(nextElement->getAACube())) { - return nextElement; + next.element = nextElement; + return; } } } } - return EntityTreeElementPointer(); + next.element.reset(); } -EntityTreeElementPointer Fork::getNextElementAgain(const ViewFrustum& view, uint64_t lastTime) { +void Fork::getNextVisibleElementAgain(VisibleElement& next, const ViewFrustum& view, uint64_t lastTime) { if (_nextIndex == -1) { // only get here for the root Fork at the very beginning of traversal - // safe to assume this element is in view + // safe to assume this element intersects view ++_nextIndex; EntityTreeElementPointer element = _weakElement.lock(); - assert(element); // should never lose root element - return element; - } else if (_nextIndex < NUMBER_OF_CHILDREN) { + // root case is special: its intersection is always INTERSECT + // and we can skip it if the content hasn't changed + if (element->getLastChangedContent() > lastTime) { + next.element = element; + next.intersection = ViewFrustum::INTERSECT; + return; + } + } + if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); if (element) { while (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); ++_nextIndex; - if (nextElement && nextElement->getLastChanged() > lastTime && - nextElement && view.cubeIntersectsKeyhole(nextElement->getAACube())) { - return nextElement; + if (nextElement && nextElement->getLastChanged() > lastTime) { + ViewFrustum::intersection intersection = view.calculateCubeKeyholeIntersection(nextElement->getAACube()); + if (intersection != ViewFrustum::OUTSIDE) { + next.element = nextElement; + next.intersection = intersection; + return; + } } } } } - return EntityTreeElementPointer(); + next.element.reset(); + next.intersection = ViewFrustum::OUTSIDE; } -EntityTreeElementPointer Fork::getNextElementDifferential(const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime) { +void Fork::getNextVisibleElementDifferential(VisibleElement& next, + const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime) { if (_nextIndex == -1) { // only get here for the root Fork at the very beginning of traversal - // safe to assume this element is in view + // safe to assume this element intersects view ++_nextIndex; EntityTreeElementPointer element = _weakElement.lock(); - assert(element); // should never lose root element - return element; - } else if (_nextIndex < NUMBER_OF_CHILDREN) { + // root case is special: its intersection is always INTERSECT + // and we can skip it if the content hasn't changed + if (element->getLastChangedContent() > lastTime) { + next.element = element; + next.intersection = ViewFrustum::INTERSECT; + return; + } + } + if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); if (element) { while (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); ++_nextIndex; - if (nextElement && - (!(nextElement->getLastChanged() < lastTime && - ViewFrustum::INSIDE == lastView.calculateCubeKeyholeIntersection(nextElement->getAACube()))) && - ViewFrustum::OUTSIDE != view.calculateCubeKeyholeIntersection(nextElement->getAACube())) { - return nextElement; + if (nextElement) { + AACube cube = nextElement->getAACube(); + // NOTE: for differential case next.intersection is against the _completedView + ViewFrustum::intersection intersection = lastView.calculateCubeKeyholeIntersection(cube); + if ( lastView.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE && + !(intersection == ViewFrustum::INSIDE && nextElement->getLastChanged() < lastTime)) { + next.element = nextElement; + next.intersection = intersection; + return; + } } } } } - return EntityTreeElementPointer(); + next.element.reset(); + next.intersection = ViewFrustum::OUTSIDE; } EntityTreeSendThread::EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) : OctreeSendThread(myServer, node) { const int32_t MIN_PATH_DEPTH = 16; - _forks.reserve(MIN_PATH_DEPTH); + _traversalPath.reserve(MIN_PATH_DEPTH); } void EntityTreeSendThread::preDistributionProcessing() { @@ -229,41 +256,31 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O startNewTraversal(viewFrustum, root); } } - if (!_forks.empty()) { - uint64_t t0 = usecTimestampNow(); - uint64_t now = t0; + if (!_traversalPath.empty()) { + uint64_t startTime = usecTimestampNow(); + uint64_t now = startTime; - ConicalView conicalView(_currentView); - EntityTreeElementPointer nextElement = getNextElement(); - while (nextElement) { - nextElement->forEachEntity([&](EntityItemPointer entity) { - bool success = false; - AACube cube = entity->getQueryAACube(success); - if (success) { - if (_currentView.cubeIntersectsKeyhole(cube)) { - float priority = conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority)); - std::cout << "adebug '" << entity->getName().toStdString() << "' send = " << (priority != DO_NOT_SEND) << std::endl; // adebug - } else { - std::cout << "adebug '" << entity->getName().toStdString() << "' out of view" << std::endl; // adebug - } - } else { - const float WHEN_IN_DOUBT_PRIORITY = 1.0f; - _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); - } - }); + VisibleElement next; + getNextVisibleElement(next); + while (next.element) { + if (next.element->hasContent()) { + _scanNextElementCallback(next); + } - now = usecTimestampNow(); + // TODO: pick a reasonable budget for each partial traversal const uint64_t PARTIAL_TRAVERSAL_TIME_BUDGET = 100000; // usec - if (now - t0 > PARTIAL_TRAVERSAL_TIME_BUDGET) { + now = usecTimestampNow(); + if (now - startTime > PARTIAL_TRAVERSAL_TIME_BUDGET) { break; } - nextElement = getNextElement(); + getNextVisibleElement(next); } - uint64_t dt1 = now - t0; - //} else if (!_sendQueue.empty()) { - size_t sendQueueSize = _sendQueue.size(); + uint64_t dt = now - startTime; + std::cout << "adebug traversal complete " << " Q.size = " << _sendQueue.size() << " dt = " << dt << std::endl; // adebug + } + if (!_sendQueue.empty()) { + // print what needs to be sent while (!_sendQueue.empty()) { PrioritizedEntity entry = _sendQueue.top(); EntityItemPointer entity = entry.getEntity(); @@ -272,14 +289,8 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O << " : " << entry.getPriority() << std::endl; // adebug } _sendQueue.pop(); + std::cout << "adebug" << std::endl; // adebug } - // std::priority_queue doesn't have a clear method, - // so we "clear" _sendQueue by setting it equal to an empty queue - _sendQueue = EntityPriorityQueue(); - std::cout << "adebug -end" - << " Q.size = " << sendQueueSize - << " dt = " << dt1 << std::endl; // adebug - std::cout << "adebug" << std::endl; // adebug } OctreeSendThread::traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); @@ -335,67 +346,149 @@ bool EntityTreeSendThread::addDescendantsToExtraFlaggedEntities(const QUuid& fil } void EntityTreeSendThread::startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root) { + // there are three types of traversal: + // + // (1) FirstTime = at login --> find everything in view + // (2) Again = view hasn't changed --> find what has changed since last complete traversal + // (3) Differential = view has changed --> find what has changed or in new view but not old + // + // For each traversal type we define two callback lambdas: + // + // _getNextVisibleElementCallback = identifies elements that need to be traversed,i + // updates VisibleElement ref argument with pointer-to-element and view-intersection + // (INSIDE, INTERSECT, or OUTSIDE) + // + // _scanNextElementCallback = identifies entities that need to be appended to _sendQueue + // + // The _conicalView is updated here as a cached view approximation used by the lambdas for efficient + // computation of entity sorting priorities. + // if (_startOfCompletedTraversal == 0) { + // first time _currentView = viewFrustum; - _getNextElementCallback = [&]() { - return _forks.back().getNextElementFirstTime(_currentView); + _conicalView.set(_currentView); + + _getNextVisibleElementCallback = [&](VisibleElement& next) { + _traversalPath.back().getNextVisibleElementFirstTime(next, _currentView); }; + _scanNextElementCallback = [&](VisibleElement& next) { + next.element->forEachEntity([&](EntityItemPointer entity) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (_currentView.cubeIntersectsKeyhole(cube)) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + } + } else { + const float WHEN_IN_DOUBT_PRIORITY = 1.0f; + _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + } + }); + }; } else if (_currentView.isVerySimilar(viewFrustum)) { - _getNextElementCallback = [&]() { - return _forks.back().getNextElementAgain(_currentView, _startOfCompletedTraversal); + // again + _getNextVisibleElementCallback = [&](VisibleElement& next) { + _traversalPath.back().getNextVisibleElementAgain(next, _currentView, _startOfCompletedTraversal); + }; + + _scanNextElementCallback = [&](VisibleElement& next) { + if (next.element->getLastChangedContent() > _startOfCompletedTraversal) { + next.element->forEachEntity([&](EntityItemPointer entity) { + if (entity->getLastEdited() > _startOfCompletedTraversal) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (next.intersection == ViewFrustum::INSIDE || _currentView.cubeIntersectsKeyhole(cube)) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + } + } else { + const float WHEN_IN_DOUBT_PRIORITY = 1.0f; + _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + } + } + }); + } }; } else { + // differential _currentView = viewFrustum; - _getNextElementCallback = [&]() { - return _forks.back().getNextElementDifferential(_currentView, _completedView, _startOfCompletedTraversal); + _conicalView.set(_currentView); + + _getNextVisibleElementCallback = [&](VisibleElement& next) { + _traversalPath.back().getNextVisibleElementDifferential(next, _currentView, _completedView, _startOfCompletedTraversal); + }; + + _scanNextElementCallback = [&](VisibleElement& next) { + // NOTE: for differential case next.intersection is against _completedView not _currentView + if (next.element->getLastChangedContent() > _startOfCompletedTraversal || next.intersection != ViewFrustum::INSIDE) { + next.element->forEachEntity([&](EntityItemPointer entity) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (_currentView.cubeIntersectsKeyhole(cube) && + (entity->getLastEdited() > _startOfCompletedTraversal || + !_completedView.cubeIntersectsKeyhole(cube))) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + } + } else { + const float WHEN_IN_DOUBT_PRIORITY = 1.0f; + _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + } + }); + } }; } - _forks.clear(); + _traversalPath.clear(); assert(root); - _forks.push_back(Fork(root)); + _traversalPath.push_back(Fork(root)); // set root fork's index such that root element returned at getNextElement() - _forks.back().initRootNextIndex(); + _traversalPath.back().initRootNextIndex(); _startOfCurrentTraversal = usecTimestampNow(); } -EntityTreeElementPointer EntityTreeSendThread::getNextElement() { - if (_forks.empty()) { - return EntityTreeElementPointer(); +void EntityTreeSendThread::getNextVisibleElement(VisibleElement& next) { + if (_traversalPath.empty()) { + next.element.reset(); + next.intersection = ViewFrustum::OUTSIDE; + return; } - EntityTreeElementPointer nextElement = _getNextElementCallback(); - if (nextElement) { - int8_t nextIndex = _forks.back().getNextIndex(); + _getNextVisibleElementCallback(next); + if (next.element) { + int8_t nextIndex = _traversalPath.back().getNextIndex(); if (nextIndex > 0) { - // nextElement needs to be added to the path - _forks.push_back(Fork(nextElement)); + // next.element needs to be added to the path + _traversalPath.push_back(Fork(next.element)); } } else { // we're done at this level - while (!nextElement) { + while (!next.element) { // pop one level - _forks.pop_back(); - if (_forks.empty()) { + _traversalPath.pop_back(); + if (_traversalPath.empty()) { // we've traversed the entire tree _completedView = _currentView; _startOfCompletedTraversal = _startOfCurrentTraversal; - return nextElement; + return; } - // keep looking for nextElement - nextElement = _getNextElementCallback(); - if (nextElement) { + // keep looking for next + _getNextVisibleElementCallback(next); + if (next.element) { // we've descended one level so add it to the path - _forks.push_back(Fork(nextElement)); + _traversalPath.push_back(Fork(next.element)); } } } - return nextElement; } +// DEBUG method: delete later void EntityTreeSendThread::dump() const { - for (size_t i = 0; i < _forks.size(); ++i) { - std::cout << (int32_t)(_forks[i].getNextIndex()) << "-->"; + for (size_t i = 0; i < _traversalPath.size(); ++i) { + std::cout << (int32_t)(_traversalPath[i].getNextIndex()) << "-->"; } } diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index e9dfa5513d..93d5a99b24 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -60,13 +60,19 @@ private: float _priority; }; +class VisibleElement { +public: + EntityTreeElementPointer element; + ViewFrustum::intersection intersection { ViewFrustum::OUTSIDE }; +}; + class Fork { public: Fork(EntityTreeElementPointer& element); - EntityTreeElementPointer getNextElementFirstTime(const ViewFrustum& view); - EntityTreeElementPointer getNextElementAgain(const ViewFrustum& view, uint64_t lastTime); - EntityTreeElementPointer getNextElementDifferential(const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime); + void getNextVisibleElementFirstTime(VisibleElement& next, const ViewFrustum& view); + void getNextVisibleElementAgain(VisibleElement& next, const ViewFrustum& view, uint64_t lastTime); + void getNextVisibleElementDifferential(VisibleElement& next, const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime); int8_t getNextIndex() const { return _nextIndex; } void initRootNextIndex() { _nextIndex = -1; } @@ -94,14 +100,16 @@ private: bool addDescendantsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root); - EntityTreeElementPointer getNextElement(); - void dump() const; + void getNextVisibleElement(VisibleElement& element); + void dump() const; // DEBUG method, delete later EntityPriorityQueue _sendQueue; ViewFrustum _currentView; ViewFrustum _completedView; - std::vector _forks; - std::function _getNextElementCallback { nullptr }; + ConicalView _conicalView; // optimized view for fast priority calculations + std::vector _traversalPath; + std::function _getNextVisibleElementCallback { nullptr }; + std::function _scanNextElementCallback { nullptr }; uint64_t _startOfCompletedTraversal { 0 }; uint64_t _startOfCurrentTraversal { 0 }; }; From 64cd209835f1f42404283d8b57c1f79cb4c983cb Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 8 Aug 2017 12:27:30 -0700 Subject: [PATCH 532/722] debug traverse again every two seconds --- assignment-client/src/entities/EntityTreeSendThread.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 54f2288491..0ddf8a21f5 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -248,6 +248,14 @@ void EntityTreeSendThread::preDistributionProcessing() { void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) { + // BEGIN EXPERIMENTAL DIFFERENTIAL TRAVERSAL + { + // DEBUG HACK: trigger traversal (Again) every so often + const uint64_t TRAVERSE_AGAIN_PERIOD = 2 * USECS_PER_SECOND; + if (!viewFrustumChanged && usecTimestampNow() > _startOfCompletedTraversal + TRAVERSE_AGAIN_PERIOD) { + viewFrustumChanged = true; + } + } if (nodeData->getUsesFrustum()) { if (viewFrustumChanged) { ViewFrustum viewFrustum; @@ -292,6 +300,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O std::cout << "adebug" << std::endl; // adebug } } + // END EXPERIMENTAL DIFFERENTIAL TRAVERSAL OctreeSendThread::traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); } From abf968aab64f42c90343574f7bd7936a45be9492 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 8 Aug 2017 13:05:08 -0700 Subject: [PATCH 533/722] split EntityPriorityQueue stuff into separate file --- .../src/entities/EntityPriorityQueue.cpp | 170 ++++++++++++++++++ .../src/entities/EntityPriorityQueue.h | 86 +++++++++ .../src/entities/EntityTreeSendThread.cpp | 165 +---------------- .../src/entities/EntityTreeSendThread.h | 71 +------- 4 files changed, 262 insertions(+), 230 deletions(-) create mode 100644 assignment-client/src/entities/EntityPriorityQueue.cpp create mode 100644 assignment-client/src/entities/EntityPriorityQueue.h diff --git a/assignment-client/src/entities/EntityPriorityQueue.cpp b/assignment-client/src/entities/EntityPriorityQueue.cpp new file mode 100644 index 0000000000..87cc77161d --- /dev/null +++ b/assignment-client/src/entities/EntityPriorityQueue.cpp @@ -0,0 +1,170 @@ +// +// EntityPriorityQueue.cpp +// assignment-client/src/entities +// +// Created by Andrew Meadows 2017.08.08 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "EntityPriorityQueue.h" + +const float DO_NOT_SEND = -1.0e-6f; + +void ConicalView::set(const ViewFrustum& viewFrustum) { + // The ConicalView has two parts: a central sphere (same as ViewFrustm) and a circular cone that bounds the frustum part. + // Why? Because approximate intersection tests are much faster to compute for a cone than for a frustum. + _position = viewFrustum.getPosition(); + _direction = viewFrustum.getDirection(); + + // We cache the sin and cos of the half angle of the cone that bounds the frustum. + // (the math here is left as an exercise for the reader) + float A = viewFrustum.getAspectRatio(); + float t = tanf(0.5f * viewFrustum.getFieldOfView()); + _cosAngle = 1.0f / sqrtf(1.0f + (A * A + 1.0f) * (t * t)); + _sinAngle = sqrtf(1.0f - _cosAngle * _cosAngle); + + _radius = viewFrustum.getCenterRadius(); +} + +float ConicalView::computePriority(const AACube& cube) const { + glm::vec3 p = cube.calcCenter() - _position; // position of bounding sphere in view-frame + float d = glm::length(p); // distance to center of bounding sphere + float r = 0.5f * cube.getScale(); // radius of bounding sphere + if (d < _radius + r) { + return r; + } + if (glm::dot(p, _direction) > sqrtf(d * d - r * r) * _cosAngle - r * _sinAngle) { + const float AVOID_DIVIDE_BY_ZERO = 0.001f; + return r / (d + AVOID_DIVIDE_BY_ZERO); + } + return DO_NOT_SEND; +} + +// static +float ConicalView::computePriority(const EntityItemPointer& entity) const { + assert(entity); + bool success; + AACube cube = entity->getQueryAACube(success); + if (success) { + return computePriority(cube); + } else { + // when in doubt give it something positive + return 1.0f; + } +} + +float PrioritizedEntity::updatePriority(const ConicalView& conicalView) { + EntityItemPointer entity = _weakEntity.lock(); + if (entity) { + _priority = conicalView.computePriority(entity); + } else { + _priority = DO_NOT_SEND; + } + return _priority; +} + +TraversalWaypoint::TraversalWaypoint(EntityTreeElementPointer& element) : _nextIndex(0) { + assert(element); + _weakElement = element; +} + +void TraversalWaypoint::getNextVisibleElementFirstTime(VisibleElement& next, const ViewFrustum& view) { + // NOTE: no need to set next.intersection in the "FirstTime" context + if (_nextIndex == -1) { + // only get here for the root TraversalWaypoint at the very beginning of traversal + // safe to assume this element intersects view + ++_nextIndex; + next.element = _weakElement.lock(); + return; + } else if (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer element = _weakElement.lock(); + if (element) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && view.cubeIntersectsKeyhole(nextElement->getAACube())) { + next.element = nextElement; + return; + } + } + } + } + next.element.reset(); +} + +void TraversalWaypoint::getNextVisibleElementAgain(VisibleElement& next, const ViewFrustum& view, uint64_t lastTime) { + if (_nextIndex == -1) { + // only get here for the root TraversalWaypoint at the very beginning of traversal + // safe to assume this element intersects view + ++_nextIndex; + EntityTreeElementPointer element = _weakElement.lock(); + // root case is special: its intersection is always INTERSECT + // and we can skip it if the content hasn't changed + if (element->getLastChangedContent() > lastTime) { + next.element = element; + next.intersection = ViewFrustum::INTERSECT; + return; + } + } + if (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer element = _weakElement.lock(); + if (element) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && nextElement->getLastChanged() > lastTime) { + ViewFrustum::intersection intersection = view.calculateCubeKeyholeIntersection(nextElement->getAACube()); + if (intersection != ViewFrustum::OUTSIDE) { + next.element = nextElement; + next.intersection = intersection; + return; + } + } + } + } + } + next.element.reset(); + next.intersection = ViewFrustum::OUTSIDE; +} + +void TraversalWaypoint::getNextVisibleElementDifferential(VisibleElement& next, + const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime) { + if (_nextIndex == -1) { + // only get here for the root TraversalWaypoint at the very beginning of traversal + // safe to assume this element intersects view + ++_nextIndex; + EntityTreeElementPointer element = _weakElement.lock(); + // root case is special: its intersection is always INTERSECT + // and we can skip it if the content hasn't changed + if (element->getLastChangedContent() > lastTime) { + next.element = element; + next.intersection = ViewFrustum::INTERSECT; + return; + } + } + if (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer element = _weakElement.lock(); + if (element) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement) { + AACube cube = nextElement->getAACube(); + // NOTE: for differential case next.intersection is against the _completedView + ViewFrustum::intersection intersection = lastView.calculateCubeKeyholeIntersection(cube); + if ( lastView.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE && + !(intersection == ViewFrustum::INSIDE && nextElement->getLastChanged() < lastTime)) { + next.element = nextElement; + next.intersection = intersection; + return; + } + } + } + } + } + next.element.reset(); + next.intersection = ViewFrustum::OUTSIDE; +} diff --git a/assignment-client/src/entities/EntityPriorityQueue.h b/assignment-client/src/entities/EntityPriorityQueue.h new file mode 100644 index 0000000000..cc233e127a --- /dev/null +++ b/assignment-client/src/entities/EntityPriorityQueue.h @@ -0,0 +1,86 @@ +// +// EntityPriorityQueue.h +// assignment-client/src/entities +// +// Created by Andrew Meadows 2017.08.08 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_EntityPriorityQueue_h +#define hifi_EntityPriorityQueue_h + +#include + +#include + +#include "EntityTreeElement.h" + +const float SQRT_TWO_OVER_TWO = 0.7071067811865; +const float DEFAULT_VIEW_RADIUS = 10.0f; + +// ConicalView is an approximation of a ViewFrustum for fast calculation of sort priority. +class ConicalView { +public: + ConicalView() {} + ConicalView(const ViewFrustum& viewFrustum) { set(viewFrustum); } + void set(const ViewFrustum& viewFrustum); + float computePriority(const AACube& cube) const; + float computePriority(const EntityItemPointer& entity) const; +private: + glm::vec3 _position { 0.0f, 0.0f, 0.0f }; + glm::vec3 _direction { 0.0f, 0.0f, 1.0f }; + float _sinAngle { SQRT_TWO_OVER_TWO }; + float _cosAngle { SQRT_TWO_OVER_TWO }; + float _radius { DEFAULT_VIEW_RADIUS }; +}; + +// PrioritizedEntity is a placeholder in a sorted queue. +class PrioritizedEntity { +public: + PrioritizedEntity(EntityItemPointer entity, float priority) : _weakEntity(entity), _priority(priority) { } + float updatePriority(const ConicalView& view); + EntityItemPointer getEntity() const { return _weakEntity.lock(); } + float getPriority() const { return _priority; } + + class Compare { + public: + bool operator() (const PrioritizedEntity& A, const PrioritizedEntity& B) { return A._priority < B._priority; } + }; + friend class Compare; + +private: + EntityItemWeakPointer _weakEntity; + float _priority; +}; + +// VisibleElement is a struct identifying an element and how it intersected the view. +// The intersection is used to optimize culling entities from the sendQueue. +class VisibleElement { +public: + EntityTreeElementPointer element; + ViewFrustum::intersection intersection { ViewFrustum::OUTSIDE }; +}; + +// TraversalWaypoint is an bookmark in a "path" of waypoints during a traversal. +class TraversalWaypoint { +public: + TraversalWaypoint(EntityTreeElementPointer& element); + + void getNextVisibleElementFirstTime(VisibleElement& next, const ViewFrustum& view); + void getNextVisibleElementAgain(VisibleElement& next, const ViewFrustum& view, uint64_t lastTime); + void getNextVisibleElementDifferential(VisibleElement& next, const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime); + + int8_t getNextIndex() const { return _nextIndex; } + void initRootNextIndex() { _nextIndex = -1; } + +protected: + EntityTreeElementWeakPointer _weakElement; + int8_t _nextIndex; +}; + +using EntityPriorityQueue = std::priority_queue< PrioritizedEntity, std::vector, PrioritizedEntity::Compare >; + +#endif // hifi_EntityPriorityQueue_h diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 0ddf8a21f5..7d6baaca3b 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -17,165 +17,6 @@ #include "EntityServer.h" -const float DO_NOT_SEND = -1.0e-6f; - -void ConicalView::set(const ViewFrustum& viewFrustum) { - // The ConicalView has two parts: a central sphere (same as ViewFrustm) and a circular cone that bounds the frustum part. - // Why? Because approximate intersection tests are much faster to compute for a cone than for a frustum. - _position = viewFrustum.getPosition(); - _direction = viewFrustum.getDirection(); - - // We cache the sin and cos of the half angle of the cone that bounds the frustum. - // (the math here is left as an exercise for the reader) - float A = viewFrustum.getAspectRatio(); - float t = tanf(0.5f * viewFrustum.getFieldOfView()); - _cosAngle = 1.0f / sqrtf(1.0f + (A * A + 1.0f) * (t * t)); - _sinAngle = sqrtf(1.0f - _cosAngle * _cosAngle); - - _radius = viewFrustum.getCenterRadius(); -} - -float ConicalView::computePriority(const AACube& cube) const { - glm::vec3 p = cube.calcCenter() - _position; // position of bounding sphere in view-frame - float d = glm::length(p); // distance to center of bounding sphere - float r = 0.5f * cube.getScale(); // radius of bounding sphere - if (d < _radius + r) { - return r; - } - if (glm::dot(p, _direction) > sqrtf(d * d - r * r) * _cosAngle - r * _sinAngle) { - const float AVOID_DIVIDE_BY_ZERO = 0.001f; - return r / (d + AVOID_DIVIDE_BY_ZERO); - } - return DO_NOT_SEND; -} - - -// static -float ConicalView::computePriority(const EntityItemPointer& entity) const { - assert(entity); - bool success; - AACube cube = entity->getQueryAACube(success); - if (success) { - return computePriority(cube); - } else { - // when in doubt give it something positive - return 1.0f; - } -} - -float PrioritizedEntity::updatePriority(const ConicalView& conicalView) { - EntityItemPointer entity = _weakEntity.lock(); - if (entity) { - _priority = conicalView.computePriority(entity); - } else { - _priority = DO_NOT_SEND; - } - return _priority; -} - -Fork::Fork(EntityTreeElementPointer& element) : _nextIndex(0) { - assert(element); - _weakElement = element; -} - -void Fork::getNextVisibleElementFirstTime(VisibleElement& next, const ViewFrustum& view) { - // NOTE: no need to set next.intersection in the "FirstTime" context - if (_nextIndex == -1) { - // only get here for the root Fork at the very beginning of traversal - // safe to assume this element intersects view - ++_nextIndex; - next.element = _weakElement.lock(); - return; - } else if (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer element = _weakElement.lock(); - if (element) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement && view.cubeIntersectsKeyhole(nextElement->getAACube())) { - next.element = nextElement; - return; - } - } - } - } - next.element.reset(); -} - -void Fork::getNextVisibleElementAgain(VisibleElement& next, const ViewFrustum& view, uint64_t lastTime) { - if (_nextIndex == -1) { - // only get here for the root Fork at the very beginning of traversal - // safe to assume this element intersects view - ++_nextIndex; - EntityTreeElementPointer element = _weakElement.lock(); - // root case is special: its intersection is always INTERSECT - // and we can skip it if the content hasn't changed - if (element->getLastChangedContent() > lastTime) { - next.element = element; - next.intersection = ViewFrustum::INTERSECT; - return; - } - } - if (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer element = _weakElement.lock(); - if (element) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement && nextElement->getLastChanged() > lastTime) { - ViewFrustum::intersection intersection = view.calculateCubeKeyholeIntersection(nextElement->getAACube()); - if (intersection != ViewFrustum::OUTSIDE) { - next.element = nextElement; - next.intersection = intersection; - return; - } - } - } - } - } - next.element.reset(); - next.intersection = ViewFrustum::OUTSIDE; -} - -void Fork::getNextVisibleElementDifferential(VisibleElement& next, - const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime) { - if (_nextIndex == -1) { - // only get here for the root Fork at the very beginning of traversal - // safe to assume this element intersects view - ++_nextIndex; - EntityTreeElementPointer element = _weakElement.lock(); - // root case is special: its intersection is always INTERSECT - // and we can skip it if the content hasn't changed - if (element->getLastChangedContent() > lastTime) { - next.element = element; - next.intersection = ViewFrustum::INTERSECT; - return; - } - } - if (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer element = _weakElement.lock(); - if (element) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement) { - AACube cube = nextElement->getAACube(); - // NOTE: for differential case next.intersection is against the _completedView - ViewFrustum::intersection intersection = lastView.calculateCubeKeyholeIntersection(cube); - if ( lastView.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE && - !(intersection == ViewFrustum::INSIDE && nextElement->getLastChanged() < lastTime)) { - next.element = nextElement; - next.intersection = intersection; - return; - } - } - } - } - } - next.element.reset(); - next.intersection = ViewFrustum::OUTSIDE; -} - EntityTreeSendThread::EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) : OctreeSendThread(myServer, node) { const int32_t MIN_PATH_DEPTH = 16; @@ -454,7 +295,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& viewFrustum, Ent _traversalPath.clear(); assert(root); - _traversalPath.push_back(Fork(root)); + _traversalPath.push_back(TraversalWaypoint(root)); // set root fork's index such that root element returned at getNextElement() _traversalPath.back().initRootNextIndex(); @@ -472,7 +313,7 @@ void EntityTreeSendThread::getNextVisibleElement(VisibleElement& next) { int8_t nextIndex = _traversalPath.back().getNextIndex(); if (nextIndex > 0) { // next.element needs to be added to the path - _traversalPath.push_back(Fork(next.element)); + _traversalPath.push_back(TraversalWaypoint(next.element)); } } else { // we're done at this level @@ -489,7 +330,7 @@ void EntityTreeSendThread::getNextVisibleElement(VisibleElement& next) { _getNextVisibleElementCallback(next); if (next.element) { // we've descended one level so add it to the path - _traversalPath.push_back(Fork(next.element)); + _traversalPath.push_back(TraversalWaypoint(next.element)); } } } diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 93d5a99b24..9a1a57b41c 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -12,80 +12,15 @@ #ifndef hifi_EntityTreeSendThread_h #define hifi_EntityTreeSendThread_h -#include - #include "../octree/OctreeSendThread.h" -#include - -#include "EntityTreeElement.h" - -const float SQRT_TWO_OVER_TWO = 0.7071067811865; -const float DEFAULT_VIEW_RADIUS = 10.0f; +#include "EntityPriorityQueue.h" class EntityNodeData; class EntityItem; - -class ConicalView { -public: - ConicalView() {} - ConicalView(const ViewFrustum& viewFrustum) { set(viewFrustum); } - void set(const ViewFrustum& viewFrustum); - float computePriority(const AACube& cube) const; - float computePriority(const EntityItemPointer& entity) const; -private: - glm::vec3 _position { 0.0f, 0.0f, 0.0f }; - glm::vec3 _direction { 0.0f, 0.0f, 1.0f }; - float _sinAngle { SQRT_TWO_OVER_TWO }; - float _cosAngle { SQRT_TWO_OVER_TWO }; - float _radius { DEFAULT_VIEW_RADIUS }; -}; - -class PrioritizedEntity { -public: - PrioritizedEntity(EntityItemPointer entity, float priority) : _weakEntity(entity), _priority(priority) { } - float updatePriority(const ConicalView& view); - EntityItemPointer getEntity() const { return _weakEntity.lock(); } - float getPriority() const { return _priority; } - - class Compare { - public: - bool operator() (const PrioritizedEntity& A, const PrioritizedEntity& B) { return A._priority < B._priority; } - }; - friend class Compare; - -private: - EntityItemWeakPointer _weakEntity; - float _priority; -}; - -class VisibleElement { -public: - EntityTreeElementPointer element; - ViewFrustum::intersection intersection { ViewFrustum::OUTSIDE }; -}; - -class Fork { -public: - Fork(EntityTreeElementPointer& element); - - void getNextVisibleElementFirstTime(VisibleElement& next, const ViewFrustum& view); - void getNextVisibleElementAgain(VisibleElement& next, const ViewFrustum& view, uint64_t lastTime); - void getNextVisibleElementDifferential(VisibleElement& next, const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime); - - int8_t getNextIndex() const { return _nextIndex; } - void initRootNextIndex() { _nextIndex = -1; } - -protected: - EntityTreeElementWeakPointer _weakElement; - int8_t _nextIndex; -}; - -using EntityPriorityQueue = std::priority_queue< PrioritizedEntity, std::vector, PrioritizedEntity::Compare >; - - class EntityTreeSendThread : public OctreeSendThread { + public: EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node); @@ -107,7 +42,7 @@ private: ViewFrustum _currentView; ViewFrustum _completedView; ConicalView _conicalView; // optimized view for fast priority calculations - std::vector _traversalPath; + std::vector _traversalPath; std::function _getNextVisibleElementCallback { nullptr }; std::function _scanNextElementCallback { nullptr }; uint64_t _startOfCompletedTraversal { 0 }; From 3eb9cd4251026b122bfe9a2e5c956540fe7aa38e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 8 Aug 2017 13:55:35 -0700 Subject: [PATCH 534/722] add TODO comments --- assignment-client/src/entities/EntityPriorityQueue.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assignment-client/src/entities/EntityPriorityQueue.cpp b/assignment-client/src/entities/EntityPriorityQueue.cpp index 87cc77161d..7e2d217b12 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.cpp +++ b/assignment-client/src/entities/EntityPriorityQueue.cpp @@ -72,6 +72,7 @@ TraversalWaypoint::TraversalWaypoint(EntityTreeElementPointer& element) : _nextI } void TraversalWaypoint::getNextVisibleElementFirstTime(VisibleElement& next, const ViewFrustum& view) { + // TODO: add LOD culling of elements // NOTE: no need to set next.intersection in the "FirstTime" context if (_nextIndex == -1) { // only get here for the root TraversalWaypoint at the very beginning of traversal @@ -96,6 +97,7 @@ void TraversalWaypoint::getNextVisibleElementFirstTime(VisibleElement& next, con } void TraversalWaypoint::getNextVisibleElementAgain(VisibleElement& next, const ViewFrustum& view, uint64_t lastTime) { + // TODO: add LOD culling of elements if (_nextIndex == -1) { // only get here for the root TraversalWaypoint at the very beginning of traversal // safe to assume this element intersects view @@ -132,6 +134,7 @@ void TraversalWaypoint::getNextVisibleElementAgain(VisibleElement& next, const V void TraversalWaypoint::getNextVisibleElementDifferential(VisibleElement& next, const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime) { + // TODO: add LOD culling of elements if (_nextIndex == -1) { // only get here for the root TraversalWaypoint at the very beginning of traversal // safe to assume this element intersects view From b537d3b1eee0acbb07d31431f68f2c7f20cc14e3 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 8 Aug 2017 14:18:33 -0700 Subject: [PATCH 535/722] more helpful comments --- assignment-client/src/entities/EntityPriorityQueue.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/assignment-client/src/entities/EntityPriorityQueue.cpp b/assignment-client/src/entities/EntityPriorityQueue.cpp index 7e2d217b12..94082593bc 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.cpp +++ b/assignment-client/src/entities/EntityPriorityQueue.cpp @@ -36,6 +36,13 @@ float ConicalView::computePriority(const AACube& cube) const { if (d < _radius + r) { return r; } + // We check the angle between the center of the cube and the _direction of the view. + // If it is less than the sum of the half-angle from center of cone to outer edge plus + // the half apparent angle of the bounding sphere then it is in view. + // + // The math here is left as an exercise for the reader with the following hints: + // (1) We actually check the dot product of the cube's local position rather than the angle and + // (2) we take advantage of this trig identity: cos(A+B) = cos(A)*cos(B) - sin(A)*sin(B) if (glm::dot(p, _direction) > sqrtf(d * d - r * r) * _cosAngle - r * _sinAngle) { const float AVOID_DIVIDE_BY_ZERO = 0.001f; return r / (d + AVOID_DIVIDE_BY_ZERO); From 5fba4cb68ca607c8e34530e42f66eaa3154b315e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 8 Aug 2017 15:00:07 -0700 Subject: [PATCH 536/722] fix warning about truncation from double to float --- assignment-client/src/entities/EntityPriorityQueue.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/entities/EntityPriorityQueue.h b/assignment-client/src/entities/EntityPriorityQueue.h index cc233e127a..8eb28dffda 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.h +++ b/assignment-client/src/entities/EntityPriorityQueue.h @@ -18,7 +18,7 @@ #include "EntityTreeElement.h" -const float SQRT_TWO_OVER_TWO = 0.7071067811865; +const float SQRT_TWO_OVER_TWO = 0.7071067811865f; const float DEFAULT_VIEW_RADIUS = 10.0f; // ConicalView is an approximation of a ViewFrustum for fast calculation of sort priority. From 0758b60afc855d77ac328d10a798d4ebd37d94b3 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 9 Aug 2017 13:14:36 -0700 Subject: [PATCH 537/722] abstract DiffTraversal out of EntityTreeSendThread --- .../src/entities/EntityPriorityQueue.cpp | 108 +------- .../src/entities/EntityPriorityQueue.h | 28 +- .../src/entities/EntityTreeSendThread.cpp | 173 +++---------- .../src/entities/EntityTreeSendThread.h | 16 +- libraries/entities/src/DiffTraversal.cpp | 239 ++++++++++++++++++ libraries/entities/src/DiffTraversal.h | 78 ++++++ 6 files changed, 366 insertions(+), 276 deletions(-) create mode 100644 libraries/entities/src/DiffTraversal.cpp create mode 100644 libraries/entities/src/DiffTraversal.h diff --git a/assignment-client/src/entities/EntityPriorityQueue.cpp b/assignment-client/src/entities/EntityPriorityQueue.cpp index 94082593bc..77b46afa24 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.cpp +++ b/assignment-client/src/entities/EntityPriorityQueue.cpp @@ -14,7 +14,7 @@ const float DO_NOT_SEND = -1.0e-6f; void ConicalView::set(const ViewFrustum& viewFrustum) { - // The ConicalView has two parts: a central sphere (same as ViewFrustm) and a circular cone that bounds the frustum part. + // The ConicalView has two parts: a central sphere (same as ViewFrustum) and a circular cone that bounds the frustum part. // Why? Because approximate intersection tests are much faster to compute for a cone than for a frustum. _position = viewFrustum.getPosition(); _direction = viewFrustum.getDirection(); @@ -72,109 +72,3 @@ float PrioritizedEntity::updatePriority(const ConicalView& conicalView) { } return _priority; } - -TraversalWaypoint::TraversalWaypoint(EntityTreeElementPointer& element) : _nextIndex(0) { - assert(element); - _weakElement = element; -} - -void TraversalWaypoint::getNextVisibleElementFirstTime(VisibleElement& next, const ViewFrustum& view) { - // TODO: add LOD culling of elements - // NOTE: no need to set next.intersection in the "FirstTime" context - if (_nextIndex == -1) { - // only get here for the root TraversalWaypoint at the very beginning of traversal - // safe to assume this element intersects view - ++_nextIndex; - next.element = _weakElement.lock(); - return; - } else if (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer element = _weakElement.lock(); - if (element) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement && view.cubeIntersectsKeyhole(nextElement->getAACube())) { - next.element = nextElement; - return; - } - } - } - } - next.element.reset(); -} - -void TraversalWaypoint::getNextVisibleElementAgain(VisibleElement& next, const ViewFrustum& view, uint64_t lastTime) { - // TODO: add LOD culling of elements - if (_nextIndex == -1) { - // only get here for the root TraversalWaypoint at the very beginning of traversal - // safe to assume this element intersects view - ++_nextIndex; - EntityTreeElementPointer element = _weakElement.lock(); - // root case is special: its intersection is always INTERSECT - // and we can skip it if the content hasn't changed - if (element->getLastChangedContent() > lastTime) { - next.element = element; - next.intersection = ViewFrustum::INTERSECT; - return; - } - } - if (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer element = _weakElement.lock(); - if (element) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement && nextElement->getLastChanged() > lastTime) { - ViewFrustum::intersection intersection = view.calculateCubeKeyholeIntersection(nextElement->getAACube()); - if (intersection != ViewFrustum::OUTSIDE) { - next.element = nextElement; - next.intersection = intersection; - return; - } - } - } - } - } - next.element.reset(); - next.intersection = ViewFrustum::OUTSIDE; -} - -void TraversalWaypoint::getNextVisibleElementDifferential(VisibleElement& next, - const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime) { - // TODO: add LOD culling of elements - if (_nextIndex == -1) { - // only get here for the root TraversalWaypoint at the very beginning of traversal - // safe to assume this element intersects view - ++_nextIndex; - EntityTreeElementPointer element = _weakElement.lock(); - // root case is special: its intersection is always INTERSECT - // and we can skip it if the content hasn't changed - if (element->getLastChangedContent() > lastTime) { - next.element = element; - next.intersection = ViewFrustum::INTERSECT; - return; - } - } - if (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer element = _weakElement.lock(); - if (element) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement) { - AACube cube = nextElement->getAACube(); - // NOTE: for differential case next.intersection is against the _completedView - ViewFrustum::intersection intersection = lastView.calculateCubeKeyholeIntersection(cube); - if ( lastView.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE && - !(intersection == ViewFrustum::INSIDE && nextElement->getLastChanged() < lastTime)) { - next.element = nextElement; - next.intersection = intersection; - return; - } - } - } - } - } - next.element.reset(); - next.intersection = ViewFrustum::OUTSIDE; -} diff --git a/assignment-client/src/entities/EntityPriorityQueue.h b/assignment-client/src/entities/EntityPriorityQueue.h index 8eb28dffda..215c5262bf 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.h +++ b/assignment-client/src/entities/EntityPriorityQueue.h @@ -15,8 +15,7 @@ #include #include - -#include "EntityTreeElement.h" +#include const float SQRT_TWO_OVER_TWO = 0.7071067811865f; const float DEFAULT_VIEW_RADIUS = 10.0f; @@ -56,31 +55,6 @@ private: float _priority; }; -// VisibleElement is a struct identifying an element and how it intersected the view. -// The intersection is used to optimize culling entities from the sendQueue. -class VisibleElement { -public: - EntityTreeElementPointer element; - ViewFrustum::intersection intersection { ViewFrustum::OUTSIDE }; -}; - -// TraversalWaypoint is an bookmark in a "path" of waypoints during a traversal. -class TraversalWaypoint { -public: - TraversalWaypoint(EntityTreeElementPointer& element); - - void getNextVisibleElementFirstTime(VisibleElement& next, const ViewFrustum& view); - void getNextVisibleElementAgain(VisibleElement& next, const ViewFrustum& view, uint64_t lastTime); - void getNextVisibleElementDifferential(VisibleElement& next, const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime); - - int8_t getNextIndex() const { return _nextIndex; } - void initRootNextIndex() { _nextIndex = -1; } - -protected: - EntityTreeElementWeakPointer _weakElement; - int8_t _nextIndex; -}; - using EntityPriorityQueue = std::priority_queue< PrioritizedEntity, std::vector, PrioritizedEntity::Compare >; #endif // hifi_EntityPriorityQueue_h diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 7d6baaca3b..031d7ac3fb 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -10,19 +10,12 @@ // #include "EntityTreeSendThread.h" -#include // adebug #include #include #include "EntityServer.h" -EntityTreeSendThread::EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) - : OctreeSendThread(myServer, node) { - const int32_t MIN_PATH_DEPTH = 16; - _traversalPath.reserve(MIN_PATH_DEPTH); -} - void EntityTreeSendThread::preDistributionProcessing() { auto node = _node.toStrongRef(); auto nodeData = static_cast(node->getLinkedData()); @@ -90,14 +83,14 @@ void EntityTreeSendThread::preDistributionProcessing() { void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) { // BEGIN EXPERIMENTAL DIFFERENTIAL TRAVERSAL - { - // DEBUG HACK: trigger traversal (Again) every so often - const uint64_t TRAVERSE_AGAIN_PERIOD = 2 * USECS_PER_SECOND; - if (!viewFrustumChanged && usecTimestampNow() > _startOfCompletedTraversal + TRAVERSE_AGAIN_PERIOD) { - viewFrustumChanged = true; - } - } if (nodeData->getUsesFrustum()) { + { + // DEBUG HACK: trigger traversal (Again) every so often + const uint64_t TRAVERSE_AGAIN_PERIOD = 4 * USECS_PER_SECOND; + if (!viewFrustumChanged && usecTimestampNow() > _traversal.getStartOfCompletedTraversal() + TRAVERSE_AGAIN_PERIOD) { + viewFrustumChanged = true; + } + } if (viewFrustumChanged) { ViewFrustum viewFrustum; nodeData->copyCurrentViewFrustum(viewFrustum); @@ -105,28 +98,14 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O startNewTraversal(viewFrustum, root); } } - if (!_traversalPath.empty()) { + if (!_traversal.finished()) { uint64_t startTime = usecTimestampNow(); - uint64_t now = startTime; - VisibleElement next; - getNextVisibleElement(next); - while (next.element) { - if (next.element->hasContent()) { - _scanNextElementCallback(next); - } + const uint64_t TIME_BUDGET = 100000; // usec + _traversal.traverse(TIME_BUDGET); - // TODO: pick a reasonable budget for each partial traversal - const uint64_t PARTIAL_TRAVERSAL_TIME_BUDGET = 100000; // usec - now = usecTimestampNow(); - if (now - startTime > PARTIAL_TRAVERSAL_TIME_BUDGET) { - break; - } - getNextVisibleElement(next); - } - - uint64_t dt = now - startTime; - std::cout << "adebug traversal complete " << " Q.size = " << _sendQueue.size() << " dt = " << dt << std::endl; // adebug + uint64_t dt = usecTimestampNow() - startTime; + std::cout << "adebug traversal complete " << " Q.size = " << _sendQueue.size() << " dt = " << dt << std::endl; // adebug } if (!_sendQueue.empty()) { // print what needs to be sent @@ -134,11 +113,9 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O PrioritizedEntity entry = _sendQueue.top(); EntityItemPointer entity = entry.getEntity(); if (entity) { - std::cout << "adebug '" << entity->getName().toStdString() << "'" - << " : " << entry.getPriority() << std::endl; // adebug + std::cout << "adebug send '" << entity->getName().toStdString() << "'" << " : " << entry.getPriority() << std::endl; // adebug } _sendQueue.pop(); - std::cout << "adebug" << std::endl; // adebug } } // END EXPERIMENTAL DIFFERENTIAL TRAVERSAL @@ -195,39 +172,29 @@ bool EntityTreeSendThread::addDescendantsToExtraFlaggedEntities(const QUuid& fil return hasNewChild || hasNewDescendants; } -void EntityTreeSendThread::startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root) { +void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root) { + DiffTraversal::Type type = _traversal.prepareNewTraversal(view, root); // there are three types of traversal: // // (1) FirstTime = at login --> find everything in view - // (2) Again = view hasn't changed --> find what has changed since last complete traversal + // (2) Repeat = view hasn't changed --> find what has changed since last complete traversal // (3) Differential = view has changed --> find what has changed or in new view but not old // - // For each traversal type we define two callback lambdas: - // - // _getNextVisibleElementCallback = identifies elements that need to be traversed,i - // updates VisibleElement ref argument with pointer-to-element and view-intersection - // (INSIDE, INTERSECT, or OUTSIDE) - // - // _scanNextElementCallback = identifies entities that need to be appended to _sendQueue + // The "scanCallback" we provide to the traversal depends on the type: // // The _conicalView is updated here as a cached view approximation used by the lambdas for efficient // computation of entity sorting priorities. // - if (_startOfCompletedTraversal == 0) { - // first time - _currentView = viewFrustum; - _conicalView.set(_currentView); + _conicalView.set(_traversal.getCurrentView()); - _getNextVisibleElementCallback = [&](VisibleElement& next) { - _traversalPath.back().getNextVisibleElementFirstTime(next, _currentView); - }; - - _scanNextElementCallback = [&](VisibleElement& next) { + switch (type) { + case DiffTraversal::First: + _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { next.element->forEachEntity([&](EntityItemPointer entity) { bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { - if (_currentView.cubeIntersectsKeyhole(cube)) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); } @@ -236,21 +203,18 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& viewFrustum, Ent _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); } }); - }; - } else if (_currentView.isVerySimilar(viewFrustum)) { - // again - _getNextVisibleElementCallback = [&](VisibleElement& next) { - _traversalPath.back().getNextVisibleElementAgain(next, _currentView, _startOfCompletedTraversal); - }; - - _scanNextElementCallback = [&](VisibleElement& next) { - if (next.element->getLastChangedContent() > _startOfCompletedTraversal) { + }); + break; + case DiffTraversal::Repeat: + _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { + if (next.element->getLastChangedContent() > _traversal.getStartOfCompletedTraversal()) { + uint64_t timestamp = _traversal.getStartOfCompletedTraversal(); next.element->forEachEntity([&](EntityItemPointer entity) { - if (entity->getLastEdited() > _startOfCompletedTraversal) { + if (entity->getLastEdited() > timestamp) { bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { - if (next.intersection == ViewFrustum::INSIDE || _currentView.cubeIntersectsKeyhole(cube)) { + if (next.intersection == ViewFrustum::INSIDE || _traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); } @@ -261,26 +225,20 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& viewFrustum, Ent } }); } - }; - } else { - // differential - _currentView = viewFrustum; - _conicalView.set(_currentView); - - _getNextVisibleElementCallback = [&](VisibleElement& next) { - _traversalPath.back().getNextVisibleElementDifferential(next, _currentView, _completedView, _startOfCompletedTraversal); - }; - - _scanNextElementCallback = [&](VisibleElement& next) { - // NOTE: for differential case next.intersection is against _completedView not _currentView - if (next.element->getLastChangedContent() > _startOfCompletedTraversal || next.intersection != ViewFrustum::INSIDE) { + }); + break; + case DiffTraversal::Differential: + _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { + // NOTE: for Differential case: next.intersection is against completedView not currentView + uint64_t timestamp = _traversal.getStartOfCompletedTraversal(); + if (next.element->getLastChangedContent() > timestamp || next.intersection != ViewFrustum::INSIDE) { next.element->forEachEntity([&](EntityItemPointer entity) { bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { - if (_currentView.cubeIntersectsKeyhole(cube) && - (entity->getLastEdited() > _startOfCompletedTraversal || - !_completedView.cubeIntersectsKeyhole(cube))) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube) && + (entity->getLastEdited() > timestamp || + !_traversal.getCompletedView().cubeIntersectsKeyhole(cube))) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); } @@ -290,55 +248,8 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& viewFrustum, Ent } }); } - }; - } - - _traversalPath.clear(); - assert(root); - _traversalPath.push_back(TraversalWaypoint(root)); - // set root fork's index such that root element returned at getNextElement() - _traversalPath.back().initRootNextIndex(); - - _startOfCurrentTraversal = usecTimestampNow(); -} - -void EntityTreeSendThread::getNextVisibleElement(VisibleElement& next) { - if (_traversalPath.empty()) { - next.element.reset(); - next.intersection = ViewFrustum::OUTSIDE; - return; - } - _getNextVisibleElementCallback(next); - if (next.element) { - int8_t nextIndex = _traversalPath.back().getNextIndex(); - if (nextIndex > 0) { - // next.element needs to be added to the path - _traversalPath.push_back(TraversalWaypoint(next.element)); - } - } else { - // we're done at this level - while (!next.element) { - // pop one level - _traversalPath.pop_back(); - if (_traversalPath.empty()) { - // we've traversed the entire tree - _completedView = _currentView; - _startOfCompletedTraversal = _startOfCurrentTraversal; - return; - } - // keep looking for next - _getNextVisibleElementCallback(next); - if (next.element) { - // we've descended one level so add it to the path - _traversalPath.push_back(TraversalWaypoint(next.element)); - } - } + }); + break; } } -// DEBUG method: delete later -void EntityTreeSendThread::dump() const { - for (size_t i = 0; i < _traversalPath.size(); ++i) { - std::cout << (int32_t)(_traversalPath[i].getNextIndex()) << "-->"; - } -} diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 9a1a57b41c..5cb2c4c76d 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -14,6 +14,8 @@ #include "../octree/OctreeSendThread.h" +#include + #include "EntityPriorityQueue.h" class EntityNodeData; @@ -22,7 +24,7 @@ class EntityItem; class EntityTreeSendThread : public OctreeSendThread { public: - EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node); + EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) : OctreeSendThread(myServer, node) { } protected: void preDistributionProcessing() override; @@ -35,18 +37,10 @@ private: bool addDescendantsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root); - void getNextVisibleElement(VisibleElement& element); - void dump() const; // DEBUG method, delete later + DiffTraversal _traversal; EntityPriorityQueue _sendQueue; - ViewFrustum _currentView; - ViewFrustum _completedView; - ConicalView _conicalView; // optimized view for fast priority calculations - std::vector _traversalPath; - std::function _getNextVisibleElementCallback { nullptr }; - std::function _scanNextElementCallback { nullptr }; - uint64_t _startOfCompletedTraversal { 0 }; - uint64_t _startOfCurrentTraversal { 0 }; + ConicalView _conicalView; // cached optimized view for fast priority calculations }; #endif // hifi_EntityTreeSendThread_h diff --git a/libraries/entities/src/DiffTraversal.cpp b/libraries/entities/src/DiffTraversal.cpp new file mode 100644 index 0000000000..b1fddcf5fb --- /dev/null +++ b/libraries/entities/src/DiffTraversal.cpp @@ -0,0 +1,239 @@ +// +// DiffTraversal.cpp +// assignment-client/src/entities +// +// Created by Andrew Meadows 2017.08.08 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "DiffTraversal.h" + +DiffTraversal::Waypoint::Waypoint(EntityTreeElementPointer& element) : _nextIndex(0) { + assert(element); + _weakElement = element; +} + +void DiffTraversal::Waypoint::getNextVisibleElementFirstTime(DiffTraversal::VisibleElement& next, const ViewFrustum& view) { + // TODO: add LOD culling of elements + // NOTE: no need to set next.intersection in the "FirstTime" context + if (_nextIndex == -1) { + // only get here for the root Waypoint at the very beginning of traversal + // safe to assume this element intersects view + ++_nextIndex; + next.element = _weakElement.lock(); + return; + } else if (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer element = _weakElement.lock(); + if (element) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && view.cubeIntersectsKeyhole(nextElement->getAACube())) { + next.element = nextElement; + return; + } + } + } + } + next.element.reset(); +} + +void DiffTraversal::Waypoint::getNextVisibleElementRepeat(DiffTraversal::VisibleElement& next, const ViewFrustum& view, uint64_t lastTime) { + // TODO: add LOD culling of elements + if (_nextIndex == -1) { + // only get here for the root Waypoint at the very beginning of traversal + // safe to assume this element intersects view + ++_nextIndex; + EntityTreeElementPointer element = _weakElement.lock(); + // root case is special: its intersection is always INTERSECT + // and we can skip it if the content hasn't changed + if (element->getLastChangedContent() > lastTime) { + next.element = element; + next.intersection = ViewFrustum::INTERSECT; + return; + } + } + if (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer element = _weakElement.lock(); + if (element) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && nextElement->getLastChanged() > lastTime) { + ViewFrustum::intersection intersection = view.calculateCubeKeyholeIntersection(nextElement->getAACube()); + if (intersection != ViewFrustum::OUTSIDE) { + next.element = nextElement; + next.intersection = intersection; + return; + } + } + } + } + } + next.element.reset(); + next.intersection = ViewFrustum::OUTSIDE; +} + +DiffTraversal::DiffTraversal() { + const int32_t MIN_PATH_DEPTH = 16; + _path.reserve(MIN_PATH_DEPTH); +} + +void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::VisibleElement& next, + const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime) { + // TODO: add LOD culling of elements + if (_nextIndex == -1) { + // only get here for the root Waypoint at the very beginning of traversal + // safe to assume this element intersects view + ++_nextIndex; + EntityTreeElementPointer element = _weakElement.lock(); + // root case is special: its intersection is always INTERSECT + // and we can skip it if the content hasn't changed + if (element->getLastChangedContent() > lastTime) { + next.element = element; + next.intersection = ViewFrustum::INTERSECT; + return; + } + } + if (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer element = _weakElement.lock(); + if (element) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement) { + AACube cube = nextElement->getAACube(); + // NOTE: for differential case next.intersection is against the _completedView + ViewFrustum::intersection intersection = lastView.calculateCubeKeyholeIntersection(cube); + if ( lastView.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE && + !(intersection == ViewFrustum::INSIDE && nextElement->getLastChanged() < lastTime)) { + next.element = nextElement; + next.intersection = intersection; + return; + } + } + } + } + } + next.element.reset(); + next.intersection = ViewFrustum::OUTSIDE; +} + +DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root) { + // there are three types of traversal: + // + // (1) First = fresh view --> find all elements in view + // (2) Repeat = view hasn't changed --> find elements changed since last complete traversal + // (3) Differential = view has changed --> find elements changed or in new view but not old + // + // For each traversal type we define assign the appropriate _getNextVisibleElementCallback + // + // _getNextVisibleElementCallback = identifies elements that need to be traversed, + // updates VisibleElement ref argument with pointer-to-element and view-intersection + // (INSIDE, INTERSECT, or OUTSIDE) + // + // External code should update the _scanElementCallback after calling prepareNewTraversal + // + + Type type = Type::First; + if (_startOfCompletedTraversal == 0) { + // first time + _currentView = viewFrustum; + _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { + _path.back().getNextVisibleElementFirstTime(next, _currentView); + }; + } else if (_currentView.isVerySimilar(viewFrustum)) { + // again + _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { + _path.back().getNextVisibleElementRepeat(next, _currentView, _startOfCompletedTraversal); + }; + type = Type::Repeat; + } else { + // differential + _currentView = viewFrustum; + _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { + _path.back().getNextVisibleElementDifferential(next, _currentView, _completedView, _startOfCompletedTraversal); + }; + type = Type::Differential; + } + + assert(root); + _path.clear(); + _path.push_back(DiffTraversal::Waypoint(root)); + // set root fork's index such that root element returned at getNextElement() + _path.back().initRootNextIndex(); + _startOfCurrentTraversal = usecTimestampNow(); + + return type; +} + +void DiffTraversal::getNextVisibleElement(DiffTraversal::VisibleElement& next) { + if (_path.empty()) { + next.element.reset(); + next.intersection = ViewFrustum::OUTSIDE; + return; + } + _getNextVisibleElementCallback(next); + if (next.element) { + int8_t nextIndex = _path.back().getNextIndex(); + if (nextIndex > 0) { + // next.element needs to be added to the path + _path.push_back(DiffTraversal::Waypoint(next.element)); + } + } else { + // we're done at this level + while (!next.element) { + // pop one level + _path.pop_back(); + if (_path.empty()) { + // we've traversed the entire tree + _completedView = _currentView; + _startOfCompletedTraversal = _startOfCurrentTraversal; + return; + } + // keep looking for next + _getNextVisibleElementCallback(next); + if (next.element) { + // we've descended one level so add it to the path + _path.push_back(DiffTraversal::Waypoint(next.element)); + } + } + } +} + +void DiffTraversal::setScanCallback(std::function cb) { + if (!cb) { + _scanElementCallback = [](DiffTraversal::VisibleElement& a){}; + } else { + _scanElementCallback = cb; + } +} + +// DEBUG method: delete later +std::ostream& operator<<(std::ostream& s, const DiffTraversal& traversal) { + for (size_t i = 0; i < traversal._path.size(); ++i) { + s << (int32_t)(traversal._path[i].getNextIndex()); + if (i < traversal._path.size() - 1) { + s << "-->"; + } + } + return s; +} + +void DiffTraversal::traverse(uint64_t timeBudget) { + uint64_t expiry = usecTimestampNow() + timeBudget; + DiffTraversal::VisibleElement next; + getNextVisibleElement(next); + while (next.element) { + if (next.element->hasContent()) { + _scanElementCallback(next); + } + if (usecTimestampNow() > expiry) { + break; + } + getNextVisibleElement(next); + } +} diff --git a/libraries/entities/src/DiffTraversal.h b/libraries/entities/src/DiffTraversal.h new file mode 100644 index 0000000000..508d1e9c6b --- /dev/null +++ b/libraries/entities/src/DiffTraversal.h @@ -0,0 +1,78 @@ +// +// DiffTraversal.h +// assignment-client/src/entities +// +// Created by Andrew Meadows 2017.08.08 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_DiffTraversal_h +#define hifi_DiffTraversal_h + +#include // DEBUG + +#include + +#include "EntityTreeElement.h" + +// DiffTraversal traverses the tree and applies _scanElementCallback on elements it finds +class DiffTraversal { +public: + // VisibleElement is a struct identifying an element and how it intersected the view. + // The intersection is used to optimize culling entities from the sendQueue. + class VisibleElement { + public: + EntityTreeElementPointer element; + ViewFrustum::intersection intersection { ViewFrustum::OUTSIDE }; + }; + + // Waypoint is an bookmark in a "path" of waypoints during a traversal. + class Waypoint { + public: + Waypoint(EntityTreeElementPointer& element); + + void getNextVisibleElementFirstTime(VisibleElement& next, const ViewFrustum& view); + void getNextVisibleElementRepeat(VisibleElement& next, const ViewFrustum& view, uint64_t lastTime); + void getNextVisibleElementDifferential(VisibleElement& next, const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime); + + int8_t getNextIndex() const { return _nextIndex; } + void initRootNextIndex() { _nextIndex = -1; } + + protected: + EntityTreeElementWeakPointer _weakElement; + int8_t _nextIndex; + }; + + typedef enum { First, Repeat, Differential } Type; + + DiffTraversal(); + + DiffTraversal::Type prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root); + + const ViewFrustum& getCurrentView() const { return _currentView; } + const ViewFrustum& getCompletedView() const { return _completedView; } + + uint64_t getStartOfCompletedTraversal() const { return _startOfCompletedTraversal; } + bool finished() const { return _path.empty(); } + + void setScanCallback(std::function cb); + void traverse(uint64_t timeBudget); + + friend std::ostream& operator<<(std::ostream& s, const DiffTraversal& traversal); // DEBUG + +private: + void getNextVisibleElement(VisibleElement& next); + + ViewFrustum _currentView; + ViewFrustum _completedView; + std::vector _path; + std::function _getNextVisibleElementCallback { nullptr }; + std::function _scanElementCallback { [](VisibleElement& e){} }; + uint64_t _startOfCompletedTraversal { 0 }; + uint64_t _startOfCurrentTraversal { 0 }; +}; + +#endif // hifi_EntityPriorityQueue_h From 8b7c43f3b1c79c85bbb98d3259eea495eba07eb0 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 9 Aug 2017 17:22:47 -0700 Subject: [PATCH 538/722] add LOD culling in DiffTraversal --- libraries/entities/src/DiffTraversal.cpp | 143 ++++++++++++++--------- libraries/entities/src/DiffTraversal.h | 26 +++-- libraries/octree/src/OctreeConstants.h | 3 +- 3 files changed, 109 insertions(+), 63 deletions(-) diff --git a/libraries/entities/src/DiffTraversal.cpp b/libraries/entities/src/DiffTraversal.cpp index b1fddcf5fb..fcaf2f06ee 100644 --- a/libraries/entities/src/DiffTraversal.cpp +++ b/libraries/entities/src/DiffTraversal.cpp @@ -11,29 +11,39 @@ #include "DiffTraversal.h" +#include + + DiffTraversal::Waypoint::Waypoint(EntityTreeElementPointer& element) : _nextIndex(0) { assert(element); _weakElement = element; } -void DiffTraversal::Waypoint::getNextVisibleElementFirstTime(DiffTraversal::VisibleElement& next, const ViewFrustum& view) { - // TODO: add LOD culling of elements +void DiffTraversal::Waypoint::getNextVisibleElementFirstTime(DiffTraversal::VisibleElement& next, + const DiffTraversal::View& view) { // NOTE: no need to set next.intersection in the "FirstTime" context if (_nextIndex == -1) { - // only get here for the root Waypoint at the very beginning of traversal - // safe to assume this element intersects view + // root case is special: + // its intersection is always INTERSECT, + // we never bother checking for LOD culling, and + // we can skip it if the content hasn't changed ++_nextIndex; next.element = _weakElement.lock(); return; } else if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); if (element) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement && view.cubeIntersectsKeyhole(nextElement->getAACube())) { - next.element = nextElement; - return; + // check for LOD truncation + float visibleLimit = boundaryDistanceForRenderLevel(element->getLevel() + view.rootLevel, view.rootSizeScale); + float distance2 = glm::distance2(view.viewFrustum.getPosition(), element->getAACube().calcCenter()); + if (distance2 < visibleLimit * visibleLimit) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && view.viewFrustum.cubeIntersectsKeyhole(nextElement->getAACube())) { + next.element = nextElement; + return; + } } } } @@ -41,15 +51,12 @@ void DiffTraversal::Waypoint::getNextVisibleElementFirstTime(DiffTraversal::Visi next.element.reset(); } -void DiffTraversal::Waypoint::getNextVisibleElementRepeat(DiffTraversal::VisibleElement& next, const ViewFrustum& view, uint64_t lastTime) { - // TODO: add LOD culling of elements +void DiffTraversal::Waypoint::getNextVisibleElementRepeat( + DiffTraversal::VisibleElement& next, const DiffTraversal::View& view, uint64_t lastTime) { if (_nextIndex == -1) { - // only get here for the root Waypoint at the very beginning of traversal - // safe to assume this element intersects view + // root case is special ++_nextIndex; EntityTreeElementPointer element = _weakElement.lock(); - // root case is special: its intersection is always INTERSECT - // and we can skip it if the content hasn't changed if (element->getLastChangedContent() > lastTime) { next.element = element; next.intersection = ViewFrustum::INTERSECT; @@ -59,15 +66,20 @@ void DiffTraversal::Waypoint::getNextVisibleElementRepeat(DiffTraversal::Visible if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); if (element) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement && nextElement->getLastChanged() > lastTime) { - ViewFrustum::intersection intersection = view.calculateCubeKeyholeIntersection(nextElement->getAACube()); - if (intersection != ViewFrustum::OUTSIDE) { - next.element = nextElement; - next.intersection = intersection; - return; + // check for LOD truncation + float visibleLimit = boundaryDistanceForRenderLevel(element->getLevel() - view.rootLevel + 1, view.rootSizeScale); + float distance2 = glm::distance2(view.viewFrustum.getPosition(), element->getAACube().calcCenter()); + if (distance2 < visibleLimit * visibleLimit) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && nextElement->getLastChanged() > lastTime) { + ViewFrustum::intersection intersection = view.viewFrustum.calculateCubeKeyholeIntersection(nextElement->getAACube()); + if (intersection != ViewFrustum::OUTSIDE) { + next.element = nextElement; + next.intersection = intersection; + return; + } } } } @@ -83,15 +95,11 @@ DiffTraversal::DiffTraversal() { } void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::VisibleElement& next, - const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime) { - // TODO: add LOD culling of elements + const DiffTraversal::View& view, const DiffTraversal::View& lastView, uint64_t lastTime) { if (_nextIndex == -1) { - // only get here for the root Waypoint at the very beginning of traversal - // safe to assume this element intersects view + // root case is special ++_nextIndex; EntityTreeElementPointer element = _weakElement.lock(); - // root case is special: its intersection is always INTERSECT - // and we can skip it if the content hasn't changed if (element->getLastChangedContent() > lastTime) { next.element = element; next.intersection = ViewFrustum::INTERSECT; @@ -101,18 +109,39 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); if (element) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement) { - AACube cube = nextElement->getAACube(); - // NOTE: for differential case next.intersection is against the _completedView - ViewFrustum::intersection intersection = lastView.calculateCubeKeyholeIntersection(cube); - if ( lastView.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE && - !(intersection == ViewFrustum::INSIDE && nextElement->getLastChanged() < lastTime)) { - next.element = nextElement; - next.intersection = intersection; - return; + // check for LOD truncation + uint32_t level = element->getLevel() - view.rootLevel + 1; + float visibleLimit = boundaryDistanceForRenderLevel(level, view.rootSizeScale); + float distance2 = glm::distance2(view.viewFrustum.getPosition(), element->getAACube().calcCenter()); + if (distance2 < visibleLimit * visibleLimit) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement) { + AACube cube = nextElement->getAACube(); + if ( view.viewFrustum.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE) { + ViewFrustum::intersection lastIntersection = lastView.viewFrustum.calculateCubeKeyholeIntersection(cube); + if (!(lastIntersection == ViewFrustum::INSIDE && nextElement->getLastChanged() < lastTime)) { + next.element = nextElement; + // NOTE: for differential case next.intersection is against the lastView + // because this helps the "external scan" optimize its culling + next.intersection = lastIntersection; + return; + } else { + // check for LOD truncation in the last traversal because + // we may need to traverse this element after all if the lastView skipped it for LOD + // + // NOTE: the element's "level" must be invariant (the differntial algorithm doesn't work otherwise) + // so we recycle the value computed higher up + visibleLimit = boundaryDistanceForRenderLevel(level, lastView.rootSizeScale); + distance2 = glm::distance2(lastView.viewFrustum.getPosition(), element->getAACube().calcCenter()); + if (distance2 >= visibleLimit * visibleLimit) { + next.element = nextElement; + next.intersection = lastIntersection; + return; + } + } + } } } } @@ -125,27 +154,27 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root) { // there are three types of traversal: // - // (1) First = fresh view --> find all elements in view - // (2) Repeat = view hasn't changed --> find elements changed since last complete traversal - // (3) Differential = view has changed --> find elements changed or in new view but not old + // (1) First = fresh view --> find all elements in view + // (2) Repeat = view hasn't changed --> find elements changed since last complete traversal + // (3) Differential = view has changed --> find elements changed or in new view but not old // - // For each traversal type we define assign the appropriate _getNextVisibleElementCallback + // for each traversal type we assign the appropriate _getNextVisibleElementCallback // - // _getNextVisibleElementCallback = identifies elements that need to be traversed, - // updates VisibleElement ref argument with pointer-to-element and view-intersection - // (INSIDE, INTERSECT, or OUTSIDE) + // _getNextVisibleElementCallback = identifies elements that need to be traversed, + // updates VisibleElement ref argument with pointer-to-element and view-intersection + // (INSIDE, INTERSECT, or OUTSIDE) // - // External code should update the _scanElementCallback after calling prepareNewTraversal + // external code should update the _scanElementCallback after calling prepareNewTraversal // Type type = Type::First; if (_startOfCompletedTraversal == 0) { // first time - _currentView = viewFrustum; + _currentView.viewFrustum = viewFrustum; _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { _path.back().getNextVisibleElementFirstTime(next, _currentView); }; - } else if (_currentView.isVerySimilar(viewFrustum)) { + } else if (_currentView.viewFrustum.isVerySimilar(viewFrustum)) { // again _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { _path.back().getNextVisibleElementRepeat(next, _currentView, _startOfCompletedTraversal); @@ -153,7 +182,7 @@ DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFr type = Type::Repeat; } else { // differential - _currentView = viewFrustum; + _currentView.viewFrustum = viewFrustum; _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { _path.back().getNextVisibleElementDifferential(next, _currentView, _completedView, _startOfCompletedTraversal); }; @@ -165,6 +194,11 @@ DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFr _path.push_back(DiffTraversal::Waypoint(root)); // set root fork's index such that root element returned at getNextElement() _path.back().initRootNextIndex(); + + // cache LOD parameters in the _currentView + _currentView.rootLevel = root->getLevel(); + _currentView.rootSizeScale = root->getScale() * MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT; + _startOfCurrentTraversal = usecTimestampNow(); return type; @@ -180,7 +214,6 @@ void DiffTraversal::getNextVisibleElement(DiffTraversal::VisibleElement& next) { if (next.element) { int8_t nextIndex = _path.back().getNextIndex(); if (nextIndex > 0) { - // next.element needs to be added to the path _path.push_back(DiffTraversal::Waypoint(next.element)); } } else { diff --git a/libraries/entities/src/DiffTraversal.h b/libraries/entities/src/DiffTraversal.h index 508d1e9c6b..53361780fc 100644 --- a/libraries/entities/src/DiffTraversal.h +++ b/libraries/entities/src/DiffTraversal.h @@ -29,14 +29,22 @@ public: ViewFrustum::intersection intersection { ViewFrustum::OUTSIDE }; }; + // View is a struct with a ViewFrustum and LOD parameters + class View { + public: + ViewFrustum viewFrustum; + float rootSizeScale { 1.0f }; + float rootLevel { 0.0f }; + }; + // Waypoint is an bookmark in a "path" of waypoints during a traversal. class Waypoint { public: Waypoint(EntityTreeElementPointer& element); - void getNextVisibleElementFirstTime(VisibleElement& next, const ViewFrustum& view); - void getNextVisibleElementRepeat(VisibleElement& next, const ViewFrustum& view, uint64_t lastTime); - void getNextVisibleElementDifferential(VisibleElement& next, const ViewFrustum& view, const ViewFrustum& lastView, uint64_t lastTime); + void getNextVisibleElementFirstTime(VisibleElement& next, const View& view); + void getNextVisibleElementRepeat(VisibleElement& next, const View& view, uint64_t lastTime); + void getNextVisibleElementDifferential(VisibleElement& next, const View& view, const View& lastView, uint64_t lastTime); int8_t getNextIndex() const { return _nextIndex; } void initRootNextIndex() { _nextIndex = -1; } @@ -52,8 +60,8 @@ public: DiffTraversal::Type prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root); - const ViewFrustum& getCurrentView() const { return _currentView; } - const ViewFrustum& getCompletedView() const { return _completedView; } + const ViewFrustum& getCurrentView() const { return _currentView.viewFrustum; } + const ViewFrustum& getCompletedView() const { return _completedView.viewFrustum; } uint64_t getStartOfCompletedTraversal() const { return _startOfCompletedTraversal; } bool finished() const { return _path.empty(); } @@ -66,13 +74,17 @@ public: private: void getNextVisibleElement(VisibleElement& next); - ViewFrustum _currentView; - ViewFrustum _completedView; + View _currentView; + View _completedView; std::vector _path; std::function _getNextVisibleElementCallback { nullptr }; std::function _scanElementCallback { [](VisibleElement& e){} }; uint64_t _startOfCompletedTraversal { 0 }; uint64_t _startOfCurrentTraversal { 0 }; + + // LOD stuff + float _rootSizeScale { 1.0f }; + uint32_t _rootLevel { 0 }; }; #endif // hifi_EntityPriorityQueue_h diff --git a/libraries/octree/src/OctreeConstants.h b/libraries/octree/src/OctreeConstants.h index 53442f52d6..06f09e557c 100644 --- a/libraries/octree/src/OctreeConstants.h +++ b/libraries/octree/src/OctreeConstants.h @@ -21,7 +21,8 @@ const int TREE_SCALE = 32768; // ~20 miles.. This is the number of meters of the const int HALF_TREE_SCALE = TREE_SCALE / 2; // This controls the LOD. Larger number will make smaller voxels visible at greater distance. -const float DEFAULT_OCTREE_SIZE_SCALE = TREE_SCALE * 400.0f; +const float MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT = 400.0f; // max distance where a 1x1x1 cube is visible for 20:20 vision +const float DEFAULT_OCTREE_SIZE_SCALE = TREE_SCALE * MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT; // Since entities like models live inside of octree cells, and they themselves can have very small mesh parts, // we want to have some constant that controls have big a mesh part must be to render even if the octree cell itself From 3e50d01734336f3c4b564848dddaae04c1f2266c Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 10 Aug 2017 09:13:50 -0700 Subject: [PATCH 539/722] more correct handling of LOD --- .../src/entities/EntityTreeSendThread.cpp | 7 ++- .../src/entities/EntityTreeSendThread.h | 2 +- libraries/entities/src/DiffTraversal.cpp | 56 +++++++++---------- libraries/entities/src/DiffTraversal.h | 15 ++--- 4 files changed, 36 insertions(+), 44 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 031d7ac3fb..48cd8ea200 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -95,7 +95,8 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O ViewFrustum viewFrustum; nodeData->copyCurrentViewFrustum(viewFrustum); EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); - startNewTraversal(viewFrustum, root); + int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); + startNewTraversal(viewFrustum, root, lodLevelOffset); } } if (!_traversal.finished()) { @@ -172,8 +173,8 @@ bool EntityTreeSendThread::addDescendantsToExtraFlaggedEntities(const QUuid& fil return hasNewChild || hasNewDescendants; } -void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root) { - DiffTraversal::Type type = _traversal.prepareNewTraversal(view, root); +void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root, int32_t lodLevelOffset) { + DiffTraversal::Type type = _traversal.prepareNewTraversal(view, root, lodLevelOffset); // there are three types of traversal: // // (1) FirstTime = at login --> find everything in view diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 5cb2c4c76d..33c22c8c4a 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -36,7 +36,7 @@ private: bool addAncestorsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); bool addDescendantsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); - void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root); + void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset); DiffTraversal _traversal; EntityPriorityQueue _sendQueue; diff --git a/libraries/entities/src/DiffTraversal.cpp b/libraries/entities/src/DiffTraversal.cpp index fcaf2f06ee..5f105f3fb5 100644 --- a/libraries/entities/src/DiffTraversal.cpp +++ b/libraries/entities/src/DiffTraversal.cpp @@ -34,7 +34,7 @@ void DiffTraversal::Waypoint::getNextVisibleElementFirstTime(DiffTraversal::Visi EntityTreeElementPointer element = _weakElement.lock(); if (element) { // check for LOD truncation - float visibleLimit = boundaryDistanceForRenderLevel(element->getLevel() + view.rootLevel, view.rootSizeScale); + float visibleLimit = boundaryDistanceForRenderLevel(element->getLevel() + view.lodLevelOffset, view.rootSizeScale); float distance2 = glm::distance2(view.viewFrustum.getPosition(), element->getAACube().calcCenter()); if (distance2 < visibleLimit * visibleLimit) { while (_nextIndex < NUMBER_OF_CHILDREN) { @@ -67,7 +67,7 @@ void DiffTraversal::Waypoint::getNextVisibleElementRepeat( EntityTreeElementPointer element = _weakElement.lock(); if (element) { // check for LOD truncation - float visibleLimit = boundaryDistanceForRenderLevel(element->getLevel() - view.rootLevel + 1, view.rootSizeScale); + float visibleLimit = boundaryDistanceForRenderLevel(element->getLevel() + view.lodLevelOffset, view.rootSizeScale); float distance2 = glm::distance2(view.viewFrustum.getPosition(), element->getAACube().calcCenter()); if (distance2 < visibleLimit * visibleLimit) { while (_nextIndex < NUMBER_OF_CHILDREN) { @@ -95,12 +95,12 @@ DiffTraversal::DiffTraversal() { } void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::VisibleElement& next, - const DiffTraversal::View& view, const DiffTraversal::View& lastView, uint64_t lastTime) { + const DiffTraversal::View& view, const DiffTraversal::View& lastView) { if (_nextIndex == -1) { // root case is special ++_nextIndex; EntityTreeElementPointer element = _weakElement.lock(); - if (element->getLastChangedContent() > lastTime) { + if (element->getLastChangedContent() > lastView.startTime) { next.element = element; next.intersection = ViewFrustum::INTERSECT; return; @@ -110,7 +110,7 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V EntityTreeElementPointer element = _weakElement.lock(); if (element) { // check for LOD truncation - uint32_t level = element->getLevel() - view.rootLevel + 1; + int32_t level = element->getLevel() + view.lodLevelOffset; float visibleLimit = boundaryDistanceForRenderLevel(level, view.rootSizeScale); float distance2 = glm::distance2(view.viewFrustum.getPosition(), element->getAACube().calcCenter()); if (distance2 < visibleLimit * visibleLimit) { @@ -121,7 +121,7 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V AACube cube = nextElement->getAACube(); if ( view.viewFrustum.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE) { ViewFrustum::intersection lastIntersection = lastView.viewFrustum.calculateCubeKeyholeIntersection(cube); - if (!(lastIntersection == ViewFrustum::INSIDE && nextElement->getLastChanged() < lastTime)) { + if (!(lastIntersection == ViewFrustum::INSIDE && nextElement->getLastChanged() < lastView.startTime)) { next.element = nextElement; // NOTE: for differential case next.intersection is against the lastView // because this helps the "external scan" optimize its culling @@ -130,10 +130,8 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V } else { // check for LOD truncation in the last traversal because // we may need to traverse this element after all if the lastView skipped it for LOD - // - // NOTE: the element's "level" must be invariant (the differntial algorithm doesn't work otherwise) - // so we recycle the value computed higher up - visibleLimit = boundaryDistanceForRenderLevel(level, lastView.rootSizeScale); + int32_t lastLevel = element->getLevel() + lastView.lodLevelOffset; + visibleLimit = boundaryDistanceForRenderLevel(lastLevel, lastView.rootSizeScale); distance2 = glm::distance2(lastView.viewFrustum.getPosition(), element->getAACube().calcCenter()); if (distance2 >= visibleLimit * visibleLimit) { next.element = nextElement; @@ -151,7 +149,9 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V next.intersection = ViewFrustum::OUTSIDE; } -DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root) { +DiffTraversal::Type DiffTraversal::prepareNewTraversal( + const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset) { + assert(root); // there are three types of traversal: // // (1) First = fresh view --> find all elements in view @@ -167,39 +167,36 @@ DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFr // external code should update the _scanElementCallback after calling prepareNewTraversal // - Type type = Type::First; - if (_startOfCompletedTraversal == 0) { - // first time + Type type; + if (_completedView.startTime == 0) { + type = Type::First; _currentView.viewFrustum = viewFrustum; + _currentView.lodLevelOffset = root->getLevel() + lodLevelOffset - 1; // -1 because true root has level=1 + _currentView.rootSizeScale = root->getScale() * MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT; _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { _path.back().getNextVisibleElementFirstTime(next, _currentView); }; - } else if (_currentView.viewFrustum.isVerySimilar(viewFrustum)) { - // again - _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { - _path.back().getNextVisibleElementRepeat(next, _currentView, _startOfCompletedTraversal); - }; + } else if (_completedView.viewFrustum.isVerySimilar(viewFrustum) && lodLevelOffset == _completedView.lodLevelOffset) { type = Type::Repeat; - } else { - // differential - _currentView.viewFrustum = viewFrustum; _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { - _path.back().getNextVisibleElementDifferential(next, _currentView, _completedView, _startOfCompletedTraversal); + _path.back().getNextVisibleElementRepeat(next, _completedView, _completedView.startTime); }; + } else { type = Type::Differential; + _currentView.viewFrustum = viewFrustum; + _currentView.lodLevelOffset = root->getLevel() + lodLevelOffset - 1; // -1 because true root has level=1 + _currentView.rootSizeScale = root->getScale() * MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT; + _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { + _path.back().getNextVisibleElementDifferential(next, _currentView, _completedView); + }; } - assert(root); _path.clear(); _path.push_back(DiffTraversal::Waypoint(root)); // set root fork's index such that root element returned at getNextElement() _path.back().initRootNextIndex(); - // cache LOD parameters in the _currentView - _currentView.rootLevel = root->getLevel(); - _currentView.rootSizeScale = root->getScale() * MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT; - - _startOfCurrentTraversal = usecTimestampNow(); + _currentView.startTime = usecTimestampNow(); return type; } @@ -224,7 +221,6 @@ void DiffTraversal::getNextVisibleElement(DiffTraversal::VisibleElement& next) { if (_path.empty()) { // we've traversed the entire tree _completedView = _currentView; - _startOfCompletedTraversal = _startOfCurrentTraversal; return; } // keep looking for next diff --git a/libraries/entities/src/DiffTraversal.h b/libraries/entities/src/DiffTraversal.h index 53361780fc..f1025f1e3a 100644 --- a/libraries/entities/src/DiffTraversal.h +++ b/libraries/entities/src/DiffTraversal.h @@ -33,8 +33,9 @@ public: class View { public: ViewFrustum viewFrustum; + uint64_t startTime { 0 }; float rootSizeScale { 1.0f }; - float rootLevel { 0.0f }; + int32_t lodLevelOffset { 0 }; }; // Waypoint is an bookmark in a "path" of waypoints during a traversal. @@ -44,7 +45,7 @@ public: void getNextVisibleElementFirstTime(VisibleElement& next, const View& view); void getNextVisibleElementRepeat(VisibleElement& next, const View& view, uint64_t lastTime); - void getNextVisibleElementDifferential(VisibleElement& next, const View& view, const View& lastView, uint64_t lastTime); + void getNextVisibleElementDifferential(VisibleElement& next, const View& view, const View& lastView); int8_t getNextIndex() const { return _nextIndex; } void initRootNextIndex() { _nextIndex = -1; } @@ -58,12 +59,12 @@ public: DiffTraversal(); - DiffTraversal::Type prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root); + Type prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset); const ViewFrustum& getCurrentView() const { return _currentView.viewFrustum; } const ViewFrustum& getCompletedView() const { return _completedView.viewFrustum; } - uint64_t getStartOfCompletedTraversal() const { return _startOfCompletedTraversal; } + uint64_t getStartOfCompletedTraversal() const { return _completedView.startTime; } bool finished() const { return _path.empty(); } void setScanCallback(std::function cb); @@ -79,12 +80,6 @@ private: std::vector _path; std::function _getNextVisibleElementCallback { nullptr }; std::function _scanElementCallback { [](VisibleElement& e){} }; - uint64_t _startOfCompletedTraversal { 0 }; - uint64_t _startOfCurrentTraversal { 0 }; - - // LOD stuff - float _rootSizeScale { 1.0f }; - uint32_t _rootLevel { 0 }; }; #endif // hifi_EntityPriorityQueue_h From e114fa1b824fcac3e85f5a89e95af42937c078da Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 10 Aug 2017 09:17:46 -0700 Subject: [PATCH 540/722] fix debug traversal repeat logic --- .../src/entities/EntityTreeSendThread.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 48cd8ea200..bfa402e913 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -84,14 +84,10 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O bool viewFrustumChanged, bool isFullScene) { // BEGIN EXPERIMENTAL DIFFERENTIAL TRAVERSAL if (nodeData->getUsesFrustum()) { - { - // DEBUG HACK: trigger traversal (Again) every so often - const uint64_t TRAVERSE_AGAIN_PERIOD = 4 * USECS_PER_SECOND; - if (!viewFrustumChanged && usecTimestampNow() > _traversal.getStartOfCompletedTraversal() + TRAVERSE_AGAIN_PERIOD) { - viewFrustumChanged = true; - } - } - if (viewFrustumChanged) { + // DEBUG HACK: trigger traversal (Repeat) every so often + const uint64_t TRAVERSE_AGAIN_PERIOD = 4 * USECS_PER_SECOND; + bool repeatTraversal = usecTimestampNow() > _traversal.getStartOfCompletedTraversal() + TRAVERSE_AGAIN_PERIOD; + if (viewFrustumChanged || repeatTraversal) { ViewFrustum viewFrustum; nodeData->copyCurrentViewFrustum(viewFrustum); EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); From 7597088c7c29bddba5fb0460408e743e1c25976d Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 16 Aug 2017 11:57:54 -0700 Subject: [PATCH 541/722] simpler logic flow --- libraries/entities/src/EntityTreeElement.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index f6d27bcc87..0c33855a61 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -432,11 +432,11 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData // and include the entity in our final count of entities packetData->endLevel(entityLevel); actualNumberOfEntities++; - } - // If the entity item got completely appended, then we can remove it from the extra encode data - if (appendEntityState == OctreeElement::COMPLETED) { - entityTreeElementExtraEncodeData->entities.remove(entity->getEntityItemID()); + // If the entity item got completely appended, then we can remove it from the extra encode data + if (appendEntityState == OctreeElement::COMPLETED) { + entityTreeElementExtraEncodeData->entities.remove(entity->getEntityItemID()); + } } // If any part of the entity items didn't fit, then the element is considered partial From 0b0de968940e5db61dd8af1f6e3430087f88f2ed Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 16 Aug 2017 12:22:05 -0700 Subject: [PATCH 542/722] use memcpy instead of copying one byte at a time --- libraries/octree/src/OctreePacketData.cpp | 25 +++++++++-------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/libraries/octree/src/OctreePacketData.cpp b/libraries/octree/src/OctreePacketData.cpp index 5fd7e4dba3..493dfdcff5 100644 --- a/libraries/octree/src/OctreePacketData.cpp +++ b/libraries/octree/src/OctreePacketData.cpp @@ -582,9 +582,7 @@ bool OctreePacketData::compressContent() { if (compressedData.size() < (int)MAX_OCTREE_PACKET_DATA_SIZE) { _compressedBytes = compressedData.size(); - for (int i = 0; i < _compressedBytes; i++) { - _compressed[i] = compressedData[i]; - } + memcpy(_compressed, compressedData.constData(), _compressedBytes); _dirty = false; success = true; } @@ -598,25 +596,22 @@ void OctreePacketData::loadFinalizedContent(const unsigned char* data, int lengt if (data && length > 0) { if (_enableCompression) { - QByteArray compressedData; - for (int i = 0; i < length; i++) { - compressedData[i] = data[i]; - _compressed[i] = compressedData[i]; - } _compressedBytes = length; + memcpy(_compressed, data, _compressedBytes); + + QByteArray compressedData; + compressedData.resize(_compressedBytes); + memcpy(compressedData.data(), data, _compressedBytes); + QByteArray uncompressedData = qUncompress(compressedData); if (uncompressedData.size() <= _bytesAvailable) { _bytesInUse = uncompressedData.size(); _bytesAvailable -= uncompressedData.size(); - - for (int i = 0; i < _bytesInUse; i++) { - _uncompressed[i] = uncompressedData[i]; - } + memcpy(_uncompressed, uncompressedData.constData(), _bytesInUse); } } else { - for (int i = 0; i < length; i++) { - _uncompressed[i] = _compressed[i] = data[i]; - } + memcpy(_uncompressed, data, length); + memcpy(_compressed, data, length); _bytesInUse = _compressedBytes = length; } } else { From 4c8f6834790ac67292c6b663ca7eab16ced6c101 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 16 Aug 2017 14:56:17 -0700 Subject: [PATCH 543/722] entity too small checks --- .../src/entities/EntityTreeSendThread.cpp | 84 ++++++++++++++++--- .../src/entities/EntityTreeSendThread.h | 2 +- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index bfa402e913..953bb0a54b 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -13,6 +13,7 @@ #include #include +#include #include "EntityServer.h" @@ -92,7 +93,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O nodeData->copyCurrentViewFrustum(viewFrustum); EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); - startNewTraversal(viewFrustum, root, lodLevelOffset); + startNewTraversal(viewFrustum, root, nodeData->getOctreeSizeScale(), lodLevelOffset); } } if (!_traversal.finished()) { @@ -169,7 +170,7 @@ bool EntityTreeSendThread::addDescendantsToExtraFlaggedEntities(const QUuid& fil return hasNewChild || hasNewDescendants; } -void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root, int32_t lodLevelOffset) { +void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root, float octreeSizeScale, int32_t lodLevelOffset) { DiffTraversal::Type type = _traversal.prepareNewTraversal(view, root, lodLevelOffset); // there are three types of traversal: // @@ -192,8 +193,30 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree AACube cube = entity->getQueryAACube(success); if (success) { if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { - float priority = _conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority)); + // Check the size of the entity, it's possible that a "too small to see" entity is included in a + // larger octree cell because of its position (for example if it crosses the boundary of a cell it + // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen + // before we consider including it. + // We can't cull a parent-entity by its dimensions because the child may be larger. We need to + // avoid sending details about a child but not the parent. The parent's queryAACube should have + // been adjusted to encompass the queryAACube of the child. + AABox entityBounds = entity->hasChildren() ? AABox(cube) : entity->getAABox(success); + if (!success) { + // if this entity is a child of an avatar, the entity-server wont be able to determine its + // AABox. If this happens, fall back to the queryAACube. + entityBounds = AABox(cube); + } + + float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), + entityBounds, + octreeSizeScale, + lodLevelOffset); + + // Only send entities if they are large enough to see + if (renderAccuracy > 0.0f) { + float priority = _conicalView.computePriority(entityBounds); + _sendQueue.push(PrioritizedEntity(entity, priority)); + } } } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; @@ -212,8 +235,22 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree AACube cube = entity->getQueryAACube(success); if (success) { if (next.intersection == ViewFrustum::INSIDE || _traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { - float priority = _conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority)); + // See the DiffTraversal::First case for an explanation of the "entity is too small" check + AABox entityBounds = entity->hasChildren() ? AABox(cube) : entity->getAABox(success); + if (!success) { + entityBounds = AABox(cube); + } + + float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), + entityBounds, + octreeSizeScale, + lodLevelOffset); + + // Only send entities if they are large enough to see + if (renderAccuracy > 0.0f) { + float priority = _conicalView.computePriority(entityBounds); + _sendQueue.push(PrioritizedEntity(entity, priority)); + } } } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; @@ -233,11 +270,36 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { - if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube) && - (entity->getLastEdited() > timestamp || - !_traversal.getCompletedView().cubeIntersectsKeyhole(cube))) { - float priority = _conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority)); + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + // See the DiffTraversal::First case for an explanation of the "entity is too small" check + AABox entityBounds = entity->hasChildren() ? AABox(cube) : entity->getAABox(success); + if (!success) { + entityBounds = AABox(cube); + } + + float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), + entityBounds, + octreeSizeScale, + lodLevelOffset); + + // Only send entities if they are large enough to see + if (renderAccuracy > 0.0f) { + if (entity->getLastEdited() > timestamp || !_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { + float priority = _conicalView.computePriority(entityBounds); + _sendQueue.push(PrioritizedEntity(entity, priority)); + } else { + // If this entity was skipped last time because it was too small, we still need to send it + float lastRenderAccuracy = calculateRenderAccuracy(_traversal.getCompletedView().getPosition(), + entityBounds, + octreeSizeScale, + lodLevelOffset); + + if (lastRenderAccuracy <= 0.0f) { + float priority = _conicalView.computePriority(entityBounds); + _sendQueue.push(PrioritizedEntity(entity, priority)); + } + } + } } } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 33c22c8c4a..6b78172617 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -36,7 +36,7 @@ private: bool addAncestorsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); bool addDescendantsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); - void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset); + void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, float octreeSizeScale, int32_t lodLevelOffset); DiffTraversal _traversal; EntityPriorityQueue _sendQueue; From b0f30acce2b3db38a8fb9e04b1e75fde61890b9b Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 16 Aug 2017 16:19:00 -0700 Subject: [PATCH 544/722] use cube instead of entityBounds --- .../src/entities/EntityTreeSendThread.cpp | 36 +++++-------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 953bb0a54b..42a391d931 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -197,24 +197,14 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree // larger octree cell because of its position (for example if it crosses the boundary of a cell it // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen // before we consider including it. - // We can't cull a parent-entity by its dimensions because the child may be larger. We need to - // avoid sending details about a child but not the parent. The parent's queryAACube should have - // been adjusted to encompass the queryAACube of the child. - AABox entityBounds = entity->hasChildren() ? AABox(cube) : entity->getAABox(success); - if (!success) { - // if this entity is a child of an avatar, the entity-server wont be able to determine its - // AABox. If this happens, fall back to the queryAACube. - entityBounds = AABox(cube); - } - float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), - entityBounds, + cube, octreeSizeScale, lodLevelOffset); // Only send entities if they are large enough to see if (renderAccuracy > 0.0f) { - float priority = _conicalView.computePriority(entityBounds); + float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); } } @@ -236,19 +226,14 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (success) { if (next.intersection == ViewFrustum::INSIDE || _traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { // See the DiffTraversal::First case for an explanation of the "entity is too small" check - AABox entityBounds = entity->hasChildren() ? AABox(cube) : entity->getAABox(success); - if (!success) { - entityBounds = AABox(cube); - } - float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), - entityBounds, + cube, octreeSizeScale, lodLevelOffset); // Only send entities if they are large enough to see if (renderAccuracy > 0.0f) { - float priority = _conicalView.computePriority(entityBounds); + float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); } } @@ -272,30 +257,25 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (success) { if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { // See the DiffTraversal::First case for an explanation of the "entity is too small" check - AABox entityBounds = entity->hasChildren() ? AABox(cube) : entity->getAABox(success); - if (!success) { - entityBounds = AABox(cube); - } - float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), - entityBounds, + cube, octreeSizeScale, lodLevelOffset); // Only send entities if they are large enough to see if (renderAccuracy > 0.0f) { if (entity->getLastEdited() > timestamp || !_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { - float priority = _conicalView.computePriority(entityBounds); + float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); } else { // If this entity was skipped last time because it was too small, we still need to send it float lastRenderAccuracy = calculateRenderAccuracy(_traversal.getCompletedView().getPosition(), - entityBounds, + cube, octreeSizeScale, lodLevelOffset); if (lastRenderAccuracy <= 0.0f) { - float priority = _conicalView.computePriority(entityBounds); + float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); } } From bb5368eb55c2c7caace8232313d64456f255c87b Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 16 Aug 2017 17:29:13 -0700 Subject: [PATCH 545/722] use correct rootSizeScale --- .../src/entities/EntityTreeSendThread.cpp | 10 +++++----- assignment-client/src/entities/EntityTreeSendThread.h | 2 +- libraries/entities/src/DiffTraversal.h | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 42a391d931..3a5fa2003f 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -170,7 +170,7 @@ bool EntityTreeSendThread::addDescendantsToExtraFlaggedEntities(const QUuid& fil return hasNewChild || hasNewDescendants; } -void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root, float octreeSizeScale, int32_t lodLevelOffset) { +void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root, int32_t lodLevelOffset) { DiffTraversal::Type type = _traversal.prepareNewTraversal(view, root, lodLevelOffset); // there are three types of traversal: // @@ -199,7 +199,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree // before we consider including it. float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), cube, - octreeSizeScale, + _traversal.getCurrentRootSizeScale(), lodLevelOffset); // Only send entities if they are large enough to see @@ -228,7 +228,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree // See the DiffTraversal::First case for an explanation of the "entity is too small" check float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), cube, - octreeSizeScale, + _traversal.getCurrentRootSizeScale(), lodLevelOffset); // Only send entities if they are large enough to see @@ -259,7 +259,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree // See the DiffTraversal::First case for an explanation of the "entity is too small" check float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), cube, - octreeSizeScale, + _traversal.getCurrentRootSizeScale(), lodLevelOffset); // Only send entities if they are large enough to see @@ -271,7 +271,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree // If this entity was skipped last time because it was too small, we still need to send it float lastRenderAccuracy = calculateRenderAccuracy(_traversal.getCompletedView().getPosition(), cube, - octreeSizeScale, + _traversal.getCompletedRootSizeScale(), lodLevelOffset); if (lastRenderAccuracy <= 0.0f) { diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 6b78172617..33c22c8c4a 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -36,7 +36,7 @@ private: bool addAncestorsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); bool addDescendantsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); - void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, float octreeSizeScale, int32_t lodLevelOffset); + void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset); DiffTraversal _traversal; EntityPriorityQueue _sendQueue; diff --git a/libraries/entities/src/DiffTraversal.h b/libraries/entities/src/DiffTraversal.h index f1025f1e3a..874e1ed869 100644 --- a/libraries/entities/src/DiffTraversal.h +++ b/libraries/entities/src/DiffTraversal.h @@ -64,6 +64,9 @@ public: const ViewFrustum& getCurrentView() const { return _currentView.viewFrustum; } const ViewFrustum& getCompletedView() const { return _completedView.viewFrustum; } + const float getCurrentRootSizeScale() const { return _currentView.rootSizeScale; } + const float getCompletedRootSizeScale() const { return _completedView.rootSizeScale; } + uint64_t getStartOfCompletedTraversal() const { return _completedView.startTime; } bool finished() const { return _path.empty(); } From cf2e500ec44672d776bc49bde4db837e94d19555 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 17 Aug 2017 15:49:11 -0700 Subject: [PATCH 546/722] remove unnecessary const qualifiers --- libraries/entities/src/DiffTraversal.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities/src/DiffTraversal.h b/libraries/entities/src/DiffTraversal.h index 874e1ed869..87bd83b70f 100644 --- a/libraries/entities/src/DiffTraversal.h +++ b/libraries/entities/src/DiffTraversal.h @@ -64,8 +64,8 @@ public: const ViewFrustum& getCurrentView() const { return _currentView.viewFrustum; } const ViewFrustum& getCompletedView() const { return _completedView.viewFrustum; } - const float getCurrentRootSizeScale() const { return _currentView.rootSizeScale; } - const float getCompletedRootSizeScale() const { return _completedView.rootSizeScale; } + float getCurrentRootSizeScale() const { return _currentView.rootSizeScale; } + float getCompletedRootSizeScale() const { return _completedView.rootSizeScale; } uint64_t getStartOfCompletedTraversal() const { return _completedView.startTime; } bool finished() const { return _path.empty(); } From 4f50b5755f45a86fdf9c53d8309e2fc2c930cb03 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 17 Aug 2017 15:50:44 -0700 Subject: [PATCH 547/722] remove crufty argument --- assignment-client/src/entities/EntityTreeSendThread.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 3a5fa2003f..a968c6e2a3 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -93,7 +93,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O nodeData->copyCurrentViewFrustum(viewFrustum); EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); - startNewTraversal(viewFrustum, root, nodeData->getOctreeSizeScale(), lodLevelOffset); + startNewTraversal(viewFrustum, root, lodLevelOffset); } } if (!_traversal.finished()) { From 9fb7eb4ba6a4a91675bb097002016610d981e4e5 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 21 Aug 2017 10:47:04 -0700 Subject: [PATCH 548/722] resort _sendQueue when previous view didn't finish --- .../src/entities/EntityTreeSendThread.cpp | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index a968c6e2a3..b2ebfab7ee 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -84,6 +84,7 @@ void EntityTreeSendThread::preDistributionProcessing() { void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) { // BEGIN EXPERIMENTAL DIFFERENTIAL TRAVERSAL + int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); if (nodeData->getUsesFrustum()) { // DEBUG HACK: trigger traversal (Repeat) every so often const uint64_t TRAVERSE_AGAIN_PERIOD = 4 * USECS_PER_SECOND; @@ -92,10 +93,14 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O ViewFrustum viewFrustum; nodeData->copyCurrentViewFrustum(viewFrustum); EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); - int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); startNewTraversal(viewFrustum, root, lodLevelOffset); } } + + // If the previous traversal didn't finish, we'll need to resort the entities still in _sendQueue after calling traverse + EntityPriorityQueue prevSendQueue; + _sendQueue.swap(prevSendQueue); + if (!_traversal.finished()) { uint64_t startTime = usecTimestampNow(); @@ -105,6 +110,34 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O uint64_t dt = usecTimestampNow() - startTime; std::cout << "adebug traversal complete " << " Q.size = " << _sendQueue.size() << " dt = " << dt << std::endl; // adebug } + + // Re-add elements from previous traveral if they still need to be sent + while (!prevSendQueue.empty()) { + EntityItemPointer entity = prevSendQueue.top().getEntity(); + prevSendQueue.pop(); + if (entity) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), + cube, + _traversal.getCurrentRootSizeScale(), + lodLevelOffset); + + // Only send entities if they are large enough to see + if (renderAccuracy > 0.0f) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + } + } + } else { + const float WHEN_IN_DOUBT_PRIORITY = 1.0f; + _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + } + } + } + if (!_sendQueue.empty()) { // print what needs to be sent while (!_sendQueue.empty()) { From 18f88a5a643bb072a1766d16f68b49e49f7ed4c5 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 21 Aug 2017 16:54:39 -0700 Subject: [PATCH 549/722] keep track of readded entities in a set to avoid rechecking them, compute priority early --- .../src/entities/EntityTreeSendThread.cpp | 72 ++++++++++++------- .../src/entities/EntityTreeSendThread.h | 3 + 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index b2ebfab7ee..adb9aedb6b 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -100,6 +100,39 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O // If the previous traversal didn't finish, we'll need to resort the entities still in _sendQueue after calling traverse EntityPriorityQueue prevSendQueue; _sendQueue.swap(prevSendQueue); + _prevEntitySet.clear(); + // Re-add elements from previous traveral if they still need to be sent + while (!prevSendQueue.empty()) { + EntityItemPointer entity = prevSendQueue.top().getEntity(); + prevSendQueue.pop(); + if (entity) { + // We can keep track of the entity regardless of if we decide to send it so that we don't have to check it again + // during the traversal + _prevEntitySet.insert(entity); + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + const float DO_NOT_SEND = -1.0e-6f; + float priority = _conicalView.computePriority(cube); + if (priority != DO_NOT_SEND) { + float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), + cube, + _traversal.getCurrentRootSizeScale(), + lodLevelOffset); + + // Only send entities if they are large enough to see + if (renderAccuracy > 0.0f) { + _sendQueue.push(PrioritizedEntity(entity, priority)); + } + } + } + } else { + const float WHEN_IN_DOUBT_PRIORITY = 1.0f; + _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + } + } + } if (!_traversal.finished()) { uint64_t startTime = usecTimestampNow(); @@ -111,33 +144,6 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O std::cout << "adebug traversal complete " << " Q.size = " << _sendQueue.size() << " dt = " << dt << std::endl; // adebug } - // Re-add elements from previous traveral if they still need to be sent - while (!prevSendQueue.empty()) { - EntityItemPointer entity = prevSendQueue.top().getEntity(); - prevSendQueue.pop(); - if (entity) { - bool success = false; - AACube cube = entity->getQueryAACube(success); - if (success) { - if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { - float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), - cube, - _traversal.getCurrentRootSizeScale(), - lodLevelOffset); - - // Only send entities if they are large enough to see - if (renderAccuracy > 0.0f) { - float priority = _conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority)); - } - } - } else { - const float WHEN_IN_DOUBT_PRIORITY = 1.0f; - _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); - } - } - } - if (!_sendQueue.empty()) { // print what needs to be sent while (!_sendQueue.empty()) { @@ -222,6 +228,10 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree case DiffTraversal::First: _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { next.element->forEachEntity([&](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_prevEntitySet.find(entity) != _prevEntitySet.end()) { + return; + } bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { @@ -253,6 +263,10 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (next.element->getLastChangedContent() > _traversal.getStartOfCompletedTraversal()) { uint64_t timestamp = _traversal.getStartOfCompletedTraversal(); next.element->forEachEntity([&](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_prevEntitySet.find(entity) != _prevEntitySet.end()) { + return; + } if (entity->getLastEdited() > timestamp) { bool success = false; AACube cube = entity->getQueryAACube(success); @@ -285,6 +299,10 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree uint64_t timestamp = _traversal.getStartOfCompletedTraversal(); if (next.element->getLastChangedContent() > timestamp || next.intersection != ViewFrustum::INSIDE) { next.element->forEachEntity([&](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_prevEntitySet.find(entity) != _prevEntitySet.end()) { + return; + } bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 33c22c8c4a..f98d007eb3 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -12,6 +12,8 @@ #ifndef hifi_EntityTreeSendThread_h #define hifi_EntityTreeSendThread_h +#include + #include "../octree/OctreeSendThread.h" #include @@ -40,6 +42,7 @@ private: DiffTraversal _traversal; EntityPriorityQueue _sendQueue; + std::unordered_set _prevEntitySet; ConicalView _conicalView; // cached optimized view for fast priority calculations }; From 1930c8f215ba0fe417b442d8f7a0f9ed1a873d47 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 22 Aug 2017 18:01:14 -0700 Subject: [PATCH 550/722] only resort if view changed --- .../src/entities/EntityTreeSendThread.cpp | 77 +++++++++++-------- .../src/entities/EntityTreeSendThread.h | 2 +- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index adb9aedb6b..946dedc3ff 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -94,42 +94,43 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O nodeData->copyCurrentViewFrustum(viewFrustum); EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); startNewTraversal(viewFrustum, root, lodLevelOffset); - } - } - // If the previous traversal didn't finish, we'll need to resort the entities still in _sendQueue after calling traverse - EntityPriorityQueue prevSendQueue; - _sendQueue.swap(prevSendQueue); - _prevEntitySet.clear(); - // Re-add elements from previous traveral if they still need to be sent - while (!prevSendQueue.empty()) { - EntityItemPointer entity = prevSendQueue.top().getEntity(); - prevSendQueue.pop(); - if (entity) { - // We can keep track of the entity regardless of if we decide to send it so that we don't have to check it again - // during the traversal - _prevEntitySet.insert(entity); - bool success = false; - AACube cube = entity->getQueryAACube(success); - if (success) { - if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { - const float DO_NOT_SEND = -1.0e-6f; - float priority = _conicalView.computePriority(cube); - if (priority != DO_NOT_SEND) { - float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), - cube, - _traversal.getCurrentRootSizeScale(), - lodLevelOffset); + // If the previous traversal didn't finish, we'll need to resort the entities still in _sendQueue after calling traverse + if (!_sendQueue.empty()) { + EntityPriorityQueue prevSendQueue; + _sendQueue.swap(prevSendQueue); + // Re-add elements from previous traveral if they still need to be sent + while (!prevSendQueue.empty()) { + EntityItemPointer entity = prevSendQueue.top().getEntity(); + prevSendQueue.pop(); + _entitiesToSend.clear(); + if (entity) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + const float DO_NOT_SEND = -1.0e-6f; + float priority = _conicalView.computePriority(cube); + if (priority != DO_NOT_SEND) { + float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), + cube, + _traversal.getCurrentRootSizeScale(), + lodLevelOffset); - // Only send entities if they are large enough to see - if (renderAccuracy > 0.0f) { - _sendQueue.push(PrioritizedEntity(entity, priority)); + // Only send entities if they are large enough to see + if (renderAccuracy > 0.0f) { + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesToSend.insert(entity); + } + } + } + } else { + const float WHEN_IN_DOUBT_PRIORITY = 1.0f; + _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + _entitiesToSend.insert(entity); } } } - } else { - const float WHEN_IN_DOUBT_PRIORITY = 1.0f; - _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); } } } @@ -153,6 +154,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O std::cout << "adebug send '" << entity->getName().toStdString() << "'" << " : " << entry.getPriority() << std::endl; // adebug } _sendQueue.pop(); + _entitiesToSend.erase(entity); } } // END EXPERIMENTAL DIFFERENTIAL TRAVERSAL @@ -229,7 +231,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame - if (_prevEntitySet.find(entity) != _prevEntitySet.end()) { + if (_entitiesToSend.find(entity) != _entitiesToSend.end()) { return; } bool success = false; @@ -249,11 +251,13 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (renderAccuracy > 0.0f) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesToSend.insert(entity); } } } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + _entitiesToSend.insert(entity); } }); }); @@ -264,7 +268,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree uint64_t timestamp = _traversal.getStartOfCompletedTraversal(); next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame - if (_prevEntitySet.find(entity) != _prevEntitySet.end()) { + if (_entitiesToSend.find(entity) != _entitiesToSend.end()) { return; } if (entity->getLastEdited() > timestamp) { @@ -282,11 +286,13 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (renderAccuracy > 0.0f) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesToSend.insert(entity); } } } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + _entitiesToSend.insert(entity); } } }); @@ -300,7 +306,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (next.element->getLastChangedContent() > timestamp || next.intersection != ViewFrustum::INSIDE) { next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame - if (_prevEntitySet.find(entity) != _prevEntitySet.end()) { + if (_entitiesToSend.find(entity) != _entitiesToSend.end()) { return; } bool success = false; @@ -318,6 +324,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (entity->getLastEdited() > timestamp || !_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesToSend.insert(entity); } else { // If this entity was skipped last time because it was too small, we still need to send it float lastRenderAccuracy = calculateRenderAccuracy(_traversal.getCompletedView().getPosition(), @@ -328,6 +335,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (lastRenderAccuracy <= 0.0f) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesToSend.insert(entity); } } } @@ -335,6 +343,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + _entitiesToSend.insert(entity); } }); } diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index f98d007eb3..50e7981938 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -42,7 +42,7 @@ private: DiffTraversal _traversal; EntityPriorityQueue _sendQueue; - std::unordered_set _prevEntitySet; + std::unordered_set _entitiesToSend; ConicalView _conicalView; // cached optimized view for fast priority calculations }; From 971f1e79249a3fefeeb8f7bf4bd2051fc95ec185 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 22 Aug 2017 18:02:35 -0700 Subject: [PATCH 551/722] put lodLevelOffset back --- .../src/entities/EntityTreeSendThread.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 946dedc3ff..a19badafe9 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -84,7 +84,6 @@ void EntityTreeSendThread::preDistributionProcessing() { void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) { // BEGIN EXPERIMENTAL DIFFERENTIAL TRAVERSAL - int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); if (nodeData->getUsesFrustum()) { // DEBUG HACK: trigger traversal (Repeat) every so often const uint64_t TRAVERSE_AGAIN_PERIOD = 4 * USECS_PER_SECOND; @@ -93,17 +92,18 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O ViewFrustum viewFrustum; nodeData->copyCurrentViewFrustum(viewFrustum); EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); + int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); startNewTraversal(viewFrustum, root, lodLevelOffset); // If the previous traversal didn't finish, we'll need to resort the entities still in _sendQueue after calling traverse if (!_sendQueue.empty()) { EntityPriorityQueue prevSendQueue; _sendQueue.swap(prevSendQueue); + _entitiesToSend.clear(); // Re-add elements from previous traveral if they still need to be sent while (!prevSendQueue.empty()) { EntityItemPointer entity = prevSendQueue.top().getEntity(); prevSendQueue.pop(); - _entitiesToSend.clear(); if (entity) { bool success = false; AACube cube = entity->getQueryAACube(success); @@ -113,9 +113,9 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O float priority = _conicalView.computePriority(cube); if (priority != DO_NOT_SEND) { float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), - cube, - _traversal.getCurrentRootSizeScale(), - lodLevelOffset); + cube, + _traversal.getCurrentRootSizeScale(), + lodLevelOffset); // Only send entities if they are large enough to see if (renderAccuracy > 0.0f) { From a32cc7f5554fcb221706c3499e265a7b784c29ca Mon Sep 17 00:00:00 2001 From: Sam Gondelman Date: Wed, 23 Aug 2017 13:50:08 -0700 Subject: [PATCH 552/722] typo --- assignment-client/src/entities/EntityTreeSendThread.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index a19badafe9..9cc0c3d34e 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -100,7 +100,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O EntityPriorityQueue prevSendQueue; _sendQueue.swap(prevSendQueue); _entitiesToSend.clear(); - // Re-add elements from previous traveral if they still need to be sent + // Re-add elements from previous traversal if they still need to be sent while (!prevSendQueue.empty()) { EntityItemPointer entity = prevSendQueue.top().getEntity(); prevSendQueue.pop(); From 1562fb153e704a4caff229f9bbf1272d9446f567 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 23 Aug 2017 14:56:18 -0700 Subject: [PATCH 553/722] cherrypick traverseTreeAndBuildNextPacketPayload() --- .../src/octree/OctreeSendThread.cpp | 2 +- .../src/octree/OctreeSendThread.h | 6 +- assignment-client/src/octree/OctreeServer.cpp | 58 +++++++++---------- libraries/octree/src/Octree.h | 3 +- 4 files changed, 34 insertions(+), 35 deletions(-) diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index 9345f96b1d..b6d9d323c1 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -27,8 +27,8 @@ quint64 startSceneSleepTime = 0; quint64 endSceneSleepTime = 0; OctreeSendThread::OctreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) : - _myServer(myServer), _node(node), + _myServer(myServer), _nodeUuid(node->getUUID()) { QString safeServerName("Octree"); diff --git a/assignment-client/src/octree/OctreeSendThread.h b/assignment-client/src/octree/OctreeSendThread.h index 8f75092528..bcfede262c 100644 --- a/assignment-client/src/octree/OctreeSendThread.h +++ b/assignment-client/src/octree/OctreeSendThread.h @@ -57,18 +57,16 @@ protected: bool viewFrustumChanged, bool isFullScene); virtual bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params); - OctreeServer* _myServer { nullptr }; + OctreePacketData _packetData; QWeakPointer _node; + OctreeServer* _myServer { nullptr }; private: int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, bool dontSuppressDuplicate = false); int packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged); - QUuid _nodeUuid; - OctreePacketData _packetData; - int _truePacketsSent { 0 }; // available for debug stats int _trueBytesSent { 0 }; // available for debug stats int _packetsSentThisInterval { 0 }; // used for bandwidth throttle condition diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 974f00326b..4a40449e30 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -883,7 +883,7 @@ OctreeServer::UniqueSendThread OctreeServer::newSendThread(const SharedNodePoint OctreeServer::UniqueSendThread OctreeServer::createSendThread(const SharedNodePointer& node) { auto sendThread = newSendThread(node); - + // we want to be notified when the thread finishes connect(sendThread.get(), &GenericThread::finished, this, &OctreeServer::removeSendThread); sendThread->initialize(true); @@ -905,13 +905,13 @@ void OctreeServer::handleOctreeQueryPacket(QSharedPointer messa // need to make sure we have it in our nodeList. auto nodeList = DependencyManager::get(); nodeList->updateNodeWithDataFromPacket(message, senderNode); - + auto it = _sendThreads.find(senderNode->getUUID()); if (it == _sendThreads.end()) { _sendThreads.emplace(senderNode->getUUID(), createSendThread(senderNode)); } else if (it->second->isShuttingDown()) { _sendThreads.erase(it); // Remove right away and wait on thread to be - + _sendThreads.emplace(senderNode->getUUID(), createSendThread(senderNode)); } } @@ -1085,7 +1085,7 @@ void OctreeServer::readConfiguration() { if (getPayload().size() > 0) { parsePayload(); } - + const QJsonObject& settingsObject = DependencyManager::get()->getDomainHandler().getSettingsObject(); QString settingsKey = getMyDomainSettingsKey(); @@ -1212,9 +1212,9 @@ void OctreeServer::run() { OctreeElement::resetPopulationStatistics(); _tree = createTree(); _tree->setIsServer(true); - + qDebug() << "Waiting for connection to domain to request settings from domain-server."; - + // wait until we have the domain-server settings, otherwise we bail DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::settingsReceived, this, &OctreeServer::domainSettingsRequestComplete); @@ -1225,9 +1225,9 @@ void OctreeServer::run() { } void OctreeServer::domainSettingsRequestComplete() { - + auto nodeList = DependencyManager::get(); - + // we need to ask the DS about agents so we can ping/reply with them nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer }); @@ -1237,26 +1237,26 @@ void OctreeServer::domainSettingsRequestComplete() { packetReceiver.registerListener(PacketType::JurisdictionRequest, this, "handleJurisdictionRequestPacket"); packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacement"); packetReceiver.registerListener(PacketType::OctreeFileReplacementFromUrl, this, "handleOctreeFileReplacementFromURL"); - + readConfiguration(); - + beforeRun(); // after payload has been processed - + connect(nodeList.data(), SIGNAL(nodeAdded(SharedNodePointer)), SLOT(nodeAdded(SharedNodePointer))); connect(nodeList.data(), SIGNAL(nodeKilled(SharedNodePointer)), SLOT(nodeKilled(SharedNodePointer))); #ifndef WIN32 setvbuf(stdout, NULL, _IOLBF, 0); #endif - + nodeList->linkedDataCreateCallback = [this](Node* node) { auto queryNodeData = createOctreeQueryNode(); queryNodeData->init(); node->setLinkedData(std::move(queryNodeData)); }; - + srand((unsigned)time(0)); - + // if we want Persistence, set up the local file and persist thread if (_wantPersist) { // If persist filename does not exist, let's see if there is one beside the application binary @@ -1351,24 +1351,24 @@ void OctreeServer::domainSettingsRequestComplete() { } } qDebug() << "Backups will be stored in: " << _backupDirectoryPath; - + // now set up PersistThread _persistThread = new OctreePersistThread(_tree, _persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval, _wantBackup, _settings, _debugTimestampNow, _persistAsFileType); _persistThread->initialize(true); } - + // set up our jurisdiction broadcaster... if (_jurisdiction) { _jurisdiction->setNodeType(getMyNodeType()); } _jurisdictionSender = new JurisdictionSender(_jurisdiction, getMyNodeType()); _jurisdictionSender->initialize(true); - + // set up our OctreeServerPacketProcessor _octreeInboundPacketProcessor = new OctreeInboundPacketProcessor(this); _octreeInboundPacketProcessor->initialize(true); - + // Convert now to tm struct for local timezone tm* localtm = localtime(&_started); const int MAX_TIME_LENGTH = 128; @@ -1380,7 +1380,7 @@ void OctreeServer::domainSettingsRequestComplete() { if (gmtm) { strftime(utcBuffer, MAX_TIME_LENGTH, " [%m/%d/%Y %X UTC]", gmtm); } - + qDebug() << "Now running... started at: " << localBuffer << utcBuffer; } @@ -1391,7 +1391,7 @@ void OctreeServer::nodeAdded(SharedNodePointer node) { void OctreeServer::nodeKilled(SharedNodePointer node) { quint64 start = usecTimestampNow(); - + // Shutdown send thread auto it = _sendThreads.find(node->getUUID()); if (it != _sendThreads.end()) { @@ -1437,13 +1437,13 @@ void OctreeServer::aboutToFinish() { if (_jurisdictionSender) { _jurisdictionSender->terminating(); } - + // Shut down all the send threads for (auto& it : _sendThreads) { auto& sendThread = *it.second; sendThread.setIsShuttingDown(); } - + // Clear will destruct all the unique_ptr to OctreeSendThreads which will call the GenericThread's dtor // which waits on the thread to be done before returning _sendThreads.clear(); // Cleans up all the send threads. @@ -1563,7 +1563,7 @@ void OctreeServer::sendStatsPacket() { threadsStats["2. packetDistributor"] = (double)howManyThreadsDidPacketDistributor(oneSecondAgo); threadsStats["3. handlePacektSend"] = (double)howManyThreadsDidHandlePacketSend(oneSecondAgo); threadsStats["4. writeDatagram"] = (double)howManyThreadsDidCallWriteDatagram(oneSecondAgo); - + QJsonObject statsArray1; statsArray1["1. configuration"] = getConfiguration(); statsArray1["2. detailed_stats_url"] = getStatusLink(); @@ -1571,13 +1571,13 @@ void OctreeServer::sendStatsPacket() { statsArray1["4. persistFileLoadTime"] = getFileLoadTime(); statsArray1["5. clients"] = getCurrentClientCount(); statsArray1["6. threads"] = threadsStats; - + // Octree Stats QJsonObject octreeStats; octreeStats["1. elementCount"] = (double)OctreeElement::getNodeCount(); octreeStats["2. internalElementCount"] = (double)OctreeElement::getInternalNodeCount(); octreeStats["3. leafElementCount"] = (double)OctreeElement::getLeafNodeCount(); - + // Stats Object 2 QJsonObject dataObject1; dataObject1["1. totalPackets"] = (double)OctreeSendThread::_totalPackets; @@ -1595,7 +1595,7 @@ void OctreeServer::sendStatsPacket() { timingArray1["5. avgCompressAndWriteTime"] = getAverageCompressAndWriteTime(); timingArray1["6. avgSendTime"] = getAveragePacketSendingTime(); timingArray1["7. nodeWaitTime"] = getAverageNodeWaitTime(); - + QJsonObject statsObject2; statsObject2["data"] = dataObject1; statsObject2["timing"] = timingArray1; @@ -1615,18 +1615,18 @@ void OctreeServer::sendStatsPacket() { timingArray2["4. avgProcessTimePerElement"] = (double)_octreeInboundPacketProcessor->getAverageProcessTimePerElement(); timingArray2["5. avgLockWaitTimePerElement"] = (double)_octreeInboundPacketProcessor->getAverageLockWaitTimePerElement(); } - + QJsonObject statsObject3; statsObject3["data"] = dataArray2; statsObject3["timing"] = timingArray2; - + // Merge everything QJsonObject jsonArray; jsonArray["1. misc"] = statsArray1; jsonArray["2. octree"] = octreeStats; jsonArray["3. outbound"] = statsObject2; jsonArray["4. inbound"] = statsObject3; - + QJsonObject statsObject; statsObject[QString(getMyServerName()) + "Server"] = jsonArray; addPacketStatsAndSendStatsPacket(statsObject); diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index 2794ca85f0..a2df5f44e5 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -92,7 +92,8 @@ public: OUT_OF_VIEW, WAS_IN_VIEW, NO_CHANGE, - OCCLUDED + OCCLUDED, + FINISHED } reason; reason stopReason; From b6818c4369b5da374a45d90ceee953847aa51446 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 23 Aug 2017 14:56:55 -0700 Subject: [PATCH 554/722] first-pass sending entities from _sendQueue --- .../src/entities/EntityTreeSendThread.cpp | 86 ++++++++++++++++++- .../src/entities/EntityTreeSendThread.h | 6 ++ libraries/entities/src/DiffTraversal.cpp | 20 ++--- libraries/entities/src/DiffTraversal.h | 2 + 4 files changed, 99 insertions(+), 15 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 9cc0c3d34e..8e5cdfed13 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -17,6 +17,8 @@ #include "EntityServer.h" +//#define SEND_SORTED_ENTITIES + void EntityTreeSendThread::preDistributionProcessing() { auto node = _node.toStrongRef(); auto nodeData = static_cast(node->getLinkedData()); @@ -145,6 +147,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O std::cout << "adebug traversal complete " << " Q.size = " << _sendQueue.size() << " dt = " << dt << std::endl; // adebug } +#ifndef SEND_SORTED_ENTITIES if (!_sendQueue.empty()) { // print what needs to be sent while (!_sendQueue.empty()) { @@ -157,6 +160,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O _entitiesToSend.erase(entity); } } +#endif // SEND_SORTED_ENTITIES // END EXPERIMENTAL DIFFERENTIAL TRAVERSAL OctreeSendThread::traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); @@ -245,7 +249,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), cube, _traversal.getCurrentRootSizeScale(), - lodLevelOffset); + _traversal.getCurrentLODOffset()); // Only send entities if they are large enough to see if (renderAccuracy > 0.0f) { @@ -280,7 +284,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), cube, _traversal.getCurrentRootSizeScale(), - lodLevelOffset); + _traversal.getCurrentLODOffset()); // Only send entities if they are large enough to see if (renderAccuracy > 0.0f) { @@ -317,7 +321,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), cube, _traversal.getCurrentRootSizeScale(), - lodLevelOffset); + _traversal.getCurrentLODOffset()); // Only send entities if they are large enough to see if (renderAccuracy > 0.0f) { @@ -330,7 +334,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree float lastRenderAccuracy = calculateRenderAccuracy(_traversal.getCompletedView().getPosition(), cube, _traversal.getCompletedRootSizeScale(), - lodLevelOffset); + _traversal.getCompletedLODOffset()); if (lastRenderAccuracy <= 0.0f) { float priority = _conicalView.computePriority(cube); @@ -352,3 +356,77 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree } } +bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params) { +#ifdef SEND_SORTED_ENTITIES + //auto entityTree = std::static_pointer_cast(_myServer->getOctree()); + if (_sendQueue.empty()) { + return false; + } + if (!_packetData.hasContent()) { + // This is the beginning of a new packet. + // We pack minimal data for this to be accepted as an OctreeElement payload for the root element. + // The Octree header bytes look like this: + // + // 0x00 octalcode for root + // 0x00 colors (1 bit where recipient should call: child->readElementDataFromBuffer()) + // 0xXX childrenInTreeMask (when params.includeExistsBits is true: 1 bit where child is existant) + // 0x00 childrenInBufferMask (1 bit where recipient should call: child->readElementData() recursively) + const uint8_t zeroByte = 0; + _packetData.appendValue(zeroByte); // octalcode + _packetData.appendValue(zeroByte); // colors + if (params.includeExistsBits) { + uint8_t childrenExistBits = 0; + EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); + for (int32_t i = 0; i < NUMBER_OF_CHILDREN; ++i) { + if (root->getChildAtIndex(i)) { + childrenExistBits += (1 << i); + } + } + _packetData.appendValue(childrenExistBits); // childrenInTreeMask + } + _packetData.appendValue(zeroByte); // childrenInBufferMask + + // Pack zero for numEntities. + // But before we do: grab current byteOffset so we can come back later + // and update this with the real number. + _numEntities = 0; + _numEntitiesOffset = _packetData.getUncompressedByteOffset(); + _packetData.appendValue(_numEntities); + } + + LevelDetails entitiesLevel = _packetData.startLevel(); + while(!_sendQueue.empty()) { + PrioritizedEntity queuedItem = _sendQueue.top(); + EntityItemPointer entity = queuedItem.getEntity(); + if (entity) { + OctreeElement::AppendState appendEntityState = entity->appendEntityData(&_packetData, params, _extraEncodeData); + + if (appendEntityState != OctreeElement::COMPLETED) { + if (appendEntityState == OctreeElement::PARTIAL) { + ++_numEntities; + } + params.stopReason = EncodeBitstreamParams::DIDNT_FIT; + break; + } + ++_numEntities; + } + _sendQueue.pop(); + } + if (_sendQueue.empty()) { + params.stopReason = EncodeBitstreamParams::FINISHED; + _extraEncodeData->entities.clear(); + } + + if (_numEntities == 0) { + _packetData.discardLevel(entitiesLevel); + return false; + } + _packetData.endLevel(entitiesLevel); + _packetData.updatePriorBytes(_numEntitiesOffset, (const unsigned char*)&_numEntities, sizeof(_numEntities)); + return true; + +#else // SEND_SORTED_ENTITIES + return OctreeSendThread::traverseTreeAndBuildNextPacketPayload(params); +#endif // SEND_SORTED_ENTITIES +} + diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 50e7981938..bda73f44ec 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -39,11 +39,17 @@ private: bool addDescendantsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset); + bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params) override; DiffTraversal _traversal; EntityPriorityQueue _sendQueue; std::unordered_set _entitiesToSend; ConicalView _conicalView; // cached optimized view for fast priority calculations + + // packet construction stuff + EntityTreeElementExtraEncodeDataPointer _extraEncodeData { new EntityTreeElementExtraEncodeData() }; + int32_t _numEntitiesOffset { 0 }; + uint16_t _numEntities { 0 }; }; #endif // hifi_EntityTreeSendThread_h diff --git a/libraries/entities/src/DiffTraversal.cpp b/libraries/entities/src/DiffTraversal.cpp index 5f105f3fb5..e8e300081a 100644 --- a/libraries/entities/src/DiffTraversal.cpp +++ b/libraries/entities/src/DiffTraversal.cpp @@ -89,22 +89,15 @@ void DiffTraversal::Waypoint::getNextVisibleElementRepeat( next.intersection = ViewFrustum::OUTSIDE; } -DiffTraversal::DiffTraversal() { - const int32_t MIN_PATH_DEPTH = 16; - _path.reserve(MIN_PATH_DEPTH); -} - void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::VisibleElement& next, const DiffTraversal::View& view, const DiffTraversal::View& lastView) { if (_nextIndex == -1) { // root case is special ++_nextIndex; EntityTreeElementPointer element = _weakElement.lock(); - if (element->getLastChangedContent() > lastView.startTime) { - next.element = element; - next.intersection = ViewFrustum::INTERSECT; - return; - } + next.element = element; + next.intersection = ViewFrustum::INTERSECT; + return; } if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); @@ -149,6 +142,11 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V next.intersection = ViewFrustum::OUTSIDE; } +DiffTraversal::DiffTraversal() { + const int32_t MIN_PATH_DEPTH = 16; + _path.reserve(MIN_PATH_DEPTH); +} + DiffTraversal::Type DiffTraversal::prepareNewTraversal( const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset) { assert(root); @@ -246,7 +244,7 @@ std::ostream& operator<<(std::ostream& s, const DiffTraversal& traversal) { for (size_t i = 0; i < traversal._path.size(); ++i) { s << (int32_t)(traversal._path[i].getNextIndex()); if (i < traversal._path.size() - 1) { - s << "-->"; + s << ":"; } } return s; diff --git a/libraries/entities/src/DiffTraversal.h b/libraries/entities/src/DiffTraversal.h index 87bd83b70f..e849b4fef3 100644 --- a/libraries/entities/src/DiffTraversal.h +++ b/libraries/entities/src/DiffTraversal.h @@ -66,6 +66,8 @@ public: float getCurrentRootSizeScale() const { return _currentView.rootSizeScale; } float getCompletedRootSizeScale() const { return _completedView.rootSizeScale; } + float getCurrentLODOffset() const { return _currentView.lodLevelOffset; } + float getCompletedLODOffset() const { return _completedView.lodLevelOffset; } uint64_t getStartOfCompletedTraversal() const { return _completedView.startTime; } bool finished() const { return _path.empty(); } From b85a5507e05f04d6f4b54e32ea0eb47f8c1830df Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 24 Aug 2017 12:19:27 -0700 Subject: [PATCH 555/722] time budget and raw pointer key for entitiesToSend --- .../src/entities/EntityPriorityQueue.h | 4 ++- .../src/entities/EntityTreeSendThread.cpp | 32 +++++++++++-------- .../src/entities/EntityTreeSendThread.h | 2 +- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/assignment-client/src/entities/EntityPriorityQueue.h b/assignment-client/src/entities/EntityPriorityQueue.h index 215c5262bf..aadaab5614 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.h +++ b/assignment-client/src/entities/EntityPriorityQueue.h @@ -39,9 +39,10 @@ private: // PrioritizedEntity is a placeholder in a sorted queue. class PrioritizedEntity { public: - PrioritizedEntity(EntityItemPointer entity, float priority) : _weakEntity(entity), _priority(priority) { } + PrioritizedEntity(EntityItemPointer entity, float priority) : _weakEntity(entity), _rawEntityPointer(entity.get()), _priority(priority) {} float updatePriority(const ConicalView& view); EntityItemPointer getEntity() const { return _weakEntity.lock(); } + EntityItem* getRawEntityPointer() const { return _rawEntityPointer; } float getPriority() const { return _priority; } class Compare { @@ -52,6 +53,7 @@ public: private: EntityItemWeakPointer _weakEntity; + EntityItem* _rawEntityPointer; float _priority; }; diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 8e5cdfed13..b837812d3d 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -122,14 +122,14 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O // Only send entities if they are large enough to see if (renderAccuracy > 0.0f) { _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesToSend.insert(entity); + _entitiesToSend.insert(entity.get()); } } } } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); - _entitiesToSend.insert(entity); + _entitiesToSend.insert(entity.get()); } } } @@ -140,7 +140,11 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O if (!_traversal.finished()) { uint64_t startTime = usecTimestampNow(); - const uint64_t TIME_BUDGET = 100000; // usec +#ifdef DEBUG + const uint64_t TIME_BUDGET = 400; // usec +#else + const uint64_t TIME_BUDGET = 200; // usec +#endif _traversal.traverse(TIME_BUDGET); uint64_t dt = usecTimestampNow() - startTime; @@ -157,7 +161,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O std::cout << "adebug send '" << entity->getName().toStdString() << "'" << " : " << entry.getPriority() << std::endl; // adebug } _sendQueue.pop(); - _entitiesToSend.erase(entity); + _entitiesToSend.erase(entry.getRawEntityPointer()); } } #endif // SEND_SORTED_ENTITIES @@ -235,7 +239,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame - if (_entitiesToSend.find(entity) != _entitiesToSend.end()) { + if (_entitiesToSend.find(entity.get()) != _entitiesToSend.end()) { return; } bool success = false; @@ -255,13 +259,13 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (renderAccuracy > 0.0f) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesToSend.insert(entity); + _entitiesToSend.insert(entity.get()); } } } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); - _entitiesToSend.insert(entity); + _entitiesToSend.insert(entity.get()); } }); }); @@ -272,7 +276,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree uint64_t timestamp = _traversal.getStartOfCompletedTraversal(); next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame - if (_entitiesToSend.find(entity) != _entitiesToSend.end()) { + if (_entitiesToSend.find(entity.get()) != _entitiesToSend.end()) { return; } if (entity->getLastEdited() > timestamp) { @@ -290,13 +294,13 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (renderAccuracy > 0.0f) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesToSend.insert(entity); + _entitiesToSend.insert(entity.get()); } } } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); - _entitiesToSend.insert(entity); + _entitiesToSend.insert(entity.get()); } } }); @@ -310,7 +314,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (next.element->getLastChangedContent() > timestamp || next.intersection != ViewFrustum::INSIDE) { next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame - if (_entitiesToSend.find(entity) != _entitiesToSend.end()) { + if (_entitiesToSend.find(entity.get()) != _entitiesToSend.end()) { return; } bool success = false; @@ -328,7 +332,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (entity->getLastEdited() > timestamp || !_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesToSend.insert(entity); + _entitiesToSend.insert(entity.get()); } else { // If this entity was skipped last time because it was too small, we still need to send it float lastRenderAccuracy = calculateRenderAccuracy(_traversal.getCompletedView().getPosition(), @@ -339,7 +343,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (lastRenderAccuracy <= 0.0f) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesToSend.insert(entity); + _entitiesToSend.insert(entity.get()); } } } @@ -347,7 +351,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); - _entitiesToSend.insert(entity); + _entitiesToSend.insert(entity.get()); } }); } diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index bda73f44ec..707a20dc94 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -43,7 +43,7 @@ private: DiffTraversal _traversal; EntityPriorityQueue _sendQueue; - std::unordered_set _entitiesToSend; + std::unordered_set _entitiesToSend; ConicalView _conicalView; // cached optimized view for fast priority calculations // packet construction stuff From cbf82a6f2cd2f74a4f2be47c9acfe01273a76bd6 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 24 Aug 2017 13:32:44 -0700 Subject: [PATCH 556/722] fix timeout for physics check --- interface/src/Application.cpp | 6 +----- interface/src/Application.h | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 85172fc73f..6762217485 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4859,13 +4859,9 @@ void Application::update(float deltaTime) { // we haven't yet enabled physics. we wait until we think we have all the collision information // for nearby entities before starting bullet up. quint64 now = usecTimestampNow(); - bool timeout = false; const int PHYSICS_CHECK_TIMEOUT = 2 * USECS_PER_SECOND; - if (_lastPhysicsCheckTime > 0 && now - _lastPhysicsCheckTime > PHYSICS_CHECK_TIMEOUT) { - timeout = true; - } - if (timeout || _fullSceneReceivedCounter > _fullSceneCounterAtLastPhysicsCheck) { + if (now - _lastPhysicsCheckTime > PHYSICS_CHECK_TIMEOUT || _fullSceneReceivedCounter > _fullSceneCounterAtLastPhysicsCheck) { // we've received a new full-scene octree stats packet, or it's been long enough to try again anyway _lastPhysicsCheckTime = now; _fullSceneCounterAtLastPhysicsCheck = _fullSceneReceivedCounter; diff --git a/interface/src/Application.h b/interface/src/Application.h index 74e84ae92c..93f7a4ab79 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -659,7 +659,7 @@ private: uint32_t _fullSceneCounterAtLastPhysicsCheck { 0 }; // _fullSceneReceivedCounter last time we checked physics ready uint32_t _nearbyEntitiesCountAtLastPhysicsCheck { 0 }; // how many in-range entities last time we checked physics ready uint32_t _nearbyEntitiesStabilityCount { 0 }; // how many times has _nearbyEntitiesCountAtLastPhysicsCheck been the same - quint64 _lastPhysicsCheckTime { 0 }; // when did we last check to see if physics was ready + quint64 _lastPhysicsCheckTime { usecTimestampNow() }; // when did we last check to see if physics was ready bool _keyboardDeviceHasFocus { true }; From d54fa205fb20f090e2851bca2ad3dd709c2f0846 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 24 Aug 2017 17:07:09 -0700 Subject: [PATCH 557/722] namechange entitiesToSend --> entitiesInQueue --- .../src/entities/EntityTreeSendThread.cpp | 28 +++++++++---------- .../src/entities/EntityTreeSendThread.h | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index b837812d3d..08fcc39a0f 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -101,7 +101,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O if (!_sendQueue.empty()) { EntityPriorityQueue prevSendQueue; _sendQueue.swap(prevSendQueue); - _entitiesToSend.clear(); + _entitiesInQueue.clear(); // Re-add elements from previous traversal if they still need to be sent while (!prevSendQueue.empty()) { EntityItemPointer entity = prevSendQueue.top().getEntity(); @@ -122,14 +122,14 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O // Only send entities if they are large enough to see if (renderAccuracy > 0.0f) { _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesToSend.insert(entity.get()); + _entitiesInQueue.insert(entity.get()); } } } } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); - _entitiesToSend.insert(entity.get()); + _entitiesInQueue.insert(entity.get()); } } } @@ -161,7 +161,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O std::cout << "adebug send '" << entity->getName().toStdString() << "'" << " : " << entry.getPriority() << std::endl; // adebug } _sendQueue.pop(); - _entitiesToSend.erase(entry.getRawEntityPointer()); + _entitiesInQueue.erase(entry.getRawEntityPointer()); } } #endif // SEND_SORTED_ENTITIES @@ -239,7 +239,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame - if (_entitiesToSend.find(entity.get()) != _entitiesToSend.end()) { + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { return; } bool success = false; @@ -259,13 +259,13 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (renderAccuracy > 0.0f) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesToSend.insert(entity.get()); + _entitiesInQueue.insert(entity.get()); } } } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); - _entitiesToSend.insert(entity.get()); + _entitiesInQueue.insert(entity.get()); } }); }); @@ -276,7 +276,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree uint64_t timestamp = _traversal.getStartOfCompletedTraversal(); next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame - if (_entitiesToSend.find(entity.get()) != _entitiesToSend.end()) { + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { return; } if (entity->getLastEdited() > timestamp) { @@ -294,13 +294,13 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (renderAccuracy > 0.0f) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesToSend.insert(entity.get()); + _entitiesInQueue.insert(entity.get()); } } } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); - _entitiesToSend.insert(entity.get()); + _entitiesInQueue.insert(entity.get()); } } }); @@ -314,7 +314,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (next.element->getLastChangedContent() > timestamp || next.intersection != ViewFrustum::INSIDE) { next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame - if (_entitiesToSend.find(entity.get()) != _entitiesToSend.end()) { + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { return; } bool success = false; @@ -332,7 +332,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (entity->getLastEdited() > timestamp || !_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesToSend.insert(entity.get()); + _entitiesInQueue.insert(entity.get()); } else { // If this entity was skipped last time because it was too small, we still need to send it float lastRenderAccuracy = calculateRenderAccuracy(_traversal.getCompletedView().getPosition(), @@ -343,7 +343,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (lastRenderAccuracy <= 0.0f) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesToSend.insert(entity.get()); + _entitiesInQueue.insert(entity.get()); } } } @@ -351,7 +351,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree } else { const float WHEN_IN_DOUBT_PRIORITY = 1.0f; _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); - _entitiesToSend.insert(entity.get()); + _entitiesInQueue.insert(entity.get()); } }); } diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 707a20dc94..5ea90005b9 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -43,7 +43,7 @@ private: DiffTraversal _traversal; EntityPriorityQueue _sendQueue; - std::unordered_set _entitiesToSend; + std::unordered_set _entitiesInQueue; ConicalView _conicalView; // cached optimized view for fast priority calculations // packet construction stuff From b788273f47f72e24b19ae63ea17084e33e20c76b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 24 Aug 2017 17:33:51 -0700 Subject: [PATCH 558/722] fix repeat First traversals mid-First-traversal --- assignment-client/src/entities/EntityTreeSendThread.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 08fcc39a0f..bdeb3bdbca 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -17,7 +17,7 @@ #include "EntityServer.h" -//#define SEND_SORTED_ENTITIES +#define SEND_SORTED_ENTITIES void EntityTreeSendThread::preDistributionProcessing() { auto node = _node.toStrongRef(); @@ -89,7 +89,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O if (nodeData->getUsesFrustum()) { // DEBUG HACK: trigger traversal (Repeat) every so often const uint64_t TRAVERSE_AGAIN_PERIOD = 4 * USECS_PER_SECOND; - bool repeatTraversal = usecTimestampNow() > _traversal.getStartOfCompletedTraversal() + TRAVERSE_AGAIN_PERIOD; + bool repeatTraversal = _traversal.finished() && usecTimestampNow() > _traversal.getStartOfCompletedTraversal() + TRAVERSE_AGAIN_PERIOD; if (viewFrustumChanged || repeatTraversal) { ViewFrustum viewFrustum; nodeData->copyCurrentViewFrustum(viewFrustum); @@ -272,8 +272,8 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree break; case DiffTraversal::Repeat: _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { - if (next.element->getLastChangedContent() > _traversal.getStartOfCompletedTraversal()) { - uint64_t timestamp = _traversal.getStartOfCompletedTraversal(); + uint64_t timestamp = _traversal.getStartOfCompletedTraversal(); + if (next.element->getLastChangedContent() > timestamp) { next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { From 535d84abc75902d41748ba6d861a58a85ce22e5b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 24 Aug 2017 18:51:03 -0700 Subject: [PATCH 559/722] cleanup and speed up repeat traversals --- .../src/entities/EntityPriorityQueue.cpp | 6 ++-- .../src/entities/EntityPriorityQueue.h | 2 ++ .../src/entities/EntityTreeSendThread.cpp | 33 ++++++++++--------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/assignment-client/src/entities/EntityPriorityQueue.cpp b/assignment-client/src/entities/EntityPriorityQueue.cpp index 77b46afa24..3755e6a5c7 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.cpp +++ b/assignment-client/src/entities/EntityPriorityQueue.cpp @@ -11,7 +11,7 @@ #include "EntityPriorityQueue.h" -const float DO_NOT_SEND = -1.0e-6f; +const float PrioritizedEntity::DO_NOT_SEND = -1.0e-6f; void ConicalView::set(const ViewFrustum& viewFrustum) { // The ConicalView has two parts: a central sphere (same as ViewFrustum) and a circular cone that bounds the frustum part. @@ -47,7 +47,7 @@ float ConicalView::computePriority(const AACube& cube) const { const float AVOID_DIVIDE_BY_ZERO = 0.001f; return r / (d + AVOID_DIVIDE_BY_ZERO); } - return DO_NOT_SEND; + return PrioritizedEntity::DO_NOT_SEND; } // static @@ -68,7 +68,7 @@ float PrioritizedEntity::updatePriority(const ConicalView& conicalView) { if (entity) { _priority = conicalView.computePriority(entity); } else { - _priority = DO_NOT_SEND; + _priority = PrioritizedEntity::DO_NOT_SEND; } return _priority; } diff --git a/assignment-client/src/entities/EntityPriorityQueue.h b/assignment-client/src/entities/EntityPriorityQueue.h index aadaab5614..10db22d695 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.h +++ b/assignment-client/src/entities/EntityPriorityQueue.h @@ -39,6 +39,8 @@ private: // PrioritizedEntity is a placeholder in a sorted queue. class PrioritizedEntity { public: + static const float DO_NOT_SEND; + PrioritizedEntity(EntityItemPointer entity, float priority) : _weakEntity(entity), _rawEntityPointer(entity.get()), _priority(priority) {} float updatePriority(const ConicalView& view); EntityItemPointer getEntity() const { return _weakEntity.lock(); } diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index bdeb3bdbca..851d0566ac 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -85,20 +85,17 @@ void EntityTreeSendThread::preDistributionProcessing() { void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) { - // BEGIN EXPERIMENTAL DIFFERENTIAL TRAVERSAL if (nodeData->getUsesFrustum()) { - // DEBUG HACK: trigger traversal (Repeat) every so often - const uint64_t TRAVERSE_AGAIN_PERIOD = 4 * USECS_PER_SECOND; - bool repeatTraversal = _traversal.finished() && usecTimestampNow() > _traversal.getStartOfCompletedTraversal() + TRAVERSE_AGAIN_PERIOD; - if (viewFrustumChanged || repeatTraversal) { + if (viewFrustumChanged || _traversal.finished()) { ViewFrustum viewFrustum; nodeData->copyCurrentViewFrustum(viewFrustum); EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); startNewTraversal(viewFrustum, root, lodLevelOffset); - // If the previous traversal didn't finish, we'll need to resort the entities still in _sendQueue after calling traverse - if (!_sendQueue.empty()) { + // When the viewFrustum changed the sort order may be incorrect, so we re-sort + // and also use the opportunity to cull anything no longer in view + if (viewFrustumChanged && !_sendQueue.empty()) { EntityPriorityQueue prevSendQueue; _sendQueue.swap(prevSendQueue); _entitiesInQueue.clear(); @@ -111,9 +108,8 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O AACube cube = entity->getQueryAACube(success); if (success) { if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { - const float DO_NOT_SEND = -1.0e-6f; float priority = _conicalView.computePriority(cube); - if (priority != DO_NOT_SEND) { + if (priority != PrioritizedEntity::DO_NOT_SEND) { float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), cube, _traversal.getCurrentRootSizeScale(), @@ -165,7 +161,6 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O } } #endif // SEND_SORTED_ENTITIES - // END EXPERIMENTAL DIFFERENTIAL TRAVERSAL OctreeSendThread::traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); } @@ -250,6 +245,10 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree // larger octree cell because of its position (for example if it crosses the boundary of a cell it // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen // before we consider including it. + // + // TODO: compare priority against a threshold rather than bother with + // calculateRenderAccuracy(). Would need to replace all calculateRenderAccuracy() + // stuff everywhere with threshold in one sweep. float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), cube, _traversal.getCurrentRootSizeScale(), @@ -272,14 +271,14 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree break; case DiffTraversal::Repeat: _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { - uint64_t timestamp = _traversal.getStartOfCompletedTraversal(); - if (next.element->getLastChangedContent() > timestamp) { + uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); + if (next.element->getLastChangedContent() > startOfCompletedTraversal) { next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { return; } - if (entity->getLastEdited() > timestamp) { + if (entity->getLastEdited() > startOfCompletedTraversal) { bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { @@ -310,8 +309,9 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree case DiffTraversal::Differential: _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { // NOTE: for Differential case: next.intersection is against completedView not currentView - uint64_t timestamp = _traversal.getStartOfCompletedTraversal(); - if (next.element->getLastChangedContent() > timestamp || next.intersection != ViewFrustum::INSIDE) { + uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); + if (next.element->getLastChangedContent() > startOfCompletedTraversal || + next.intersection != ViewFrustum::INSIDE) { next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { @@ -329,7 +329,8 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree // Only send entities if they are large enough to see if (renderAccuracy > 0.0f) { - if (entity->getLastEdited() > timestamp || !_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { + if (entity->getLastEdited() > startOfCompletedTraversal || + !_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); _entitiesInQueue.insert(entity.get()); From 6edba6d545b4f774d095de3a33e7f75274e4d4dc Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 25 Aug 2017 11:04:24 -0700 Subject: [PATCH 560/722] erase in _entitiesInQueue when pop _sendQueue --- assignment-client/src/entities/EntityTreeSendThread.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 851d0566ac..0d71aa9619 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -143,8 +143,10 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O #endif _traversal.traverse(TIME_BUDGET); - uint64_t dt = usecTimestampNow() - startTime; - std::cout << "adebug traversal complete " << " Q.size = " << _sendQueue.size() << " dt = " << dt << std::endl; // adebug + if (_sendQueue.size() > 0) { + uint64_t dt = usecTimestampNow() - startTime; + std::cout << "adebug traversal complete " << " Q.size = " << _sendQueue.size() << " dt = " << dt << std::endl; // adebug + } } #ifndef SEND_SORTED_ENTITIES @@ -416,8 +418,10 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream ++_numEntities; } _sendQueue.pop(); + _entitiesInQueue.erase(entity.get()); } if (_sendQueue.empty()) { + assert(_entitiesInQueue.empty()); params.stopReason = EncodeBitstreamParams::FINISHED; _extraEncodeData->entities.clear(); } From 247764b67964d1fc7b6a89a4e12ba92a24dd9b37 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 28 Aug 2017 13:47:27 -0700 Subject: [PATCH 561/722] add total entity packets in stat (kbps) --- interface/resources/qml/Stats.qml | 4 ++++ interface/src/ui/Stats.cpp | 3 +++ interface/src/ui/Stats.h | 2 ++ 3 files changed, 9 insertions(+) diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 119f24c71f..159a696e5f 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -206,6 +206,10 @@ Item { text: "Audio Codec: " + root.audioCodec + " Noise Gate: " + root.audioNoiseGate; } + StatText { + visible: root.expanded; + text: "Entity Mixer In: " + root.entityPacketsInKbps + " kbps"; + } StatText { visible: root.expanded; text: "Downloads: " + root.downloads + "/" + root.downloadLimit + diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 8e3636dd7e..767e499503 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -180,10 +180,12 @@ void Stats::updateStats(bool force) { int totalPingOctree = 0; int octreeServerCount = 0; int pingOctreeMax = 0; + int totalEntityKbps = 0; nodeList->eachNode([&](const SharedNodePointer& node) { // TODO: this should also support entities if (node->getType() == NodeType::EntityServer) { totalPingOctree += node->getPingMs(); + totalEntityKbps += node->getInboundBandwidth(); octreeServerCount++; if (pingOctreeMax < node->getPingMs()) { pingOctreeMax = node->getPingMs(); @@ -248,6 +250,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(audioCodec, audioClient->getSelectedAudioFormat()); STAT_UPDATE(audioNoiseGate, audioClient->getNoiseGateOpen() ? "Open" : "Closed"); + STAT_UPDATE(entityPacketsInKbps, octreeServerCount ? totalEntityKbps / octreeServerCount : -1); auto loadingRequests = ResourceCache::getLoadingRequests(); STAT_UPDATE(downloads, loadingRequests.size()); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 74d2589c35..b3c920d4ef 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -85,6 +85,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, audioPacketLoss, 0) STATS_PROPERTY(QString, audioCodec, QString()) STATS_PROPERTY(QString, audioNoiseGate, QString()) + STATS_PROPERTY(int, entityPacketsInKbps, 0) STATS_PROPERTY(int, downloads, 0) STATS_PROPERTY(int, downloadLimit, 0) @@ -212,6 +213,7 @@ signals: void audioPacketLossChanged(); void audioCodecChanged(); void audioNoiseGateChanged(); + void entityPacketsInKbpsChanged(); void downloadsChanged(); void downloadLimitChanged(); From 6c066605cd5fbc0b90d557d595e8fc2209db2ec8 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 29 Aug 2017 15:35:36 -0700 Subject: [PATCH 562/722] add state to entity tree send thread --- .../src/entities/EntityTreeSendThread.cpp | 76 ++++++++++++------- .../src/entities/EntityTreeSendThread.h | 7 +- libraries/entities/src/EntityTree.cpp | 6 ++ libraries/entities/src/EntityTree.h | 1 + 4 files changed, 60 insertions(+), 30 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 0d71aa9619..e71331177b 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -19,6 +19,12 @@ #define SEND_SORTED_ENTITIES +EntityTreeSendThread::EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) : + OctreeSendThread(myServer, node) +{ + connect(std::static_pointer_cast(myServer->getOctree()).get(), &EntityTree::deletingEntityPointer, this, &EntityTreeSendThread::deletingEntityPointer, Qt::QueuedConnection); +} + void EntityTreeSendThread::preDistributionProcessing() { auto node = _node.toStrongRef(); auto nodeData = static_cast(node->getLinkedData()); @@ -151,12 +157,14 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O #ifndef SEND_SORTED_ENTITIES if (!_sendQueue.empty()) { + uint64_t sendTime = usecTimestampNow(); // print what needs to be sent while (!_sendQueue.empty()) { PrioritizedEntity entry = _sendQueue.top(); EntityItemPointer entity = entry.getEntity(); if (entity) { std::cout << "adebug send '" << entity->getName().toStdString() << "'" << " : " << entry.getPriority() << std::endl; // adebug + _knownState[entity] = sendTime; } _sendQueue.pop(); _entitiesInQueue.erase(entry.getRawEntityPointer()); @@ -234,6 +242,8 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree switch (type) { case DiffTraversal::First: _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { + // When we get to a First traversal, clear the _knownState + _knownState.clear(); next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { @@ -280,7 +290,8 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { return; } - if (entity->getLastEdited() > startOfCompletedTraversal) { + if (_knownState.find(entity.get()) == _knownState.end() || + (_knownState.find(entity.get()) != _knownState.end() && entity->getLastEdited() > _knownState[entity.get()])) { bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { @@ -319,42 +330,44 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { return; } - bool success = false; - AACube cube = entity->getQueryAACube(success); - if (success) { - if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { - // See the DiffTraversal::First case for an explanation of the "entity is too small" check - float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), - cube, - _traversal.getCurrentRootSizeScale(), - _traversal.getCurrentLODOffset()); + if (_knownState.find(entity.get()) == _knownState.end() || + (_knownState.find(entity.get()) != _knownState.end() && entity->getLastEdited() > _knownState[entity.get()])) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + // See the DiffTraversal::First case for an explanation of the "entity is too small" check + float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), + cube, + _traversal.getCurrentRootSizeScale(), + _traversal.getCurrentLODOffset()); - // Only send entities if they are large enough to see - if (renderAccuracy > 0.0f) { - if (entity->getLastEdited() > startOfCompletedTraversal || - !_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { - float priority = _conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesInQueue.insert(entity.get()); - } else { - // If this entity was skipped last time because it was too small, we still need to send it - float lastRenderAccuracy = calculateRenderAccuracy(_traversal.getCompletedView().getPosition(), - cube, - _traversal.getCompletedRootSizeScale(), - _traversal.getCompletedLODOffset()); - - if (lastRenderAccuracy <= 0.0f) { + // Only send entities if they are large enough to see + if (renderAccuracy > 0.0f) { + if (!_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); _entitiesInQueue.insert(entity.get()); + } else { + // If this entity was skipped last time because it was too small, we still need to send it + float lastRenderAccuracy = calculateRenderAccuracy(_traversal.getCompletedView().getPosition(), + cube, + _traversal.getCompletedRootSizeScale(), + _traversal.getCompletedLODOffset()); + + if (lastRenderAccuracy <= 0.0f) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesInQueue.insert(entity.get()); + } } } } + } else { + const float WHEN_IN_DOUBT_PRIORITY = 1.0f; + _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); } - } else { - const float WHEN_IN_DOUBT_PRIORITY = 1.0f; - _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); - _entitiesInQueue.insert(entity.get()); } }); } @@ -402,11 +415,13 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream } LevelDetails entitiesLevel = _packetData.startLevel(); + uint64_t sendTime = usecTimestampNow(); while(!_sendQueue.empty()) { PrioritizedEntity queuedItem = _sendQueue.top(); EntityItemPointer entity = queuedItem.getEntity(); if (entity) { OctreeElement::AppendState appendEntityState = entity->appendEntityData(&_packetData, params, _extraEncodeData); + _knownState[entity.get()] = sendTime; if (appendEntityState != OctreeElement::COMPLETED) { if (appendEntityState == OctreeElement::PARTIAL) { @@ -439,3 +454,6 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream #endif // SEND_SORTED_ENTITIES } +void EntityTreeSendThread::deletingEntityPointer(EntityItem* entity) { + _knownState.erase(entity); +} \ No newline at end of file diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 5ea90005b9..db59d97e6f 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -24,9 +24,10 @@ class EntityNodeData; class EntityItem; class EntityTreeSendThread : public OctreeSendThread { + Q_OBJECT public: - EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) : OctreeSendThread(myServer, node) { } + EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node); protected: void preDistributionProcessing() override; @@ -44,12 +45,16 @@ private: DiffTraversal _traversal; EntityPriorityQueue _sendQueue; std::unordered_set _entitiesInQueue; + std::unordered_map _knownState; ConicalView _conicalView; // cached optimized view for fast priority calculations // packet construction stuff EntityTreeElementExtraEncodeDataPointer _extraEncodeData { new EntityTreeElementExtraEncodeData() }; int32_t _numEntitiesOffset { 0 }; uint16_t _numEntities { 0 }; + +private slots: + void deletingEntityPointer(EntityItem* entity); }; #endif // hifi_EntityTreeSendThread_h diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 518d3bd883..ebf479574b 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -557,6 +557,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign unhookChildAvatar(entityID); emit deletingEntity(entityID); + emit deletingEntityPointer(existingEntity.get()); // NOTE: callers must lock the tree before using this method DeleteEntityOperator theOperator(getThisPointer(), entityID); @@ -565,6 +566,10 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign auto descendantID = descendant->getID(); theOperator.addEntityIDToDeleteList(descendantID); emit deletingEntity(descendantID); + EntityItemPointer descendantEntity = std::static_pointer_cast(descendant); + if (descendantEntity) { + emit deletingEntityPointer(descendantEntity.get()); + } }); recurseTreeWithOperator(&theOperator); @@ -614,6 +619,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i unhookChildAvatar(entityID); theOperator.addEntityIDToDeleteList(entityID); emit deletingEntity(entityID); + emit deletingEntityPointer(existingEntity.get()); } if (theOperator.getEntities().size() > 0) { diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 17dda32b53..b4155f474b 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -269,6 +269,7 @@ public: signals: void deletingEntity(const EntityItemID& entityID); + void deletingEntityPointer(EntityItem* entityID); void addingEntity(const EntityItemID& entityID); void entityScriptChanging(const EntityItemID& entityItemID, const bool reload); void entityServerScriptChanging(const EntityItemID& entityItemID, const bool reload); From 0ad5f47bfd77eb3d577dbee18f79e11df68dfe79 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 29 Aug 2017 18:14:27 -0700 Subject: [PATCH 563/722] trying to fix entity editing bugs, needs testing --- .../src/entities/EntityPriorityQueue.cpp | 1 + .../src/entities/EntityPriorityQueue.h | 5 ++- .../src/entities/EntityTreeSendThread.cpp | 39 +++++++++++++------ .../src/entities/EntityTreeSendThread.h | 1 + libraries/entities/src/EntityItem.cpp | 3 +- libraries/entities/src/EntityItem.h | 3 +- libraries/entities/src/EntityTree.cpp | 10 +++-- libraries/entities/src/EntityTree.h | 1 + 8 files changed, 45 insertions(+), 18 deletions(-) diff --git a/assignment-client/src/entities/EntityPriorityQueue.cpp b/assignment-client/src/entities/EntityPriorityQueue.cpp index 3755e6a5c7..f6c1161308 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.cpp +++ b/assignment-client/src/entities/EntityPriorityQueue.cpp @@ -12,6 +12,7 @@ #include "EntityPriorityQueue.h" const float PrioritizedEntity::DO_NOT_SEND = -1.0e-6f; +const float PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY = 1.0f; void ConicalView::set(const ViewFrustum& viewFrustum) { // The ConicalView has two parts: a central sphere (same as ViewFrustum) and a circular cone that bounds the frustum part. diff --git a/assignment-client/src/entities/EntityPriorityQueue.h b/assignment-client/src/entities/EntityPriorityQueue.h index 10db22d695..29712b3fd3 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.h +++ b/assignment-client/src/entities/EntityPriorityQueue.h @@ -40,12 +40,14 @@ private: class PrioritizedEntity { public: static const float DO_NOT_SEND; + static const float WHEN_IN_DOUBT_PRIORITY; - PrioritizedEntity(EntityItemPointer entity, float priority) : _weakEntity(entity), _rawEntityPointer(entity.get()), _priority(priority) {} + PrioritizedEntity(EntityItemPointer entity, float priority, bool forceSend = false) : _weakEntity(entity), _rawEntityPointer(entity.get()), _priority(priority), _forceSend(forceSend) {} float updatePriority(const ConicalView& view); EntityItemPointer getEntity() const { return _weakEntity.lock(); } EntityItem* getRawEntityPointer() const { return _rawEntityPointer; } float getPriority() const { return _priority; } + bool shouldForceSend() const { return _forceSend; } class Compare { public: @@ -57,6 +59,7 @@ private: EntityItemWeakPointer _weakEntity; EntityItem* _rawEntityPointer; float _priority; + bool _forceSend; }; using EntityPriorityQueue = std::priority_queue< PrioritizedEntity, std::vector, PrioritizedEntity::Compare >; diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index e71331177b..e34dfd97e7 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -22,6 +22,7 @@ EntityTreeSendThread::EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) : OctreeSendThread(myServer, node) { + connect(std::static_pointer_cast(myServer->getOctree()).get(), &EntityTree::editingEntityPointer, this, &EntityTreeSendThread::editingEntityPointer, Qt::QueuedConnection); connect(std::static_pointer_cast(myServer->getOctree()).get(), &EntityTree::deletingEntityPointer, this, &EntityTreeSendThread::deletingEntityPointer, Qt::QueuedConnection); } @@ -108,29 +109,29 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O // Re-add elements from previous traversal if they still need to be sent while (!prevSendQueue.empty()) { EntityItemPointer entity = prevSendQueue.top().getEntity(); + bool forceSend = prevSendQueue.top().shouldForceSend(); prevSendQueue.pop(); if (entity) { bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { - if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + if (forceSend || _traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { float priority = _conicalView.computePriority(cube); - if (priority != PrioritizedEntity::DO_NOT_SEND) { + if (forceSend || priority != PrioritizedEntity::DO_NOT_SEND) { float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), cube, _traversal.getCurrentRootSizeScale(), lodLevelOffset); - // Only send entities if they are large enough to see - if (renderAccuracy > 0.0f) { + // Only send entities if they are large enough to see, or we need to update them to be out of view + if (forceSend || renderAccuracy > 0.0f) { _sendQueue.push(PrioritizedEntity(entity, priority)); _entitiesInQueue.insert(entity.get()); } } } } else { - const float WHEN_IN_DOUBT_PRIORITY = 1.0f; - _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); _entitiesInQueue.insert(entity.get()); } } @@ -274,8 +275,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree } } } else { - const float WHEN_IN_DOUBT_PRIORITY = 1.0f; - _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); _entitiesInQueue.insert(entity.get()); } }); @@ -310,8 +310,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree } } } else { - const float WHEN_IN_DOUBT_PRIORITY = 1.0f; - _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); _entitiesInQueue.insert(entity.get()); } } @@ -364,8 +363,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree } } } else { - const float WHEN_IN_DOUBT_PRIORITY = 1.0f; - _sendQueue.push(PrioritizedEntity(entity, WHEN_IN_DOUBT_PRIORITY)); + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); _entitiesInQueue.insert(entity.get()); } } @@ -454,6 +452,23 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream #endif // SEND_SORTED_ENTITIES } +void EntityTreeSendThread::editingEntityPointer(const EntityItemPointer entity) { + if (entity) { + if (_entitiesInQueue.find(entity.get()) == _entitiesInQueue.end() && _knownState.find(entity.get()) != _knownState.end()) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority, true)); + _entitiesInQueue.insert(entity.get()); + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY, true)); + _entitiesInQueue.insert(entity.get()); + } + } + } +} + void EntityTreeSendThread::deletingEntityPointer(EntityItem* entity) { _knownState.erase(entity); } \ No newline at end of file diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index db59d97e6f..8b763a1c94 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -54,6 +54,7 @@ private: uint16_t _numEntities { 0 }; private slots: + void editingEntityPointer(const EntityItemPointer entity); void deletingEntityPointer(EntityItem* entity); }; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 71b119f415..6e2e52b380 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -32,7 +32,8 @@ #include "EntitySimulation.h" #include "EntityDynamicFactoryInterface.h" - +Q_DECLARE_METATYPE(EntityItemPointer); +int entityItemPointernMetaTypeId = qRegisterMetaType(); int EntityItem::_maxActionsDataSize = 800; quint64 EntityItem::_rememberDeletedActionTime = 20 * USECS_PER_SECOND; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 4eac23c867..88750da463 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -62,7 +62,8 @@ class MeshProxyList; /// EntityItem class this is the base class for all entity types. It handles the basic properties and functionality available /// to all other entity types. In particular: postion, size, rotation, age, lifetime, velocity, gravity. You can not instantiate /// one directly, instead you must only construct one of it's derived classes with additional features. -class EntityItem : public SpatiallyNestable, public ReadWriteLockable { +class EntityItem : public QObject, public SpatiallyNestable, public ReadWriteLockable { + Q_OBJECT // These two classes manage lists of EntityItem pointers and must be able to cleanup pointers when an EntityItem is deleted. // To make the cleanup robust each EntityItem has backpointers to its manager classes (which are only ever set/cleared by // the managers themselves, hence they are fiends) whose NULL status can be used to determine which managers still need to diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index ebf479574b..99ab5a7677 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -307,7 +307,9 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti } UpdateEntityOperator theOperator(getThisPointer(), containingElement, entity, queryCube); recurseTreeWithOperator(&theOperator); - entity->setProperties(tempProperties); + if (entity->setProperties(tempProperties)) { + emit editingEntityPointer(entity); + } _isDirty = true; } } @@ -382,7 +384,9 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti } UpdateEntityOperator theOperator(getThisPointer(), containingElement, entity, newQueryAACube); recurseTreeWithOperator(&theOperator); - entity->setProperties(properties); + if (entity->setProperties(properties)) { + emit editingEntityPointer(entity); + } // if the entity has children, run UpdateEntityOperator on them. If the children have children, recurse QQueue toProcess; @@ -1257,7 +1261,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c if (!isPhysics) { properties.setLastEditedBy(senderNode->getUUID()); } - updateEntity(entityItemID, properties, senderNode); + updateEntity(existingEntity, properties, senderNode); existingEntity->markAsChangedOnServer(); endUpdate = usecTimestampNow(); _totalUpdates++; diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index b4155f474b..d0448f438a 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -271,6 +271,7 @@ signals: void deletingEntity(const EntityItemID& entityID); void deletingEntityPointer(EntityItem* entityID); void addingEntity(const EntityItemID& entityID); + void editingEntityPointer(const EntityItemPointer& entityID); void entityScriptChanging(const EntityItemID& entityItemID, const bool reload); void entityServerScriptChanging(const EntityItemID& entityItemID, const bool reload); void newCollisionSoundURL(const QUrl& url, const EntityItemID& entityID); From 7938e301e732f09dadd9de3df99a6bc70a0e30be Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 1 Sep 2017 14:13:43 -0700 Subject: [PATCH 564/722] full scene traversal and json filters --- .../src/entities/EntityTreeSendThread.cpp | 53 ++++++++++++++----- .../src/entities/EntityTreeSendThread.h | 4 +- .../src/octree/OctreeSendThread.cpp | 4 +- .../src/octree/OctreeSendThread.h | 2 +- libraries/entities/src/DiffTraversal.cpp | 39 ++++++++++++-- libraries/entities/src/DiffTraversal.h | 5 +- 6 files changed, 82 insertions(+), 25 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index e34dfd97e7..024855235a 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -98,7 +98,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O nodeData->copyCurrentViewFrustum(viewFrustum); EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); - startNewTraversal(viewFrustum, root, lodLevelOffset); + startNewTraversal(viewFrustum, root, lodLevelOffset, true); // When the viewFrustum changed the sort order may be incorrect, so we re-sort // and also use the opportunity to cull anything no longer in view @@ -138,6 +138,11 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O } } } + } else if (_traversal.finished()) { + ViewFrustum viewFrustum; + EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); + int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + NO_BOUNDARY_ADJUST; + startNewTraversal(viewFrustum, root, lodLevelOffset, false); } if (!_traversal.finished()) { @@ -165,7 +170,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O EntityItemPointer entity = entry.getEntity(); if (entity) { std::cout << "adebug send '" << entity->getName().toStdString() << "'" << " : " << entry.getPriority() << std::endl; // adebug - _knownState[entity] = sendTime; + _knownState[entity.get()] = sendTime; } _sendQueue.pop(); _entitiesInQueue.erase(entry.getRawEntityPointer()); @@ -225,13 +230,14 @@ bool EntityTreeSendThread::addDescendantsToExtraFlaggedEntities(const QUuid& fil return hasNewChild || hasNewDescendants; } -void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root, int32_t lodLevelOffset) { - DiffTraversal::Type type = _traversal.prepareNewTraversal(view, root, lodLevelOffset); - // there are three types of traversal: +void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesFrustum) { + DiffTraversal::Type type = _traversal.prepareNewTraversal(view, root, lodLevelOffset, usesFrustum); + // there are four types of traversal: // // (1) FirstTime = at login --> find everything in view // (2) Repeat = view hasn't changed --> find what has changed since last complete traversal // (3) Differential = view has changed --> find what has changed or in new view but not old + // (4) FullScene = no view frustum -> send everything // // The "scanCallback" we provide to the traversal depends on the type: // @@ -371,10 +377,26 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree } }); break; + case DiffTraversal::FullScene: + _traversal.setScanCallback([&](DiffTraversal::VisibleElement& next) { + next.element->forEachEntity([&](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + if (_knownState.find(entity.get()) == _knownState.end() || + (_knownState.find(entity.get()) != _knownState.end() && entity->getLastEdited() > _knownState[entity.get()])) { + // We don't have a view frustum from which to compute priority + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + }); + }); + break; } } -bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params) { +bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters) { #ifdef SEND_SORTED_ENTITIES //auto entityTree = std::static_pointer_cast(_myServer->getOctree()); if (_sendQueue.empty()) { @@ -418,17 +440,20 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream PrioritizedEntity queuedItem = _sendQueue.top(); EntityItemPointer entity = queuedItem.getEntity(); if (entity) { - OctreeElement::AppendState appendEntityState = entity->appendEntityData(&_packetData, params, _extraEncodeData); - _knownState[entity.get()] = sendTime; + // Only send entities that match the jsonFilters, but keep track of everything we've tried to send so we don't try to send it again + if (entity->matchesJSONFilters(jsonFilters)) { + OctreeElement::AppendState appendEntityState = entity->appendEntityData(&_packetData, params, _extraEncodeData); - if (appendEntityState != OctreeElement::COMPLETED) { - if (appendEntityState == OctreeElement::PARTIAL) { - ++_numEntities; + if (appendEntityState != OctreeElement::COMPLETED) { + if (appendEntityState == OctreeElement::PARTIAL) { + ++_numEntities; + } + params.stopReason = EncodeBitstreamParams::DIDNT_FIT; + break; } - params.stopReason = EncodeBitstreamParams::DIDNT_FIT; - break; + ++_numEntities; } - ++_numEntities; + _knownState[entity.get()] = sendTime; } _sendQueue.pop(); _entitiesInQueue.erase(entity.get()); diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 8b763a1c94..bdda159ed5 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -39,8 +39,8 @@ private: bool addAncestorsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); bool addDescendantsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); - void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset); - bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params) override; + void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesFrustum); + bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters) override; DiffTraversal _traversal; EntityPriorityQueue _sendQueue; diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index b6d9d323c1..5a563037bc 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -458,7 +458,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* return _truePacketsSent; } -bool OctreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params) { +bool OctreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters) { bool somethingToSend = false; OctreeQueryNode* nodeData = static_cast(params.nodeData); if (!nodeData->elementBag.isEmpty()) { @@ -523,7 +523,7 @@ void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, Octre bool lastNodeDidntFit = false; // assume each node fits params.stopReason = EncodeBitstreamParams::UNKNOWN; // reset params.stopReason before traversal - somethingToSend = traverseTreeAndBuildNextPacketPayload(params); + somethingToSend = traverseTreeAndBuildNextPacketPayload(params, nodeData->getJSONParameters()); if (params.stopReason == EncodeBitstreamParams::DIDNT_FIT) { lastNodeDidntFit = true; diff --git a/assignment-client/src/octree/OctreeSendThread.h b/assignment-client/src/octree/OctreeSendThread.h index bcfede262c..a6ceba0e95 100644 --- a/assignment-client/src/octree/OctreeSendThread.h +++ b/assignment-client/src/octree/OctreeSendThread.h @@ -55,7 +55,7 @@ protected: virtual void preDistributionProcessing() {}; virtual void traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene); - virtual bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params); + virtual bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters); OctreePacketData _packetData; QWeakPointer _node; diff --git a/libraries/entities/src/DiffTraversal.cpp b/libraries/entities/src/DiffTraversal.cpp index e8e300081a..2cbe3f1064 100644 --- a/libraries/entities/src/DiffTraversal.cpp +++ b/libraries/entities/src/DiffTraversal.cpp @@ -142,19 +142,45 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V next.intersection = ViewFrustum::OUTSIDE; } +void DiffTraversal::Waypoint::getNextVisibleElementFullScene(DiffTraversal::VisibleElement& next, uint64_t lastTime) { + // NOTE: no need to set next.intersection or check LOD truncation in the "FullScene" context + if (_nextIndex == -1) { + ++_nextIndex; + EntityTreeElementPointer element = _weakElement.lock(); + if (element->getLastChangedContent() > lastTime) { + next.element = element; + return; + } + } + if (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer element = _weakElement.lock(); + if (element) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && nextElement->getLastChanged() > lastTime) { + next.element = nextElement; + return; + } + } + } + } + next.element.reset(); +} + DiffTraversal::DiffTraversal() { const int32_t MIN_PATH_DEPTH = 16; _path.reserve(MIN_PATH_DEPTH); } -DiffTraversal::Type DiffTraversal::prepareNewTraversal( - const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset) { +DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesFrustum) { assert(root); - // there are three types of traversal: + // there are four types of traversal: // // (1) First = fresh view --> find all elements in view // (2) Repeat = view hasn't changed --> find elements changed since last complete traversal // (3) Differential = view has changed --> find elements changed or in new view but not old + // (4) FullScene = no view frustum -> send everything // // for each traversal type we assign the appropriate _getNextVisibleElementCallback // @@ -166,7 +192,12 @@ DiffTraversal::Type DiffTraversal::prepareNewTraversal( // Type type; - if (_completedView.startTime == 0) { + if (!usesFrustum) { + type = Type::FullScene; + _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { + _path.back().getNextVisibleElementFullScene(next, _completedView.startTime); + }; + } else if (_completedView.startTime == 0) { type = Type::First; _currentView.viewFrustum = viewFrustum; _currentView.lodLevelOffset = root->getLevel() + lodLevelOffset - 1; // -1 because true root has level=1 diff --git a/libraries/entities/src/DiffTraversal.h b/libraries/entities/src/DiffTraversal.h index e849b4fef3..83b2c99bb9 100644 --- a/libraries/entities/src/DiffTraversal.h +++ b/libraries/entities/src/DiffTraversal.h @@ -46,6 +46,7 @@ public: void getNextVisibleElementFirstTime(VisibleElement& next, const View& view); void getNextVisibleElementRepeat(VisibleElement& next, const View& view, uint64_t lastTime); void getNextVisibleElementDifferential(VisibleElement& next, const View& view, const View& lastView); + void getNextVisibleElementFullScene(VisibleElement& next, uint64_t lastTime); int8_t getNextIndex() const { return _nextIndex; } void initRootNextIndex() { _nextIndex = -1; } @@ -55,11 +56,11 @@ public: int8_t _nextIndex; }; - typedef enum { First, Repeat, Differential } Type; + typedef enum { First, Repeat, Differential, FullScene } Type; DiffTraversal(); - Type prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset); + Type prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset, bool isFullScene); const ViewFrustum& getCurrentView() const { return _currentView.viewFrustum; } const ViewFrustum& getCompletedView() const { return _completedView.viewFrustum; } From defed80be7aeb3c9fec1121ade6adebf7a895583 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 6 Sep 2017 13:42:32 -0700 Subject: [PATCH 565/722] edited entities are not repeatedly sent if out of view, handles cases where usesViewFrustum changes --- .../src/entities/EntityPriorityQueue.cpp | 1 + .../src/entities/EntityPriorityQueue.h | 7 +- .../src/entities/EntityTreeSendThread.cpp | 261 ++++++++++-------- .../src/entities/EntityTreeSendThread.h | 2 +- libraries/entities/src/DiffTraversal.cpp | 95 +++---- libraries/entities/src/DiffTraversal.h | 7 +- libraries/shared/src/ViewFrustum.cpp | 12 +- 7 files changed, 203 insertions(+), 182 deletions(-) diff --git a/assignment-client/src/entities/EntityPriorityQueue.cpp b/assignment-client/src/entities/EntityPriorityQueue.cpp index f6c1161308..6d94f911ea 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.cpp +++ b/assignment-client/src/entities/EntityPriorityQueue.cpp @@ -12,6 +12,7 @@ #include "EntityPriorityQueue.h" const float PrioritizedEntity::DO_NOT_SEND = -1.0e-6f; +const float PrioritizedEntity::FORCE_REMOVE = -1.0e-5f; const float PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY = 1.0f; void ConicalView::set(const ViewFrustum& viewFrustum) { diff --git a/assignment-client/src/entities/EntityPriorityQueue.h b/assignment-client/src/entities/EntityPriorityQueue.h index 29712b3fd3..a5d0ab05ff 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.h +++ b/assignment-client/src/entities/EntityPriorityQueue.h @@ -40,14 +40,15 @@ private: class PrioritizedEntity { public: static const float DO_NOT_SEND; + static const float FORCE_REMOVE; static const float WHEN_IN_DOUBT_PRIORITY; - PrioritizedEntity(EntityItemPointer entity, float priority, bool forceSend = false) : _weakEntity(entity), _rawEntityPointer(entity.get()), _priority(priority), _forceSend(forceSend) {} + PrioritizedEntity(EntityItemPointer entity, float priority, bool forceRemove = false) : _weakEntity(entity), _rawEntityPointer(entity.get()), _priority(priority), _forceRemove(forceRemove) {} float updatePriority(const ConicalView& view); EntityItemPointer getEntity() const { return _weakEntity.lock(); } EntityItem* getRawEntityPointer() const { return _rawEntityPointer; } float getPriority() const { return _priority; } - bool shouldForceSend() const { return _forceSend; } + bool shouldForceRemove() const { return _forceRemove; } class Compare { public: @@ -59,7 +60,7 @@ private: EntityItemWeakPointer _weakEntity; EntityItem* _rawEntityPointer; float _priority; - bool _forceSend; + bool _forceRemove; }; using EntityPriorityQueue = std::priority_queue< PrioritizedEntity, std::vector, PrioritizedEntity::Compare >; diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 024855235a..817df55625 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -92,39 +92,39 @@ void EntityTreeSendThread::preDistributionProcessing() { void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) { - if (nodeData->getUsesFrustum()) { - if (viewFrustumChanged || _traversal.finished()) { - ViewFrustum viewFrustum; - nodeData->copyCurrentViewFrustum(viewFrustum); - EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); - int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); - startNewTraversal(viewFrustum, root, lodLevelOffset, true); + if (viewFrustumChanged || _traversal.finished()) { + ViewFrustum viewFrustum; + nodeData->copyCurrentViewFrustum(viewFrustum); + EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); + int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); + startNewTraversal(viewFrustum, root, lodLevelOffset, nodeData->getUsesFrustum()); - // When the viewFrustum changed the sort order may be incorrect, so we re-sort - // and also use the opportunity to cull anything no longer in view - if (viewFrustumChanged && !_sendQueue.empty()) { - EntityPriorityQueue prevSendQueue; - _sendQueue.swap(prevSendQueue); - _entitiesInQueue.clear(); - // Re-add elements from previous traversal if they still need to be sent - while (!prevSendQueue.empty()) { - EntityItemPointer entity = prevSendQueue.top().getEntity(); - bool forceSend = prevSendQueue.top().shouldForceSend(); - prevSendQueue.pop(); - if (entity) { + // When the viewFrustum changed the sort order may be incorrect, so we re-sort + // and also use the opportunity to cull anything no longer in view + if (viewFrustumChanged && !_sendQueue.empty()) { + EntityPriorityQueue prevSendQueue; + _sendQueue.swap(prevSendQueue); + _entitiesInQueue.clear(); + // Re-add elements from previous traversal if they still need to be sent + while (!prevSendQueue.empty()) { + EntityItemPointer entity = prevSendQueue.top().getEntity(); + bool forceRemove = prevSendQueue.top().shouldForceRemove(); + prevSendQueue.pop(); + if (entity) { + if (!forceRemove) { bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { - if (forceSend || _traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { float priority = _conicalView.computePriority(cube); - if (forceSend || priority != PrioritizedEntity::DO_NOT_SEND) { + if (priority != PrioritizedEntity::DO_NOT_SEND) { float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), cube, _traversal.getCurrentRootSizeScale(), lodLevelOffset); - // Only send entities if they are large enough to see, or we need to update them to be out of view - if (forceSend || renderAccuracy > 0.0f) { + // Only send entities if they are large enough to see + if (renderAccuracy > 0.0f) { _sendQueue.push(PrioritizedEntity(entity, priority)); _entitiesInQueue.insert(entity.get()); } @@ -134,15 +134,13 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); _entitiesInQueue.insert(entity.get()); } + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::FORCE_REMOVE, true)); + _entitiesInQueue.insert(entity.get()); } } } } - } else if (_traversal.finished()) { - ViewFrustum viewFrustum; - EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot()); - int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + NO_BOUNDARY_ADJUST; - startNewTraversal(viewFrustum, root, lodLevelOffset, false); } if (!_traversal.finished()) { @@ -230,14 +228,13 @@ bool EntityTreeSendThread::addDescendantsToExtraFlaggedEntities(const QUuid& fil return hasNewChild || hasNewDescendants; } -void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesFrustum) { - DiffTraversal::Type type = _traversal.prepareNewTraversal(view, root, lodLevelOffset, usesFrustum); - // there are four types of traversal: +void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesViewFrustum) { + DiffTraversal::Type type = _traversal.prepareNewTraversal(view, root, lodLevelOffset, usesViewFrustum); + // there are three types of traversal: // // (1) FirstTime = at login --> find everything in view // (2) Repeat = view hasn't changed --> find what has changed since last complete traversal // (3) Differential = view has changed --> find what has changed or in new view but not old - // (4) FullScene = no view frustum -> send everything // // The "scanCallback" we provide to the traversal depends on the type: // @@ -248,83 +245,118 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree switch (type) { case DiffTraversal::First: - _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { - // When we get to a First traversal, clear the _knownState - _knownState.clear(); - next.element->forEachEntity([&](EntityItemPointer entity) { - // Bail early if we've already checked this entity this frame - if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { - return; - } - bool success = false; - AACube cube = entity->getQueryAACube(success); - if (success) { - if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { - // Check the size of the entity, it's possible that a "too small to see" entity is included in a - // larger octree cell because of its position (for example if it crosses the boundary of a cell it - // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen - // before we consider including it. - // - // TODO: compare priority against a threshold rather than bother with - // calculateRenderAccuracy(). Would need to replace all calculateRenderAccuracy() - // stuff everywhere with threshold in one sweep. - float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), - cube, - _traversal.getCurrentRootSizeScale(), - _traversal.getCurrentLODOffset()); - - // Only send entities if they are large enough to see - if (renderAccuracy > 0.0f) { - float priority = _conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesInQueue.insert(entity.get()); - } - } - } else { - _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); - _entitiesInQueue.insert(entity.get()); - } - }); - }); - break; - case DiffTraversal::Repeat: - _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { - uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); - if (next.element->getLastChangedContent() > startOfCompletedTraversal) { + // When we get to a First traversal, clear the _knownState + _knownState.clear(); + if (usesViewFrustum) { + _traversal.setScanCallback([&](DiffTraversal::VisibleElement& next) { next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { return; } - if (_knownState.find(entity.get()) == _knownState.end() || - (_knownState.find(entity.get()) != _knownState.end() && entity->getLastEdited() > _knownState[entity.get()])) { - bool success = false; - AACube cube = entity->getQueryAACube(success); - if (success) { - if (next.intersection == ViewFrustum::INSIDE || _traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { - // See the DiffTraversal::First case for an explanation of the "entity is too small" check - float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), - cube, - _traversal.getCurrentRootSizeScale(), - _traversal.getCurrentLODOffset()); + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + // Check the size of the entity, it's possible that a "too small to see" entity is included in a + // larger octree cell because of its position (for example if it crosses the boundary of a cell it + // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen + // before we consider including it. + // + // TODO: compare priority against a threshold rather than bother with + // calculateRenderAccuracy(). Would need to replace all calculateRenderAccuracy() + // stuff everywhere with threshold in one sweep. + float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), + cube, + _traversal.getCurrentRootSizeScale(), + _traversal.getCurrentLODOffset()); - // Only send entities if they are large enough to see - if (renderAccuracy > 0.0f) { - float priority = _conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesInQueue.insert(entity.get()); - } + // Only send entities if they are large enough to see + if (renderAccuracy > 0.0f) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesInQueue.insert(entity.get()); } - } else { + } + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + }); + }); + } else { + _traversal.setScanCallback([&](DiffTraversal::VisibleElement& next) { + next.element->forEachEntity([&](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + }); + }); + } + break; + case DiffTraversal::Repeat: + if (usesViewFrustum) { + _traversal.setScanCallback([&](DiffTraversal::VisibleElement& next) { + uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); + if (next.element->getLastChangedContent() > startOfCompletedTraversal) { + next.element->forEachEntity([&](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + auto knownTimestamp = _knownState.find(entity.get()); + if (knownTimestamp == _knownState.end() || + (knownTimestamp != _knownState.end() && entity->getLastEdited() > knownTimestamp->second)) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (next.intersection == ViewFrustum::INSIDE || _traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + // See the DiffTraversal::First case for an explanation of the "entity is too small" check + float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), + cube, + _traversal.getCurrentRootSizeScale(), + _traversal.getCurrentLODOffset()); + + // Only send entities if they are large enough to see + if (renderAccuracy > 0.0f) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesInQueue.insert(entity.get()); + } + } + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + } + }); + } + }); + } else { + _traversal.setScanCallback([&](DiffTraversal::VisibleElement& next) { + uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); + if (next.element->getLastChangedContent() > startOfCompletedTraversal) { + next.element->forEachEntity([&](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + auto knownTimestamp = _knownState.find(entity.get()); + if (knownTimestamp == _knownState.end() || + (knownTimestamp != _knownState.end() && entity->getLastEdited() > knownTimestamp->second)) { _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); _entitiesInQueue.insert(entity.get()); } - } - }); - } - }); + }); + } + }); + } break; case DiffTraversal::Differential: + assert(usesViewFrustum); _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { // NOTE: for Differential case: next.intersection is against completedView not currentView uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); @@ -335,8 +367,9 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { return; } - if (_knownState.find(entity.get()) == _knownState.end() || - (_knownState.find(entity.get()) != _knownState.end() && entity->getLastEdited() > _knownState[entity.get()])) { + auto knownTimestamp = _knownState.find(entity.get()); + if (knownTimestamp == _knownState.end() || + (knownTimestamp != _knownState.end() && entity->getLastEdited() > knownTimestamp->second)) { bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { @@ -377,22 +410,6 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree } }); break; - case DiffTraversal::FullScene: - _traversal.setScanCallback([&](DiffTraversal::VisibleElement& next) { - next.element->forEachEntity([&](EntityItemPointer entity) { - // Bail early if we've already checked this entity this frame - if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { - return; - } - if (_knownState.find(entity.get()) == _knownState.end() || - (_knownState.find(entity.get()) != _knownState.end() && entity->getLastEdited() > _knownState[entity.get()])) { - // We don't have a view frustum from which to compute priority - _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); - _entitiesInQueue.insert(entity.get()); - } - }); - }); - break; } } @@ -453,7 +470,11 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream } ++_numEntities; } - _knownState[entity.get()] = sendTime; + if (queuedItem.shouldForceRemove()) { + _knownState.erase(entity.get()); + } else { + _knownState[entity.get()] = sendTime; + } } _sendQueue.pop(); _entitiesInQueue.erase(entity.get()); @@ -483,9 +504,11 @@ void EntityTreeSendThread::editingEntityPointer(const EntityItemPointer entity) bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { - float priority = _conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority, true)); - _entitiesInQueue.insert(entity.get()); + // We can force a removal from _knownState if the current view is used and entity is out of view + if (_traversal.doesCurrentUseViewFrustum() && !_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::FORCE_REMOVE, true)); + _entitiesInQueue.insert(entity.get()); + } } else { _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY, true)); _entitiesInQueue.insert(entity.get()); diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index bdda159ed5..c9751c5835 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -39,7 +39,7 @@ private: bool addAncestorsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); bool addDescendantsToExtraFlaggedEntities(const QUuid& filteredEntityID, EntityItem& entityItem, EntityNodeData& nodeData); - void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesFrustum); + void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesViewFrustum); bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters) override; DiffTraversal _traversal; diff --git a/libraries/entities/src/DiffTraversal.cpp b/libraries/entities/src/DiffTraversal.cpp index 2cbe3f1064..86296c82de 100644 --- a/libraries/entities/src/DiffTraversal.cpp +++ b/libraries/entities/src/DiffTraversal.cpp @@ -33,18 +33,30 @@ void DiffTraversal::Waypoint::getNextVisibleElementFirstTime(DiffTraversal::Visi } else if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); if (element) { - // check for LOD truncation - float visibleLimit = boundaryDistanceForRenderLevel(element->getLevel() + view.lodLevelOffset, view.rootSizeScale); - float distance2 = glm::distance2(view.viewFrustum.getPosition(), element->getAACube().calcCenter()); - if (distance2 < visibleLimit * visibleLimit) { + // No LOD truncation if we aren't using the view frustum + if (!view.usesViewFrustum) { while (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); ++_nextIndex; - if (nextElement && view.viewFrustum.cubeIntersectsKeyhole(nextElement->getAACube())) { + if (nextElement) { next.element = nextElement; return; } } + } else { + // check for LOD truncation + float visibleLimit = boundaryDistanceForRenderLevel(element->getLevel() + view.lodLevelOffset, view.rootSizeScale); + float distance2 = glm::distance2(view.viewFrustum.getPosition(), element->getAACube().calcCenter()); + if (distance2 < visibleLimit * visibleLimit) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && view.viewFrustum.cubeIntersectsKeyhole(nextElement->getAACube())) { + next.element = nextElement; + return; + } + } + } } } } @@ -66,19 +78,32 @@ void DiffTraversal::Waypoint::getNextVisibleElementRepeat( if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); if (element) { - // check for LOD truncation - float visibleLimit = boundaryDistanceForRenderLevel(element->getLevel() + view.lodLevelOffset, view.rootSizeScale); - float distance2 = glm::distance2(view.viewFrustum.getPosition(), element->getAACube().calcCenter()); - if (distance2 < visibleLimit * visibleLimit) { + // No LOD truncation if we aren't using the view frustum + if (!view.usesViewFrustum) { while (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); ++_nextIndex; if (nextElement && nextElement->getLastChanged() > lastTime) { - ViewFrustum::intersection intersection = view.viewFrustum.calculateCubeKeyholeIntersection(nextElement->getAACube()); - if (intersection != ViewFrustum::OUTSIDE) { - next.element = nextElement; - next.intersection = intersection; - return; + next.element = nextElement; + next.intersection = ViewFrustum::INSIDE; + return; + } + } + } else { + // check for LOD truncation + float visibleLimit = boundaryDistanceForRenderLevel(element->getLevel() + view.lodLevelOffset, view.rootSizeScale); + float distance2 = glm::distance2(view.viewFrustum.getPosition(), element->getAACube().calcCenter()); + if (distance2 < visibleLimit * visibleLimit) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && nextElement->getLastChanged() > lastTime) { + ViewFrustum::intersection intersection = view.viewFrustum.calculateCubeKeyholeIntersection(nextElement->getAACube()); + if (intersection != ViewFrustum::OUTSIDE) { + next.element = nextElement; + next.intersection = intersection; + return; + } } } } @@ -142,45 +167,18 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V next.intersection = ViewFrustum::OUTSIDE; } -void DiffTraversal::Waypoint::getNextVisibleElementFullScene(DiffTraversal::VisibleElement& next, uint64_t lastTime) { - // NOTE: no need to set next.intersection or check LOD truncation in the "FullScene" context - if (_nextIndex == -1) { - ++_nextIndex; - EntityTreeElementPointer element = _weakElement.lock(); - if (element->getLastChangedContent() > lastTime) { - next.element = element; - return; - } - } - if (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer element = _weakElement.lock(); - if (element) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement && nextElement->getLastChanged() > lastTime) { - next.element = nextElement; - return; - } - } - } - } - next.element.reset(); -} - DiffTraversal::DiffTraversal() { const int32_t MIN_PATH_DEPTH = 16; _path.reserve(MIN_PATH_DEPTH); } -DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesFrustum) { +DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesViewFrustum) { assert(root); - // there are four types of traversal: + // there are three types of traversal: // // (1) First = fresh view --> find all elements in view // (2) Repeat = view hasn't changed --> find elements changed since last complete traversal // (3) Differential = view has changed --> find elements changed or in new view but not old - // (4) FullScene = no view frustum -> send everything // // for each traversal type we assign the appropriate _getNextVisibleElementCallback // @@ -190,14 +188,11 @@ DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFr // // external code should update the _scanElementCallback after calling prepareNewTraversal // + _currentView.usesViewFrustum = usesViewFrustum; Type type; - if (!usesFrustum) { - type = Type::FullScene; - _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { - _path.back().getNextVisibleElementFullScene(next, _completedView.startTime); - }; - } else if (_completedView.startTime == 0) { + // If usesViewFrustum changes, treat it as a First traversal + if (_completedView.startTime == 0 || _currentView.usesViewFrustum != _completedView.usesViewFrustum) { type = Type::First; _currentView.viewFrustum = viewFrustum; _currentView.lodLevelOffset = root->getLevel() + lodLevelOffset - 1; // -1 because true root has level=1 @@ -205,7 +200,7 @@ DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFr _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { _path.back().getNextVisibleElementFirstTime(next, _currentView); }; - } else if (_completedView.viewFrustum.isVerySimilar(viewFrustum) && lodLevelOffset == _completedView.lodLevelOffset) { + } else if (!_currentView.usesViewFrustum || (_completedView.viewFrustum.isVerySimilar(viewFrustum) && lodLevelOffset == _completedView.lodLevelOffset)) { type = Type::Repeat; _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { _path.back().getNextVisibleElementRepeat(next, _completedView, _completedView.startTime); diff --git a/libraries/entities/src/DiffTraversal.h b/libraries/entities/src/DiffTraversal.h index 83b2c99bb9..2bd44d041e 100644 --- a/libraries/entities/src/DiffTraversal.h +++ b/libraries/entities/src/DiffTraversal.h @@ -36,6 +36,7 @@ public: uint64_t startTime { 0 }; float rootSizeScale { 1.0f }; int32_t lodLevelOffset { 0 }; + bool usesViewFrustum { true }; }; // Waypoint is an bookmark in a "path" of waypoints during a traversal. @@ -46,7 +47,6 @@ public: void getNextVisibleElementFirstTime(VisibleElement& next, const View& view); void getNextVisibleElementRepeat(VisibleElement& next, const View& view, uint64_t lastTime); void getNextVisibleElementDifferential(VisibleElement& next, const View& view, const View& lastView); - void getNextVisibleElementFullScene(VisibleElement& next, uint64_t lastTime); int8_t getNextIndex() const { return _nextIndex; } void initRootNextIndex() { _nextIndex = -1; } @@ -56,15 +56,16 @@ public: int8_t _nextIndex; }; - typedef enum { First, Repeat, Differential, FullScene } Type; + typedef enum { First, Repeat, Differential } Type; DiffTraversal(); - Type prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset, bool isFullScene); + Type prepareNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesViewFrustum); const ViewFrustum& getCurrentView() const { return _currentView.viewFrustum; } const ViewFrustum& getCompletedView() const { return _completedView.viewFrustum; } + bool doesCurrentUseViewFrustum() const { return _currentView.usesViewFrustum; } float getCurrentRootSizeScale() const { return _currentView.rootSizeScale; } float getCompletedRootSizeScale() const { return _completedView.rootSizeScale; } float getCurrentLODOffset() const { return _currentView.lodLevelOffset; } diff --git a/libraries/shared/src/ViewFrustum.cpp b/libraries/shared/src/ViewFrustum.cpp index 7e4f64686b..d84826cf01 100644 --- a/libraries/shared/src/ViewFrustum.cpp +++ b/libraries/shared/src/ViewFrustum.cpp @@ -403,12 +403,12 @@ bool ViewFrustum::isVerySimilar(const ViewFrustum& compareTo, bool debug) const bool result = testMatches(0, positionDistance, POSITION_SIMILAR_ENOUGH) && testMatches(0, angleOrientation, ORIENTATION_SIMILAR_ENOUGH) && - testMatches(compareTo._fieldOfView, _fieldOfView) && - testMatches(compareTo._aspectRatio, _aspectRatio) && - testMatches(compareTo._nearClip, _nearClip) && - testMatches(compareTo._farClip, _farClip) && - testMatches(compareTo._focalLength, _focalLength); - + testMatches(compareTo._centerSphereRadius, _centerSphereRadius) && + testMatches(compareTo._fieldOfView, _fieldOfView) && + testMatches(compareTo._aspectRatio, _aspectRatio) && + testMatches(compareTo._nearClip, _nearClip) && + testMatches(compareTo._farClip, _farClip) && + testMatches(compareTo._focalLength, _focalLength); if (!result && debug) { qCDebug(shared, "ViewFrustum::isVerySimilar()... result=%s\n", debug::valueOf(result)); From 0597970bb4462aae837a9b2ac0fb99f4e2600f54 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 7 Sep 2017 15:57:56 -0700 Subject: [PATCH 566/722] faster, more correct ViewFrustum::isVerySimilar() remove OVERSEND hack add ViewFrustum::calculateProjection() method used by OctreeQueryNode --- libraries/shared/src/ViewFrustum.cpp | 160 +++++++-------------------- libraries/shared/src/ViewFrustum.h | 8 +- 2 files changed, 39 insertions(+), 129 deletions(-) diff --git a/libraries/shared/src/ViewFrustum.cpp b/libraries/shared/src/ViewFrustum.cpp index d84826cf01..0ba5d4c9b3 100644 --- a/libraries/shared/src/ViewFrustum.cpp +++ b/libraries/shared/src/ViewFrustum.cpp @@ -56,16 +56,17 @@ void ViewFrustum::setProjection(const glm::mat4& projection) { _projection = projection; glm::mat4 inverseProjection = glm::inverse(projection); - // compute our dimensions the usual way + // compute frustum corners for (int i = 0; i < NUM_FRUSTUM_CORNERS; ++i) { _corners[i] = inverseProjection * NDC_VALUES[i]; _corners[i] /= _corners[i].w; } + + // compute frustum properties _nearClip = -_corners[BOTTOM_LEFT_NEAR].z; _farClip = -_corners[BOTTOM_LEFT_FAR].z; _aspectRatio = (_corners[TOP_RIGHT_NEAR].x - _corners[BOTTOM_LEFT_NEAR].x) / (_corners[TOP_RIGHT_NEAR].y - _corners[BOTTOM_LEFT_NEAR].y); - glm::vec4 top = inverseProjection * vec4(0.0f, 1.0f, -1.0f, 1.0f); top /= top.w; _fieldOfView = abs(glm::degrees(2.0f * abs(glm::angle(vec3(0.0f, 0.0f, -1.0f), glm::normalize(vec3(top)))))); @@ -117,6 +118,23 @@ void ViewFrustum::calculate() { _ourModelViewProjectionMatrix = _projection * view; // Remember, matrix multiplication is the other way around } +void ViewFrustum::calculateProjection() { + if (0.0f != _aspectRatio && 0.0f != _nearClip && 0.0f != _farClip && _nearClip != _farClip) { + // _projection is calculated from the frustum parameters + _projection = glm::perspective( glm::radians(_fieldOfView), _aspectRatio, _nearClip, _farClip); + + // frustum corners are computed from inverseProjection + glm::mat4 inverseProjection = glm::inverse(_projection); + for (int i = 0; i < NUM_FRUSTUM_CORNERS; ++i) { + _corners[i] = inverseProjection * NDC_VALUES[i]; + _corners[i] /= _corners[i].w; + } + + // finally calculate planes and _ourModelViewProjectionMatrix + calculate(); + } +} + //enum { TOP_PLANE = 0, BOTTOM_PLANE, LEFT_PLANE, RIGHT_PLANE, NEAR_PLANE, FAR_PLANE }; const char* ViewFrustum::debugPlaneName (int plane) const { switch (plane) { @@ -160,16 +178,12 @@ void ViewFrustum::fromByteArray(const QByteArray& input) { setCenterRadius(cameraCenterRadius); // Also make sure it's got the correct lens details from the camera - const float VIEW_FRUSTUM_FOV_OVERSEND = 60.0f; - float originalFOV = cameraFov; - float wideFOV = originalFOV + VIEW_FRUSTUM_FOV_OVERSEND; - if (0.0f != cameraAspectRatio && 0.0f != cameraNearClip && 0.0f != cameraFarClip && cameraNearClip != cameraFarClip) { setProjection(glm::perspective( - glm::radians(wideFOV), // hack + glm::radians(cameraFov), cameraAspectRatio, cameraNearClip, cameraFarClip)); @@ -324,125 +338,25 @@ bool ViewFrustum::boxIntersectsKeyhole(const AABox& box) const { return true; } -bool testMatches(glm::quat lhs, glm::quat rhs, float epsilon = EPSILON) { - return (fabs(lhs.x - rhs.x) <= epsilon && fabs(lhs.y - rhs.y) <= epsilon && fabs(lhs.z - rhs.z) <= epsilon - && fabs(lhs.w - rhs.w) <= epsilon); +bool closeEnough(float a, float b, float relativeError) { + assert(relativeError >= 0.0f); + // NOTE: we add EPSILON to the denominator so we can avoid checking for division by zero. + // This method works fine when: fabsf(a + b) >> EPSILON + return fabsf(a - b) / (0.5f * fabsf(a + b) + EPSILON) < relativeError; } -bool testMatches(glm::vec3 lhs, glm::vec3 rhs, float epsilon = EPSILON) { - return (fabs(lhs.x - rhs.x) <= epsilon && fabs(lhs.y - rhs.y) <= epsilon && fabs(lhs.z - rhs.z) <= epsilon); -} +bool ViewFrustum::isVerySimilar(const ViewFrustum& other) const { + const float MIN_POSITION_SLOP_SQUARED = 25.0f; // 5 meters squared + const float MIN_ORIENTATION_DOT = 0.9924039f; // dot product of two quaternions 10 degrees apart + const float MIN_RELATIVE_ERROR = 0.01f; // 1% -bool testMatches(float lhs, float rhs, float epsilon = EPSILON) { - return (fabs(lhs - rhs) <= epsilon); -} - -bool ViewFrustum::matches(const ViewFrustum& compareTo, bool debug) const { - bool result = - testMatches(compareTo._position, _position) && - testMatches(compareTo._direction, _direction) && - testMatches(compareTo._up, _up) && - testMatches(compareTo._right, _right) && - testMatches(compareTo._fieldOfView, _fieldOfView) && - testMatches(compareTo._aspectRatio, _aspectRatio) && - testMatches(compareTo._nearClip, _nearClip) && - testMatches(compareTo._farClip, _farClip) && - testMatches(compareTo._focalLength, _focalLength); - - if (!result && debug) { - qCDebug(shared, "ViewFrustum::matches()... result=%s", debug::valueOf(result)); - qCDebug(shared, "%s -- compareTo._position=%f,%f,%f _position=%f,%f,%f", - (testMatches(compareTo._position,_position) ? "MATCHES " : "NO MATCH"), - (double)compareTo._position.x, (double)compareTo._position.y, (double)compareTo._position.z, - (double)_position.x, (double)_position.y, (double)_position.z); - qCDebug(shared, "%s -- compareTo._direction=%f,%f,%f _direction=%f,%f,%f", - (testMatches(compareTo._direction, _direction) ? "MATCHES " : "NO MATCH"), - (double)compareTo._direction.x, (double)compareTo._direction.y, (double)compareTo._direction.z, - (double)_direction.x, (double)_direction.y, (double)_direction.z ); - qCDebug(shared, "%s -- compareTo._up=%f,%f,%f _up=%f,%f,%f", - (testMatches(compareTo._up, _up) ? "MATCHES " : "NO MATCH"), - (double)compareTo._up.x, (double)compareTo._up.y, (double)compareTo._up.z, - (double)_up.x, (double)_up.y, (double)_up.z ); - qCDebug(shared, "%s -- compareTo._right=%f,%f,%f _right=%f,%f,%f", - (testMatches(compareTo._right, _right) ? "MATCHES " : "NO MATCH"), - (double)compareTo._right.x, (double)compareTo._right.y, (double)compareTo._right.z, - (double)_right.x, (double)_right.y, (double)_right.z ); - qCDebug(shared, "%s -- compareTo._fieldOfView=%f _fieldOfView=%f", - (testMatches(compareTo._fieldOfView, _fieldOfView) ? "MATCHES " : "NO MATCH"), - (double)compareTo._fieldOfView, (double)_fieldOfView); - qCDebug(shared, "%s -- compareTo._aspectRatio=%f _aspectRatio=%f", - (testMatches(compareTo._aspectRatio, _aspectRatio) ? "MATCHES " : "NO MATCH"), - (double)compareTo._aspectRatio, (double)_aspectRatio); - qCDebug(shared, "%s -- compareTo._nearClip=%f _nearClip=%f", - (testMatches(compareTo._nearClip, _nearClip) ? "MATCHES " : "NO MATCH"), - (double)compareTo._nearClip, (double)_nearClip); - qCDebug(shared, "%s -- compareTo._farClip=%f _farClip=%f", - (testMatches(compareTo._farClip, _farClip) ? "MATCHES " : "NO MATCH"), - (double)compareTo._farClip, (double)_farClip); - qCDebug(shared, "%s -- compareTo._focalLength=%f _focalLength=%f", - (testMatches(compareTo._focalLength, _focalLength) ? "MATCHES " : "NO MATCH"), - (double)compareTo._focalLength, (double)_focalLength); - } - return result; -} - -bool ViewFrustum::isVerySimilar(const ViewFrustum& compareTo, bool debug) const { - - // Compute distance between the two positions - const float POSITION_SIMILAR_ENOUGH = 5.0f; // 5 meters - float positionDistance = glm::distance(_position, compareTo._position); - - // Compute the angular distance between the two orientations - const float ORIENTATION_SIMILAR_ENOUGH = 10.0f; // 10 degrees in any direction - glm::quat dQOrientation = _orientation * glm::inverse(compareTo._orientation); - float angleOrientation = compareTo._orientation == _orientation ? 0.0f : glm::degrees(glm::angle(dQOrientation)); - if (isNaN(angleOrientation)) { - angleOrientation = 0.0f; - } - - bool result = - testMatches(0, positionDistance, POSITION_SIMILAR_ENOUGH) && - testMatches(0, angleOrientation, ORIENTATION_SIMILAR_ENOUGH) && - testMatches(compareTo._centerSphereRadius, _centerSphereRadius) && - testMatches(compareTo._fieldOfView, _fieldOfView) && - testMatches(compareTo._aspectRatio, _aspectRatio) && - testMatches(compareTo._nearClip, _nearClip) && - testMatches(compareTo._farClip, _farClip) && - testMatches(compareTo._focalLength, _focalLength); - - if (!result && debug) { - qCDebug(shared, "ViewFrustum::isVerySimilar()... result=%s\n", debug::valueOf(result)); - qCDebug(shared, "%s -- compareTo._position=%f,%f,%f _position=%f,%f,%f", - (testMatches(compareTo._position,_position, POSITION_SIMILAR_ENOUGH) ? - "IS SIMILAR ENOUGH " : "IS NOT SIMILAR ENOUGH"), - (double)compareTo._position.x, (double)compareTo._position.y, (double)compareTo._position.z, - (double)_position.x, (double)_position.y, (double)_position.z ); - - qCDebug(shared, "%s -- positionDistance=%f", - (testMatches(0,positionDistance, POSITION_SIMILAR_ENOUGH) ? "IS SIMILAR ENOUGH " : "IS NOT SIMILAR ENOUGH"), - (double)positionDistance); - - qCDebug(shared, "%s -- angleOrientation=%f", - (testMatches(0, angleOrientation, ORIENTATION_SIMILAR_ENOUGH) ? "IS SIMILAR ENOUGH " : "IS NOT SIMILAR ENOUGH"), - (double)angleOrientation); - - qCDebug(shared, "%s -- compareTo._fieldOfView=%f _fieldOfView=%f", - (testMatches(compareTo._fieldOfView, _fieldOfView) ? "MATCHES " : "NO MATCH"), - (double)compareTo._fieldOfView, (double)_fieldOfView); - qCDebug(shared, "%s -- compareTo._aspectRatio=%f _aspectRatio=%f", - (testMatches(compareTo._aspectRatio, _aspectRatio) ? "MATCHES " : "NO MATCH"), - (double)compareTo._aspectRatio, (double)_aspectRatio); - qCDebug(shared, "%s -- compareTo._nearClip=%f _nearClip=%f", - (testMatches(compareTo._nearClip, _nearClip) ? "MATCHES " : "NO MATCH"), - (double)compareTo._nearClip, (double)_nearClip); - qCDebug(shared, "%s -- compareTo._farClip=%f _farClip=%f", - (testMatches(compareTo._farClip, _farClip) ? "MATCHES " : "NO MATCH"), - (double)compareTo._farClip, (double)_farClip); - qCDebug(shared, "%s -- compareTo._focalLength=%f _focalLength=%f", - (testMatches(compareTo._focalLength, _focalLength) ? "MATCHES " : "NO MATCH"), - (double)compareTo._focalLength, (double)_focalLength); - } - return result; + return glm::distance2(_position, other._position) < MIN_POSITION_SLOP_SQUARED && + fabsf(glm::dot(_orientation, other._orientation)) > MIN_ORIENTATION_DOT && + closeEnough(_fieldOfView, other._fieldOfView, MIN_RELATIVE_ERROR) && + closeEnough(_aspectRatio, other._aspectRatio, MIN_RELATIVE_ERROR) && + closeEnough(_nearClip, other._nearClip, MIN_RELATIVE_ERROR) && + closeEnough(_farClip, other._farClip, MIN_RELATIVE_ERROR) && + closeEnough(_focalLength, other._focalLength, MIN_RELATIVE_ERROR); } PickRay ViewFrustum::computePickRay(float x, float y) { diff --git a/libraries/shared/src/ViewFrustum.h b/libraries/shared/src/ViewFrustum.h index 221b0b5a07..acf463810d 100644 --- a/libraries/shared/src/ViewFrustum.h +++ b/libraries/shared/src/ViewFrustum.h @@ -91,6 +91,7 @@ public: float getCenterRadius() const { return _centerSphereRadius; } void calculate(); + void calculateProjection(); typedef enum { OUTSIDE = 0, INTERSECT, INSIDE } intersection; @@ -107,12 +108,7 @@ public: bool cubeIntersectsKeyhole(const AACube& cube) const; bool boxIntersectsKeyhole(const AABox& box) const; - // some frustum comparisons - bool matches(const ViewFrustum& compareTo, bool debug = false) const; - bool matches(const ViewFrustum* compareTo, bool debug = false) const { return matches(*compareTo, debug); } - - bool isVerySimilar(const ViewFrustum& compareTo, bool debug = false) const; - bool isVerySimilar(const ViewFrustum* compareTo, bool debug = false) const { return isVerySimilar(*compareTo, debug); } + bool isVerySimilar(const ViewFrustum& compareTo) const; PickRay computePickRay(float x, float y); void computePickRay(float x, float y, glm::vec3& origin, glm::vec3& direction) const; From d061627a1d63a104626e1bf90c99d89637103f7c Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 7 Sep 2017 16:00:19 -0700 Subject: [PATCH 567/722] reasonable values for iitial OctreeQueryNode view make sure newViewFrustum is fully initialized before using it --- libraries/octree/src/OctreeQuery.cpp | 13 ++++++++++++- libraries/octree/src/OctreeQuery.h | 16 ++++++++-------- libraries/octree/src/OctreeQueryNode.cpp | 3 ++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/libraries/octree/src/OctreeQuery.cpp b/libraries/octree/src/OctreeQuery.cpp index 7d9fc7d08c..a88f730a50 100644 --- a/libraries/octree/src/OctreeQuery.cpp +++ b/libraries/octree/src/OctreeQuery.cpp @@ -17,7 +17,18 @@ #include "OctreeConstants.h" #include "OctreeQuery.h" -OctreeQuery::OctreeQuery() { +const float DEFAULT_FOV = 45.0f; // degrees +const float DEFAULT_ASPECT_RATIO = 1.0f; +const float DEFAULT_NEAR_CLIP = 0.1f; +const float DEFAULT_FAR_CLIP = 3.0f; + +OctreeQuery::OctreeQuery() : + _cameraFov(DEFAULT_FOV), + _cameraAspectRatio(DEFAULT_ASPECT_RATIO), + _cameraNearClip(DEFAULT_NEAR_CLIP), + _cameraFarClip(DEFAULT_FAR_CLIP), + _cameraCenterRadius(DEFAULT_FAR_CLIP) +{ _maxQueryPPS = DEFAULT_MAX_OCTREE_PPS; } diff --git a/libraries/octree/src/OctreeQuery.h b/libraries/octree/src/OctreeQuery.h index 058c1dc585..81a63a696c 100644 --- a/libraries/octree/src/OctreeQuery.h +++ b/libraries/octree/src/OctreeQuery.h @@ -89,14 +89,14 @@ public slots: protected: // camera details for the avatar - glm::vec3 _cameraPosition = glm::vec3(0.0f); - glm::quat _cameraOrientation = glm::quat(); - float _cameraFov = 0.0f; - float _cameraAspectRatio = 1.0f; - float _cameraNearClip = 0.0f; - float _cameraFarClip = 0.0f; - float _cameraCenterRadius { 0.0f }; - glm::vec3 _cameraEyeOffsetPosition = glm::vec3(0.0f); + glm::vec3 _cameraPosition { glm::vec3(0.0f) }; + glm::quat _cameraOrientation { glm::quat() }; + float _cameraFov; + float _cameraAspectRatio; + float _cameraNearClip; + float _cameraFarClip; + float _cameraCenterRadius; + glm::vec3 _cameraEyeOffsetPosition { glm::vec3(0.0f) }; // octree server sending items int _maxQueryPPS = DEFAULT_MAX_OCTREE_PPS; diff --git a/libraries/octree/src/OctreeQueryNode.cpp b/libraries/octree/src/OctreeQueryNode.cpp index 4ebe650f6a..3003d76d14 100644 --- a/libraries/octree/src/OctreeQueryNode.cpp +++ b/libraries/octree/src/OctreeQueryNode.cpp @@ -182,6 +182,7 @@ bool OctreeQueryNode::updateCurrentViewFrustum() { getCameraAspectRatio(), getCameraNearClip(), getCameraFarClip())); + newestViewFrustum.calculate(); } @@ -189,7 +190,7 @@ bool OctreeQueryNode::updateCurrentViewFrustum() { QMutexLocker viewLocker(&_viewMutex); if (!newestViewFrustum.isVerySimilar(_currentViewFrustum)) { _currentViewFrustum = newestViewFrustum; - _currentViewFrustum.calculate(); + //_currentViewFrustum.calculateProjection(); currentViewFrustumChanged = true; } } From a22e5771002e133680e8e32049ae91505b4f953b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 7 Sep 2017 16:02:51 -0700 Subject: [PATCH 568/722] zero out OVERSEND hack --- libraries/octree/src/OctreeConstants.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/octree/src/OctreeConstants.h b/libraries/octree/src/OctreeConstants.h index 06f09e557c..b839b90fce 100644 --- a/libraries/octree/src/OctreeConstants.h +++ b/libraries/octree/src/OctreeConstants.h @@ -37,7 +37,7 @@ const int NUMBER_OF_CHILDREN = 8; const int MAX_TREE_SLICE_BYTES = 26; -const float VIEW_FRUSTUM_FOV_OVERSEND = 60.0f; +const float VIEW_FRUSTUM_FOV_OVERSEND = 0.0f; // These are guards to prevent our voxel tree recursive routines from spinning out of control const int UNREASONABLY_DEEP_RECURSION = 29; // use this for something that you want to be shallow, but not spin out From fb3e74398fd6d733279892d09ce0efa2d67c2be5 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 7 Sep 2017 16:03:20 -0700 Subject: [PATCH 569/722] don't invalidate viewFrustum sent to server --- interface/src/Application.cpp | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6762217485..60a653fdc9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5433,6 +5433,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node << perUnknownServer << " to send us jurisdiction."; } + // TODO: remove this hackery: it no longer makes sense for streaming of entities in scene. // set the query's position/orientation to be degenerate in a manner that will get the scene quickly // If there's only one server, then don't do this, and just let the normal voxel query pass through // as expected... this way, we will actually get a valid scene if there is one to be seen @@ -5455,16 +5456,6 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node _octreeQuery.setMaxQueryPacketsPerSecond(0); } - // if asked to forceResend, then set the query's position/orientation to be degenerate in a manner - // that will cause our next query to be guarenteed to be different and the server will resend to us - if (forceResend) { - _octreeQuery.setCameraPosition(glm::vec3(-0.1, -0.1, -0.1)); - const glm::quat OFF_IN_NEGATIVE_SPACE = glm::quat(-0.5, 0, -0.5, 1.0); - _octreeQuery.setCameraOrientation(OFF_IN_NEGATIVE_SPACE); - _octreeQuery.setCameraNearClip(0.1f); - _octreeQuery.setCameraFarClip(0.1f); - } - // encode the query data int packetSize = _octreeQuery.getBroadcastData(reinterpret_cast(queryPacket->getPayload())); queryPacket->setPayloadSize(packetSize); From 3433c5c4145cd6786244532d9e089a6573e17fdd Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 8 Sep 2017 15:01:36 -0700 Subject: [PATCH 570/722] remove redundant boolean logic --- .../src/entities/EntityTreeSendThread.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 817df55625..59a47f44aa 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -308,8 +308,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree return; } auto knownTimestamp = _knownState.find(entity.get()); - if (knownTimestamp == _knownState.end() || - (knownTimestamp != _knownState.end() && entity->getLastEdited() > knownTimestamp->second)) { + if (knownTimestamp == _knownState.end() || entity->getLastEdited() > knownTimestamp->second) { bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { @@ -345,8 +344,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree return; } auto knownTimestamp = _knownState.find(entity.get()); - if (knownTimestamp == _knownState.end() || - (knownTimestamp != _knownState.end() && entity->getLastEdited() > knownTimestamp->second)) { + if (knownTimestamp == _knownState.end() || entity->getLastEdited() > knownTimestamp->second) { _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); _entitiesInQueue.insert(entity.get()); } @@ -368,8 +366,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree return; } auto knownTimestamp = _knownState.find(entity.get()); - if (knownTimestamp == _knownState.end() || - (knownTimestamp != _knownState.end() && entity->getLastEdited() > knownTimestamp->second)) { + if (knownTimestamp == _knownState.end() || entity->getLastEdited() > knownTimestamp->second) { bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { @@ -519,4 +516,4 @@ void EntityTreeSendThread::editingEntityPointer(const EntityItemPointer entity) void EntityTreeSendThread::deletingEntityPointer(EntityItem* entity) { _knownState.erase(entity); -} \ No newline at end of file +} From a55661e1ff96b5c2fcc400dee6e797abe8636c87 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 8 Sep 2017 15:04:14 -0700 Subject: [PATCH 571/722] remove ViewFrustum::calculateProjection() --- libraries/octree/src/OctreeQueryNode.cpp | 1 - libraries/shared/src/ViewFrustum.cpp | 17 ----------------- libraries/shared/src/ViewFrustum.h | 1 - 3 files changed, 19 deletions(-) diff --git a/libraries/octree/src/OctreeQueryNode.cpp b/libraries/octree/src/OctreeQueryNode.cpp index 3003d76d14..c26b4ce77b 100644 --- a/libraries/octree/src/OctreeQueryNode.cpp +++ b/libraries/octree/src/OctreeQueryNode.cpp @@ -190,7 +190,6 @@ bool OctreeQueryNode::updateCurrentViewFrustum() { QMutexLocker viewLocker(&_viewMutex); if (!newestViewFrustum.isVerySimilar(_currentViewFrustum)) { _currentViewFrustum = newestViewFrustum; - //_currentViewFrustum.calculateProjection(); currentViewFrustumChanged = true; } } diff --git a/libraries/shared/src/ViewFrustum.cpp b/libraries/shared/src/ViewFrustum.cpp index 0ba5d4c9b3..aac8683b08 100644 --- a/libraries/shared/src/ViewFrustum.cpp +++ b/libraries/shared/src/ViewFrustum.cpp @@ -118,23 +118,6 @@ void ViewFrustum::calculate() { _ourModelViewProjectionMatrix = _projection * view; // Remember, matrix multiplication is the other way around } -void ViewFrustum::calculateProjection() { - if (0.0f != _aspectRatio && 0.0f != _nearClip && 0.0f != _farClip && _nearClip != _farClip) { - // _projection is calculated from the frustum parameters - _projection = glm::perspective( glm::radians(_fieldOfView), _aspectRatio, _nearClip, _farClip); - - // frustum corners are computed from inverseProjection - glm::mat4 inverseProjection = glm::inverse(_projection); - for (int i = 0; i < NUM_FRUSTUM_CORNERS; ++i) { - _corners[i] = inverseProjection * NDC_VALUES[i]; - _corners[i] /= _corners[i].w; - } - - // finally calculate planes and _ourModelViewProjectionMatrix - calculate(); - } -} - //enum { TOP_PLANE = 0, BOTTOM_PLANE, LEFT_PLANE, RIGHT_PLANE, NEAR_PLANE, FAR_PLANE }; const char* ViewFrustum::debugPlaneName (int plane) const { switch (plane) { diff --git a/libraries/shared/src/ViewFrustum.h b/libraries/shared/src/ViewFrustum.h index acf463810d..d1b88fb2a5 100644 --- a/libraries/shared/src/ViewFrustum.h +++ b/libraries/shared/src/ViewFrustum.h @@ -91,7 +91,6 @@ public: float getCenterRadius() const { return _centerSphereRadius; } void calculate(); - void calculateProjection(); typedef enum { OUTSIDE = 0, INTERSECT, INSIDE } intersection; From d55d45f6aa284cc479632c282e974b19ef780461 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 8 Sep 2017 15:24:21 -0700 Subject: [PATCH 572/722] check radius in ViewFrustum::isVerySimilar() --- libraries/shared/src/ViewFrustum.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/shared/src/ViewFrustum.cpp b/libraries/shared/src/ViewFrustum.cpp index aac8683b08..7c93b103cf 100644 --- a/libraries/shared/src/ViewFrustum.cpp +++ b/libraries/shared/src/ViewFrustum.cpp @@ -339,7 +339,8 @@ bool ViewFrustum::isVerySimilar(const ViewFrustum& other) const { closeEnough(_aspectRatio, other._aspectRatio, MIN_RELATIVE_ERROR) && closeEnough(_nearClip, other._nearClip, MIN_RELATIVE_ERROR) && closeEnough(_farClip, other._farClip, MIN_RELATIVE_ERROR) && - closeEnough(_focalLength, other._focalLength, MIN_RELATIVE_ERROR); + closeEnough(_focalLength, other._focalLength, MIN_RELATIVE_ERROR), + closeEnough(_centerSphereRadius, other._centerSphereRadius, MIN_RELATIVE_ERROR); } PickRay ViewFrustum::computePickRay(float x, float y) { From 355a59edb1fec825a721a8df889a8397a54b9f3b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 8 Sep 2017 20:10:18 -0700 Subject: [PATCH 573/722] fix missing entities in differential traversal --- libraries/entities/src/DiffTraversal.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/entities/src/DiffTraversal.cpp b/libraries/entities/src/DiffTraversal.cpp index 86296c82de..d69cef5b8e 100644 --- a/libraries/entities/src/DiffTraversal.cpp +++ b/libraries/entities/src/DiffTraversal.cpp @@ -137,9 +137,9 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V ++_nextIndex; if (nextElement) { AACube cube = nextElement->getAACube(); - if ( view.viewFrustum.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE) { + if (view.viewFrustum.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE) { ViewFrustum::intersection lastIntersection = lastView.viewFrustum.calculateCubeKeyholeIntersection(cube); - if (!(lastIntersection == ViewFrustum::INSIDE && nextElement->getLastChanged() < lastView.startTime)) { + if (lastIntersection != ViewFrustum::INSIDE || nextElement->getLastChanged() > lastView.startTime) { next.element = nextElement; // NOTE: for differential case next.intersection is against the lastView // because this helps the "external scan" optimize its culling @@ -153,7 +153,8 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V distance2 = glm::distance2(lastView.viewFrustum.getPosition(), element->getAACube().calcCenter()); if (distance2 >= visibleLimit * visibleLimit) { next.element = nextElement; - next.intersection = lastIntersection; + // element's intersection with lastView was effectively OUTSIDE + next.intersection = ViewFrustum::OUTSIDE; return; } } From a0f95ca5bd57833542dfa9248a535b995031af63 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 8 Sep 2017 20:11:17 -0700 Subject: [PATCH 574/722] swap order of evaluation for minor theoretical speedup --- assignment-client/src/entities/EntityTreeSendThread.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 59a47f44aa..d08cc725ce 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -358,8 +358,8 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { // NOTE: for Differential case: next.intersection is against completedView not currentView uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); - if (next.element->getLastChangedContent() > startOfCompletedTraversal || - next.intersection != ViewFrustum::INSIDE) { + if (next.intersection != ViewFrustum::INSIDE || + next.element->getLastChangedContent() > startOfCompletedTraversal) { next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { From bf1065b56ebc409aed2d1ade77e1a5f8006d8fa7 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 11 Sep 2017 10:06:18 -0700 Subject: [PATCH 575/722] track encode stats --- assignment-client/src/entities/EntityTreeSendThread.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index d08cc725ce..5e4a44fe71 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -414,8 +414,10 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream #ifdef SEND_SORTED_ENTITIES //auto entityTree = std::static_pointer_cast(_myServer->getOctree()); if (_sendQueue.empty()) { + OctreeServer::trackEncodeTime(OctreeServer::SKIP_TIME); return false; } + quint64 encodeStart = usecTimestampNow(); if (!_packetData.hasContent()) { // This is the beginning of a new packet. // We pack minimal data for this to be accepted as an OctreeElement payload for the root element. @@ -450,6 +452,8 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream LevelDetails entitiesLevel = _packetData.startLevel(); uint64_t sendTime = usecTimestampNow(); + auto nodeData = static_cast(params.nodeData); + nodeData->stats.encodeStarted(); while(!_sendQueue.empty()) { PrioritizedEntity queuedItem = _sendQueue.top(); EntityItemPointer entity = queuedItem.getEntity(); @@ -476,6 +480,7 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream _sendQueue.pop(); _entitiesInQueue.erase(entity.get()); } + nodeData->stats.encodeStopped(); if (_sendQueue.empty()) { assert(_entitiesInQueue.empty()); params.stopReason = EncodeBitstreamParams::FINISHED; @@ -484,10 +489,12 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream if (_numEntities == 0) { _packetData.discardLevel(entitiesLevel); + OctreeServer::trackEncodeTime((float)(usecTimestampNow() - encodeStart)); return false; } _packetData.endLevel(entitiesLevel); _packetData.updatePriorBytes(_numEntitiesOffset, (const unsigned char*)&_numEntities, sizeof(_numEntities)); + OctreeServer::trackEncodeTime((float)(usecTimestampNow() - encodeStart)); return true; #else // SEND_SORTED_ENTITIES From cbd20f89ddc2b417ad157892bb98e4b512bd5c43 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 11 Sep 2017 10:26:27 -0700 Subject: [PATCH 576/722] separate elementBag logic from EntityTreeSendThread --- .../src/entities/EntityTreeSendThread.h | 7 ++- .../src/octree/OctreeSendThread.cpp | 47 ++++++++++--------- .../src/octree/OctreeSendThread.h | 10 +++- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index c9751c5835..72ad2d0b10 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -30,7 +30,6 @@ public: EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node); protected: - void preDistributionProcessing() override; void traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) override; @@ -42,6 +41,12 @@ private: void startNewTraversal(const ViewFrustum& viewFrustum, EntityTreeElementPointer root, int32_t lodLevelOffset, bool usesViewFrustum); bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters) override; + void preDistributionProcessing() override; + bool hasSomethingToSend(OctreeQueryNode* nodeData) override { return !_sendQueue.empty(); } + bool shouldStartNewTraversal(OctreeQueryNode* nodeData, bool viewFrustumChanged) override { return viewFrustumChanged || _traversal.finished(); } + void preStartNewScene(OctreeQueryNode* nodeData, bool isFullScene) override {}; + bool shouldTraverseAndSend(OctreeQueryNode* nodeData) override { return true; } + DiffTraversal _traversal; EntityPriorityQueue _sendQueue; std::unordered_set _entitiesInQueue; diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index 5a563037bc..7d209e64dc 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -17,7 +17,6 @@ #include #include -#include "OctreeQueryNode.h" #include "OctreeSendThread.h" #include "OctreeServer.h" #include "OctreeServerConsts.h" @@ -301,9 +300,25 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* return numPackets; } +void OctreeSendThread::preStartNewScene(OctreeQueryNode* nodeData, bool isFullScene) { + // If we're starting a full scene, then definitely we want to empty the elementBag + if (isFullScene) { + nodeData->elementBag.deleteAll(); + } + + // This is the start of "resending" the scene. + bool dontRestartSceneOnMove = false; // this is experimental + if (dontRestartSceneOnMove) { + if (nodeData->elementBag.isEmpty()) { + nodeData->elementBag.insert(_myServer->getOctree()->getRoot()); + } + } else { + nodeData->elementBag.insert(_myServer->getOctree()->getRoot()); + } +} + /// Version of octree element distributor that sends the deepest LOD level at once int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged) { - OctreeServer::didPacketDistributor(this); // if shutting down, exit early @@ -311,7 +326,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* return 0; } - if (nodeData->elementBag.isEmpty()) { + if (shouldStartNewTraversal(nodeData, viewFrustumChanged)) { // if we're about to do a fresh pass, // give our pre-distribution processing a chance to do what it needs preDistributionProcessing(); @@ -345,7 +360,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* // If the current view frustum has changed OR we have nothing to send, then search against // the current view frustum for things to send. - if (viewFrustumChanged || nodeData->elementBag.isEmpty()) { + if (shouldStartNewTraversal(nodeData, viewFrustumChanged)) { // if our view has changed, we need to reset these things... if (viewFrustumChanged) { @@ -367,11 +382,6 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* _packetsSentThisInterval += handlePacketSend(node, nodeData, isFullScene); - // If we're starting a full scene, then definitely we want to empty the elementBag - if (isFullScene) { - nodeData->elementBag.deleteAll(); - } - // TODO: add these to stats page //::startSceneSleepTime = _usleepTime; @@ -380,19 +390,11 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData->stats.sceneStarted(isFullScene, viewFrustumChanged, _myServer->getOctree()->getRoot(), _myServer->getJurisdiction()); - // This is the start of "resending" the scene. - bool dontRestartSceneOnMove = false; // this is experimental - if (dontRestartSceneOnMove) { - if (nodeData->elementBag.isEmpty()) { - nodeData->elementBag.insert(_myServer->getOctree()->getRoot()); - } - } else { - nodeData->elementBag.insert(_myServer->getOctree()->getRoot()); - } + preStartNewScene(nodeData, isFullScene); } // If we have something in our elementBag, then turn them into packets and send them out... - if (!nodeData->elementBag.isEmpty()) { + if (shouldTraverseAndSend(nodeData)) { quint64 start = usecTimestampNow(); traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); @@ -441,7 +443,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* // if after sending packets we've emptied our bag, then we want to remember that we've sent all // the octree elements from the current view frustum - if (nodeData->elementBag.isEmpty()) { + if (!hasSomethingToSend(nodeData)) { nodeData->updateLastKnownViewFrustum(); nodeData->setViewSent(true); @@ -502,8 +504,7 @@ void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, Octre EncodeBitstreamParams params(INT_MAX, WANT_EXISTS_BITS, DONT_CHOP, viewFrustumChanged, boundaryLevelAdjust, octreeSizeScale, isFullScene, _myServer->getJurisdiction(), nodeData); - // Our trackSend() function is implemented by the server subclass, and will be called back - // during the encodeTreeBitstream() as new entities/data elements are sent + // Our trackSend() function is implemented by the server subclass, and will be called back as new entities/data elements are sent params.trackSend = [this](const QUuid& dataID, quint64 dataEdited) { _myServer->trackSend(dataID, dataEdited, _nodeUuid); }; @@ -513,7 +514,7 @@ void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, Octre } bool somethingToSend = true; // assume we have something - bool bagHadSomething = !nodeData->elementBag.isEmpty(); + bool bagHadSomething = hasSomethingToSend(nodeData); while (somethingToSend && _packetsSentThisInterval < maxPacketsPerInterval && !nodeData->isShuttingDown()) { float compressAndWriteElapsedUsec = OctreeServer::SKIP_TIME; float packetSendingElapsedUsec = OctreeServer::SKIP_TIME; diff --git a/assignment-client/src/octree/OctreeSendThread.h b/assignment-client/src/octree/OctreeSendThread.h index a6ceba0e95..bc7d2c2588 100644 --- a/assignment-client/src/octree/OctreeSendThread.h +++ b/assignment-client/src/octree/OctreeSendThread.h @@ -19,6 +19,7 @@ #include #include #include +#include "OctreeQueryNode.h" class OctreeQueryNode; class OctreeServer; @@ -51,8 +52,6 @@ protected: /// Implements generic processing behavior for this thread. virtual bool process() override; - /// Called before a packetDistributor pass to allow for pre-distribution processing - virtual void preDistributionProcessing() {}; virtual void traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene); virtual bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters); @@ -62,9 +61,16 @@ protected: OctreeServer* _myServer { nullptr }; private: + /// Called before a packetDistributor pass to allow for pre-distribution processing + virtual void preDistributionProcessing() {}; int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, bool dontSuppressDuplicate = false); int packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged); + virtual bool hasSomethingToSend(OctreeQueryNode* nodeData) { return !nodeData->elementBag.isEmpty(); } + virtual bool shouldStartNewTraversal(OctreeQueryNode* nodeData, bool viewFrustumChanged) { return viewFrustumChanged || !hasSomethingToSend(nodeData); } + virtual void preStartNewScene(OctreeQueryNode* nodeData, bool isFullScene); + virtual bool shouldTraverseAndSend(OctreeQueryNode* nodeData) { return hasSomethingToSend(nodeData); } + QUuid _nodeUuid; int _truePacketsSent { 0 }; // available for debug stats From c39ac93fc8a5d15eacf11ba6a9d2629f63a580ca Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 11 Sep 2017 11:24:24 -0700 Subject: [PATCH 577/722] fix isVerySimilar --- libraries/shared/src/ViewFrustum.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/shared/src/ViewFrustum.cpp b/libraries/shared/src/ViewFrustum.cpp index 7c93b103cf..ccdeb830b6 100644 --- a/libraries/shared/src/ViewFrustum.cpp +++ b/libraries/shared/src/ViewFrustum.cpp @@ -339,7 +339,7 @@ bool ViewFrustum::isVerySimilar(const ViewFrustum& other) const { closeEnough(_aspectRatio, other._aspectRatio, MIN_RELATIVE_ERROR) && closeEnough(_nearClip, other._nearClip, MIN_RELATIVE_ERROR) && closeEnough(_farClip, other._farClip, MIN_RELATIVE_ERROR) && - closeEnough(_focalLength, other._focalLength, MIN_RELATIVE_ERROR), + closeEnough(_focalLength, other._focalLength, MIN_RELATIVE_ERROR) && closeEnough(_centerSphereRadius, other._centerSphereRadius, MIN_RELATIVE_ERROR); } From b1b77640561726e70695fe82522520fdc169a381 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 11 Sep 2017 12:50:52 -0700 Subject: [PATCH 578/722] use 20 degrees of OVERSEND --- libraries/octree/src/OctreeConstants.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/octree/src/OctreeConstants.h b/libraries/octree/src/OctreeConstants.h index b839b90fce..06b4748f51 100644 --- a/libraries/octree/src/OctreeConstants.h +++ b/libraries/octree/src/OctreeConstants.h @@ -37,7 +37,7 @@ const int NUMBER_OF_CHILDREN = 8; const int MAX_TREE_SLICE_BYTES = 26; -const float VIEW_FRUSTUM_FOV_OVERSEND = 0.0f; +const float VIEW_FRUSTUM_FOV_OVERSEND = 20.0f; // These are guards to prevent our voxel tree recursive routines from spinning out of control const int UNREASONABLY_DEEP_RECURSION = 29; // use this for something that you want to be shallow, but not spin out From 624d0c12a27c7d320ad67273172c0b78cfbeef4c Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 11 Sep 2017 12:51:01 -0700 Subject: [PATCH 579/722] minor cleanup --- assignment-client/src/octree/OctreeSendThread.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index 7d209e64dc..32d7ce5053 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -47,7 +47,7 @@ OctreeSendThread::OctreeSendThread(OctreeServer* myServer, const SharedNodePoint OctreeSendThread::~OctreeSendThread() { setIsShuttingDown(); - + QString safeServerName("Octree"); if (_myServer) { safeServerName = _myServer->getMyServerName(); @@ -514,7 +514,7 @@ void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, Octre } bool somethingToSend = true; // assume we have something - bool bagHadSomething = hasSomethingToSend(nodeData); + bool hadSomething = hasSomethingToSend(nodeData); while (somethingToSend && _packetsSentThisInterval < maxPacketsPerInterval && !nodeData->isShuttingDown()) { float compressAndWriteElapsedUsec = OctreeServer::SKIP_TIME; float packetSendingElapsedUsec = OctreeServer::SKIP_TIME; From f2de03bc38af175644be6469480c1e0623173577 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 12 Sep 2017 16:11:58 -0700 Subject: [PATCH 580/722] small fixes and LOD cull children instead of parent --- .../src/entities/EntityTreeSendThread.cpp | 12 +- libraries/entities/src/DiffTraversal.cpp | 103 +++++++++--------- libraries/entities/src/DiffTraversal.h | 4 +- 3 files changed, 58 insertions(+), 61 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 5e4a44fe71..3d9bbe374e 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -121,7 +121,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), cube, _traversal.getCurrentRootSizeScale(), - lodLevelOffset); + _traversal.getCurrentLODOffset()); // Only send entities if they are large enough to see if (renderAccuracy > 0.0f) { @@ -385,12 +385,12 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree _entitiesInQueue.insert(entity.get()); } else { // If this entity was skipped last time because it was too small, we still need to send it - float lastRenderAccuracy = calculateRenderAccuracy(_traversal.getCompletedView().getPosition(), - cube, - _traversal.getCompletedRootSizeScale(), - _traversal.getCompletedLODOffset()); + renderAccuracy = calculateRenderAccuracy(_traversal.getCompletedView().getPosition(), + cube, + _traversal.getCompletedRootSizeScale(), + _traversal.getCompletedLODOffset()); - if (lastRenderAccuracy <= 0.0f) { + if (renderAccuracy <= 0.0f) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); _entitiesInQueue.insert(entity.get()); diff --git a/libraries/entities/src/DiffTraversal.cpp b/libraries/entities/src/DiffTraversal.cpp index d69cef5b8e..7658a7ac99 100644 --- a/libraries/entities/src/DiffTraversal.cpp +++ b/libraries/entities/src/DiffTraversal.cpp @@ -33,25 +33,22 @@ void DiffTraversal::Waypoint::getNextVisibleElementFirstTime(DiffTraversal::Visi } else if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); if (element) { - // No LOD truncation if we aren't using the view frustum - if (!view.usesViewFrustum) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement) { + if (!view.usesViewFrustum) { + // No LOD truncation if we aren't using the view frustum next.element = nextElement; return; - } - } - } else { - // check for LOD truncation - float visibleLimit = boundaryDistanceForRenderLevel(element->getLevel() + view.lodLevelOffset, view.rootSizeScale); - float distance2 = glm::distance2(view.viewFrustum.getPosition(), element->getAACube().calcCenter()); - if (distance2 < visibleLimit * visibleLimit) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement && view.viewFrustum.cubeIntersectsKeyhole(nextElement->getAACube())) { + } else if (view.viewFrustum.cubeIntersectsKeyhole(nextElement->getAACube())) { + // check for LOD truncation + float renderAccuracy = calculateRenderAccuracy(view.viewFrustum.getPosition(), + nextElement->getAACube(), + view.rootSizeScale, + view.lodLevelOffset); + + if (renderAccuracy > 0.0f) { next.element = nextElement; return; } @@ -78,28 +75,25 @@ void DiffTraversal::Waypoint::getNextVisibleElementRepeat( if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); if (element) { - // No LOD truncation if we aren't using the view frustum - if (!view.usesViewFrustum) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement && nextElement->getLastChanged() > lastTime) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement && nextElement->getLastChanged() > lastTime) { + if (!view.usesViewFrustum) { + // No LOD truncation if we aren't using the view frustum next.element = nextElement; next.intersection = ViewFrustum::INSIDE; return; - } - } - } else { - // check for LOD truncation - float visibleLimit = boundaryDistanceForRenderLevel(element->getLevel() + view.lodLevelOffset, view.rootSizeScale); - float distance2 = glm::distance2(view.viewFrustum.getPosition(), element->getAACube().calcCenter()); - if (distance2 < visibleLimit * visibleLimit) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement && nextElement->getLastChanged() > lastTime) { - ViewFrustum::intersection intersection = view.viewFrustum.calculateCubeKeyholeIntersection(nextElement->getAACube()); - if (intersection != ViewFrustum::OUTSIDE) { + } else { + ViewFrustum::intersection intersection = view.viewFrustum.calculateCubeKeyholeIntersection(nextElement->getAACube()); + if (intersection != ViewFrustum::OUTSIDE) { + // check for LOD truncation + float renderAccuracy = calculateRenderAccuracy(view.viewFrustum.getPosition(), + nextElement->getAACube(), + view.rootSizeScale, + view.lodLevelOffset); + + if (renderAccuracy > 0.0f) { next.element = nextElement; next.intersection = intersection; return; @@ -123,21 +117,22 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V next.element = element; next.intersection = ViewFrustum::INTERSECT; return; - } - if (_nextIndex < NUMBER_OF_CHILDREN) { + } else if (_nextIndex < NUMBER_OF_CHILDREN) { EntityTreeElementPointer element = _weakElement.lock(); if (element) { - // check for LOD truncation - int32_t level = element->getLevel() + view.lodLevelOffset; - float visibleLimit = boundaryDistanceForRenderLevel(level, view.rootSizeScale); - float distance2 = glm::distance2(view.viewFrustum.getPosition(), element->getAACube().calcCenter()); - if (distance2 < visibleLimit * visibleLimit) { - while (_nextIndex < NUMBER_OF_CHILDREN) { - EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); - ++_nextIndex; - if (nextElement) { - AACube cube = nextElement->getAACube(); - if (view.viewFrustum.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE) { + while (_nextIndex < NUMBER_OF_CHILDREN) { + EntityTreeElementPointer nextElement = element->getChildAtIndex(_nextIndex); + ++_nextIndex; + if (nextElement) { + AACube cube = nextElement->getAACube(); + if (view.viewFrustum.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE) { + // check for LOD truncation + float renderAccuracy = calculateRenderAccuracy(view.viewFrustum.getPosition(), + cube, + view.rootSizeScale, + view.lodLevelOffset); + + if (renderAccuracy > 0.0f) { ViewFrustum::intersection lastIntersection = lastView.viewFrustum.calculateCubeKeyholeIntersection(cube); if (lastIntersection != ViewFrustum::INSIDE || nextElement->getLastChanged() > lastView.startTime) { next.element = nextElement; @@ -148,10 +143,12 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V } else { // check for LOD truncation in the last traversal because // we may need to traverse this element after all if the lastView skipped it for LOD - int32_t lastLevel = element->getLevel() + lastView.lodLevelOffset; - visibleLimit = boundaryDistanceForRenderLevel(lastLevel, lastView.rootSizeScale); - distance2 = glm::distance2(lastView.viewFrustum.getPosition(), element->getAACube().calcCenter()); - if (distance2 >= visibleLimit * visibleLimit) { + renderAccuracy = calculateRenderAccuracy(lastView.viewFrustum.getPosition(), + cube, + lastView.rootSizeScale, + lastView.lodLevelOffset); + + if (renderAccuracy <= 0.0f) { next.element = nextElement; // element's intersection with lastView was effectively OUTSIDE next.intersection = ViewFrustum::OUTSIDE; diff --git a/libraries/entities/src/DiffTraversal.h b/libraries/entities/src/DiffTraversal.h index 2bd44d041e..bffe6c651e 100644 --- a/libraries/entities/src/DiffTraversal.h +++ b/libraries/entities/src/DiffTraversal.h @@ -68,8 +68,8 @@ public: bool doesCurrentUseViewFrustum() const { return _currentView.usesViewFrustum; } float getCurrentRootSizeScale() const { return _currentView.rootSizeScale; } float getCompletedRootSizeScale() const { return _completedView.rootSizeScale; } - float getCurrentLODOffset() const { return _currentView.lodLevelOffset; } - float getCompletedLODOffset() const { return _completedView.lodLevelOffset; } + int32_t getCurrentLODOffset() const { return _currentView.lodLevelOffset; } + int32_t getCompletedLODOffset() const { return _completedView.lodLevelOffset; } uint64_t getStartOfCompletedTraversal() const { return _completedView.startTime; } bool finished() const { return _path.empty(); } From 49e11d2173743a807c894946456da32ab78d4b2d Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 12 Sep 2017 22:27:42 -0700 Subject: [PATCH 581/722] fix Differential scan logic for LOD culling --- .../src/entities/EntityTreeSendThread.cpp | 152 +++++++++--------- libraries/entities/src/DiffTraversal.cpp | 73 +++------ libraries/entities/src/DiffTraversal.h | 9 +- libraries/octree/src/OctreeUtils.h | 6 + 4 files changed, 105 insertions(+), 135 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 3d9bbe374e..5552c2b580 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -106,6 +106,8 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O _sendQueue.swap(prevSendQueue); _entitiesInQueue.clear(); // Re-add elements from previous traversal if they still need to be sent + float lodScaleFactor = _traversal.getCurrentLODScaleFactor(); + glm::vec3 viewPosition = _traversal.getCurrentView().getPosition(); while (!prevSendQueue.empty()) { EntityItemPointer entity = prevSendQueue.top().getEntity(); bool forceRemove = prevSendQueue.top().shouldForceRemove(); @@ -118,13 +120,9 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { float priority = _conicalView.computePriority(cube); if (priority != PrioritizedEntity::DO_NOT_SEND) { - float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), - cube, - _traversal.getCurrentRootSizeScale(), - _traversal.getCurrentLODOffset()); - - // Only send entities if they are large enough to see - if (renderAccuracy > 0.0f) { + float distance = (glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE); + float apparentAngle = cube.getScale() / distance; + if (apparentAngle > MIN_ENTITY_APPARENT_ANGLE * lodScaleFactor) { _sendQueue.push(PrioritizedEntity(entity, priority)); _entitiesInQueue.insert(entity.get()); } @@ -144,19 +142,20 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O } if (!_traversal.finished()) { - uint64_t startTime = usecTimestampNow(); #ifdef DEBUG + uint64_t startTime = usecTimestampNow(); const uint64_t TIME_BUDGET = 400; // usec -#else - const uint64_t TIME_BUDGET = 200; // usec -#endif _traversal.traverse(TIME_BUDGET); if (_sendQueue.size() > 0) { uint64_t dt = usecTimestampNow() - startTime; std::cout << "adebug traversal complete " << " Q.size = " << _sendQueue.size() << " dt = " << dt << std::endl; // adebug } +#else + const uint64_t TIME_BUDGET = 200; // usec + _traversal.traverse(TIME_BUDGET); +#endif } #ifndef SEND_SORTED_ENTITIES @@ -248,8 +247,10 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree // When we get to a First traversal, clear the _knownState _knownState.clear(); if (usesViewFrustum) { - _traversal.setScanCallback([&](DiffTraversal::VisibleElement& next) { - next.element->forEachEntity([&](EntityItemPointer entity) { + float lodScaleFactor = _traversal.getCurrentLODScaleFactor(); + glm::vec3 viewPosition = _traversal.getCurrentView().getPosition(); + _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) { + next.element->forEachEntity([=](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { return; @@ -262,17 +263,9 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree // larger octree cell because of its position (for example if it crosses the boundary of a cell it // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen // before we consider including it. - // - // TODO: compare priority against a threshold rather than bother with - // calculateRenderAccuracy(). Would need to replace all calculateRenderAccuracy() - // stuff everywhere with threshold in one sweep. - float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), - cube, - _traversal.getCurrentRootSizeScale(), - _traversal.getCurrentLODOffset()); - - // Only send entities if they are large enough to see - if (renderAccuracy > 0.0f) { + float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; + float apparentAngle = cube.getScale() / distance; + if (apparentAngle > MIN_ENTITY_APPARENT_ANGLE * lodScaleFactor) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); _entitiesInQueue.insert(entity.get()); @@ -285,7 +278,7 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree }); }); } else { - _traversal.setScanCallback([&](DiffTraversal::VisibleElement& next) { + _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) { next.element->forEachEntity([&](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { @@ -299,28 +292,26 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree break; case DiffTraversal::Repeat: if (usesViewFrustum) { - _traversal.setScanCallback([&](DiffTraversal::VisibleElement& next) { + float lodScaleFactor = _traversal.getCurrentLODScaleFactor(); + glm::vec3 viewPosition = _traversal.getCurrentView().getPosition(); + _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) { uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); if (next.element->getLastChangedContent() > startOfCompletedTraversal) { - next.element->forEachEntity([&](EntityItemPointer entity) { + next.element->forEachEntity([=](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { return; } auto knownTimestamp = _knownState.find(entity.get()); - if (knownTimestamp == _knownState.end() || entity->getLastEdited() > knownTimestamp->second) { + if (knownTimestamp == _knownState.end()) { bool success = false; AACube cube = entity->getQueryAACube(success); if (success) { if (next.intersection == ViewFrustum::INSIDE || _traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { // See the DiffTraversal::First case for an explanation of the "entity is too small" check - float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), - cube, - _traversal.getCurrentRootSizeScale(), - _traversal.getCurrentLODOffset()); - - // Only send entities if they are large enough to see - if (renderAccuracy > 0.0f) { + float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; + float apparentAngle = cube.getScale() / distance; + if (apparentAngle > MIN_ENTITY_APPARENT_ANGLE * lodScaleFactor) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); _entitiesInQueue.insert(entity.get()); @@ -330,15 +321,20 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); _entitiesInQueue.insert(entity.get()); } + } else if (entity->getLastEdited() > knownTimestamp->second) { + // it is known and it changed --> put it on the queue with any priority + // TODO: sort these correctly + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); } }); } }); } else { - _traversal.setScanCallback([&](DiffTraversal::VisibleElement& next) { + _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) { uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); if (next.element->getLastChangedContent() > startOfCompletedTraversal) { - next.element->forEachEntity([&](EntityItemPointer entity) { + next.element->forEachEntity([=](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { return; @@ -355,56 +351,54 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree break; case DiffTraversal::Differential: assert(usesViewFrustum); - _traversal.setScanCallback([&] (DiffTraversal::VisibleElement& next) { - // NOTE: for Differential case: next.intersection is against completedView not currentView - uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); - if (next.intersection != ViewFrustum::INSIDE || - next.element->getLastChangedContent() > startOfCompletedTraversal) { - next.element->forEachEntity([&](EntityItemPointer entity) { - // Bail early if we've already checked this entity this frame - if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { - return; - } - auto knownTimestamp = _knownState.find(entity.get()); - if (knownTimestamp == _knownState.end() || entity->getLastEdited() > knownTimestamp->second) { - bool success = false; - AACube cube = entity->getQueryAACube(success); - if (success) { - if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { - // See the DiffTraversal::First case for an explanation of the "entity is too small" check - float renderAccuracy = calculateRenderAccuracy(_traversal.getCurrentView().getPosition(), - cube, - _traversal.getCurrentRootSizeScale(), - _traversal.getCurrentLODOffset()); - - // Only send entities if they are large enough to see - if (renderAccuracy > 0.0f) { - if (!_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { + float lodScaleFactor = _traversal.getCurrentLODScaleFactor(); + glm::vec3 viewPosition = _traversal.getCurrentView().getPosition(); + float completedLODScaleFactor = _traversal.getCompletedLODScaleFactor(); + glm::vec3 completedViewPosition = _traversal.getCompletedView().getPosition(); + _traversal.setScanCallback([=] (DiffTraversal::VisibleElement& next) { + next.element->forEachEntity([=](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + auto knownTimestamp = _knownState.find(entity.get()); + if (knownTimestamp == _knownState.end()) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + // See the DiffTraversal::First case for an explanation of the "entity is too small" check + float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; + float apparentAngle = cube.getScale() / distance; + if (apparentAngle > MIN_ENTITY_APPARENT_ANGLE * lodScaleFactor) { + if (!_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesInQueue.insert(entity.get()); + } else { + // If this entity was skipped last time because it was too small, we still need to send it + distance = glm::distance(cube.calcCenter(), completedViewPosition) + MIN_VISIBLE_DISTANCE; + apparentAngle = cube.getScale() / distance; + if (apparentAngle <= MIN_ENTITY_APPARENT_ANGLE * completedLODScaleFactor) { + // this object was skipped in last completed traversal float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); _entitiesInQueue.insert(entity.get()); - } else { - // If this entity was skipped last time because it was too small, we still need to send it - renderAccuracy = calculateRenderAccuracy(_traversal.getCompletedView().getPosition(), - cube, - _traversal.getCompletedRootSizeScale(), - _traversal.getCompletedLODOffset()); - - if (renderAccuracy <= 0.0f) { - float priority = _conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesInQueue.insert(entity.get()); - } } } } - } else { - _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); - _entitiesInQueue.insert(entity.get()); } + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); } - }); - } + } else if (entity->getLastEdited() > knownTimestamp->second) { + // it is known and it changed --> put it on the queue with any priority + // TODO: sort these correctly + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + }); }); break; } diff --git a/libraries/entities/src/DiffTraversal.cpp b/libraries/entities/src/DiffTraversal.cpp index 7658a7ac99..b33d9141c3 100644 --- a/libraries/entities/src/DiffTraversal.cpp +++ b/libraries/entities/src/DiffTraversal.cpp @@ -43,12 +43,9 @@ void DiffTraversal::Waypoint::getNextVisibleElementFirstTime(DiffTraversal::Visi return; } else if (view.viewFrustum.cubeIntersectsKeyhole(nextElement->getAACube())) { // check for LOD truncation - float renderAccuracy = calculateRenderAccuracy(view.viewFrustum.getPosition(), - nextElement->getAACube(), - view.rootSizeScale, - view.lodLevelOffset); - - if (renderAccuracy > 0.0f) { + float distance = glm::distance(view.viewFrustum.getPosition(), nextElement->getAACube().calcCenter()) + MIN_VISIBLE_DISTANCE; + float apparentAngle = nextElement->getAACube().getScale() / distance; + if (apparentAngle > MIN_ELEMENT_APPARENT_ANGLE * view.lodScaleFactor) { next.element = nextElement; return; } @@ -85,15 +82,12 @@ void DiffTraversal::Waypoint::getNextVisibleElementRepeat( next.intersection = ViewFrustum::INSIDE; return; } else { - ViewFrustum::intersection intersection = view.viewFrustum.calculateCubeKeyholeIntersection(nextElement->getAACube()); - if (intersection != ViewFrustum::OUTSIDE) { - // check for LOD truncation - float renderAccuracy = calculateRenderAccuracy(view.viewFrustum.getPosition(), - nextElement->getAACube(), - view.rootSizeScale, - view.lodLevelOffset); - - if (renderAccuracy > 0.0f) { + // check for LOD truncation + float distance = glm::distance(view.viewFrustum.getPosition(), nextElement->getAACube().calcCenter()) + MIN_VISIBLE_DISTANCE; + float apparentAngle = nextElement->getAACube().getScale() / distance; + if (apparentAngle > MIN_ELEMENT_APPARENT_ANGLE * view.lodScaleFactor) { + ViewFrustum::intersection intersection = view.viewFrustum.calculateCubeKeyholeIntersection(nextElement->getAACube()); + if (intersection != ViewFrustum::OUTSIDE) { next.element = nextElement; next.intersection = intersection; return; @@ -125,36 +119,14 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V ++_nextIndex; if (nextElement) { AACube cube = nextElement->getAACube(); - if (view.viewFrustum.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE) { - // check for LOD truncation - float renderAccuracy = calculateRenderAccuracy(view.viewFrustum.getPosition(), - cube, - view.rootSizeScale, - view.lodLevelOffset); - - if (renderAccuracy > 0.0f) { - ViewFrustum::intersection lastIntersection = lastView.viewFrustum.calculateCubeKeyholeIntersection(cube); - if (lastIntersection != ViewFrustum::INSIDE || nextElement->getLastChanged() > lastView.startTime) { - next.element = nextElement; - // NOTE: for differential case next.intersection is against the lastView - // because this helps the "external scan" optimize its culling - next.intersection = lastIntersection; - return; - } else { - // check for LOD truncation in the last traversal because - // we may need to traverse this element after all if the lastView skipped it for LOD - renderAccuracy = calculateRenderAccuracy(lastView.viewFrustum.getPosition(), - cube, - lastView.rootSizeScale, - lastView.lodLevelOffset); - - if (renderAccuracy <= 0.0f) { - next.element = nextElement; - // element's intersection with lastView was effectively OUTSIDE - next.intersection = ViewFrustum::OUTSIDE; - return; - } - } + // check for LOD truncation + float distance = glm::distance(view.viewFrustum.getPosition(), cube.calcCenter()) + MIN_VISIBLE_DISTANCE; + float apparentAngle = cube.getScale() / distance; + if (apparentAngle > MIN_ELEMENT_APPARENT_ANGLE * view.lodScaleFactor) { + if (view.viewFrustum.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE) { + next.element = nextElement; + next.intersection = ViewFrustum::OUTSIDE; + return; } } } @@ -187,18 +159,20 @@ DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFr // external code should update the _scanElementCallback after calling prepareNewTraversal // _currentView.usesViewFrustum = usesViewFrustum; + float lodScaleFactor = powf(2.0f, lodLevelOffset); Type type; // If usesViewFrustum changes, treat it as a First traversal if (_completedView.startTime == 0 || _currentView.usesViewFrustum != _completedView.usesViewFrustum) { type = Type::First; _currentView.viewFrustum = viewFrustum; - _currentView.lodLevelOffset = root->getLevel() + lodLevelOffset - 1; // -1 because true root has level=1 - _currentView.rootSizeScale = root->getScale() * MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT; + _currentView.lodScaleFactor = lodScaleFactor; _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { _path.back().getNextVisibleElementFirstTime(next, _currentView); }; - } else if (!_currentView.usesViewFrustum || (_completedView.viewFrustum.isVerySimilar(viewFrustum) && lodLevelOffset == _completedView.lodLevelOffset)) { + } else if (!_currentView.usesViewFrustum || + (_completedView.viewFrustum.isVerySimilar(viewFrustum) && + lodScaleFactor == _completedView.lodScaleFactor)) { type = Type::Repeat; _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { _path.back().getNextVisibleElementRepeat(next, _completedView, _completedView.startTime); @@ -206,8 +180,7 @@ DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFr } else { type = Type::Differential; _currentView.viewFrustum = viewFrustum; - _currentView.lodLevelOffset = root->getLevel() + lodLevelOffset - 1; // -1 because true root has level=1 - _currentView.rootSizeScale = root->getScale() * MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT; + _currentView.lodScaleFactor = lodScaleFactor; _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { _path.back().getNextVisibleElementDifferential(next, _currentView, _completedView); }; diff --git a/libraries/entities/src/DiffTraversal.h b/libraries/entities/src/DiffTraversal.h index bffe6c651e..6b5fa15075 100644 --- a/libraries/entities/src/DiffTraversal.h +++ b/libraries/entities/src/DiffTraversal.h @@ -34,8 +34,7 @@ public: public: ViewFrustum viewFrustum; uint64_t startTime { 0 }; - float rootSizeScale { 1.0f }; - int32_t lodLevelOffset { 0 }; + float lodScaleFactor { 1.0f }; bool usesViewFrustum { true }; }; @@ -66,10 +65,8 @@ public: const ViewFrustum& getCompletedView() const { return _completedView.viewFrustum; } bool doesCurrentUseViewFrustum() const { return _currentView.usesViewFrustum; } - float getCurrentRootSizeScale() const { return _currentView.rootSizeScale; } - float getCompletedRootSizeScale() const { return _completedView.rootSizeScale; } - int32_t getCurrentLODOffset() const { return _currentView.lodLevelOffset; } - int32_t getCompletedLODOffset() const { return _completedView.lodLevelOffset; } + float getCurrentLODScaleFactor() const { return _currentView.lodScaleFactor; } + float getCompletedLODScaleFactor() const { return _completedView.lodScaleFactor; } uint64_t getStartOfCompletedTraversal() const { return _completedView.startTime; } bool finished() const { return _path.empty(); } diff --git a/libraries/octree/src/OctreeUtils.h b/libraries/octree/src/OctreeUtils.h index 279eb51509..3264520aac 100644 --- a/libraries/octree/src/OctreeUtils.h +++ b/libraries/octree/src/OctreeUtils.h @@ -27,4 +27,10 @@ float boundaryDistanceForRenderLevel(unsigned int renderLevel, float voxelSizeSc float getAccuracyAngle(float octreeSizeScale, int boundaryLevelAdjust); +const float MIN_ELEMENT_APPARENT_ANGLE = 0.0087266f; // ~0.57 degrees in radians +// NOTE: the entity bounding cube is larger than the smallest containing octree element by sqrt(3) +const float SQRT_THREE = 1.73205080f; +const float MIN_ENTITY_APPARENT_ANGLE = MIN_ELEMENT_APPARENT_ANGLE * SQRT_THREE; +const float MIN_VISIBLE_DISTANCE = 0.0001f; // helps avoid divide-by-zero check + #endif // hifi_OctreeUtils_h From 25d250898b462a5dc6cbdd8453feb2d7a724320a Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 12 Sep 2017 22:29:53 -0700 Subject: [PATCH 582/722] remove old debug info --- .../src/entities/EntityTreeSendThread.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 5552c2b580..8e166f7a22 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -142,20 +142,12 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O } if (!_traversal.finished()) { - #ifdef DEBUG - uint64_t startTime = usecTimestampNow(); const uint64_t TIME_BUDGET = 400; // usec - _traversal.traverse(TIME_BUDGET); - - if (_sendQueue.size() > 0) { - uint64_t dt = usecTimestampNow() - startTime; - std::cout << "adebug traversal complete " << " Q.size = " << _sendQueue.size() << " dt = " << dt << std::endl; // adebug - } #else const uint64_t TIME_BUDGET = 200; // usec - _traversal.traverse(TIME_BUDGET); #endif + _traversal.traverse(TIME_BUDGET); } #ifndef SEND_SORTED_ENTITIES From 99265a575871a2bcd69840409ada9cebe02e983d Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 13 Sep 2017 10:20:41 -0700 Subject: [PATCH 583/722] remove extra parens --- assignment-client/src/entities/EntityTreeSendThread.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 8e166f7a22..3d8af26757 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -120,7 +120,7 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { float priority = _conicalView.computePriority(cube); if (priority != PrioritizedEntity::DO_NOT_SEND) { - float distance = (glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE); + float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; float apparentAngle = cube.getScale() / distance; if (apparentAngle > MIN_ENTITY_APPARENT_ANGLE * lodScaleFactor) { _sendQueue.push(PrioritizedEntity(entity, priority)); From a56c07614912aced7324a59048475b7e3ece957e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 13 Sep 2017 13:41:48 -0700 Subject: [PATCH 584/722] fix bad resolution during rebase --- assignment-client/src/octree/OctreeSendThread.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index 32d7ce5053..89e3d403fc 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -532,7 +532,7 @@ void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, Octre } // If the bag had contents but is now empty then we know we've sent the entire scene. - bool completedScene = bagHadSomething && nodeData->elementBag.isEmpty(); + bool completedScene = hadSomething && nodeData->elementBag.isEmpty(); if (completedScene || lastNodeDidntFit) { // we probably want to flush what has accumulated in nodeData but: // do we have more data to send? and is there room? From f7af581c71a04630b2b582b1bcb7bcd80fae4c5e Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 14 Sep 2017 16:14:43 -0700 Subject: [PATCH 585/722] track traversal time, rename entity server stat --- .../src/entities/EntityTreeSendThread.cpp | 3 +++ assignment-client/src/octree/OctreeServer.cpp | 10 +++++++++- assignment-client/src/octree/OctreeServer.h | 5 +++++ interface/resources/qml/Stats.qml | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 3d8af26757..00f5750cb2 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -142,12 +142,15 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O } if (!_traversal.finished()) { + quint64 startTime = usecTimestampNow(); + #ifdef DEBUG const uint64_t TIME_BUDGET = 400; // usec #else const uint64_t TIME_BUDGET = 200; // usec #endif _traversal.traverse(TIME_BUDGET); + OctreeServer::trackTreeTraverseTime((float)(usecTimestampNow() - startTime)); } #ifndef SEND_SORTED_ENTITIES diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 4a40449e30..45cf35820f 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -60,6 +60,8 @@ int OctreeServer::_longTreeWait = 0; int OctreeServer::_shortTreeWait = 0; int OctreeServer::_noTreeWait = 0; +SimpleMovingAverage OctreeServer::_averageTreeTraverseTime(MOVING_AVERAGE_SAMPLE_COUNTS); + SimpleMovingAverage OctreeServer::_averageNodeWaitTime(MOVING_AVERAGE_SAMPLE_COUNTS); SimpleMovingAverage OctreeServer::_averageCompressAndWriteTime(MOVING_AVERAGE_SAMPLE_COUNTS); @@ -106,6 +108,8 @@ void OctreeServer::resetSendingStats() { _shortTreeWait = 0; _noTreeWait = 0; + _averageTreeTraverseTime.reset(); + _averageNodeWaitTime.reset(); _averageCompressAndWriteTime.reset(); @@ -522,6 +526,10 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url (double)_averageTreeExtraLongWaitTime.getAverage(), (double)(extraLongVsTotal * AS_PERCENT), _extraLongTreeWait); + // traverse + float averageTreeTraverseTime = getAverageTreeTraverseTime(); + statsString += QString().sprintf(" Average tree traverse time: %9.2f usecs\r\n", (double)averageTreeTraverseTime); + // encode float averageEncodeTime = getAverageEncodeTime(); statsString += QString().sprintf(" Average encode time: %9.2f usecs\r\n", (double)averageEncodeTime); @@ -1590,7 +1598,7 @@ void OctreeServer::sendStatsPacket() { QJsonObject timingArray1; timingArray1["1. avgLoopTime"] = getAverageLoopTime(); timingArray1["2. avgInsideTime"] = getAverageInsideTime(); - timingArray1["3. avgTreeLockTime"] = getAverageTreeWaitTime(); + timingArray1["3. avgTreeTraverseTime"] = getAverageTreeTraverseTime(); timingArray1["4. avgEncodeTime"] = getAverageEncodeTime(); timingArray1["5. avgCompressAndWriteTime"] = getAverageCompressAndWriteTime(); timingArray1["6. avgSendTime"] = getAveragePacketSendingTime(); diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index 5043ea681c..f930f299f3 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -96,6 +96,9 @@ public: static void trackTreeWaitTime(float time); static float getAverageTreeWaitTime() { return _averageTreeWaitTime.getAverage(); } + static void trackTreeTraverseTime(float time) { _averageTreeTraverseTime.updateAverage(time); } + static float getAverageTreeTraverseTime() { return _averageTreeTraverseTime.getAverage(); } + static void trackNodeWaitTime(float time) { _averageNodeWaitTime.updateAverage(time); } static float getAverageNodeWaitTime() { return _averageNodeWaitTime.getAverage(); } @@ -228,6 +231,8 @@ protected: static int _shortTreeWait; static int _noTreeWait; + static SimpleMovingAverage _averageTreeTraverseTime; + static SimpleMovingAverage _averageNodeWaitTime; static SimpleMovingAverage _averageCompressAndWriteTime; diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 159a696e5f..96e267f67f 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -208,7 +208,7 @@ Item { } StatText { visible: root.expanded; - text: "Entity Mixer In: " + root.entityPacketsInKbps + " kbps"; + text: "Entity Servers In: " + root.entityPacketsInKbps + " kbps"; } StatText { visible: root.expanded; From 3ae41b9b750fdb9cb6ebbbb2ae1944a939ebbf90 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 19 Sep 2017 10:03:35 -0700 Subject: [PATCH 586/722] cleanup client and stats string --- assignment-client/src/octree/OctreeServer.cpp | 2 +- interface/src/Application.cpp | 24 ++++++------------- interface/src/Application.h | 4 +--- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 45cf35820f..4a1aade59d 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -528,7 +528,7 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url // traverse float averageTreeTraverseTime = getAverageTreeTraverseTime(); - statsString += QString().sprintf(" Average tree traverse time: %9.2f usecs\r\n", (double)averageTreeTraverseTime); + statsString += QString().sprintf(" Average tree traverse time: %9.2f usecs\r\n\r\n", (double)averageTreeTraverseTime); // encode float averageEncodeTime = getAverageEncodeTime(); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 60a653fdc9..06ee4b68d2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4696,12 +4696,8 @@ void Application::resetPhysicsReadyInformation() { void Application::reloadResourceCaches() { resetPhysicsReadyInformation(); - { - QMutexLocker viewLocker(&_viewMutex); - _viewFrustum.setPosition(glm::vec3(0.0f, 0.0f, TREE_SCALE)); - _viewFrustum.setOrientation(glm::quat()); - } - // Clear entities out of view frustum + // Query the octree to refresh everything in view + _lastQueriedTime = 0; queryOctree(NodeType::EntityServer, PacketType::EntityQuery, _entityServerJurisdictions); DependencyManager::get()->clearCache(); @@ -5299,7 +5295,7 @@ int Application::sendNackPackets() { return packetsSent; } -void Application::queryOctree(NodeType_t serverType, PacketType packetType, NodeToJurisdictionMap& jurisdictions, bool forceResend) { +void Application::queryOctree(NodeType_t serverType, PacketType packetType, NodeToJurisdictionMap& jurisdictions) { if (!_settingsLoaded) { return; // bail early if settings are not loaded @@ -5704,8 +5700,6 @@ void Application::clearDomainOctreeDetails() { skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT); - _recentlyClearedDomain = true; - DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); @@ -5752,14 +5746,10 @@ void Application::nodeActivated(SharedNodePointer node) { } } - // If we get a new EntityServer activated, do a "forceRedraw" query. This will send a degenerate - // query so that the server will think our next non-degenerate query is "different enough" to send - // us a full scene - if (_recentlyClearedDomain && node->getType() == NodeType::EntityServer) { - _recentlyClearedDomain = false; - if (DependencyManager::get()->shouldRenderEntities()) { - queryOctree(NodeType::EntityServer, PacketType::EntityQuery, _entityServerJurisdictions, true); - } + // If we get a new EntityServer activated, reset lastQueried time + // so we will do a proper query during update + if (node->getType() == NodeType::EntityServer) { + _lastQueriedTime = 0; } if (node->getType() == NodeType::AudioMixer) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 93f7a4ab79..a706ce2b63 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -467,7 +467,7 @@ private: void updateThreads(float deltaTime); void updateDialogs(float deltaTime) const; - void queryOctree(NodeType_t serverType, PacketType packetType, NodeToJurisdictionMap& jurisdictions, bool forceResend = false); + void queryOctree(NodeType_t serverType, PacketType packetType, NodeToJurisdictionMap& jurisdictions); void renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed); @@ -663,8 +663,6 @@ private: bool _keyboardDeviceHasFocus { true }; - bool _recentlyClearedDomain { false }; - QString _returnFromFullScreenMirrorTo; ConnectionMonitor _connectionMonitor; From 1c30f7424e229b64b705566dea92460f5cc2c4c6 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 22 Sep 2017 15:54:25 -0700 Subject: [PATCH 587/722] remove cruft and add comments --- .../src/entities/EntityTreeSendThread.cpp | 30 ++----------------- interface/src/Application.cpp | 1 - libraries/entities/src/DiffTraversal.cpp | 11 ------- libraries/entities/src/DiffTraversal.h | 4 --- libraries/entities/src/EntityTree.cpp | 11 ++----- libraries/octree/src/OctreeConstants.h | 3 ++ libraries/octree/src/OctreeUtils.h | 5 ++-- libraries/shared/src/ViewFrustum.cpp | 1 + 8 files changed, 12 insertions(+), 54 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 00f5750cb2..1d506ba47c 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -17,7 +17,6 @@ #include "EntityServer.h" -#define SEND_SORTED_ENTITIES EntityTreeSendThread::EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) : OctreeSendThread(myServer, node) @@ -144,32 +143,15 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O if (!_traversal.finished()) { quint64 startTime = usecTimestampNow(); -#ifdef DEBUG + #ifdef DEBUG const uint64_t TIME_BUDGET = 400; // usec -#else + #else const uint64_t TIME_BUDGET = 200; // usec -#endif + #endif _traversal.traverse(TIME_BUDGET); OctreeServer::trackTreeTraverseTime((float)(usecTimestampNow() - startTime)); } -#ifndef SEND_SORTED_ENTITIES - if (!_sendQueue.empty()) { - uint64_t sendTime = usecTimestampNow(); - // print what needs to be sent - while (!_sendQueue.empty()) { - PrioritizedEntity entry = _sendQueue.top(); - EntityItemPointer entity = entry.getEntity(); - if (entity) { - std::cout << "adebug send '" << entity->getName().toStdString() << "'" << " : " << entry.getPriority() << std::endl; // adebug - _knownState[entity.get()] = sendTime; - } - _sendQueue.pop(); - _entitiesInQueue.erase(entry.getRawEntityPointer()); - } - } -#endif // SEND_SORTED_ENTITIES - OctreeSendThread::traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); } @@ -400,8 +382,6 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree } bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters) { -#ifdef SEND_SORTED_ENTITIES - //auto entityTree = std::static_pointer_cast(_myServer->getOctree()); if (_sendQueue.empty()) { OctreeServer::trackEncodeTime(OctreeServer::SKIP_TIME); return false; @@ -485,10 +465,6 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream _packetData.updatePriorBytes(_numEntitiesOffset, (const unsigned char*)&_numEntities, sizeof(_numEntities)); OctreeServer::trackEncodeTime((float)(usecTimestampNow() - encodeStart)); return true; - -#else // SEND_SORTED_ENTITIES - return OctreeSendThread::traverseTreeAndBuildNextPacketPayload(params); -#endif // SEND_SORTED_ENTITIES } void EntityTreeSendThread::editingEntityPointer(const EntityItemPointer entity) { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 06ee4b68d2..36e5b3d859 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5429,7 +5429,6 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node << perUnknownServer << " to send us jurisdiction."; } - // TODO: remove this hackery: it no longer makes sense for streaming of entities in scene. // set the query's position/orientation to be degenerate in a manner that will get the scene quickly // If there's only one server, then don't do this, and just let the normal voxel query pass through // as expected... this way, we will actually get a valid scene if there is one to be seen diff --git a/libraries/entities/src/DiffTraversal.cpp b/libraries/entities/src/DiffTraversal.cpp index b33d9141c3..a2923c003a 100644 --- a/libraries/entities/src/DiffTraversal.cpp +++ b/libraries/entities/src/DiffTraversal.cpp @@ -236,17 +236,6 @@ void DiffTraversal::setScanCallback(std::function // DEBUG - #include #include "EntityTreeElement.h" @@ -74,8 +72,6 @@ public: void setScanCallback(std::function cb); void traverse(uint64_t timeBudget); - friend std::ostream& operator<<(std::ostream& s, const DiffTraversal& traversal); // DEBUG - private: void getNextVisibleElement(VisibleElement& next); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 99ab5a7677..3feb9cc03c 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -192,21 +192,14 @@ int EntityTree::readEntityDataFromBuffer(const unsigned char* data, int bytesLef if (!isDeletedEntity(entityItemID)) { _entitiesToAdd.insert(entityItemID, entity); - /* - addEntityMapEntry(entity); - oldElement->addEntityItem(entity); // add this new entity to this elements entities - entityItemID = entity->getEntityItemID(); - postAddEntity(entity); - */ - if (entity->getCreated() == UNKNOWN_CREATED_TIME) { entity->recordCreationTime(); } + #ifdef WANT_DEBUG } else { - #ifdef WANT_DEBUG qCDebug(entities) << "Received packet for previously deleted entity [" << entityItemID << "] ignoring. (inside " << __FUNCTION__ << ")"; - #endif + #endif } } } diff --git a/libraries/octree/src/OctreeConstants.h b/libraries/octree/src/OctreeConstants.h index 06b4748f51..062d4e1ef2 100644 --- a/libraries/octree/src/OctreeConstants.h +++ b/libraries/octree/src/OctreeConstants.h @@ -37,6 +37,9 @@ const int NUMBER_OF_CHILDREN = 8; const int MAX_TREE_SLICE_BYTES = 26; +// The oversend below is 20 degrees because that is the minimum oversend necessary to prevent missing entities +// near the edge of the view. The value here is determined by hard-coded values in ViewFrsutum::isVerySimilar(). +// TODO: move this parameter to the OctreeQueryNode context. const float VIEW_FRUSTUM_FOV_OVERSEND = 20.0f; // These are guards to prevent our voxel tree recursive routines from spinning out of control diff --git a/libraries/octree/src/OctreeUtils.h b/libraries/octree/src/OctreeUtils.h index 3264520aac..122d82e267 100644 --- a/libraries/octree/src/OctreeUtils.h +++ b/libraries/octree/src/OctreeUtils.h @@ -27,8 +27,9 @@ float boundaryDistanceForRenderLevel(unsigned int renderLevel, float voxelSizeSc float getAccuracyAngle(float octreeSizeScale, int boundaryLevelAdjust); -const float MIN_ELEMENT_APPARENT_ANGLE = 0.0087266f; // ~0.57 degrees in radians -// NOTE: the entity bounding cube is larger than the smallest containing octree element by sqrt(3) +// MIN_ELEMENT_APPARENT_ANGLE = angular diameter of 1x1x1m cube at 400m = sqrt(3) / 400 = 0.0043301f; +const float MIN_ELEMENT_APPARENT_ANGLE = 0.0043301f; +// NOTE: the entity bounding cube is larger than the smallest possible containing octree element by sqrt(3) const float SQRT_THREE = 1.73205080f; const float MIN_ENTITY_APPARENT_ANGLE = MIN_ELEMENT_APPARENT_ANGLE * SQRT_THREE; const float MIN_VISIBLE_DISTANCE = 0.0001f; // helps avoid divide-by-zero check diff --git a/libraries/shared/src/ViewFrustum.cpp b/libraries/shared/src/ViewFrustum.cpp index ccdeb830b6..978221e167 100644 --- a/libraries/shared/src/ViewFrustum.cpp +++ b/libraries/shared/src/ViewFrustum.cpp @@ -328,6 +328,7 @@ bool closeEnough(float a, float b, float relativeError) { return fabsf(a - b) / (0.5f * fabsf(a + b) + EPSILON) < relativeError; } +// TODO: the slop and relative error should be passed in by argument rather than hard-coded. bool ViewFrustum::isVerySimilar(const ViewFrustum& other) const { const float MIN_POSITION_SLOP_SQUARED = 25.0f; // 5 meters squared const float MIN_ORIENTATION_DOT = 0.9924039f; // dot product of two quaternions 10 degrees apart From 5dcd6bc496fb777a25a1fc127941c037b0661627 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 22 Sep 2017 15:58:01 -0700 Subject: [PATCH 588/722] namechange: apparentAngle --> angularDiameter --- .../src/entities/EntityTreeSendThread.cpp | 20 +++++++++---------- libraries/entities/src/DiffTraversal.cpp | 12 +++++------ libraries/octree/src/OctreeUtils.h | 6 +++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index 1d506ba47c..e04dc5553e 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -120,8 +120,8 @@ void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O float priority = _conicalView.computePriority(cube); if (priority != PrioritizedEntity::DO_NOT_SEND) { float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; - float apparentAngle = cube.getScale() / distance; - if (apparentAngle > MIN_ENTITY_APPARENT_ANGLE * lodScaleFactor) { + float angularDiameter = cube.getScale() / distance; + if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { _sendQueue.push(PrioritizedEntity(entity, priority)); _entitiesInQueue.insert(entity.get()); } @@ -241,8 +241,8 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen // before we consider including it. float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; - float apparentAngle = cube.getScale() / distance; - if (apparentAngle > MIN_ENTITY_APPARENT_ANGLE * lodScaleFactor) { + float angularDiameter = cube.getScale() / distance; + if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); _entitiesInQueue.insert(entity.get()); @@ -287,8 +287,8 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (next.intersection == ViewFrustum::INSIDE || _traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { // See the DiffTraversal::First case for an explanation of the "entity is too small" check float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; - float apparentAngle = cube.getScale() / distance; - if (apparentAngle > MIN_ENTITY_APPARENT_ANGLE * lodScaleFactor) { + float angularDiameter = cube.getScale() / distance; + if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); _entitiesInQueue.insert(entity.get()); @@ -346,8 +346,8 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { // See the DiffTraversal::First case for an explanation of the "entity is too small" check float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; - float apparentAngle = cube.getScale() / distance; - if (apparentAngle > MIN_ENTITY_APPARENT_ANGLE * lodScaleFactor) { + float angularDiameter = cube.getScale() / distance; + if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { if (!_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); @@ -355,8 +355,8 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree } else { // If this entity was skipped last time because it was too small, we still need to send it distance = glm::distance(cube.calcCenter(), completedViewPosition) + MIN_VISIBLE_DISTANCE; - apparentAngle = cube.getScale() / distance; - if (apparentAngle <= MIN_ENTITY_APPARENT_ANGLE * completedLODScaleFactor) { + angularDiameter = cube.getScale() / distance; + if (angularDiameter <= MIN_ENTITY_ANGULAR_DIAMETER * completedLODScaleFactor) { // this object was skipped in last completed traversal float priority = _conicalView.computePriority(cube); _sendQueue.push(PrioritizedEntity(entity, priority)); diff --git a/libraries/entities/src/DiffTraversal.cpp b/libraries/entities/src/DiffTraversal.cpp index a2923c003a..9a48028045 100644 --- a/libraries/entities/src/DiffTraversal.cpp +++ b/libraries/entities/src/DiffTraversal.cpp @@ -44,8 +44,8 @@ void DiffTraversal::Waypoint::getNextVisibleElementFirstTime(DiffTraversal::Visi } else if (view.viewFrustum.cubeIntersectsKeyhole(nextElement->getAACube())) { // check for LOD truncation float distance = glm::distance(view.viewFrustum.getPosition(), nextElement->getAACube().calcCenter()) + MIN_VISIBLE_DISTANCE; - float apparentAngle = nextElement->getAACube().getScale() / distance; - if (apparentAngle > MIN_ELEMENT_APPARENT_ANGLE * view.lodScaleFactor) { + float angularDiameter = nextElement->getAACube().getScale() / distance; + if (angularDiameter > MIN_ELEMENT_ANGULAR_DIAMETER * view.lodScaleFactor) { next.element = nextElement; return; } @@ -84,8 +84,8 @@ void DiffTraversal::Waypoint::getNextVisibleElementRepeat( } else { // check for LOD truncation float distance = glm::distance(view.viewFrustum.getPosition(), nextElement->getAACube().calcCenter()) + MIN_VISIBLE_DISTANCE; - float apparentAngle = nextElement->getAACube().getScale() / distance; - if (apparentAngle > MIN_ELEMENT_APPARENT_ANGLE * view.lodScaleFactor) { + float angularDiameter = nextElement->getAACube().getScale() / distance; + if (angularDiameter > MIN_ELEMENT_ANGULAR_DIAMETER * view.lodScaleFactor) { ViewFrustum::intersection intersection = view.viewFrustum.calculateCubeKeyholeIntersection(nextElement->getAACube()); if (intersection != ViewFrustum::OUTSIDE) { next.element = nextElement; @@ -121,8 +121,8 @@ void DiffTraversal::Waypoint::getNextVisibleElementDifferential(DiffTraversal::V AACube cube = nextElement->getAACube(); // check for LOD truncation float distance = glm::distance(view.viewFrustum.getPosition(), cube.calcCenter()) + MIN_VISIBLE_DISTANCE; - float apparentAngle = cube.getScale() / distance; - if (apparentAngle > MIN_ELEMENT_APPARENT_ANGLE * view.lodScaleFactor) { + float angularDiameter = cube.getScale() / distance; + if (angularDiameter > MIN_ELEMENT_ANGULAR_DIAMETER * view.lodScaleFactor) { if (view.viewFrustum.calculateCubeKeyholeIntersection(cube) != ViewFrustum::OUTSIDE) { next.element = nextElement; next.intersection = ViewFrustum::OUTSIDE; diff --git a/libraries/octree/src/OctreeUtils.h b/libraries/octree/src/OctreeUtils.h index 122d82e267..7819db852c 100644 --- a/libraries/octree/src/OctreeUtils.h +++ b/libraries/octree/src/OctreeUtils.h @@ -27,11 +27,11 @@ float boundaryDistanceForRenderLevel(unsigned int renderLevel, float voxelSizeSc float getAccuracyAngle(float octreeSizeScale, int boundaryLevelAdjust); -// MIN_ELEMENT_APPARENT_ANGLE = angular diameter of 1x1x1m cube at 400m = sqrt(3) / 400 = 0.0043301f; -const float MIN_ELEMENT_APPARENT_ANGLE = 0.0043301f; +// MIN_ELEMENT_ANGULAR_DIAMETER = angular diameter of 1x1x1m cube at 400m = sqrt(3) / 400 = 0.0043301f +const float MIN_ELEMENT_ANGULAR_DIAMETER = 0.0043301f; // NOTE: the entity bounding cube is larger than the smallest possible containing octree element by sqrt(3) const float SQRT_THREE = 1.73205080f; -const float MIN_ENTITY_APPARENT_ANGLE = MIN_ELEMENT_APPARENT_ANGLE * SQRT_THREE; +const float MIN_ENTITY_ANGULAR_DIAMETER = MIN_ELEMENT_ANGULAR_DIAMETER * SQRT_THREE; const float MIN_VISIBLE_DISTANCE = 0.0001f; // helps avoid divide-by-zero check #endif // hifi_OctreeUtils_h From 0c934e863b65142013cc7c00f5b0ccf7d1c03474 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 22 Sep 2017 16:00:44 -0700 Subject: [PATCH 589/722] clarify some comments --- libraries/octree/src/OctreeUtils.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/octree/src/OctreeUtils.h b/libraries/octree/src/OctreeUtils.h index 7819db852c..c257bcd5f1 100644 --- a/libraries/octree/src/OctreeUtils.h +++ b/libraries/octree/src/OctreeUtils.h @@ -27,8 +27,8 @@ float boundaryDistanceForRenderLevel(unsigned int renderLevel, float voxelSizeSc float getAccuracyAngle(float octreeSizeScale, int boundaryLevelAdjust); -// MIN_ELEMENT_ANGULAR_DIAMETER = angular diameter of 1x1x1m cube at 400m = sqrt(3) / 400 = 0.0043301f -const float MIN_ELEMENT_ANGULAR_DIAMETER = 0.0043301f; +// MIN_ELEMENT_ANGULAR_DIAMETER = angular diameter of 1x1x1m cube at 400m = sqrt(3) / 400 = 0.0043301 radians ~= 0.25 degrees +const float MIN_ELEMENT_ANGULAR_DIAMETER = 0.0043301f; // radians // NOTE: the entity bounding cube is larger than the smallest possible containing octree element by sqrt(3) const float SQRT_THREE = 1.73205080f; const float MIN_ENTITY_ANGULAR_DIAMETER = MIN_ELEMENT_ANGULAR_DIAMETER * SQRT_THREE; From b16d66602649a4483ff106b03f218e30e637e6aa Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 25 Sep 2017 08:50:58 -0700 Subject: [PATCH 590/722] remove dupe addToNeedsParentFixupList() call --- libraries/entities/src/EntityTree.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 3feb9cc03c..6cf3b51001 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -116,9 +116,6 @@ void EntityTree::readBitstreamToTree(const unsigned char* bitstream, EntityItemPointer entityItem = itr.value(); AddEntityOperator theOperator(getThisPointer(), entityItem); recurseTreeWithOperator(&theOperator); - if (!entityItem->getParentID().isNull()) { - addToNeedsParentFixupList(entityItem); - } postAddEntity(entityItem); } _entitiesToAdd.clear(); From f5f1a64c92b9a0a8a92bd0a3e07c196a831874c6 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 25 Sep 2017 17:38:56 -0700 Subject: [PATCH 591/722] use const ref on pointer, and use dynamic_cast --- libraries/entities/src/EntityTree.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 6cf3b51001..6769af45a3 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -113,7 +113,7 @@ void EntityTree::readBitstreamToTree(const unsigned char* bitstream, // add entities QHash::const_iterator itr; for (itr = _entitiesToAdd.constBegin(); itr != _entitiesToAdd.constEnd(); ++itr) { - EntityItemPointer entityItem = itr.value(); + EntityItemPointer& entityItem = itr.value(); AddEntityOperator theOperator(getThisPointer(), entityItem); recurseTreeWithOperator(&theOperator); postAddEntity(entityItem); @@ -560,7 +560,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign auto descendantID = descendant->getID(); theOperator.addEntityIDToDeleteList(descendantID); emit deletingEntity(descendantID); - EntityItemPointer descendantEntity = std::static_pointer_cast(descendant); + EntityItemPointer descendantEntity = std::dynamic_pointer_cast(descendant); if (descendantEntity) { emit deletingEntityPointer(descendantEntity.get()); } From 32910e6f40b33e4ead5fa6d69efbce85aa51b49c Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 25 Sep 2017 17:40:15 -0700 Subject: [PATCH 592/722] use [this] for lambda capture list --- libraries/entities/src/DiffTraversal.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/entities/src/DiffTraversal.cpp b/libraries/entities/src/DiffTraversal.cpp index 9a48028045..2f9423daa3 100644 --- a/libraries/entities/src/DiffTraversal.cpp +++ b/libraries/entities/src/DiffTraversal.cpp @@ -167,21 +167,21 @@ DiffTraversal::Type DiffTraversal::prepareNewTraversal(const ViewFrustum& viewFr type = Type::First; _currentView.viewFrustum = viewFrustum; _currentView.lodScaleFactor = lodScaleFactor; - _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { + _getNextVisibleElementCallback = [this](DiffTraversal::VisibleElement& next) { _path.back().getNextVisibleElementFirstTime(next, _currentView); }; } else if (!_currentView.usesViewFrustum || (_completedView.viewFrustum.isVerySimilar(viewFrustum) && lodScaleFactor == _completedView.lodScaleFactor)) { type = Type::Repeat; - _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { + _getNextVisibleElementCallback = [this](DiffTraversal::VisibleElement& next) { _path.back().getNextVisibleElementRepeat(next, _completedView, _completedView.startTime); }; } else { type = Type::Differential; _currentView.viewFrustum = viewFrustum; _currentView.lodScaleFactor = lodScaleFactor; - _getNextVisibleElementCallback = [&](DiffTraversal::VisibleElement& next) { + _getNextVisibleElementCallback = [this](DiffTraversal::VisibleElement& next) { _path.back().getNextVisibleElementDifferential(next, _currentView, _completedView); }; } From 01304de8c20988216a0fa65d41c6cb95b561ab00 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 25 Sep 2017 17:41:14 -0700 Subject: [PATCH 593/722] indent switch statement, use const ref --- .../src/entities/EntityTreeSendThread.cpp | 290 +++++++++--------- .../src/entities/EntityTreeSendThread.h | 2 +- 2 files changed, 146 insertions(+), 146 deletions(-) diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index e04dc5553e..03014bae6a 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -220,164 +220,164 @@ void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTree _conicalView.set(_traversal.getCurrentView()); switch (type) { - case DiffTraversal::First: - // When we get to a First traversal, clear the _knownState - _knownState.clear(); - if (usesViewFrustum) { + case DiffTraversal::First: + // When we get to a First traversal, clear the _knownState + _knownState.clear(); + if (usesViewFrustum) { + float lodScaleFactor = _traversal.getCurrentLODScaleFactor(); + glm::vec3 viewPosition = _traversal.getCurrentView().getPosition(); + _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) { + next.element->forEachEntity([=](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + // Check the size of the entity, it's possible that a "too small to see" entity is included in a + // larger octree cell because of its position (for example if it crosses the boundary of a cell it + // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen + // before we consider including it. + float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; + float angularDiameter = cube.getScale() / distance; + if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesInQueue.insert(entity.get()); + } + } + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + }); + }); + } else { + _traversal.setScanCallback([this](DiffTraversal::VisibleElement& next) { + next.element->forEachEntity([this](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + }); + }); + } + break; + case DiffTraversal::Repeat: + if (usesViewFrustum) { + float lodScaleFactor = _traversal.getCurrentLODScaleFactor(); + glm::vec3 viewPosition = _traversal.getCurrentView().getPosition(); + _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) { + uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); + if (next.element->getLastChangedContent() > startOfCompletedTraversal) { + next.element->forEachEntity([=](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + auto knownTimestamp = _knownState.find(entity.get()); + if (knownTimestamp == _knownState.end()) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (next.intersection == ViewFrustum::INSIDE || _traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + // See the DiffTraversal::First case for an explanation of the "entity is too small" check + float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; + float angularDiameter = cube.getScale() / distance; + if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesInQueue.insert(entity.get()); + } + } + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + } else if (entity->getLastEdited() > knownTimestamp->second) { + // it is known and it changed --> put it on the queue with any priority + // TODO: sort these correctly + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + }); + } + }); + } else { + _traversal.setScanCallback([this](DiffTraversal::VisibleElement& next) { + uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); + if (next.element->getLastChangedContent() > startOfCompletedTraversal) { + next.element->forEachEntity([this](EntityItemPointer entity) { + // Bail early if we've already checked this entity this frame + if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { + return; + } + auto knownTimestamp = _knownState.find(entity.get()); + if (knownTimestamp == _knownState.end() || entity->getLastEdited() > knownTimestamp->second) { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); + } + }); + } + }); + } + break; + case DiffTraversal::Differential: + assert(usesViewFrustum); float lodScaleFactor = _traversal.getCurrentLODScaleFactor(); glm::vec3 viewPosition = _traversal.getCurrentView().getPosition(); - _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) { + float completedLODScaleFactor = _traversal.getCompletedLODScaleFactor(); + glm::vec3 completedViewPosition = _traversal.getCompletedView().getPosition(); + _traversal.setScanCallback([=] (DiffTraversal::VisibleElement& next) { next.element->forEachEntity([=](EntityItemPointer entity) { // Bail early if we've already checked this entity this frame if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { return; } - bool success = false; - AACube cube = entity->getQueryAACube(success); - if (success) { - if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { - // Check the size of the entity, it's possible that a "too small to see" entity is included in a - // larger octree cell because of its position (for example if it crosses the boundary of a cell it - // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen - // before we consider including it. - float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; - float angularDiameter = cube.getScale() / distance; - if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { - float priority = _conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesInQueue.insert(entity.get()); + auto knownTimestamp = _knownState.find(entity.get()); + if (knownTimestamp == _knownState.end()) { + bool success = false; + AACube cube = entity->getQueryAACube(success); + if (success) { + if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { + // See the DiffTraversal::First case for an explanation of the "entity is too small" check + float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; + float angularDiameter = cube.getScale() / distance; + if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { + if (!_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesInQueue.insert(entity.get()); + } else { + // If this entity was skipped last time because it was too small, we still need to send it + distance = glm::distance(cube.calcCenter(), completedViewPosition) + MIN_VISIBLE_DISTANCE; + angularDiameter = cube.getScale() / distance; + if (angularDiameter <= MIN_ENTITY_ANGULAR_DIAMETER * completedLODScaleFactor) { + // this object was skipped in last completed traversal + float priority = _conicalView.computePriority(cube); + _sendQueue.push(PrioritizedEntity(entity, priority)); + _entitiesInQueue.insert(entity.get()); + } + } + } } + } else { + _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); + _entitiesInQueue.insert(entity.get()); } - } else { + } else if (entity->getLastEdited() > knownTimestamp->second) { + // it is known and it changed --> put it on the queue with any priority + // TODO: sort these correctly _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); _entitiesInQueue.insert(entity.get()); } }); }); - } else { - _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) { - next.element->forEachEntity([&](EntityItemPointer entity) { - // Bail early if we've already checked this entity this frame - if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { - return; - } - _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); - _entitiesInQueue.insert(entity.get()); - }); - }); - } - break; - case DiffTraversal::Repeat: - if (usesViewFrustum) { - float lodScaleFactor = _traversal.getCurrentLODScaleFactor(); - glm::vec3 viewPosition = _traversal.getCurrentView().getPosition(); - _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) { - uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); - if (next.element->getLastChangedContent() > startOfCompletedTraversal) { - next.element->forEachEntity([=](EntityItemPointer entity) { - // Bail early if we've already checked this entity this frame - if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { - return; - } - auto knownTimestamp = _knownState.find(entity.get()); - if (knownTimestamp == _knownState.end()) { - bool success = false; - AACube cube = entity->getQueryAACube(success); - if (success) { - if (next.intersection == ViewFrustum::INSIDE || _traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { - // See the DiffTraversal::First case for an explanation of the "entity is too small" check - float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; - float angularDiameter = cube.getScale() / distance; - if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { - float priority = _conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesInQueue.insert(entity.get()); - } - } - } else { - _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); - _entitiesInQueue.insert(entity.get()); - } - } else if (entity->getLastEdited() > knownTimestamp->second) { - // it is known and it changed --> put it on the queue with any priority - // TODO: sort these correctly - _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); - _entitiesInQueue.insert(entity.get()); - } - }); - } - }); - } else { - _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) { - uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal(); - if (next.element->getLastChangedContent() > startOfCompletedTraversal) { - next.element->forEachEntity([=](EntityItemPointer entity) { - // Bail early if we've already checked this entity this frame - if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { - return; - } - auto knownTimestamp = _knownState.find(entity.get()); - if (knownTimestamp == _knownState.end() || entity->getLastEdited() > knownTimestamp->second) { - _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); - _entitiesInQueue.insert(entity.get()); - } - }); - } - }); - } - break; - case DiffTraversal::Differential: - assert(usesViewFrustum); - float lodScaleFactor = _traversal.getCurrentLODScaleFactor(); - glm::vec3 viewPosition = _traversal.getCurrentView().getPosition(); - float completedLODScaleFactor = _traversal.getCompletedLODScaleFactor(); - glm::vec3 completedViewPosition = _traversal.getCompletedView().getPosition(); - _traversal.setScanCallback([=] (DiffTraversal::VisibleElement& next) { - next.element->forEachEntity([=](EntityItemPointer entity) { - // Bail early if we've already checked this entity this frame - if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) { - return; - } - auto knownTimestamp = _knownState.find(entity.get()); - if (knownTimestamp == _knownState.end()) { - bool success = false; - AACube cube = entity->getQueryAACube(success); - if (success) { - if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) { - // See the DiffTraversal::First case for an explanation of the "entity is too small" check - float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE; - float angularDiameter = cube.getScale() / distance; - if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) { - if (!_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) { - float priority = _conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesInQueue.insert(entity.get()); - } else { - // If this entity was skipped last time because it was too small, we still need to send it - distance = glm::distance(cube.calcCenter(), completedViewPosition) + MIN_VISIBLE_DISTANCE; - angularDiameter = cube.getScale() / distance; - if (angularDiameter <= MIN_ENTITY_ANGULAR_DIAMETER * completedLODScaleFactor) { - // this object was skipped in last completed traversal - float priority = _conicalView.computePriority(cube); - _sendQueue.push(PrioritizedEntity(entity, priority)); - _entitiesInQueue.insert(entity.get()); - } - } - } - } - } else { - _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); - _entitiesInQueue.insert(entity.get()); - } - } else if (entity->getLastEdited() > knownTimestamp->second) { - // it is known and it changed --> put it on the queue with any priority - // TODO: sort these correctly - _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY)); - _entitiesInQueue.insert(entity.get()); - } - }); - }); - break; + break; } } @@ -467,7 +467,7 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream return true; } -void EntityTreeSendThread::editingEntityPointer(const EntityItemPointer entity) { +void EntityTreeSendThread::editingEntityPointer(const EntityItemPointer& entity) { if (entity) { if (_entitiesInQueue.find(entity.get()) == _entitiesInQueue.end() && _knownState.find(entity.get()) != _knownState.end()) { bool success = false; diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 72ad2d0b10..49901491ff 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -59,7 +59,7 @@ private: uint16_t _numEntities { 0 }; private slots: - void editingEntityPointer(const EntityItemPointer entity); + void editingEntityPointer(const EntityItemPointer& entity); void deletingEntityPointer(EntityItem* entity); }; From 86cbea73c8bffefc6371cd2c3fe41a59fded4602 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 25 Sep 2017 17:41:45 -0700 Subject: [PATCH 594/722] less magic --- assignment-client/src/entities/EntityPriorityQueue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/entities/EntityPriorityQueue.cpp b/assignment-client/src/entities/EntityPriorityQueue.cpp index 6d94f911ea..dcd5892fba 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.cpp +++ b/assignment-client/src/entities/EntityPriorityQueue.cpp @@ -61,7 +61,7 @@ float ConicalView::computePriority(const EntityItemPointer& entity) const { return computePriority(cube); } else { // when in doubt give it something positive - return 1.0f; + return PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY; } } From 8134e2b7f9a36cd62b04ae3917d44baf3e62130d Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 25 Sep 2017 17:52:50 -0700 Subject: [PATCH 595/722] fix const violation --- libraries/entities/src/EntityTree.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 6769af45a3..bf37a08386 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -113,7 +113,7 @@ void EntityTree::readBitstreamToTree(const unsigned char* bitstream, // add entities QHash::const_iterator itr; for (itr = _entitiesToAdd.constBegin(); itr != _entitiesToAdd.constEnd(); ++itr) { - EntityItemPointer& entityItem = itr.value(); + const EntityItemPointer& entityItem = itr.value(); AddEntityOperator theOperator(getThisPointer(), entityItem); recurseTreeWithOperator(&theOperator); postAddEntity(entityItem); From ad9a239b45ec4e5766e5e0a9d2f5aa3289667d15 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 26 Sep 2017 08:12:53 -0700 Subject: [PATCH 596/722] remove unused cruft --- .../src/entities/EntityPriorityQueue.cpp | 23 ------------------- .../src/entities/EntityPriorityQueue.h | 2 -- 2 files changed, 25 deletions(-) diff --git a/assignment-client/src/entities/EntityPriorityQueue.cpp b/assignment-client/src/entities/EntityPriorityQueue.cpp index dcd5892fba..999a05f2e2 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.cpp +++ b/assignment-client/src/entities/EntityPriorityQueue.cpp @@ -51,26 +51,3 @@ float ConicalView::computePriority(const AACube& cube) const { } return PrioritizedEntity::DO_NOT_SEND; } - -// static -float ConicalView::computePriority(const EntityItemPointer& entity) const { - assert(entity); - bool success; - AACube cube = entity->getQueryAACube(success); - if (success) { - return computePriority(cube); - } else { - // when in doubt give it something positive - return PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY; - } -} - -float PrioritizedEntity::updatePriority(const ConicalView& conicalView) { - EntityItemPointer entity = _weakEntity.lock(); - if (entity) { - _priority = conicalView.computePriority(entity); - } else { - _priority = PrioritizedEntity::DO_NOT_SEND; - } - return _priority; -} diff --git a/assignment-client/src/entities/EntityPriorityQueue.h b/assignment-client/src/entities/EntityPriorityQueue.h index a5d0ab05ff..e308d9b549 100644 --- a/assignment-client/src/entities/EntityPriorityQueue.h +++ b/assignment-client/src/entities/EntityPriorityQueue.h @@ -27,7 +27,6 @@ public: ConicalView(const ViewFrustum& viewFrustum) { set(viewFrustum); } void set(const ViewFrustum& viewFrustum); float computePriority(const AACube& cube) const; - float computePriority(const EntityItemPointer& entity) const; private: glm::vec3 _position { 0.0f, 0.0f, 0.0f }; glm::vec3 _direction { 0.0f, 0.0f, 1.0f }; @@ -44,7 +43,6 @@ public: static const float WHEN_IN_DOUBT_PRIORITY; PrioritizedEntity(EntityItemPointer entity, float priority, bool forceRemove = false) : _weakEntity(entity), _rawEntityPointer(entity.get()), _priority(priority), _forceRemove(forceRemove) {} - float updatePriority(const ConicalView& view); EntityItemPointer getEntity() const { return _weakEntity.lock(); } EntityItem* getRawEntityPointer() const { return _rawEntityPointer; } float getPriority() const { return _priority; } From fd917917c40004ecfc8125981502bccaad969180 Mon Sep 17 00:00:00 2001 From: humbletim Date: Fri, 29 Sep 2017 14:48:01 -0400 Subject: [PATCH 597/722] include the source basename in Script.print() && QML/Script console.*() debug output --- libraries/script-engine/src/ScriptEngine.cpp | 13 ++++++++----- libraries/shared/src/LogHandler.cpp | 7 +++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 2260cea616..f394216357 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -107,10 +107,13 @@ static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) { } message += context->argument(i).toString(); } - qCDebug(scriptengineScript).noquote() << message; // noquote() so that \n is treated as newline if (ScriptEngine *scriptEngine = qobject_cast(engine)) { scriptEngine->print(message); + // prefix the script engine name to help disambiguate messages in the main debug log + qCDebug(scriptengineScript, "[%s] %s", qUtf8Printable(scriptEngine->getFilename()), qUtf8Printable(message)); + } else { + qCDebug(scriptengineScript, "%s", qUtf8Printable(message)); } return QScriptValue(); @@ -465,22 +468,22 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { } void ScriptEngine::scriptErrorMessage(const QString& message) { - qCCritical(scriptengine) << qPrintable(message); + qCCritical(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit errorMessage(message, getFilename()); } void ScriptEngine::scriptWarningMessage(const QString& message) { - qCWarning(scriptengine) << qPrintable(message); + qCWarning(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit warningMessage(message, getFilename()); } void ScriptEngine::scriptInfoMessage(const QString& message) { - qCInfo(scriptengine) << qPrintable(message); + qCInfo(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit infoMessage(message, getFilename()); } void ScriptEngine::scriptPrintedMessage(const QString& message) { - qCDebug(scriptengine) << qPrintable(message); + qCDebug(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit printedMessage(message, getFilename()); } diff --git a/libraries/shared/src/LogHandler.cpp b/libraries/shared/src/LogHandler.cpp index e40ef814ea..aa67c14c4b 100644 --- a/libraries/shared/src/LogHandler.cpp +++ b/libraries/shared/src/LogHandler.cpp @@ -177,6 +177,13 @@ QString LogHandler::printMessage(LogMsgType type, const QMessageLogContext& cont prefixString.append(QString(" [%1]").arg(_targetName)); } + // for [qml] console.* messages include an abbreviated source filename + if (context.category && context.file && !strcmp("qml", context.category)) { + if (const char* basename = strrchr(context.file, '/')) { + prefixString.append(QString(" [%1]").arg(basename+1)); + } + } + QString logMessage = QString("%1 %2").arg(prefixString, message.split('\n').join('\n' + prefixString + " ")); fprintf(stdout, "%s\n", qPrintable(logMessage)); return logMessage; From 7f6773b1f48925c84a940311cb204fcd41687dcd Mon Sep 17 00:00:00 2001 From: vladest Date: Fri, 29 Sep 2017 21:12:21 +0200 Subject: [PATCH 598/722] Added semicolons --- .../qml/hifi/commerce/wallet/PassphraseSelection.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml index 653f81501c..93341d74cd 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -76,8 +76,8 @@ Item { height: 50; echoMode: TextInput.Password; placeholderText: "enter current passphrase"; - activeFocusOnPress: true - activeFocusOnTab: true + activeFocusOnPress: true; + activeFocusOnTab: true; onFocusChanged: { if (focus) { @@ -103,8 +103,8 @@ Item { height: 50; echoMode: TextInput.Password; placeholderText: root.isShowingTip ? "" : "enter new passphrase"; - activeFocusOnPress: true - activeFocusOnTab: true + activeFocusOnPress: true; + activeFocusOnTab: true; onFocusChanged: { if (focus) { @@ -128,8 +128,8 @@ Item { height: 50; echoMode: TextInput.Password; placeholderText: root.isShowingTip ? "" : "re-enter new passphrase"; - activeFocusOnPress: true - activeFocusOnTab: true + activeFocusOnPress: true; + activeFocusOnTab: true; onFocusChanged: { if (focus) { From 96b6a2f013678afcf7c30be3916cb73505cf4953 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 28 Sep 2017 16:07:15 -0700 Subject: [PATCH 599/722] Fix entity add after incomplete rezCertified implementation (cherry picked from commit 0f66fb41fd3ff2b58c4fcdef8f5145830559e3bf) --- libraries/entities/src/EntityTree.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 5c5aee97ff..c8675bdcba 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1148,11 +1148,8 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } else if (!senderNode->getCanRez() && !senderNode->getCanRezTmp()) { failedAdd = true; qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID() - << "] attempted to add an entity ID:" << entityItemID; - // FIXME after Cert ID property integrated - } else if (/*!properties.getCertificateID().isNull() && */!senderNode->getCanRezCertified() && !senderNode->getCanRezTmpCertified()) { - qCDebug(entities) << "User without 'certified rez rights' [" << senderNode->getUUID() - << "] attempted to add a certified entity with ID:" << entityItemID; + << "] attempted to add an entity ID:" << entityItemID; + } else { // this is a new entity... assign a new entityID properties.setCreated(properties.getLastEdited()); From 4fd447949776adceb83f7dd920f361031652c6c7 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 28 Sep 2017 16:20:09 -0700 Subject: [PATCH 600/722] Oculus: Bug fix for head offset on large/small scaled avatars. (cherry picked from commit d8e2cbf871fd1b10a7507b979e832f65e13f8c44) --- plugins/oculus/src/OculusControllerManager.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 6f7be26554..d0c717bd20 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -334,10 +334,8 @@ void OculusControllerManager::TouchDevice::handleHeadPose(float deltaTime, glm::mat4 defaultHeadOffset = glm::inverse(inputCalibrationData.defaultCenterEyeMat) * inputCalibrationData.defaultHeadMat; - controller::Pose hmdHeadPose = pose.transform(sensorToAvatar); - pose.valid = true; - _poseStateMap[controller::HEAD] = hmdHeadPose.postTransform(defaultHeadOffset); + _poseStateMap[controller::HEAD] = pose.postTransform(defaultHeadOffset).transform(sensorToAvatar); } void OculusControllerManager::TouchDevice::handleRotationForUntrackedHand(const controller::InputCalibrationData& inputCalibrationData, From 63396a2fc3cb43c6a7766a8a4ed025f40ce50285 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 29 Sep 2017 12:14:20 -0700 Subject: [PATCH 601/722] WalletScriptingInterface; wallet status refactor --- .../qml/hifi/commerce/checkout/Checkout.qml | 63 ++++++--------- .../common/EmulatedMarketplaceHeader.qml | 19 ++--- .../qml/hifi/commerce/purchases/Purchases.qml | 78 +++++++------------ .../qml/hifi/commerce/wallet/Wallet.qml | 57 ++++++-------- interface/src/Application.cpp | 4 + interface/src/commerce/QmlCommerce.cpp | 31 +++++++- interface/src/commerce/QmlCommerce.h | 11 +++ interface/src/commerce/Wallet.cpp | 13 ++-- interface/src/commerce/Wallet.h | 4 +- .../scripting/WalletScriptingInterface.cpp | 15 ++++ .../src/scripting/WalletScriptingInterface.h | 37 +++++++++ scripts/system/html/js/marketplacesInject.js | 48 +++++++++++- scripts/system/marketplaces/marketplaces.js | 14 +--- 13 files changed, 242 insertions(+), 152 deletions(-) create mode 100644 interface/src/scripting/WalletScriptingInterface.cpp create mode 100644 interface/src/scripting/WalletScriptingInterface.h diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 7859333b21..c5a76d2c54 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -29,7 +29,6 @@ Rectangle { property string activeView: "initialize"; property bool purchasesReceived: false; property bool balanceReceived: false; - property bool securityImageResultReceived: false; property string itemId; property string itemPreviewImageUrl; property string itemHref; @@ -45,47 +44,30 @@ Rectangle { Hifi.QmlCommerce { id: commerce; + onWalletStatusResult: { + if (walletStatus === 0) { + if (root.activeView !== "needsLogIn") { + root.activeView = "needsLogIn"; + } + } else if (walletStatus === 1) { + if (root.activeView !== "notSetUp") { + root.activeView = "notSetUp"; + notSetUpTimer.start(); + } + } else if (walletStatus === 2) { + if (root.activeView !== "passphraseModal") { + root.activeView = "passphraseModal"; + } + } else if (walletStatus === 3) { + authSuccessStep(); + } else { + console.log("ERROR in Checkout.qml: Unknown wallet status: " + walletStatus); + } + } + onLoginStatusResult: { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; - } else if (isLoggedIn) { - root.activeView = "initialize"; - commerce.account(); - } - } - - onAccountResult: { - if (result.status === "success") { - commerce.getKeyFilePathIfExists(); - } else { - // unsure how to handle a failure here. We definitely cannot proceed. - } - } - - onKeyFilePathIfExistsResult: { - if (path === "" && root.activeView !== "notSetUp") { - root.activeView = "notSetUp"; - notSetUpTimer.start(); - } else if (path !== "" && root.activeView === "initialize") { - commerce.getSecurityImage(); - } - } - - onSecurityImageResult: { - securityImageResultReceived = true; - if (!exists && root.activeView !== "notSetUp") { // "If security image is not set up" - root.activeView = "notSetUp"; - notSetUpTimer.start(); - } else if (exists && root.activeView === "initialize") { - commerce.getWalletAuthenticatedStatus(); - } - } - - onWalletAuthenticatedStatusResult: { - if (!isAuthenticated && root.activeView !== "passphraseModal") { - root.activeView = "passphraseModal"; - } else if (isAuthenticated) { - authSuccessStep(); } } @@ -197,10 +179,9 @@ Rectangle { color: hifi.colors.white; Component.onCompleted: { - securityImageResultReceived = false; purchasesReceived = false; balanceReceived = false; - commerce.getLoginStatus(); + commerce.getWalletStatus(); } } diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index 9e52b2c50a..52e8c8fa42 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -34,17 +34,19 @@ Item { Hifi.QmlCommerce { id: commerce; - onLoginStatusResult: { - if (!isLoggedIn) { + onWalletStatusResult: { + if (walletStatus === 0) { sendToParent({method: "needsLogIn"}); + } else if (walletStatus === 3) { + commerce.getSecurityImage(); + } else { + console.log("ERROR in EmulatedMarketplaceHeader.qml: Unknown wallet status: " + walletStatus); } } - onAccountResult: { - if (result.status === "success") { - commerce.getKeyFilePathIfExists(); - } else { - // unsure how to handle a failure here. We definitely cannot proceed. + onLoginStatusResult: { + if (!isLoggedIn) { + sendToParent({method: "needsLogIn"}); } } @@ -57,8 +59,7 @@ Item { } Component.onCompleted: { - commerce.getLoginStatus(); - commerce.getSecurityImage(); + commerce.getWalletStatus(); } Connections { diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index abb5f19732..4149d5246e 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -41,47 +41,35 @@ Rectangle { Hifi.QmlCommerce { id: commerce; + onWalletStatusResult: { + if (walletStatus === 0) { + if (root.activeView !== "needsLogIn") { + root.activeView = "needsLogIn"; + } + } else if (walletStatus === 1) { + if (root.activeView !== "notSetUp") { + root.activeView = "notSetUp"; + notSetUpTimer.start(); + } + } else if (walletStatus === 2) { + if (root.activeView !== "passphraseModal") { + root.activeView = "passphraseModal"; + } + } else if (walletStatus === 3) { + if ((Settings.getValue("isFirstUseOfPurchases", true) || root.isDebuggingFirstUseTutorial) && root.activeView !== "firstUseTutorial") { + root.activeView = "firstUseTutorial"; + } else if (!Settings.getValue("isFirstUseOfPurchases", true) && root.activeView === "initialize") { + root.activeView = "purchasesMain"; + commerce.inventory(); + } + } else { + console.log("ERROR in Purchases.qml: Unknown wallet status: " + walletStatus); + } + } + onLoginStatusResult: { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; - } else if (isLoggedIn) { - root.activeView = "initialize"; - commerce.account(); - } - } - - onAccountResult: { - if (result.status === "success") { - commerce.getKeyFilePathIfExists(); - } else { - // unsure how to handle a failure here. We definitely cannot proceed. - } - } - - onKeyFilePathIfExistsResult: { - if (path === "" && root.activeView !== "notSetUp") { - root.activeView = "notSetUp"; - notSetUpTimer.start(); - } else if (path !== "" && root.activeView === "initialize") { - commerce.getSecurityImage(); - } - } - - onSecurityImageResult: { - securityImageResultReceived = true; - if (!exists && root.activeView !== "notSetUp") { // "If security image is not set up" - root.activeView = "notSetUp"; - notSetUpTimer.start(); - } else if (exists && root.activeView === "initialize") { - commerce.getWalletAuthenticatedStatus(); - } - } - - onWalletAuthenticatedStatusResult: { - if (!isAuthenticated && root.activeView !== "passphraseModal") { - root.activeView = "passphraseModal"; - } else if (isAuthenticated) { - sendToScript({method: 'purchases_getIsFirstUse'}); } } @@ -205,7 +193,7 @@ Rectangle { Component.onCompleted: { securityImageResultReceived = false; purchasesReceived = false; - commerce.getLoginStatus(); + commerce.getWalletStatus(); } } @@ -241,7 +229,7 @@ Rectangle { onSendSignalToParent: { if (msg.method === "authSuccess") { root.activeView = "initialize"; - sendToScript({method: 'purchases_getIsFirstUse'}); + commerce.getWalletStatus(); } else { sendToScript(msg); } @@ -260,7 +248,7 @@ Rectangle { switch (message.method) { case 'tutorial_skipClicked': case 'tutorial_finished': - sendToScript({method: 'purchases_setIsFirstUse'}); + Settings.setValue("isFirstUseOfPurchases", false); root.activeView = "purchasesMain"; commerce.inventory(); break; @@ -679,14 +667,6 @@ Rectangle { titleBarContainer.referrerURL = message.referrerURL; filterBar.text = message.filterText ? message.filterText : ""; break; - case 'purchases_getIsFirstUseResult': - if ((message.isFirstUseOfPurchases || root.isDebuggingFirstUseTutorial) && root.activeView !== "firstUseTutorial") { - root.activeView = "firstUseTutorial"; - } else if (!message.isFirstUseOfPurchases && root.activeView === "initialize") { - root.activeView = "purchasesMain"; - commerce.inventory(); - } - break; case 'inspectionCertificate_setMarketplaceId': case 'inspectionCertificate_setItemInfo': inspectionCertificate.fromScript(message); diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 4cc1f2f8ec..251b747f83 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -38,48 +38,39 @@ Rectangle { Hifi.QmlCommerce { id: commerce; + onWalletStatusResult: { + if (walletStatus === 0) { + if (root.activeView !== "needsLogIn") { + root.activeView = "needsLogIn"; + } + } else if (walletStatus === 1) { + if (root.activeView !== "walletSetup") { + root.activeView = "walletSetup"; + } + } else if (walletStatus === 2) { + if (root.activeView !== "passphraseModal") { + root.activeView = "passphraseModal"; + } + } else if (walletStatus === 3) { + root.activeView = "walletHome"; + commerce.getSecurityImage(); + } else { + console.log("ERROR in Wallet.qml: Unknown wallet status: " + walletStatus); + } + } + onLoginStatusResult: { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; - } else if (isLoggedIn) { - root.activeView = "initialize"; - commerce.account(); - } - } - - onAccountResult: { - if (result.status === "success") { - commerce.getKeyFilePathIfExists(); - } else { - // unsure how to handle a failure here. We definitely cannot proceed. - } - } - - onKeyFilePathIfExistsResult: { - if (path === "" && root.activeView !== "walletSetup") { - root.activeView = "walletSetup"; - } else if (path !== "" && root.activeView === "initialize") { - commerce.getSecurityImage(); } } onSecurityImageResult: { - if (!exists && root.activeView !== "walletSetup") { // "If security image is not set up" - root.activeView = "walletSetup"; - } else if (exists && root.activeView === "initialize") { - commerce.getWalletAuthenticatedStatus(); + if (exists) { titleBarSecurityImage.source = ""; titleBarSecurityImage.source = "image://security/securityImage"; } } - - onWalletAuthenticatedStatusResult: { - if (!isAuthenticated && passphraseModal && root.activeView !== "passphraseModal") { - root.activeView = "passphraseModal"; - } else if (isAuthenticated) { - root.activeView = "walletHome"; - } - } } SecurityImageModel { @@ -179,7 +170,7 @@ Rectangle { if (msg.method === 'walletSetup_finished') { if (msg.referrer === '') { root.activeView = "initialize"; - commerce.getLoginStatus(); + commerce.getWalletStatus(); } else if (msg.referrer === 'purchases') { sendToScript({method: 'goToPurchases'}); } @@ -254,7 +245,7 @@ Rectangle { color: hifi.colors.baseGray; Component.onCompleted: { - commerce.getLoginStatus(); + commerce.getWalletStatus(); } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 85172fc73f..30d4d818b5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -167,6 +167,7 @@ #include "scripting/ControllerScriptingInterface.h" #include "scripting/RatesScriptingInterface.h" #include "scripting/SelectionScriptingInterface.h" +#include "scripting/WalletScriptingInterface.h" #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" #endif @@ -683,6 +684,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); @@ -2328,6 +2330,7 @@ void Application::initializeUi() { surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); surfaceContext->setContextProperty("Selection", DependencyManager::get().data()); surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); + surfaceContext->setContextProperty("Wallet", DependencyManager::get().data()); if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); @@ -6080,6 +6083,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance()); scriptEngine->registerGlobalObject("Selection", DependencyManager::get().data()); scriptEngine->registerGlobalObject("ContextOverlay", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Wallet", DependencyManager::get().data()); qScriptRegisterMetaType(scriptEngine.data(), OverlayIDtoScriptValue, OverlayIDfromScriptValue); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 93de2ef566..255d6ef516 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -15,6 +15,7 @@ #include "Ledger.h" #include "Wallet.h" #include +#include "scripting/WalletScriptingInterface.h" HIFI_QML_DEF(QmlCommerce) @@ -30,13 +31,41 @@ QmlCommerce::QmlCommerce(QQuickItem* parent) : OffscreenQmlDialog(parent) { connect(ledger.data(), &Ledger::accountResult, this, &QmlCommerce::accountResult); } +void QmlCommerce::getWalletStatus() { + auto wallet = DependencyManager::get(); + auto ledger = DependencyManager::get(); + auto walletScriptingInterface = DependencyManager::get(); + uint status; + + if (DependencyManager::get()->isLoggedIn()) { + // This will set account info for the wallet, allowing us to decrypt and display the security image. + ledger->account(); + } else { + status = (uint)WalletStatus::WALLET_STATUS_NOT_LOGGED_IN; + emit walletStatusResult(status); + walletScriptingInterface->setWalletStatus(status); + return; + } + + if (wallet->getKeyFilePath() == "" || !wallet->getSecurityImage()) { + status = (uint)WalletStatus::WALLET_STATUS_NOT_SET_UP; + } else if (!wallet->walletIsAuthenticatedWithPassphrase()) { + status = (uint)WalletStatus::WALLET_STATUS_NOT_AUTHENTICATED; + } else { + status = (uint)WalletStatus::WALLET_STATUS_READY; + } + + walletScriptingInterface->setWalletStatus(status); + emit walletStatusResult(status); +} + void QmlCommerce::getLoginStatus() { emit loginStatusResult(DependencyManager::get()->isLoggedIn()); } void QmlCommerce::getKeyFilePathIfExists() { auto wallet = DependencyManager::get(); - wallet->sendKeyFilePathIfExists(); + emit keyFilePathIfExistsResult(wallet->getKeyFilePath()); } void QmlCommerce::getWalletAuthenticatedStatus() { diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 42f44a3a85..9323791a82 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -27,7 +27,16 @@ class QmlCommerce : public OffscreenQmlDialog { public: QmlCommerce(QQuickItem* parent = nullptr); + enum WalletStatus { + WALLET_STATUS_NOT_LOGGED_IN = 0, + WALLET_STATUS_NOT_SET_UP, + WALLET_STATUS_NOT_AUTHENTICATED, + WALLET_STATUS_READY + }; + signals: + void walletStatusResult(uint walletStatus); + void loginStatusResult(bool isLoggedIn); void keyFilePathIfExistsResult(const QString& path); void securityImageResult(bool exists); @@ -42,6 +51,8 @@ signals: void accountResult(QJsonObject result); protected: + Q_INVOKABLE void getWalletStatus(); + Q_INVOKABLE void getLoginStatus(); Q_INVOKABLE void getKeyFilePathIfExists(); Q_INVOKABLE void getSecurityImage(); diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index e1e2849a04..5af3011f17 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -468,7 +468,7 @@ bool Wallet::generateKeyPair() { // TODO: redo this soon -- need error checking and so on writeSecurityImage(_securityImage, keyFilePath()); - sendKeyFilePathIfExists(); + emit keyFilePathIfExistsResult(getKeyFilePath()); QString oldKey = _publicKeys.count() == 0 ? "" : _publicKeys.last(); QString key = keyPair.first->toBase64(); _publicKeys.push_back(key); @@ -559,14 +559,14 @@ void Wallet::chooseSecurityImage(const QString& filename) { emit securityImageResult(success); } -void Wallet::getSecurityImage() { +bool Wallet::getSecurityImage() { unsigned char* data; int dataLen; // if already decrypted, don't do it again if (_securityImage) { emit securityImageResult(true); - return; + return true; } bool success = false; @@ -585,14 +585,15 @@ void Wallet::getSecurityImage() { success = true; } emit securityImageResult(success); + return success; } -void Wallet::sendKeyFilePathIfExists() { +QString Wallet::getKeyFilePath() { QString filePath(keyFilePath()); QFileInfo fileInfo(filePath); if (fileInfo.exists()) { - emit keyFilePathIfExistsResult(filePath); + return filePath; } else { - emit keyFilePathIfExistsResult(""); + return ""; } } diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index 59812d5222..807080e6ea 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -32,8 +32,8 @@ public: QStringList listPublicKeys(); QString signWithKey(const QByteArray& text, const QString& key); void chooseSecurityImage(const QString& imageFile); - void getSecurityImage(); - void sendKeyFilePathIfExists(); + bool getSecurityImage(); + QString getKeyFilePath(); void setSalt(const QByteArray& salt) { _salt = salt; } QByteArray getSalt() { return _salt; } diff --git a/interface/src/scripting/WalletScriptingInterface.cpp b/interface/src/scripting/WalletScriptingInterface.cpp new file mode 100644 index 0000000000..94f8030fa2 --- /dev/null +++ b/interface/src/scripting/WalletScriptingInterface.cpp @@ -0,0 +1,15 @@ +// +// WalletScriptingInterface.cpp +// interface/src/scripting +// +// Created by Zach Fox on 2017-09-29. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "WalletScriptingInterface.h" + +WalletScriptingInterface::WalletScriptingInterface() { +} diff --git a/interface/src/scripting/WalletScriptingInterface.h b/interface/src/scripting/WalletScriptingInterface.h new file mode 100644 index 0000000000..111a6eea3d --- /dev/null +++ b/interface/src/scripting/WalletScriptingInterface.h @@ -0,0 +1,37 @@ + +// WalletScriptingInterface.h +// interface/src/scripting +// +// Created by Zach Fox on 2017-09-29. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_WalletScriptingInterface_h +#define hifi_WalletScriptingInterface_h + +#include +#include + + +class WalletScriptingInterface : public QObject, public Dependency { + Q_OBJECT + + Q_PROPERTY(uint walletStatus READ getWalletStatus WRITE setWalletStatus NOTIFY walletStatusChanged) + +public: + WalletScriptingInterface(); + + Q_INVOKABLE uint getWalletStatus() { return _walletStatus; } + void setWalletStatus(const uint& status) { _walletStatus = status; } + +signals: + void walletStatusChanged(); + +private: + uint _walletStatus; +}; + +#endif // hifi_WalletScriptingInterface_h diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index bdee322381..082e3ab4c0 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -28,6 +28,7 @@ var confirmAllPurchases = false; // Set this to "true" to cause Checkout.qml to popup for all items, even if free var userIsLoggedIn = false; + var walletNeedsSetup = false; function injectCommonCode(isDirectoryPage) { @@ -91,6 +92,48 @@ }); } + emitWalletSetupEvent = function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "WALLET_SETUP" + })); + } + + function maybeAddSetupWalletButton() { + if (userIsLoggedIn && walletNeedsSetup) { + var resultsElement = document.getElementById('results'); + var setupWalletElement = document.createElement('div'); + setupWalletElement.classList.add("row"); + setupWalletElement.id = "setupWalletDiv"; + setupWalletElement.style = "height:60px;margin:20px 10px 10px 10px;padding:12px 5px;" + + "background-color:#D6F4D8;border-color:#aee9b2;border-width:2px;border-style:solid;border-radius:5px;"; + + var span = document.createElement('span'); + span.style = "margin:10px 5px;color:#1b6420;font-size:15px;"; + span.innerHTML = "Setup your Wallet to get money and shop in Marketplace."; + + var xButton = document.createElement('a'); + xButton.id = "xButton"; + xButton.setAttribute('href', "#"); + xButton.style = "width:50px;height:100%;margin:0;color:#ccc;font-size:20px;"; + xButton.innerHTML = "X"; + xButton.onclick = function () { + setupWalletElement.remove(); + dummyRow.remove(); + }; + + setupWalletElement.appendChild(span); + setupWalletElement.appendChild(xButton); + + resultsElement.insertBefore(setupWalletElement, resultsElement.firstChild); + + // Dummy row for padding + var dummyRow = document.createElement('div'); + dummyRow.classList.add("row"); + dummyRow.style = "height:15px;"; + resultsElement.insertBefore(dummyRow, resultsElement.firstChild); + } + } + function maybeAddLogInButton() { if (!userIsLoggedIn) { var resultsElement = document.getElementById('results'); @@ -240,6 +283,7 @@ $('body').addClass("code-injected"); maybeAddLogInButton(); + maybeAddSetupWalletButton(); var target = document.getElementById('templated-items'); // MutationObserver is necessary because the DOM is populated after the page is loaded. @@ -267,6 +311,7 @@ $('body').addClass("code-injected"); maybeAddLogInButton(); + maybeAddSetupWalletButton(); var purchaseButton = $('#side-info').find('.btn').first(); @@ -555,7 +600,8 @@ if (parsedJsonMessage.type === "marketplaces") { if (parsedJsonMessage.action === "commerceSetting") { confirmAllPurchases = !!parsedJsonMessage.data.commerceMode; - userIsLoggedIn = !!parsedJsonMessage.data.userIsLoggedIn + userIsLoggedIn = !!parsedJsonMessage.data.userIsLoggedIn; + walletNeedsSetup = !!parsedJsonMessage.data.walletNeedsSetup; injectCode(); } } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 7fc5e22554..859ddb76b3 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -202,7 +202,8 @@ action: "commerceSetting", data: { commerceMode: Settings.getValue("commerce", false), - userIsLoggedIn: Account.loggedIn + userIsLoggedIn: Account.loggedIn, + walletNeedsSetup: Wallet.walletStatus !== 3 } })); } else if (parsedJsonMessage.type === "PURCHASES") { @@ -211,6 +212,8 @@ tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); } else if (parsedJsonMessage.type === "LOGIN") { openLoginWindow(); + } else if (parsedJsonMessage.type === "WALLET_SETUP") { + tablet.pushOntoStack(MARKETPLACE_WALLET_QML_PATH); } } } @@ -324,15 +327,6 @@ case 'maybeEnableHmdPreview': Menu.setIsOptionChecked("Disable Preview", isHmdPreviewDisabled); break; - case 'purchases_getIsFirstUse': - tablet.sendToQml({ - method: 'purchases_getIsFirstUseResult', - isFirstUseOfPurchases: Settings.getValue("isFirstUseOfPurchases", true) - }); - break; - case 'purchases_setIsFirstUse': - Settings.setValue("isFirstUseOfPurchases", false); - break; case 'purchases_openGoTo': tablet.loadQMLSource("TabletAddressDialog.qml"); break; From b8ea6c22fa55cdc840a0f803433ff745edc873a0 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 29 Sep 2017 11:14:29 -0700 Subject: [PATCH 602/722] no tpose when switching avatars (cherry picked from commit fcfac9efc0b4787872eda616165cc70f43be093c) --- libraries/animation/src/Rig.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 86a1e629b4..712c728dcb 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -249,6 +249,7 @@ void Rig::reset(const FBXGeometry& geometry) { _rightShoulderJointIndex = _rightElbowJointIndex >= 0 ? geometry.joints.at(_rightElbowJointIndex).parentIndex : -1; if (!_animGraphURL.isEmpty()) { + _animNode.reset(); initAnimGraph(_animGraphURL); } } @@ -1619,7 +1620,7 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo } void Rig::initAnimGraph(const QUrl& url) { - if (_animGraphURL != url) { + if (_animGraphURL != url || !_animNode) { _animGraphURL = url; _animNode.reset(); From 9273e0249796335dbbb9a7aa5ba0c2908da8b582 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 28 Sep 2017 18:03:22 -0700 Subject: [PATCH 603/722] Bug fix for offset of animated parts of oculus touch controller display Ensure position/rotations are updated with a consistent scale and are animated correctly as the controller values change. --- .../system/controllers/controllerDisplay.js | 153 +++++++++--------- 1 file changed, 74 insertions(+), 79 deletions(-) diff --git a/scripts/system/controllers/controllerDisplay.js b/scripts/system/controllers/controllerDisplay.js index af8cfa74f4..3c2794cf96 100644 --- a/scripts/system/controllers/controllerDisplay.js +++ b/scripts/system/controllers/controllerDisplay.js @@ -49,6 +49,7 @@ createControllerDisplay = function(config) { partOverlays: {}, parts: {}, mappingName: "mapping-display-" + Math.random(), + partValues: {}, setVisible: function(visible) { for (var i = 0; i < this.overlays.length; ++i) { @@ -109,12 +110,53 @@ createControllerDisplay = function(config) { for (var partName in controller.parts) { overlayID = this.overlays[i++]; var part = controller.parts[partName]; - var partPosition = Vec3.multiply(sensorScaleFactor, Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, part.naturalPosition))); - var partDimensions = Vec3.multiply(sensorScaleFactor, part.naturalDimensions); - Overlays.editOverlay(overlayID, { - dimensions: partDimensions, - localPosition: partPosition - }); + localPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, part.naturalPosition)); + var localRotation; + var value = this.partValues[partName]; + var offset, rotation; + if (value !== undefined) { + if (part.type === "linear") { + var axis = Vec3.multiplyQbyV(controller.rotation, part.axis); + offset = Vec3.multiply(part.maxTranslation * value, axis); + localPosition = Vec3.sum(localPosition, offset); + localRotation = undefined; + } else if (part.type === "joystick") { + rotation = Quat.fromPitchYawRollDegrees(value.y * part.xHalfAngle, 0, value.x * part.yHalfAngle); + if (part.originOffset) { + offset = Vec3.multiplyQbyV(rotation, part.originOffset); + offset = Vec3.subtract(part.originOffset, offset); + } else { + offset = { x: 0, y: 0, z: 0 }; + } + localPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, Vec3.sum(offset, part.naturalPosition))); + localRotation = Quat.multiply(controller.rotation, rotation); + } else if (part.type === "rotational") { + value = clamp(value, part.minValue, part.maxValue); + var pct = (value - part.minValue) / part.maxValue; + var angle = pct * part.maxAngle; + rotation = Quat.angleAxis(angle, part.axis); + if (part.origin) { + offset = Vec3.multiplyQbyV(rotation, part.origin); + offset = Vec3.subtract(offset, part.origin); + } else { + offset = { x: 0, y: 0, z: 0 }; + } + localPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, Vec3.sum(offset, part.naturalPosition))); + localRotation = Quat.multiply(controller.rotation, rotation); + } + } + if (localRotation !== undefined) { + Overlays.editOverlay(overlayID, { + dimensions: Vec3.multiply(sensorScaleFactor, part.naturalDimensions), + localPosition: Vec3.multiply(sensorScaleFactor, localPosition), + localRotation: localRotation + }); + } else { + Overlays.editOverlay(overlayID, { + dimensions: Vec3.multiply(sensorScaleFactor, part.naturalDimensions), + localPosition: Vec3.multiply(sensorScaleFactor, localPosition) + }); + } } } } @@ -172,29 +214,13 @@ createControllerDisplay = function(config) { if (part.type === "rotational") { var input = resolveHardware(part.input); print("Mapping to: ", part.input, input); - mapping.from([input]).peek().to(function(controller, overlayID, part) { + mapping.from([input]).peek().to(function(partName) { return function(value) { - value = clamp(value, part.minValue, part.maxValue); - - var pct = (value - part.minValue) / part.maxValue; - var angle = pct * part.maxAngle; - var rotation = Quat.angleAxis(angle, part.axis); - - var offset = { x: 0, y: 0, z: 0 }; - if (part.origin) { - offset = Vec3.multiplyQbyV(rotation, part.origin); - offset = Vec3.subtract(offset, part.origin); - } - - var partPosition = Vec3.sum(controller.position, - Vec3.multiplyQbyV(controller.rotation, Vec3.sum(offset, part.naturalPosition))); - - Overlays.editOverlay(overlayID, { - localPosition: partPosition, - localRotation: Quat.multiply(controller.rotation, rotation) - }); + // insert the most recent controller value into controllerDisplay.partValues. + controllerDisplay.partValues[partName] = value; + controllerDisplay.resize(MyAvatar.sensorToWorldScale); }; - }(controller, overlayID, part)); + }(partName)); } else if (part.type === "touchpad") { var visibleInput = resolveHardware(part.visibleInput); var xInput = resolveHardware(part.xInput); @@ -210,69 +236,38 @@ createControllerDisplay = function(config) { mapping.from([yInput]).peek().invert().to(function(value) { }); } else if (part.type === "joystick") { - (function(controller, overlayID, part) { + (function(part, partName) { var xInput = resolveHardware(part.xInput); var yInput = resolveHardware(part.yInput); - - var xvalue = 0; - var yvalue = 0; - - function calculatePositionAndRotation(xValue, yValue) { - var rotation = Quat.fromPitchYawRollDegrees(yValue * part.xHalfAngle, 0, xValue * part.yHalfAngle); - - var offset = { x: 0, y: 0, z: 0 }; - if (part.originOffset) { - offset = Vec3.multiplyQbyV(rotation, part.originOffset); - offset = Vec3.subtract(part.originOffset, offset); - } - - var partPosition = Vec3.sum(controller.position, - Vec3.multiplyQbyV(controller.rotation, Vec3.sum(offset, part.naturalPosition))); - - var partRotation = Quat.multiply(controller.rotation, rotation); - - return { - position: partPosition, - rotation: partRotation - }; - } - mapping.from([xInput]).peek().to(function(value) { - xvalue = value; - var posRot = calculatePositionAndRotation(xvalue, yvalue); - Overlays.editOverlay(overlayID, { - localPosition: posRot.position, - localRotation: posRot.rotation - }); + // insert the most recent controller value into controllerDisplay.partValues. + if (controllerDisplay.partValues[partName]) { + controllerDisplay.partValues[partName].x = value; + } else { + controllerDisplay.partValues[partName] = {x: value, y: 0}; + } + controllerDisplay.resize(MyAvatar.sensorToWorldScale); }); - mapping.from([yInput]).peek().to(function(value) { - yvalue = value; - var posRot = calculatePositionAndRotation(xvalue, yvalue); - Overlays.editOverlay(overlayID, { - localPosition: posRot.position, - localRotation: posRot.rotation - }); + // insert the most recent controller value into controllerDisplay.partValues. + if (controllerDisplay.partValues[partName]) { + controllerDisplay.partValues[partName].y = value; + } else { + controllerDisplay.partValues[partName] = {x: 0, y: value}; + } + controllerDisplay.resize(MyAvatar.sensorToWorldScale); }); - })(controller, overlayID, part); + })(part, partName); } else if (part.type === "linear") { - (function(controller, overlayID, part) { + (function(part, partName) { var input = resolveHardware(part.input); - mapping.from([input]).peek().to(function(value) { - var axis = Vec3.multiplyQbyV(controller.rotation, part.axis); - var offset = Vec3.multiply(part.maxTranslation * value, axis); - - var partPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, part.naturalPosition)); - var position = Vec3.sum(partPosition, offset); - - Overlays.editOverlay(overlayID, { - localPosition: position - }); + // insert the most recent controller value into controllerDisplay.partValues. + controllerDisplay.partValues[partName] = value; + controllerDisplay.resize(MyAvatar.sensorToWorldScale); }); - - })(controller, overlayID, part); + })(part, partName); } else if (part.type === "static") { // do nothing From bbbce964075bdf3bdf6c42f53d68941e2e07935a Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 29 Sep 2017 14:30:02 -0700 Subject: [PATCH 604/722] Fix gray securityimage --- .../resources/qml/hifi/commerce/common/CommerceLightbox.qml | 1 + .../qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml | 1 + .../resources/qml/hifi/commerce/wallet/PassphraseModal.qml | 1 + .../qml/hifi/commerce/wallet/SecurityImageSelection.qml | 1 + interface/resources/qml/hifi/commerce/wallet/Wallet.qml | 1 + interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml | 5 +++-- 6 files changed, 8 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml index 0b236d4566..145b78bdc4 100644 --- a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml +++ b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml @@ -82,6 +82,7 @@ Rectangle { height: 140; fillMode: Image.PreserveAspectFit; mipmap: true; + cache: false; } RalewayRegular { diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index 52e8c8fa42..125c2c756d 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -212,6 +212,7 @@ Item { anchors.bottomMargin: 16; width: height; mipmap: true; + cache: false; MouseArea { enabled: securityImage.visible; diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml index 8ab0c3af60..5bd88ba790 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml @@ -129,6 +129,7 @@ Item { width: height; fillMode: Image.PreserveAspectFit; mipmap: true; + cache: false; MouseArea { enabled: titleBarSecurityImage.visible; diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml index 706684ed39..e12332cd0c 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml @@ -65,6 +65,7 @@ Item { anchors.verticalCenter: parent.verticalCenter; fillMode: Image.PreserveAspectFit; mipmap: true; + cache: false; } } MouseArea { diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 251b747f83..480e7c921c 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -140,6 +140,7 @@ Rectangle { anchors.bottomMargin: 6; width: height; mipmap: true; + cache: false; MouseArea { enabled: titleBarSecurityImage.visible; diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml index b90dc925a6..bc3ebb258e 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml @@ -43,7 +43,7 @@ Item { if (!exists && root.lastPage === "step_2") { // ERROR! Invalid security image. root.activeView = "step_2"; - } else { + } else if (exists) { titleBarSecurityImage.source = ""; titleBarSecurityImage.source = "image://security/securityImage"; } @@ -116,7 +116,7 @@ Item { Image { id: titleBarSecurityImage; source: ""; - visible: !securityImageTip.visible && titleBarSecurityImage.source !== ""; + visible: !securityImageTip.visible && titleBarSecurityImage.source !== "" && root.activeView !== "step_1" && root.activeView !== "step_2"; anchors.right: parent.right; anchors.rightMargin: 6; anchors.top: parent.top; @@ -125,6 +125,7 @@ Item { anchors.bottomMargin: 6; width: height; mipmap: true; + cache: false; MouseArea { enabled: titleBarSecurityImage.visible; From 46682943894fc83791775715a4fbc83d477a4121 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 29 Sep 2017 15:25:24 -0700 Subject: [PATCH 605/722] Fix login flow regression --- interface/resources/qml/hifi/commerce/checkout/Checkout.qml | 2 ++ .../qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml | 2 ++ interface/resources/qml/hifi/commerce/purchases/Purchases.qml | 2 ++ interface/resources/qml/hifi/commerce/wallet/Wallet.qml | 2 ++ 4 files changed, 8 insertions(+) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index c5a76d2c54..f337e55dc9 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -68,6 +68,8 @@ Rectangle { onLoginStatusResult: { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; + } else { + commerce.getWalletStatus(); } } diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index 125c2c756d..420b51ba15 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -47,6 +47,8 @@ Item { onLoginStatusResult: { if (!isLoggedIn) { sendToParent({method: "needsLogIn"}); + } else { + commerce.getWalletStatus(); } } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 4149d5246e..0bb1515b69 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -70,6 +70,8 @@ Rectangle { onLoginStatusResult: { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; + } else { + commerce.getWalletStatus(); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 480e7c921c..9056d5bed3 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -62,6 +62,8 @@ Rectangle { onLoginStatusResult: { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; + } else if (isLoggedIn) { + commerce.getWalletStatus(); } } From e6ea76eefe21841f4b737bc878d523363cf5d29c Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 29 Sep 2017 15:44:44 -0700 Subject: [PATCH 606/722] Tiny language fix - Thanks Cain! --- interface/resources/qml/hifi/commerce/wallet/WalletHome.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index a277d643d6..b94616bd7a 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -215,7 +215,7 @@ Item { AnonymousProRegular { id: pendingCountText; anchors.fill: parent; - text: root.pendingCount + ' Transactions Pending'; + text: root.pendingCount + ' Transaction' + (root.pendingCount > 1 ? 's' : '') + ' Pending'; size: 18; color: hifi.colors.blueAccent; verticalAlignment: Text.AlignVCenter; From 255cede808606a4eb58a3bd580d25eb48a90f84f Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 29 Sep 2017 16:05:26 -0700 Subject: [PATCH 607/722] Correctly determine if we're on a marketplace screen --- scripts/system/marketplaces/marketplaces.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 859ddb76b3..b035bac3ec 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -106,7 +106,7 @@ var referrerURL; // Used for updating Purchases QML var filterText; // Used for updating Purchases QML function onScreenChanged(type, url) { - onMarketplaceScreen = type === "Web" && url === MARKETPLACE_URL_INITIAL; + onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; onCommerceScreen = type === "QML" && (url === MARKETPLACE_CHECKOUT_QML_PATH || url === MARKETPLACE_PURCHASES_QML_PATH || url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); wireEventBridge(onCommerceScreen); From 58255abe12b4272d7f46afbd79b8cda6d71be4b2 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 29 Sep 2017 16:32:25 -0700 Subject: [PATCH 608/722] animation url and computing certificateID --- libraries/entities/src/EntityItem.cpp | 105 ++++++++---------- libraries/entities/src/EntityItem.h | 8 +- .../entities/src/EntityScriptingInterface.cpp | 28 +++++ .../entities/src/EntityScriptingInterface.h | 5 + 4 files changed, 88 insertions(+), 58 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 5b9d10a759..0c8f6c6882 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -13,6 +13,11 @@ #include #include +#include +#include // see comments for DEBUG_CERT +#include +#include +#include #include @@ -32,7 +37,6 @@ #include "EntitySimulation.h" #include "EntityDynamicFactoryInterface.h" - int EntityItem::_maxActionsDataSize = 800; quint64 EntityItem::_rememberDeletedActionTime = 20 * USECS_PER_SECOND; @@ -1569,11 +1573,6 @@ float EntityItem::getRadius() const { } // Checking Certifiable Properties -#include // fixme -#include -#include -#include -#include // fixme #define ADD_STRING_PROPERTY(n, N) if (!propertySet.get##N().isEmpty()) json[#n] = propertySet.get##N() #define ADD_ENUM_PROPERTY(n, N) json[#n] = propertySet.get##N##AsString() #define ADD_INT_PROPERTY(n, N) if (propertySet.get##N() != 0) json[#n] = (propertySet.get##N() == -1) ? -1.0 : ((double) propertySet.get##N()) @@ -1586,7 +1585,9 @@ QByteArray EntityItem::getStaticCertificateJSON() const { EntityItemProperties propertySet = getProperties(); // Note: neither EntityItem nor EntityitemProperties "properties" are QObject "properties"! // It is important that this be reproducible in the same order each time. Since we also generate these on the server, we do it alphabetically // to help maintainence in two different code bases. - // animation + if (!propertySet.getAnimation().getURL().isEmpty()) { + json["animation.url"] = propertySet.getAnimation().getURL(); + } ADD_STRING_PROPERTY(collisionSoundURL, CollisionSoundURL); ADD_STRING_PROPERTY(compoundShapeURL, CompoundShapeURL); ADD_INT_PROPERTY(editionNumber, EditionNumber); @@ -1606,30 +1607,18 @@ QByteArray EntityItem::getStaticCertificateJSON() const { return QJsonDocument(json).toJson(QJsonDocument::Compact); } QByteArray EntityItem::getStaticCertificateHash() const { - // The base64 encoded, sha224 hash of static certificate json. return QCryptographicHash::hash(getStaticCertificateJSON(), QCryptographicHash::Sha256); } -bool EntityItem::verifyStaticCertificateProperties() const { - // True IIF a non-empty certificateID matches the static certificate json. - // I.e., if we can verify that the certificateID was produced by High Fidelity signing the static certificate hash. - if (_certificateID.isEmpty()) { - return false; - } - // FIXME: really verify(hifi-pub-key, certificateID-as-signature-for-getStaticCertifcateHash) - - //const char text[] = "{\"collisionSoundURL\":\"colSound02\",\"compoundShapeURL\":\"http://mpassets.highfidelity.com/31479af7-94b0-45f2-84ba-478d27e5af90-v1/gnome_phys.obj\",\"entityInstanceNumber\":2,\"itemName\":\"Explosive Garden Nomex\",\"limitedRun\":-1,\"marketplaceID\":\"31479af7-94b0-45f2-84ba-478d27e5af90\",\"modelURL\":\"http://mpassets.highfidelity.com/31479af7-94b0-45f2-84ba-478d27e5af90-v1/gnome_green.fbx\",\"script\":\"http://mpassets.highfidelity.com/31479af7-94b0-45f2-84ba-478d27e5af90-v1/explodingGnomeEntity.js\",\"shapeType\":\"compound\",\"type\":\"Model\"}"; - // auto textLength = sizeof(text); - auto hash = getStaticCertificateHash(); - const char* text = hash.constData(); - auto textLength = hash.length(); - qDebug() << "HRS FIXME text" << getStaticCertificateJSON() << "hash base64" << hash.toBase64(); - //const char signatureBase64[] = "QpRnN7XGeVFnF/a+FVjZDWhdbHM3P5Cu69rL0/X2DMnqQEGwhx/oBs/7guTs6aNuO+ahmbTTc0+Nqdcqv36KGA=="; - //auto signatureBytes = QByteArray::fromBase64(signatureBase64); - //const char* signature = signatureBytes.constData(); - //auto signatureLength = signatureBytes.length(); +#ifdef DEBUG_CERT +QString EntityItem::computeCertificateID() { + // Until the marketplace generates it, compute and answer the certificateID here. + // Does not set it, as that will have to be done from script engine in order to update server, etc. + const auto hash = getStaticCertificateHash(); + const auto text = reinterpret_cast(hash.constData()); + const unsigned int textLength = hash.length(); - const char key[] = "-----BEGIN RSA PRIVATE KEY-----\n\ + const char privateKey[] = "-----BEGIN RSA PRIVATE KEY-----\n\ MIIBOQIBAAJBALCoBiDAZOClO26tC5pd7JikBL61WIgpAqbcNnrV/TcG6LPI7Zbi\n\ MjdUixmTNvYMRZH3Wlqtl2IKG1W68y3stKECAwEAAQJABvOlwhYwIhL+gr12jm2R\n\ yPPzZ9nVEQ6kFxLlZfIT09119fd6OU1X5d4sHWfMfSIEgjwQIDS3ZU1kY3XKo87X\n\ @@ -1638,47 +1627,49 @@ yuyV3yHvr5LkZKBGqhjmOTmDfgtX7ncCIChGbgX3nQuHVOLhD/nTxHssPNozVGl5\n\ KxHof+LmYSYZAiB4U+yEh9SsXdq40W/3fpLMPuNq1PRezJ5jGidGMcvF+wIgUNec\n\ 3Kg2U+CVZr8/bDT/vXRrsKj1zfobYuvbfVH02QY=\n\ -----END RSA PRIVATE KEY-----"; - BIO* vbio = BIO_new(BIO_s_mem()); - int vlen = BIO_write(vbio, key, sizeof(key)); - RSA* vrsa = PEM_read_bio_RSAPrivateKey(vbio, NULL, NULL, NULL); - qDebug() << "HRS FIXME private key bufio" << !!vbio << vlen << !!vrsa << key; + BIO* bio = BIO_new_mem_buf((void*)privateKey, sizeof(privateKey)); + RSA* rsa = PEM_read_bio_RSAPrivateKey(bio, NULL, NULL, NULL); - QByteArray signature(RSA_size(vrsa), 0); - unsigned int signatureLength = 0; - int signOK = RSA_sign(NID_sha256, reinterpret_cast(text), textLength, reinterpret_cast(signature.data()), &signatureLength, vrsa); - QByteArray signature64 = signature.toBase64(); - qDebug() << "HRS FIXME signature" << signature64.length() << signature64 << "ok:" << signOK; + QByteArray signature(RSA_size(rsa), 0); + unsigned int signatureLength = 0; + const int signOK = RSA_sign(NID_sha256, text, textLength, reinterpret_cast(signature.data()), &signatureLength, rsa); + BIO_free(bio); + if (!signOK) { + qCWarning(entities) << "Unable to compute signature for" << getName() << getEntityItemID(); + return ""; + } + return signature.toBase64(); +#endif +} + +bool EntityItem::verifyStaticCertificateProperties() { + // True IIF a non-empty certificateID matches the static certificate json. + // I.e., if we can verify that the certificateID was produced by High Fidelity signing the static certificate hash. + + if (getCertificateID().isEmpty()) { + return false; + } + const auto signatureBytes = QByteArray::fromBase64(getCertificateID().toLatin1()); + const auto signature = reinterpret_cast(signatureBytes.constData()); + const unsigned int signatureLength = signatureBytes.length(); + + const auto hash = getStaticCertificateHash(); + const auto text = reinterpret_cast(hash.constData()); + const unsigned int textLength = hash.length(); - ///* + // After DEBUG_CERT ends, we will get/cache this once from the marketplace when needed, and it likely won't be RSA. const char publicKey[] = "-----BEGIN PUBLIC KEY-----\n\ MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALCoBiDAZOClO26tC5pd7JikBL61WIgp\n\ AqbcNnrV/TcG6LPI7ZbiMjdUixmTNvYMRZH3Wlqtl2IKG1W68y3stKECAwEAAQ==\n\ -----END PUBLIC KEY-----"; - //*/ - // const char publicKey[] = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALCoBiDAZOClO26tC5pd7JikBL61WIgpAqbcNnrV/TcG6LPI7ZbiMjdUixmTNvYMRZH3Wlqtl2IKG1W68y3stKECAwEAAQ=="; - //const unsigned char* publicKeyData = reinterpret_cast(publicKey); - //RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, sizeof(publicKey)); - qDebug() << "HRS FIXME key:" << sizeof(publicKey) << QString(publicKey) << "text:" << textLength << QString(text) << "signature:" << signatureLength << QString(signature); - - - //BIO *bio = BIO_new_mem_buf((void*)publicKey, sizeof(publicKey)); - BIO* bio = BIO_new(BIO_s_mem()); - int len = BIO_write(bio, publicKey, sizeof(publicKey)); + BIO *bio = BIO_new_mem_buf((void*)publicKey, sizeof(publicKey)); EVP_PKEY* evp_key = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); - qDebug() << "HRS FIXME bufio" << !!bio << len << !!evp_key; - //RSA *rsa; - //PEM_read_bio_RSA_PUBKEY(bio, &rsa, 0, NULL); RSA* rsa = EVP_PKEY_get1_RSA(evp_key); - //RSA* rsa = PEM_read_bio_RSAPublicKey(bio, NULL, NULL, NULL); - qDebug() << "HRS FIXME rsa" << !!rsa; - - bool answer = RSA_verify(NID_sha256, reinterpret_cast(text), textLength, reinterpret_cast(signature.constData()), signatureLength, rsa); - qDebug() << "HRS FIXME key:" << sizeof(publicKey) << QString(publicKey) << "text:" << textLength << QString(text) << "signature:" << signatureLength << QString(signature) << "verified:" << answer; - //return _certificateID == getStaticCertificateHash(); + bool answer = RSA_verify(NID_sha256, text, textLength, signature, signatureLength, rsa); + BIO_free(bio); return answer; } - void EntityItem::adjustShapeInfoByRegistration(ShapeInfo& info) const { if (_registrationPoint != ENTITY_ITEM_DEFAULT_REGISTRATION_POINT) { glm::mat4 scale = glm::scale(getDimensions()); diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index b92f6120f0..c2f505fa91 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -36,6 +36,9 @@ #include "SimulationFlags.h" #include "EntityDynamicInterface.h" +// FIXME: The server-side marketplace will soon create the certificateID. At that point, all of the DEBUG_CERT stuff will go away. +#define DEBUG_CERT 1 + class EntitySimulation; class EntityTreeElement; class EntityTreeElementExtraEncodeData; @@ -326,7 +329,10 @@ public: void setCertificateID(const QString& value); QByteArray getStaticCertificateJSON() const; QByteArray getStaticCertificateHash() const; - bool verifyStaticCertificateProperties() const; + bool verifyStaticCertificateProperties(); +#ifdef DEBUG_CERT + QString EntityItem::computeCertificateID(); +#endif // TODO: get rid of users of getRadius()... float getRadius() const; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index f91bc14fe4..6626b8d3f7 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1753,3 +1753,31 @@ glm::mat4 EntityScriptingInterface::getEntityLocalTransform(const QUuid& entityI } return result; } + +bool EntityScriptingInterface::verifyStaticCertificateProperties(const QUuid& entityID) { + bool result; + if (_entityTree) { + _entityTree->withReadLock([&] { + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + if (entity) { + result = entity->verifyStaticCertificateProperties(); + } + }); + } + return result; +} + +#ifdef DEBUG_CERT +QString EntityScriptingInterface::computeCertificateID(const QUuid& entityID) { + QString result; + if (_entityTree) { + _entityTree->withReadLock([&] { + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + if (entity) { + result = entity->computeCertificateID(); + } + }); + } + return result; +} +#endif \ No newline at end of file diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 9b2b6360f3..32a8a83be3 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -374,6 +374,11 @@ public slots: */ Q_INVOKABLE glm::mat4 getEntityLocalTransform(const QUuid& entityID); + Q_INVOKABLE bool verifyStaticCertificateProperties(const QUuid& entityID); +#ifdef DEBUG_CERT + Q_INVOKABLE QString computeCertificateID(const QUuid& entityID); +#endif + signals: void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); From 1e52e7824bef5eef646247e1a5ab455a07e5fc08 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 29 Sep 2017 16:36:31 -0700 Subject: [PATCH 609/722] Fix injection --- scripts/system/marketplaces/marketplaces.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index b035bac3ec..711f94d53e 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -203,7 +203,7 @@ data: { commerceMode: Settings.getValue("commerce", false), userIsLoggedIn: Account.loggedIn, - walletNeedsSetup: Wallet.walletStatus !== 3 + walletNeedsSetup: Wallet.walletStatus === 1 } })); } else if (parsedJsonMessage.type === "PURCHASES") { From 20918641f48b85cdc8283f7b0a131d41f7b83d60 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Fri, 29 Sep 2017 16:48:25 -0700 Subject: [PATCH 610/722] saving work --- interface/resources/qml/desktop/Desktop.qml | 19 ++++++++++++++++++- .../scripting/WindowScriptingInterface.cpp | 5 +++++ .../src/scripting/WindowScriptingInterface.h | 1 + libraries/ui/src/OffscreenUi.cpp | 9 +++++++++ libraries/ui/src/OffscreenUi.h | 1 + .../controllerModules/hudOverlayPointer.js | 4 ++-- 6 files changed, 36 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 579b4e7fd6..6bf832865f 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -223,7 +223,7 @@ FocusScope { //offscreenWindow.activeFocusItemChanged.connect(onWindowFocusChanged); focusHack.start(); } - + function onWindowFocusChanged() { //console.log("Focus item is " + offscreenWindow.activeFocusItem); @@ -298,6 +298,23 @@ FocusScope { pinned = !pinned } + function isPointOnWindow(point) { + for (var i = 0; i < desktop.visibleChildren.length; i++) { + var child = desktop.visibleChildren[i]; + if (child.visible) { + if (child.hasOwnProperty("modality")) { + var mappedPoint = child.mapFromGlobal(point.x, point.y); + console.log(mappedPoint); + if (child.contains(mappedPoint)) { + return true; + console.log(child); + } + } + } + } + return false; + } + function setPinned(newPinned) { pinned = newPinned } diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 4b981207f1..c99e190d12 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -171,6 +171,11 @@ void WindowScriptingInterface::setPreviousBrowseAssetLocation(const QString& loc Setting::Handle(LAST_BROWSE_ASSETS_LOCATION_SETTING).set(location); } +bool WindowScriptingInterface::isPointOnDesktopWindow(QVariant point) { + auto offscreenUi = DependencyManager::get(); + return offscreenUi->isPointOnDesktopWindow(point); +} + /// Makes sure that the reticle is visible, use this in blocking forms that require a reticle and /// might be in same thread as a script that sets the reticle to invisible void WindowScriptingInterface::ensureReticleVisible() const { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 0d58e6162d..61aaec7bea 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -72,6 +72,7 @@ public slots: void shareSnapshot(const QString& path, const QUrl& href = QUrl("")); bool isPhysicsEnabled(); bool setDisplayTexture(const QString& name); + bool isPointOnDesktopWindow(QVariant point); int openMessageBox(QString title, QString text, int buttons, int defaultButton); void updateMessageBox(int id, QString title, QString text, int buttons, int defaultButton); diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 9dfe831081..1cd30132ae 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -136,6 +136,15 @@ void OffscreenUi::toggle(const QUrl& url, const QString& name, std::function invoke method isPointOnWindow <------"; + return result.toBool(); +} + void OffscreenUi::hide(const QString& name) { QQuickItem* item = getRootItem()->findChild(name); if (item) { diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 391d7da6c7..93c55d06f7 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -78,6 +78,7 @@ public: bool eventFilter(QObject* originalDestination, QEvent* event) override; void addMenuInitializer(std::function f); QObject* getFlags(); + Q_INVOKABLE bool isPointOnDesktopWindow(QVariant point); QQuickItem* getDesktop(); QQuickItem* getToolWindow(); QObject* getRootMenu(); diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js index 487e491201..d286f26108 100644 --- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js +++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js @@ -178,11 +178,11 @@ } var hudRayPick = controllerData.hudRayPicks[this.hand]; var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); - this.setReticlePosition(point2d); - if (!Reticle.isPointingAtSystemOverlay(point2d)) { + if (!Window.isPointOnDesktopWindow(point2d)) { this.exitModule(); return false; } + this.setReticlePosition(point2d); Reticle.visible = false; this.movedAway = false; this.triggerClicked = controllerData.triggerClicks[this.hand]; From 577378f5392ee1e37f9da71abf3755e4157711cd Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 29 Sep 2017 17:45:10 -0700 Subject: [PATCH 611/722] Adding stuff that seems to break? --- interface/src/Application.cpp | 3 ++ libraries/render/src/render/Scene.cpp | 60 +++++++++++++++++++-------- libraries/render/src/render/Scene.h | 12 ++++++ 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 218c1eb5eb..5bf5f52a53 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5400,6 +5400,9 @@ void Application::update(float deltaTime) { AnimDebugDraw::getInstance().update(); DependencyManager::get()->update(); + + // Game loopis done, mark the end of the frame for the scene transactions + getMain3DScene()->enqueueFrame(); } void Application::sendAvatarViewFrustum() { diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index d2d3ad6de6..714be2a8c6 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -15,9 +15,6 @@ #include "Logging.h" #include "TransitionStage.h" -// Comment this to disable transitions (fades) -#define SCENE_ENABLE_TRANSITIONS - using namespace render; void Transaction::resetItem(ItemID id, const PayloadPointer& payload) { @@ -101,16 +98,46 @@ void consolidateTransaction(TransactionQueue& queue, Transaction& singleBatch) { queue.pop(); }; } - -void Scene::processTransactionQueue() { + +uint32_t Scene::enqueueFrame() { PROFILE_RANGE(render, __FUNCTION__); Transaction consolidatedTransaction; - { std::unique_lock lock(_transactionQueueMutex); consolidateTransaction(_transactionQueue, consolidatedTransaction); } - + + uint32_t frameNumber = 0; + { + std::unique_lock lock(_transactionFramesMutex); + _transactionFrames.push_back(consolidatedTransaction); + _transactionFrameNumber++; + frameNumber = _transactionFrameNumber; + } + + return frameNumber; +} + + +void Scene::processTransactionQueue() { + PROFILE_RANGE(render, __FUNCTION__); + + TransactionFrames queuedFrames; + { + // capture the queued frames and clear the queue + std::unique_lock lock(_transactionFramesMutex); + queuedFrames = _transactionFrames; + _transactionFrames.clear(); + } + + // go through the queue of frames and process them + for (auto& frame : queuedFrames) { + processTransactionFrame(frame); + } +} + +void Scene::processTransactionFrame(const Transaction& transaction) { + PROFILE_RANGE(render, __FUNCTION__); { std::unique_lock lock(_itemsMutex); // Here we should be able to check the value of last ItemID allocated @@ -123,32 +150,31 @@ void Scene::processTransactionQueue() { // capture anything coming from the transaction // resets and potential NEW items - resetItems(consolidatedTransaction._resetItems); + resetItems(transaction._resetItems); // Update the numItemsAtomic counter AFTER the reset changes went through _numAllocatedItems.exchange(maxID); // updates - updateItems(consolidatedTransaction._updatedItems); + updateItems(transaction._updatedItems); // removes - removeItems(consolidatedTransaction._removedItems); + removeItems(transaction._removedItems); -#ifdef SCENE_ENABLE_TRANSITIONS // add transitions - transitionItems(consolidatedTransaction._addedTransitions); - reApplyTransitions(consolidatedTransaction._reAppliedTransitions); - queryTransitionItems(consolidatedTransaction._queriedTransitions); -#endif + transitionItems(transaction._addedTransitions); + reApplyTransitions(transaction._reAppliedTransitions); + queryTransitionItems(transaction._queriedTransitions); + // Update the numItemsAtomic counter AFTER the pending changes went through _numAllocatedItems.exchange(maxID); } - if (consolidatedTransaction.touchTransactions()) { + if (transaction.touchTransactions()) { std::unique_lock lock(_selectionsMutex); // resets and potential NEW items - resetSelections(consolidatedTransaction._resetSelections); + resetSelections(transaction._resetSelections); } } diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index 3b61a20f24..fef2077897 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -117,6 +117,9 @@ public: // Enqueue transaction to the scene void enqueueTransaction(const Transaction& transaction); + // Enqueue end of frame transactions boundary + uint32_t enqueueFrame(); + // Process the pending transactions queued void processTransactionQueue(); @@ -162,6 +165,15 @@ protected: std::mutex _transactionQueueMutex; TransactionQueue _transactionQueue; + + std::mutex _transactionFramesMutex; + using TransactionFrames = std::list; + TransactionFrames _transactionFrames; + uint32_t _transactionFrameNumber{ 0 }; + + // Process one transaction frame + void processTransactionFrame(const Transaction& transaction); + // The actual database // database of items is protected for editing by a mutex std::mutex _itemsMutex; From 0bb27a7165a874766b7625928fd00eed444d87af Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 29 Sep 2017 17:48:59 -0700 Subject: [PATCH 612/722] fix modeloverlay visible change --- interface/src/ui/overlays/Base3DOverlay.h | 2 +- interface/src/ui/overlays/ModelOverlay.cpp | 38 ++++++++++++++-------- interface/src/ui/overlays/ModelOverlay.h | 8 ++++- interface/src/ui/overlays/Overlay.h | 2 +- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index 93a973e60a..3e65f163e2 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -47,7 +47,7 @@ public: void setIsSolid(bool isSolid) { _isSolid = isSolid; } void setIsDashedLine(bool isDashedLine) { _isDashedLine = isDashedLine; } void setIgnoreRayIntersection(bool value) { _ignoreRayIntersection = value; } - void setDrawInFront(bool value) { _drawInFront = value; } + virtual void setDrawInFront(bool value) { _drawInFront = value; } void setIsGrabbable(bool value) { _isGrabbable = value; } virtual AABox getBounds() const override = 0; diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index c857ad97ab..8ce6e7f1f3 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -72,6 +72,23 @@ void ModelOverlay::update(float deltatime) { animate(); } + // check to see if when we added our model to the scene they were ready, if they were not ready, then + // fix them up in the scene + render::ScenePointer scene = qApp->getMain3DScene(); + render::Transaction transaction; + if (_model->needsFixupInScene()) { + _model->removeFromScene(scene, transaction); + _model->addToScene(scene, transaction); + } + if (_visibleDirty) { + _visibleDirty = false; + _model->setVisibleInScene(getVisible(), scene); + } + if (_drawInFrontDirty) { + _drawInFrontDirty = false; + _model->setLayeredInFront(getDrawInFront(), scene); + } + scene->enqueueTransaction(transaction); } bool ModelOverlay::addToScene(Overlay::Pointer overlay, const render::ScenePointer& scene, render::Transaction& transaction) { @@ -85,21 +102,14 @@ void ModelOverlay::removeFromScene(Overlay::Pointer overlay, const render::Scene _model->removeFromScene(scene, transaction); } -void ModelOverlay::render(RenderArgs* args) { +void ModelOverlay::setVisible(bool visible) { + Overlay::setVisible(visible); + _visibleDirty = true; +} - // check to see if when we added our model to the scene they were ready, if they were not ready, then - // fix them up in the scene - render::ScenePointer scene = qApp->getMain3DScene(); - render::Transaction transaction; - if (_model->needsFixupInScene()) { - _model->removeFromScene(scene, transaction); - _model->addToScene(scene, transaction); - } - - _model->setVisibleInScene(_visible, scene); - _model->setLayeredInFront(getDrawInFront(), scene); - - scene->enqueueTransaction(transaction); +void ModelOverlay::setDrawInFront(bool drawInFront) { + Base3DOverlay::setDrawInFront(drawInFront); + _drawInFrontDirty = true; } void ModelOverlay::setProperties(const QVariantMap& properties) { diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index edee4f7ac6..ba1ffa86c1 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -29,7 +29,7 @@ public: ModelOverlay(const ModelOverlay* modelOverlay); virtual void update(float deltatime) override; - virtual void render(RenderArgs* args) override; + virtual void render(RenderArgs* args) override {}; void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, @@ -49,6 +49,9 @@ public: bool hasAnimation() const { return !_animationURL.isEmpty(); } bool jointsMapped() const { return _jointMappingURL == _animationURL && _jointMappingCompleted; } + void setVisible(bool visible) override; + void setDrawInFront(bool drawInFront) override; + protected: Transform evalRenderTransform() override; @@ -93,6 +96,9 @@ private: bool _jointMappingCompleted { false }; QVector _jointMapping; // domain is index into model-joints, range is index into animation-joints + bool _visibleDirty { false }; + bool _drawInFrontDirty { false }; + }; #endif // hifi_ModelOverlay_h diff --git a/interface/src/ui/overlays/Overlay.h b/interface/src/ui/overlays/Overlay.h index db2979b4d5..775c597397 100644 --- a/interface/src/ui/overlays/Overlay.h +++ b/interface/src/ui/overlays/Overlay.h @@ -73,7 +73,7 @@ public: float getAlphaPulse() const { return _alphaPulse; } // setters - void setVisible(bool visible) { _visible = visible; } + virtual void setVisible(bool visible) { _visible = visible; } void setDrawHUDLayer(bool drawHUDLayer); void setColor(const xColor& color) { _color = color; } void setAlpha(float alpha) { _alpha = alpha; } From 0daa5012ca2dce766c1c6a5c7cb344bd49448917 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 30 Sep 2017 15:00:15 +1300 Subject: [PATCH 613/722] Enable toolbar and tablet icons to load from local script directory --- interface/resources/qml/hifi/tablet/TabletButton.qml | 2 +- interface/resources/qml/hifi/toolbars/ToolbarButton.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/tablet/TabletButton.qml b/interface/resources/qml/hifi/tablet/TabletButton.qml index 58091d9fab..6d0fe810b2 100644 --- a/interface/resources/qml/hifi/tablet/TabletButton.qml +++ b/interface/resources/qml/hifi/tablet/TabletButton.qml @@ -84,7 +84,7 @@ Item { } function urlHelper(src) { - if (src.match(/\bhttp/)) { + if (src.match(/\bhttp/) || src.match(/\bfile:/)) { return src; } else { return "../../../" + src; diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml index bbf2d019fb..63149ad23b 100644 --- a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -34,7 +34,7 @@ StateImage { } function urlHelper(src) { - if (src.match(/\bhttp/)) { + if (src.match(/\bhttp/) || src.match(/\bfile:/)) { return src; } else { return "../../../" + src; From b56b3a2e6124106cf59bbb2ee3d13b95cebe939c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 30 Sep 2017 15:01:21 +1300 Subject: [PATCH 614/722] Change app name from "VR EDIT" to "SHAPES" and use new icon --- scripts/vr-edit/assets/shapes-a.svg | 15 +++++++++++++++ scripts/vr-edit/assets/shapes-d.svg | 18 ++++++++++++++++++ scripts/vr-edit/assets/shapes-i.svg | 18 ++++++++++++++++++ scripts/vr-edit/vr-edit.js | 8 ++++---- 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 scripts/vr-edit/assets/shapes-a.svg create mode 100644 scripts/vr-edit/assets/shapes-d.svg create mode 100644 scripts/vr-edit/assets/shapes-i.svg diff --git a/scripts/vr-edit/assets/shapes-a.svg b/scripts/vr-edit/assets/shapes-a.svg new file mode 100644 index 0000000000..07918ba294 --- /dev/null +++ b/scripts/vr-edit/assets/shapes-a.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/scripts/vr-edit/assets/shapes-d.svg b/scripts/vr-edit/assets/shapes-d.svg new file mode 100644 index 0000000000..fa64b519b9 --- /dev/null +++ b/scripts/vr-edit/assets/shapes-d.svg @@ -0,0 +1,18 @@ + + + + + + diff --git a/scripts/vr-edit/assets/shapes-i.svg b/scripts/vr-edit/assets/shapes-i.svg new file mode 100644 index 0000000000..cc9df9e64a --- /dev/null +++ b/scripts/vr-edit/assets/shapes-i.svg @@ -0,0 +1,18 @@ + + + + + + diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index d78b66f60f..87dfe64020 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -12,10 +12,10 @@ "use strict"; - var APP_NAME = "VR EDIT", // TODO: App name. - APP_ICON_INACTIVE = "icons/tablet-icons/edit-i.svg", // TODO: App icons. - APP_ICON_ACTIVE = "icons/tablet-icons/edit-a.svg", - APP_ICON_DISABLED = "icons/tablet-icons/edit-disabled.svg", + var APP_NAME = "SHAPES", + APP_ICON_INACTIVE = Script.resolvePath("./assets/shapes-i.svg"), + APP_ICON_ACTIVE = Script.resolvePath("./assets/shapes-a.svg"), + APP_ICON_DISABLED = Script.resolvePath("./assets/shapes-d.svg"), ENABLED_CAPTION_COLOR_OVERRIDE = "", DISABLED_CAPTION_COLOR_OVERRIDE = "#888888", START_DELAY = 2000, // ms From 229c1869013ee94f1a56e5068cabdc103a793053 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 30 Sep 2017 15:09:35 +1300 Subject: [PATCH 615/722] Rename script from vr-edit.js to shapes.js --- scripts/{vr-edit => shapes}/assets/audio/clone.wav | Bin scripts/{vr-edit => shapes}/assets/audio/create.wav | Bin scripts/{vr-edit => shapes}/assets/audio/delete.wav | Bin scripts/{vr-edit => shapes}/assets/audio/drop.wav | Bin scripts/{vr-edit => shapes}/assets/audio/equip.wav | Bin scripts/{vr-edit => shapes}/assets/audio/error.wav | Bin scripts/{vr-edit => shapes}/assets/audio/select.wav | Bin .../{vr-edit => shapes}/assets/blue-header-bar.fbx | Bin .../{vr-edit => shapes}/assets/create/circle.fbx | Bin scripts/{vr-edit => shapes}/assets/create/cone.fbx | Bin .../assets/create/create-heading.svg | 0 scripts/{vr-edit => shapes}/assets/create/cube.fbx | Bin .../{vr-edit => shapes}/assets/create/cylinder.fbx | Bin .../assets/create/dodecahedron.fbx | Bin .../{vr-edit => shapes}/assets/create/hexagon.fbx | Bin .../assets/create/icosahedron.fbx | Bin .../{vr-edit => shapes}/assets/create/octagon.fbx | Bin .../assets/create/octahedron.fbx | Bin scripts/{vr-edit => shapes}/assets/create/prism.fbx | Bin .../{vr-edit => shapes}/assets/create/sphere.fbx | Bin .../assets/create/tetrahedron.fbx | Bin scripts/{vr-edit => shapes}/assets/gray-header.fbx | Bin .../{vr-edit => shapes}/assets/green-header-bar.fbx | Bin scripts/{vr-edit => shapes}/assets/green-header.fbx | Bin .../{vr-edit => shapes}/assets/horizontal-rule.svg | 0 scripts/{vr-edit => shapes}/assets/shapes-a.svg | 0 scripts/{vr-edit => shapes}/assets/shapes-d.svg | 0 scripts/{vr-edit => shapes}/assets/shapes-i.svg | 0 .../assets/tools/back-heading.svg | 0 .../{vr-edit => shapes}/assets/tools/back-icon.svg | 0 .../{vr-edit => shapes}/assets/tools/clone-icon.svg | 0 .../assets/tools/clone-label.svg | 0 .../assets/tools/clone-tool-heading.svg | 0 .../{vr-edit => shapes}/assets/tools/color-icon.svg | 0 .../assets/tools/color-label.svg | 0 .../assets/tools/color-tool-heading.svg | 0 .../assets/tools/color/color-circle-black.png | Bin .../assets/tools/color/color-circle.png | Bin .../assets/tools/color/pick-color-label.svg | 0 .../assets/tools/color/slider-alpha.png | Bin .../assets/tools/color/slider-white.png | Bin .../assets/tools/color/swatches-label.svg | 0 .../assets/tools/common/actions-label.svg | 0 .../assets/tools/common/down-arrow.svg | 0 .../assets/tools/common/finish-label.svg | 0 .../assets/tools/common/info-icon.svg | 0 .../assets/tools/common/up-arrow.svg | 0 .../assets/tools/delete-icon.svg | 0 .../assets/tools/delete-label.svg | 0 .../assets/tools/delete-tool-heading.svg | 0 .../assets/tools/delete/info-text.svg | 0 .../{vr-edit => shapes}/assets/tools/group-icon.svg | 0 .../assets/tools/group-label.svg | 0 .../assets/tools/group-tool-heading.svg | 0 .../assets/tools/group/clear-label.svg | 0 .../assets/tools/group/group-label.svg | 0 .../assets/tools/group/selection-box-label.svg | 0 .../assets/tools/group/ungroup-label.svg | 0 .../assets/tools/physics-icon.svg | 0 .../assets/tools/physics-label.svg | 0 .../assets/tools/physics-tool-heading.svg | 0 .../tools/physics/buttons/collisions-label.svg | 0 .../tools/physics/buttons/grabbable-label.svg | 0 .../assets/tools/physics/buttons/gravity-label.svg | 0 .../assets/tools/physics/buttons/off-label.svg | 0 .../assets/tools/physics/buttons/on-label.svg | 0 .../assets/tools/physics/presets-label.svg | 0 .../assets/tools/physics/presets/balloon-label.svg | 0 .../assets/tools/physics/presets/cotton-label.svg | 0 .../assets/tools/physics/presets/custom-label.svg | 0 .../assets/tools/physics/presets/default-label.svg | 0 .../assets/tools/physics/presets/ice-label.svg | 0 .../assets/tools/physics/presets/lead-label.svg | 0 .../assets/tools/physics/presets/rubber-label.svg | 0 .../tools/physics/presets/tumbleweed-label.svg | 0 .../assets/tools/physics/presets/wood-label.svg | 0 .../assets/tools/physics/presets/zero-g-label.svg | 0 .../assets/tools/physics/properties-label.svg | 0 .../assets/tools/physics/sliders/bounce-label.svg | 0 .../assets/tools/physics/sliders/density-label.svg | 0 .../assets/tools/physics/sliders/friction-label.svg | 0 .../assets/tools/physics/sliders/gravity-label.svg | 0 .../{vr-edit => shapes}/assets/tools/redo-icon.svg | 0 .../{vr-edit => shapes}/assets/tools/redo-label.svg | 0 .../assets/tools/stretch-icon.svg | 0 .../assets/tools/stretch-label.svg | 0 .../assets/tools/stretch-tool-heading.svg | 0 .../assets/tools/stretch/info-text.svg | 0 .../{vr-edit => shapes}/assets/tools/tool-icon.fbx | Bin .../{vr-edit => shapes}/assets/tools/tool-label.svg | 0 .../assets/tools/tools-heading.svg | 0 .../{vr-edit => shapes}/assets/tools/undo-icon.svg | 0 .../{vr-edit => shapes}/assets/tools/undo-label.svg | 0 .../{vr-edit => shapes}/modules/createPalette.js | 0 scripts/{vr-edit => shapes}/modules/feedback.js | 0 scripts/{vr-edit => shapes}/modules/groups.js | 0 scripts/{vr-edit => shapes}/modules/hand.js | 0 scripts/{vr-edit => shapes}/modules/handles.js | 0 scripts/{vr-edit => shapes}/modules/highlights.js | 0 scripts/{vr-edit => shapes}/modules/history.js | 0 scripts/{vr-edit => shapes}/modules/laser.js | 0 scripts/{vr-edit => shapes}/modules/selection.js | 0 scripts/{vr-edit => shapes}/modules/toolIcon.js | 0 scripts/{vr-edit => shapes}/modules/toolsMenu.js | 0 scripts/{vr-edit => shapes}/modules/uit.js | 0 scripts/{vr-edit/vr-edit.js => shapes/shapes.js} | 2 +- scripts/{vr-edit => shapes}/utilities/utilities.js | 0 107 files changed, 1 insertion(+), 1 deletion(-) rename scripts/{vr-edit => shapes}/assets/audio/clone.wav (100%) rename scripts/{vr-edit => shapes}/assets/audio/create.wav (100%) rename scripts/{vr-edit => shapes}/assets/audio/delete.wav (100%) rename scripts/{vr-edit => shapes}/assets/audio/drop.wav (100%) rename scripts/{vr-edit => shapes}/assets/audio/equip.wav (100%) rename scripts/{vr-edit => shapes}/assets/audio/error.wav (100%) rename scripts/{vr-edit => shapes}/assets/audio/select.wav (100%) rename scripts/{vr-edit => shapes}/assets/blue-header-bar.fbx (100%) rename scripts/{vr-edit => shapes}/assets/create/circle.fbx (100%) rename scripts/{vr-edit => shapes}/assets/create/cone.fbx (100%) rename scripts/{vr-edit => shapes}/assets/create/create-heading.svg (100%) rename scripts/{vr-edit => shapes}/assets/create/cube.fbx (100%) rename scripts/{vr-edit => shapes}/assets/create/cylinder.fbx (100%) rename scripts/{vr-edit => shapes}/assets/create/dodecahedron.fbx (100%) rename scripts/{vr-edit => shapes}/assets/create/hexagon.fbx (100%) rename scripts/{vr-edit => shapes}/assets/create/icosahedron.fbx (100%) rename scripts/{vr-edit => shapes}/assets/create/octagon.fbx (100%) rename scripts/{vr-edit => shapes}/assets/create/octahedron.fbx (100%) rename scripts/{vr-edit => shapes}/assets/create/prism.fbx (100%) rename scripts/{vr-edit => shapes}/assets/create/sphere.fbx (100%) rename scripts/{vr-edit => shapes}/assets/create/tetrahedron.fbx (100%) rename scripts/{vr-edit => shapes}/assets/gray-header.fbx (100%) rename scripts/{vr-edit => shapes}/assets/green-header-bar.fbx (100%) rename scripts/{vr-edit => shapes}/assets/green-header.fbx (100%) rename scripts/{vr-edit => shapes}/assets/horizontal-rule.svg (100%) rename scripts/{vr-edit => shapes}/assets/shapes-a.svg (100%) rename scripts/{vr-edit => shapes}/assets/shapes-d.svg (100%) rename scripts/{vr-edit => shapes}/assets/shapes-i.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/back-heading.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/back-icon.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/clone-icon.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/clone-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/clone-tool-heading.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/color-icon.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/color-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/color-tool-heading.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/color/color-circle-black.png (100%) rename scripts/{vr-edit => shapes}/assets/tools/color/color-circle.png (100%) rename scripts/{vr-edit => shapes}/assets/tools/color/pick-color-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/color/slider-alpha.png (100%) rename scripts/{vr-edit => shapes}/assets/tools/color/slider-white.png (100%) rename scripts/{vr-edit => shapes}/assets/tools/color/swatches-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/common/actions-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/common/down-arrow.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/common/finish-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/common/info-icon.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/common/up-arrow.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/delete-icon.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/delete-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/delete-tool-heading.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/delete/info-text.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/group-icon.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/group-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/group-tool-heading.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/group/clear-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/group/group-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/group/selection-box-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/group/ungroup-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics-icon.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics-tool-heading.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/buttons/collisions-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/buttons/grabbable-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/buttons/gravity-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/buttons/off-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/buttons/on-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/presets-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/presets/balloon-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/presets/cotton-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/presets/custom-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/presets/default-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/presets/ice-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/presets/lead-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/presets/rubber-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/presets/tumbleweed-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/presets/wood-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/presets/zero-g-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/properties-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/sliders/bounce-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/sliders/density-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/sliders/friction-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/physics/sliders/gravity-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/redo-icon.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/redo-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/stretch-icon.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/stretch-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/stretch-tool-heading.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/stretch/info-text.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/tool-icon.fbx (100%) rename scripts/{vr-edit => shapes}/assets/tools/tool-label.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/tools-heading.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/undo-icon.svg (100%) rename scripts/{vr-edit => shapes}/assets/tools/undo-label.svg (100%) rename scripts/{vr-edit => shapes}/modules/createPalette.js (100%) rename scripts/{vr-edit => shapes}/modules/feedback.js (100%) rename scripts/{vr-edit => shapes}/modules/groups.js (100%) rename scripts/{vr-edit => shapes}/modules/hand.js (100%) rename scripts/{vr-edit => shapes}/modules/handles.js (100%) rename scripts/{vr-edit => shapes}/modules/highlights.js (100%) rename scripts/{vr-edit => shapes}/modules/history.js (100%) rename scripts/{vr-edit => shapes}/modules/laser.js (100%) rename scripts/{vr-edit => shapes}/modules/selection.js (100%) rename scripts/{vr-edit => shapes}/modules/toolIcon.js (100%) rename scripts/{vr-edit => shapes}/modules/toolsMenu.js (100%) rename scripts/{vr-edit => shapes}/modules/uit.js (100%) rename scripts/{vr-edit/vr-edit.js => shapes/shapes.js} (99%) rename scripts/{vr-edit => shapes}/utilities/utilities.js (100%) diff --git a/scripts/vr-edit/assets/audio/clone.wav b/scripts/shapes/assets/audio/clone.wav similarity index 100% rename from scripts/vr-edit/assets/audio/clone.wav rename to scripts/shapes/assets/audio/clone.wav diff --git a/scripts/vr-edit/assets/audio/create.wav b/scripts/shapes/assets/audio/create.wav similarity index 100% rename from scripts/vr-edit/assets/audio/create.wav rename to scripts/shapes/assets/audio/create.wav diff --git a/scripts/vr-edit/assets/audio/delete.wav b/scripts/shapes/assets/audio/delete.wav similarity index 100% rename from scripts/vr-edit/assets/audio/delete.wav rename to scripts/shapes/assets/audio/delete.wav diff --git a/scripts/vr-edit/assets/audio/drop.wav b/scripts/shapes/assets/audio/drop.wav similarity index 100% rename from scripts/vr-edit/assets/audio/drop.wav rename to scripts/shapes/assets/audio/drop.wav diff --git a/scripts/vr-edit/assets/audio/equip.wav b/scripts/shapes/assets/audio/equip.wav similarity index 100% rename from scripts/vr-edit/assets/audio/equip.wav rename to scripts/shapes/assets/audio/equip.wav diff --git a/scripts/vr-edit/assets/audio/error.wav b/scripts/shapes/assets/audio/error.wav similarity index 100% rename from scripts/vr-edit/assets/audio/error.wav rename to scripts/shapes/assets/audio/error.wav diff --git a/scripts/vr-edit/assets/audio/select.wav b/scripts/shapes/assets/audio/select.wav similarity index 100% rename from scripts/vr-edit/assets/audio/select.wav rename to scripts/shapes/assets/audio/select.wav diff --git a/scripts/vr-edit/assets/blue-header-bar.fbx b/scripts/shapes/assets/blue-header-bar.fbx similarity index 100% rename from scripts/vr-edit/assets/blue-header-bar.fbx rename to scripts/shapes/assets/blue-header-bar.fbx diff --git a/scripts/vr-edit/assets/create/circle.fbx b/scripts/shapes/assets/create/circle.fbx similarity index 100% rename from scripts/vr-edit/assets/create/circle.fbx rename to scripts/shapes/assets/create/circle.fbx diff --git a/scripts/vr-edit/assets/create/cone.fbx b/scripts/shapes/assets/create/cone.fbx similarity index 100% rename from scripts/vr-edit/assets/create/cone.fbx rename to scripts/shapes/assets/create/cone.fbx diff --git a/scripts/vr-edit/assets/create/create-heading.svg b/scripts/shapes/assets/create/create-heading.svg similarity index 100% rename from scripts/vr-edit/assets/create/create-heading.svg rename to scripts/shapes/assets/create/create-heading.svg diff --git a/scripts/vr-edit/assets/create/cube.fbx b/scripts/shapes/assets/create/cube.fbx similarity index 100% rename from scripts/vr-edit/assets/create/cube.fbx rename to scripts/shapes/assets/create/cube.fbx diff --git a/scripts/vr-edit/assets/create/cylinder.fbx b/scripts/shapes/assets/create/cylinder.fbx similarity index 100% rename from scripts/vr-edit/assets/create/cylinder.fbx rename to scripts/shapes/assets/create/cylinder.fbx diff --git a/scripts/vr-edit/assets/create/dodecahedron.fbx b/scripts/shapes/assets/create/dodecahedron.fbx similarity index 100% rename from scripts/vr-edit/assets/create/dodecahedron.fbx rename to scripts/shapes/assets/create/dodecahedron.fbx diff --git a/scripts/vr-edit/assets/create/hexagon.fbx b/scripts/shapes/assets/create/hexagon.fbx similarity index 100% rename from scripts/vr-edit/assets/create/hexagon.fbx rename to scripts/shapes/assets/create/hexagon.fbx diff --git a/scripts/vr-edit/assets/create/icosahedron.fbx b/scripts/shapes/assets/create/icosahedron.fbx similarity index 100% rename from scripts/vr-edit/assets/create/icosahedron.fbx rename to scripts/shapes/assets/create/icosahedron.fbx diff --git a/scripts/vr-edit/assets/create/octagon.fbx b/scripts/shapes/assets/create/octagon.fbx similarity index 100% rename from scripts/vr-edit/assets/create/octagon.fbx rename to scripts/shapes/assets/create/octagon.fbx diff --git a/scripts/vr-edit/assets/create/octahedron.fbx b/scripts/shapes/assets/create/octahedron.fbx similarity index 100% rename from scripts/vr-edit/assets/create/octahedron.fbx rename to scripts/shapes/assets/create/octahedron.fbx diff --git a/scripts/vr-edit/assets/create/prism.fbx b/scripts/shapes/assets/create/prism.fbx similarity index 100% rename from scripts/vr-edit/assets/create/prism.fbx rename to scripts/shapes/assets/create/prism.fbx diff --git a/scripts/vr-edit/assets/create/sphere.fbx b/scripts/shapes/assets/create/sphere.fbx similarity index 100% rename from scripts/vr-edit/assets/create/sphere.fbx rename to scripts/shapes/assets/create/sphere.fbx diff --git a/scripts/vr-edit/assets/create/tetrahedron.fbx b/scripts/shapes/assets/create/tetrahedron.fbx similarity index 100% rename from scripts/vr-edit/assets/create/tetrahedron.fbx rename to scripts/shapes/assets/create/tetrahedron.fbx diff --git a/scripts/vr-edit/assets/gray-header.fbx b/scripts/shapes/assets/gray-header.fbx similarity index 100% rename from scripts/vr-edit/assets/gray-header.fbx rename to scripts/shapes/assets/gray-header.fbx diff --git a/scripts/vr-edit/assets/green-header-bar.fbx b/scripts/shapes/assets/green-header-bar.fbx similarity index 100% rename from scripts/vr-edit/assets/green-header-bar.fbx rename to scripts/shapes/assets/green-header-bar.fbx diff --git a/scripts/vr-edit/assets/green-header.fbx b/scripts/shapes/assets/green-header.fbx similarity index 100% rename from scripts/vr-edit/assets/green-header.fbx rename to scripts/shapes/assets/green-header.fbx diff --git a/scripts/vr-edit/assets/horizontal-rule.svg b/scripts/shapes/assets/horizontal-rule.svg similarity index 100% rename from scripts/vr-edit/assets/horizontal-rule.svg rename to scripts/shapes/assets/horizontal-rule.svg diff --git a/scripts/vr-edit/assets/shapes-a.svg b/scripts/shapes/assets/shapes-a.svg similarity index 100% rename from scripts/vr-edit/assets/shapes-a.svg rename to scripts/shapes/assets/shapes-a.svg diff --git a/scripts/vr-edit/assets/shapes-d.svg b/scripts/shapes/assets/shapes-d.svg similarity index 100% rename from scripts/vr-edit/assets/shapes-d.svg rename to scripts/shapes/assets/shapes-d.svg diff --git a/scripts/vr-edit/assets/shapes-i.svg b/scripts/shapes/assets/shapes-i.svg similarity index 100% rename from scripts/vr-edit/assets/shapes-i.svg rename to scripts/shapes/assets/shapes-i.svg diff --git a/scripts/vr-edit/assets/tools/back-heading.svg b/scripts/shapes/assets/tools/back-heading.svg similarity index 100% rename from scripts/vr-edit/assets/tools/back-heading.svg rename to scripts/shapes/assets/tools/back-heading.svg diff --git a/scripts/vr-edit/assets/tools/back-icon.svg b/scripts/shapes/assets/tools/back-icon.svg similarity index 100% rename from scripts/vr-edit/assets/tools/back-icon.svg rename to scripts/shapes/assets/tools/back-icon.svg diff --git a/scripts/vr-edit/assets/tools/clone-icon.svg b/scripts/shapes/assets/tools/clone-icon.svg similarity index 100% rename from scripts/vr-edit/assets/tools/clone-icon.svg rename to scripts/shapes/assets/tools/clone-icon.svg diff --git a/scripts/vr-edit/assets/tools/clone-label.svg b/scripts/shapes/assets/tools/clone-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/clone-label.svg rename to scripts/shapes/assets/tools/clone-label.svg diff --git a/scripts/vr-edit/assets/tools/clone-tool-heading.svg b/scripts/shapes/assets/tools/clone-tool-heading.svg similarity index 100% rename from scripts/vr-edit/assets/tools/clone-tool-heading.svg rename to scripts/shapes/assets/tools/clone-tool-heading.svg diff --git a/scripts/vr-edit/assets/tools/color-icon.svg b/scripts/shapes/assets/tools/color-icon.svg similarity index 100% rename from scripts/vr-edit/assets/tools/color-icon.svg rename to scripts/shapes/assets/tools/color-icon.svg diff --git a/scripts/vr-edit/assets/tools/color-label.svg b/scripts/shapes/assets/tools/color-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/color-label.svg rename to scripts/shapes/assets/tools/color-label.svg diff --git a/scripts/vr-edit/assets/tools/color-tool-heading.svg b/scripts/shapes/assets/tools/color-tool-heading.svg similarity index 100% rename from scripts/vr-edit/assets/tools/color-tool-heading.svg rename to scripts/shapes/assets/tools/color-tool-heading.svg diff --git a/scripts/vr-edit/assets/tools/color/color-circle-black.png b/scripts/shapes/assets/tools/color/color-circle-black.png similarity index 100% rename from scripts/vr-edit/assets/tools/color/color-circle-black.png rename to scripts/shapes/assets/tools/color/color-circle-black.png diff --git a/scripts/vr-edit/assets/tools/color/color-circle.png b/scripts/shapes/assets/tools/color/color-circle.png similarity index 100% rename from scripts/vr-edit/assets/tools/color/color-circle.png rename to scripts/shapes/assets/tools/color/color-circle.png diff --git a/scripts/vr-edit/assets/tools/color/pick-color-label.svg b/scripts/shapes/assets/tools/color/pick-color-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/color/pick-color-label.svg rename to scripts/shapes/assets/tools/color/pick-color-label.svg diff --git a/scripts/vr-edit/assets/tools/color/slider-alpha.png b/scripts/shapes/assets/tools/color/slider-alpha.png similarity index 100% rename from scripts/vr-edit/assets/tools/color/slider-alpha.png rename to scripts/shapes/assets/tools/color/slider-alpha.png diff --git a/scripts/vr-edit/assets/tools/color/slider-white.png b/scripts/shapes/assets/tools/color/slider-white.png similarity index 100% rename from scripts/vr-edit/assets/tools/color/slider-white.png rename to scripts/shapes/assets/tools/color/slider-white.png diff --git a/scripts/vr-edit/assets/tools/color/swatches-label.svg b/scripts/shapes/assets/tools/color/swatches-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/color/swatches-label.svg rename to scripts/shapes/assets/tools/color/swatches-label.svg diff --git a/scripts/vr-edit/assets/tools/common/actions-label.svg b/scripts/shapes/assets/tools/common/actions-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/common/actions-label.svg rename to scripts/shapes/assets/tools/common/actions-label.svg diff --git a/scripts/vr-edit/assets/tools/common/down-arrow.svg b/scripts/shapes/assets/tools/common/down-arrow.svg similarity index 100% rename from scripts/vr-edit/assets/tools/common/down-arrow.svg rename to scripts/shapes/assets/tools/common/down-arrow.svg diff --git a/scripts/vr-edit/assets/tools/common/finish-label.svg b/scripts/shapes/assets/tools/common/finish-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/common/finish-label.svg rename to scripts/shapes/assets/tools/common/finish-label.svg diff --git a/scripts/vr-edit/assets/tools/common/info-icon.svg b/scripts/shapes/assets/tools/common/info-icon.svg similarity index 100% rename from scripts/vr-edit/assets/tools/common/info-icon.svg rename to scripts/shapes/assets/tools/common/info-icon.svg diff --git a/scripts/vr-edit/assets/tools/common/up-arrow.svg b/scripts/shapes/assets/tools/common/up-arrow.svg similarity index 100% rename from scripts/vr-edit/assets/tools/common/up-arrow.svg rename to scripts/shapes/assets/tools/common/up-arrow.svg diff --git a/scripts/vr-edit/assets/tools/delete-icon.svg b/scripts/shapes/assets/tools/delete-icon.svg similarity index 100% rename from scripts/vr-edit/assets/tools/delete-icon.svg rename to scripts/shapes/assets/tools/delete-icon.svg diff --git a/scripts/vr-edit/assets/tools/delete-label.svg b/scripts/shapes/assets/tools/delete-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/delete-label.svg rename to scripts/shapes/assets/tools/delete-label.svg diff --git a/scripts/vr-edit/assets/tools/delete-tool-heading.svg b/scripts/shapes/assets/tools/delete-tool-heading.svg similarity index 100% rename from scripts/vr-edit/assets/tools/delete-tool-heading.svg rename to scripts/shapes/assets/tools/delete-tool-heading.svg diff --git a/scripts/vr-edit/assets/tools/delete/info-text.svg b/scripts/shapes/assets/tools/delete/info-text.svg similarity index 100% rename from scripts/vr-edit/assets/tools/delete/info-text.svg rename to scripts/shapes/assets/tools/delete/info-text.svg diff --git a/scripts/vr-edit/assets/tools/group-icon.svg b/scripts/shapes/assets/tools/group-icon.svg similarity index 100% rename from scripts/vr-edit/assets/tools/group-icon.svg rename to scripts/shapes/assets/tools/group-icon.svg diff --git a/scripts/vr-edit/assets/tools/group-label.svg b/scripts/shapes/assets/tools/group-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/group-label.svg rename to scripts/shapes/assets/tools/group-label.svg diff --git a/scripts/vr-edit/assets/tools/group-tool-heading.svg b/scripts/shapes/assets/tools/group-tool-heading.svg similarity index 100% rename from scripts/vr-edit/assets/tools/group-tool-heading.svg rename to scripts/shapes/assets/tools/group-tool-heading.svg diff --git a/scripts/vr-edit/assets/tools/group/clear-label.svg b/scripts/shapes/assets/tools/group/clear-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/group/clear-label.svg rename to scripts/shapes/assets/tools/group/clear-label.svg diff --git a/scripts/vr-edit/assets/tools/group/group-label.svg b/scripts/shapes/assets/tools/group/group-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/group/group-label.svg rename to scripts/shapes/assets/tools/group/group-label.svg diff --git a/scripts/vr-edit/assets/tools/group/selection-box-label.svg b/scripts/shapes/assets/tools/group/selection-box-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/group/selection-box-label.svg rename to scripts/shapes/assets/tools/group/selection-box-label.svg diff --git a/scripts/vr-edit/assets/tools/group/ungroup-label.svg b/scripts/shapes/assets/tools/group/ungroup-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/group/ungroup-label.svg rename to scripts/shapes/assets/tools/group/ungroup-label.svg diff --git a/scripts/vr-edit/assets/tools/physics-icon.svg b/scripts/shapes/assets/tools/physics-icon.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics-icon.svg rename to scripts/shapes/assets/tools/physics-icon.svg diff --git a/scripts/vr-edit/assets/tools/physics-label.svg b/scripts/shapes/assets/tools/physics-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics-label.svg rename to scripts/shapes/assets/tools/physics-label.svg diff --git a/scripts/vr-edit/assets/tools/physics-tool-heading.svg b/scripts/shapes/assets/tools/physics-tool-heading.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics-tool-heading.svg rename to scripts/shapes/assets/tools/physics-tool-heading.svg diff --git a/scripts/vr-edit/assets/tools/physics/buttons/collisions-label.svg b/scripts/shapes/assets/tools/physics/buttons/collisions-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/buttons/collisions-label.svg rename to scripts/shapes/assets/tools/physics/buttons/collisions-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/buttons/grabbable-label.svg b/scripts/shapes/assets/tools/physics/buttons/grabbable-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/buttons/grabbable-label.svg rename to scripts/shapes/assets/tools/physics/buttons/grabbable-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/buttons/gravity-label.svg b/scripts/shapes/assets/tools/physics/buttons/gravity-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/buttons/gravity-label.svg rename to scripts/shapes/assets/tools/physics/buttons/gravity-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/buttons/off-label.svg b/scripts/shapes/assets/tools/physics/buttons/off-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/buttons/off-label.svg rename to scripts/shapes/assets/tools/physics/buttons/off-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/buttons/on-label.svg b/scripts/shapes/assets/tools/physics/buttons/on-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/buttons/on-label.svg rename to scripts/shapes/assets/tools/physics/buttons/on-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/presets-label.svg b/scripts/shapes/assets/tools/physics/presets-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/presets-label.svg rename to scripts/shapes/assets/tools/physics/presets-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/presets/balloon-label.svg b/scripts/shapes/assets/tools/physics/presets/balloon-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/presets/balloon-label.svg rename to scripts/shapes/assets/tools/physics/presets/balloon-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/presets/cotton-label.svg b/scripts/shapes/assets/tools/physics/presets/cotton-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/presets/cotton-label.svg rename to scripts/shapes/assets/tools/physics/presets/cotton-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/presets/custom-label.svg b/scripts/shapes/assets/tools/physics/presets/custom-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/presets/custom-label.svg rename to scripts/shapes/assets/tools/physics/presets/custom-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/presets/default-label.svg b/scripts/shapes/assets/tools/physics/presets/default-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/presets/default-label.svg rename to scripts/shapes/assets/tools/physics/presets/default-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/presets/ice-label.svg b/scripts/shapes/assets/tools/physics/presets/ice-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/presets/ice-label.svg rename to scripts/shapes/assets/tools/physics/presets/ice-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/presets/lead-label.svg b/scripts/shapes/assets/tools/physics/presets/lead-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/presets/lead-label.svg rename to scripts/shapes/assets/tools/physics/presets/lead-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/presets/rubber-label.svg b/scripts/shapes/assets/tools/physics/presets/rubber-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/presets/rubber-label.svg rename to scripts/shapes/assets/tools/physics/presets/rubber-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/presets/tumbleweed-label.svg b/scripts/shapes/assets/tools/physics/presets/tumbleweed-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/presets/tumbleweed-label.svg rename to scripts/shapes/assets/tools/physics/presets/tumbleweed-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/presets/wood-label.svg b/scripts/shapes/assets/tools/physics/presets/wood-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/presets/wood-label.svg rename to scripts/shapes/assets/tools/physics/presets/wood-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/presets/zero-g-label.svg b/scripts/shapes/assets/tools/physics/presets/zero-g-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/presets/zero-g-label.svg rename to scripts/shapes/assets/tools/physics/presets/zero-g-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/properties-label.svg b/scripts/shapes/assets/tools/physics/properties-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/properties-label.svg rename to scripts/shapes/assets/tools/physics/properties-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/sliders/bounce-label.svg b/scripts/shapes/assets/tools/physics/sliders/bounce-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/sliders/bounce-label.svg rename to scripts/shapes/assets/tools/physics/sliders/bounce-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/sliders/density-label.svg b/scripts/shapes/assets/tools/physics/sliders/density-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/sliders/density-label.svg rename to scripts/shapes/assets/tools/physics/sliders/density-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/sliders/friction-label.svg b/scripts/shapes/assets/tools/physics/sliders/friction-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/sliders/friction-label.svg rename to scripts/shapes/assets/tools/physics/sliders/friction-label.svg diff --git a/scripts/vr-edit/assets/tools/physics/sliders/gravity-label.svg b/scripts/shapes/assets/tools/physics/sliders/gravity-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/physics/sliders/gravity-label.svg rename to scripts/shapes/assets/tools/physics/sliders/gravity-label.svg diff --git a/scripts/vr-edit/assets/tools/redo-icon.svg b/scripts/shapes/assets/tools/redo-icon.svg similarity index 100% rename from scripts/vr-edit/assets/tools/redo-icon.svg rename to scripts/shapes/assets/tools/redo-icon.svg diff --git a/scripts/vr-edit/assets/tools/redo-label.svg b/scripts/shapes/assets/tools/redo-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/redo-label.svg rename to scripts/shapes/assets/tools/redo-label.svg diff --git a/scripts/vr-edit/assets/tools/stretch-icon.svg b/scripts/shapes/assets/tools/stretch-icon.svg similarity index 100% rename from scripts/vr-edit/assets/tools/stretch-icon.svg rename to scripts/shapes/assets/tools/stretch-icon.svg diff --git a/scripts/vr-edit/assets/tools/stretch-label.svg b/scripts/shapes/assets/tools/stretch-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/stretch-label.svg rename to scripts/shapes/assets/tools/stretch-label.svg diff --git a/scripts/vr-edit/assets/tools/stretch-tool-heading.svg b/scripts/shapes/assets/tools/stretch-tool-heading.svg similarity index 100% rename from scripts/vr-edit/assets/tools/stretch-tool-heading.svg rename to scripts/shapes/assets/tools/stretch-tool-heading.svg diff --git a/scripts/vr-edit/assets/tools/stretch/info-text.svg b/scripts/shapes/assets/tools/stretch/info-text.svg similarity index 100% rename from scripts/vr-edit/assets/tools/stretch/info-text.svg rename to scripts/shapes/assets/tools/stretch/info-text.svg diff --git a/scripts/vr-edit/assets/tools/tool-icon.fbx b/scripts/shapes/assets/tools/tool-icon.fbx similarity index 100% rename from scripts/vr-edit/assets/tools/tool-icon.fbx rename to scripts/shapes/assets/tools/tool-icon.fbx diff --git a/scripts/vr-edit/assets/tools/tool-label.svg b/scripts/shapes/assets/tools/tool-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/tool-label.svg rename to scripts/shapes/assets/tools/tool-label.svg diff --git a/scripts/vr-edit/assets/tools/tools-heading.svg b/scripts/shapes/assets/tools/tools-heading.svg similarity index 100% rename from scripts/vr-edit/assets/tools/tools-heading.svg rename to scripts/shapes/assets/tools/tools-heading.svg diff --git a/scripts/vr-edit/assets/tools/undo-icon.svg b/scripts/shapes/assets/tools/undo-icon.svg similarity index 100% rename from scripts/vr-edit/assets/tools/undo-icon.svg rename to scripts/shapes/assets/tools/undo-icon.svg diff --git a/scripts/vr-edit/assets/tools/undo-label.svg b/scripts/shapes/assets/tools/undo-label.svg similarity index 100% rename from scripts/vr-edit/assets/tools/undo-label.svg rename to scripts/shapes/assets/tools/undo-label.svg diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/shapes/modules/createPalette.js similarity index 100% rename from scripts/vr-edit/modules/createPalette.js rename to scripts/shapes/modules/createPalette.js diff --git a/scripts/vr-edit/modules/feedback.js b/scripts/shapes/modules/feedback.js similarity index 100% rename from scripts/vr-edit/modules/feedback.js rename to scripts/shapes/modules/feedback.js diff --git a/scripts/vr-edit/modules/groups.js b/scripts/shapes/modules/groups.js similarity index 100% rename from scripts/vr-edit/modules/groups.js rename to scripts/shapes/modules/groups.js diff --git a/scripts/vr-edit/modules/hand.js b/scripts/shapes/modules/hand.js similarity index 100% rename from scripts/vr-edit/modules/hand.js rename to scripts/shapes/modules/hand.js diff --git a/scripts/vr-edit/modules/handles.js b/scripts/shapes/modules/handles.js similarity index 100% rename from scripts/vr-edit/modules/handles.js rename to scripts/shapes/modules/handles.js diff --git a/scripts/vr-edit/modules/highlights.js b/scripts/shapes/modules/highlights.js similarity index 100% rename from scripts/vr-edit/modules/highlights.js rename to scripts/shapes/modules/highlights.js diff --git a/scripts/vr-edit/modules/history.js b/scripts/shapes/modules/history.js similarity index 100% rename from scripts/vr-edit/modules/history.js rename to scripts/shapes/modules/history.js diff --git a/scripts/vr-edit/modules/laser.js b/scripts/shapes/modules/laser.js similarity index 100% rename from scripts/vr-edit/modules/laser.js rename to scripts/shapes/modules/laser.js diff --git a/scripts/vr-edit/modules/selection.js b/scripts/shapes/modules/selection.js similarity index 100% rename from scripts/vr-edit/modules/selection.js rename to scripts/shapes/modules/selection.js diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/shapes/modules/toolIcon.js similarity index 100% rename from scripts/vr-edit/modules/toolIcon.js rename to scripts/shapes/modules/toolIcon.js diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/shapes/modules/toolsMenu.js similarity index 100% rename from scripts/vr-edit/modules/toolsMenu.js rename to scripts/shapes/modules/toolsMenu.js diff --git a/scripts/vr-edit/modules/uit.js b/scripts/shapes/modules/uit.js similarity index 100% rename from scripts/vr-edit/modules/uit.js rename to scripts/shapes/modules/uit.js diff --git a/scripts/vr-edit/vr-edit.js b/scripts/shapes/shapes.js similarity index 99% rename from scripts/vr-edit/vr-edit.js rename to scripts/shapes/shapes.js index 87dfe64020..da82baa947 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/shapes/shapes.js @@ -1,5 +1,5 @@ // -// vr-edit.js +// shapes.js // // Created by David Rowe on 27 Jun 2017. // Copyright 2017 High Fidelity, Inc. diff --git a/scripts/vr-edit/utilities/utilities.js b/scripts/shapes/utilities/utilities.js similarity index 100% rename from scripts/vr-edit/utilities/utilities.js rename to scripts/shapes/utilities/utilities.js From 659b2d8a99889cef8034a192d0c37b71482f2990 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 30 Sep 2017 15:14:07 +1300 Subject: [PATCH 616/722] Disable debug log info --- scripts/shapes/shapes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/shapes/shapes.js b/scripts/shapes/shapes.js index da82baa947..2154262229 100644 --- a/scripts/shapes/shapes.js +++ b/scripts/shapes/shapes.js @@ -71,7 +71,7 @@ button, DOMAIN_CHANGED_MESSAGE = "Toolbar-DomainChanged", - DEBUG = true; + DEBUG = false; // Utilities Script.include("./utilities/utilities.js"); From 03e0e21ce0a609784002eb6641a238d2117c7814 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 1 Oct 2017 13:19:54 +1300 Subject: [PATCH 617/722] JSLint ==> ESLint --- scripts/shapes/modules/createPalette.js | 20 +- scripts/shapes/modules/feedback.js | 34 +- scripts/shapes/modules/groups.js | 6 +- scripts/shapes/modules/hand.js | 10 +- scripts/shapes/modules/handles.js | 6 +- scripts/shapes/modules/highlights.js | 4 +- scripts/shapes/modules/history.js | 14 +- scripts/shapes/modules/laser.js | 20 +- scripts/shapes/modules/selection.js | 39 +- scripts/shapes/modules/toolIcon.js | 19 +- scripts/shapes/modules/toolsMenu.js | 883 +++++++++++---------- scripts/shapes/modules/uit.js | 26 +- scripts/shapes/shapes.js | 975 ++++++++++++------------ scripts/shapes/utilities/utilities.js | 8 +- 14 files changed, 1031 insertions(+), 1033 deletions(-) diff --git a/scripts/shapes/modules/createPalette.js b/scripts/shapes/modules/createPalette.js index 968be03ed4..505853f482 100644 --- a/scripts/shapes/modules/createPalette.js +++ b/scripts/shapes/modules/createPalette.js @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global App, CreatePalette */ +/* global CreatePalette: true, App, Feedback, History, UIT */ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { // Tool menu displayed on top of forearm. @@ -51,7 +51,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { PALETTE_HEADER_HEADING_PROPERTIES = { url: Script.resolvePath("../assets/gray-header.fbx"), - dimensions: UIT.dimensions.headerHeading, // Model is in rotated coordinate system but can override. + dimensions: UIT.dimensions.headerHeading, // Model is in rotated coordinate system but can override. localPosition: { x: 0, y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.headerHeading.y / 2, @@ -66,7 +66,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { PALETTE_HEADER_BAR_PROPERTIES = { url: Script.resolvePath("../assets/blue-header-bar.fbx"), - dimensions: UIT.dimensions.headerBar, // Model is in rotated coordinate system but can override. + dimensions: UIT.dimensions.headerBar, // Model is in rotated coordinate system but can override. localPosition: { x: 0, y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.headerHeading.y - UIT.dimensions.headerBar.y / 2, @@ -111,14 +111,14 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { ENTITY_CREATION_COLOR = { red: 192, green: 192, blue: 192 }, PALETTE_ITEM = { - overlay: "cube", // Invisible cube for hit area. + overlay: "cube", // Invisible cube for hit area. properties: { dimensions: UIT.dimensions.itemCollisionZone, localRotation: Quat.ZERO, - alpha: 0.0, // Invisible. + alpha: 0.0, // Invisible. solid: true, ignoreRayIntersection: false, - visible: true // So that laser intersects. + visible: true // So that laser intersects. }, hoverButton: { // Relative to root overlay. @@ -129,7 +129,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { localRotation: Quat.ZERO, color: UIT.colors.blueHighlight, alpha: 1.0, - emissive: true, // TODO: This has no effect. + emissive: true, // TODO: This has no effect. solid: true, ignoreRayIntersection: true, visible: false @@ -142,7 +142,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: UIT.dimensions.paletteItemIconDimensions, localPosition: UIT.dimensions.paletteItemIconOffset, localRotation: Quat.ZERO, - emissive: true, // TODO: This has no effect. + emissive: true, // TODO: This has no effect. ignoreRayIntersection: true } }, @@ -327,7 +327,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { // References. controlHand; - if (!this instanceof CreatePalette) { + if (!(this instanceof CreatePalette)) { return new CreatePalette(); } @@ -518,7 +518,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { if (!isDisplaying) { return; } - Overlays.deleteOverlay(paletteOriginOverlay); // Automatically deletes all other overlays because they're children. + Overlays.deleteOverlay(paletteOriginOverlay); // Automatically deletes all other overlays because they're children. paletteItemOverlays = []; paletteItemHoverOverlays = []; iconOverlays = []; diff --git a/scripts/shapes/modules/feedback.js b/scripts/shapes/modules/feedback.js index bf9b00ebc1..0d45ae2019 100644 --- a/scripts/shapes/modules/feedback.js +++ b/scripts/shapes/modules/feedback.js @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Feedback */ +/* global Feedback:true */ Feedback = (function () { // Provide audio and haptic user feedback. @@ -27,24 +27,24 @@ Feedback = (function () { REDO_SOUND = DROP_SOUND, FEEDBACK_PARAMETERS = { - DROP_TOOL: { sound: DROP_SOUND, volume: 0.3, haptic: 0.75 }, - DELETE_ENTITY: { sound: DELETE_SOUND, volume: 0.5, haptic: 0.2 }, - SELECT_ENTITY: { sound: SELECT_SOUND, volume: 0.2, haptic: 0.1 }, // E.g., Group tool. - CLONE_ENTITY: { sound: CLONE_SOUND, volume: 0.2, haptic: 0.1 }, - CREATE_ENTITY: { sound: CREATE_SOUND, volume: 0.4, haptic: 0.2 }, - HOVER_MENU_ITEM: { sound: null, volume: 0, haptic: 0.1 }, // Tools menu. - HOVER_BUTTON: { sound: null, volume: 0, haptic: 0.075 }, // Tools options and Create palette items. - EQUIP_TOOL: { sound: EQUIP_SOUND, volume: 0.3, haptic: 0.6 }, - APPLY_PROPERTY: { sound: null, volume: 0, haptic: 0.3 }, - APPLY_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 }, - UNDO_ACTION: { sound: UNDO_SOUND, volume: 0.1, haptic: 0.2 }, - REDO_ACTION: { sound: REDO_SOUND, volume: 0.1, haptic: 0.2 }, - GENERAL_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 } + DROP_TOOL: { sound: DROP_SOUND, volume: 0.3, haptic: 0.75 }, + DELETE_ENTITY: { sound: DELETE_SOUND, volume: 0.5, haptic: 0.2 }, + SELECT_ENTITY: { sound: SELECT_SOUND, volume: 0.2, haptic: 0.1 }, // E.g., Group tool. + CLONE_ENTITY: { sound: CLONE_SOUND, volume: 0.2, haptic: 0.1 }, + CREATE_ENTITY: { sound: CREATE_SOUND, volume: 0.4, haptic: 0.2 }, + HOVER_MENU_ITEM: { sound: null, volume: 0, haptic: 0.1 }, // Tools menu. + HOVER_BUTTON: { sound: null, volume: 0, haptic: 0.075 }, // Tools options and Create palette items. + EQUIP_TOOL: { sound: EQUIP_SOUND, volume: 0.3, haptic: 0.6 }, + APPLY_PROPERTY: { sound: null, volume: 0, haptic: 0.3 }, + APPLY_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 }, + UNDO_ACTION: { sound: UNDO_SOUND, volume: 0.1, haptic: 0.2 }, + REDO_ACTION: { sound: REDO_SOUND, volume: 0.1, haptic: 0.2 }, + GENERAL_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 } }, - VOLUME_MULTPLIER = 0.5, // Resulting volume range should be within 0.0 - 1.0. - HAPTIC_STRENGTH_MULTIPLIER = 1.3, // Resulting strength range should be within 0.0 - 1.0. - HAPTIC_LENGTH_MULTIPLIER = 75.0; // Resulting length range should be within 0 - 50, say. + VOLUME_MULTPLIER = 0.5, // Resulting volume range should be within 0.0 - 1.0. + HAPTIC_STRENGTH_MULTIPLIER = 1.3, // Resulting strength range should be within 0.0 - 1.0. + HAPTIC_LENGTH_MULTIPLIER = 75.0; // Resulting length range should be within 0 - 50, say. function play(side, item) { var parameters = FEEDBACK_PARAMETERS[item]; diff --git a/scripts/shapes/modules/groups.js b/scripts/shapes/modules/groups.js index 9331071fe2..6147df98af 100644 --- a/scripts/shapes/modules/groups.js +++ b/scripts/shapes/modules/groups.js @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Groups */ +/* global Groups:true, App, History */ Groups = function () { // Groups and ungroups trees of entities. @@ -19,7 +19,7 @@ Groups = function () { selections = [], entitiesSelectedCount = 0; - if (!this instanceof Groups) { + if (!(this instanceof Groups)) { return new Groups(); } @@ -200,7 +200,7 @@ Groups = function () { updateGroupInformation(); } } - childrenIndexes.push(selections[0].length); // Special extra item at end to aid updating selection. + childrenIndexes.push(selections[0].length); // Special extra item at end to aid updating selection. updateGroupInformation(); // Unlink children. diff --git a/scripts/shapes/modules/hand.js b/scripts/shapes/modules/hand.js index bd899c4724..1e8af70e57 100644 --- a/scripts/shapes/modules/hand.js +++ b/scripts/shapes/modules/hand.js @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Hand */ +/* global Hand:true */ Hand = function (side) { @@ -27,11 +27,11 @@ Hand = function (side) { isTriggerPressed, isTriggerClicked, - TRIGGER_ON_VALUE = 0.15, // Per controllerDispatcherUtils.js. - TRIGGER_OFF_VALUE = 0.1, // Per controllerDispatcherUtils.js. + TRIGGER_ON_VALUE = 0.15, // Per controllerDispatcherUtils.js. + TRIGGER_OFF_VALUE = 0.1, // Per controllerDispatcherUtils.js. TRIGGER_CLICKED_VALUE = 1.0, - NEAR_GRAB_RADIUS = 0.05, // Different from controllerDispatcherUtils.js. + NEAR_GRAB_RADIUS = 0.05, // Different from controllerDispatcherUtils.js. NEAR_HOVER_RADIUS = 0.025, LEFT_HAND = 0, @@ -45,7 +45,7 @@ Hand = function (side) { handleOverlayIDs = [], intersection = {}; - if (!this instanceof Hand) { + if (!(this instanceof Hand)) { return new Hand(side); } diff --git a/scripts/shapes/modules/handles.js b/scripts/shapes/modules/handles.js index 0318b21efb..c1d41b6066 100644 --- a/scripts/shapes/modules/handles.js +++ b/scripts/shapes/modules/handles.js @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Handles */ +/* global Handles:true */ Handles = function (side) { // Draws scaling handles. @@ -48,7 +48,7 @@ Handles = function (side) { i; - if (!this instanceof Handles) { + if (!(this instanceof Handles)) { return new Handles(side); } @@ -112,7 +112,7 @@ Handles = function (side) { function handleOffset(overlayID) { // Distance from overlay position to entity surface. if (isCornerHandle(overlayID)) { - return 0; // Corner overlays are centered on the corner. + return 0; // Corner overlays are centered on the corner. } return faceHandleOffsets.y / 2; } diff --git a/scripts/shapes/modules/highlights.js b/scripts/shapes/modules/highlights.js index d893e69f10..1af309a38e 100644 --- a/scripts/shapes/modules/highlights.js +++ b/scripts/shapes/modules/highlights.js @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Highlights */ +/* global Highlights: true */ Highlights = function (side) { // Draws highlights on selected entities. @@ -28,7 +28,7 @@ Highlights = function (side) { HAND_HIGHLIGHT_OFFSET = { x: 0.0, y: 0.11, z: 0.02 }, LEFT_HAND = 0; - if (!this instanceof Highlights) { + if (!(this instanceof Highlights)) { return new Highlights(); } diff --git a/scripts/shapes/modules/history.js b/scripts/shapes/modules/history.js index 88537710b9..6451000f04 100644 --- a/scripts/shapes/modules/history.js +++ b/scripts/shapes/modules/history.js @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global History */ +/* global History: true */ History = (function () { // Provides undo facility. @@ -46,14 +46,14 @@ History = (function () { */ ], MAX_HISTORY_ITEMS = 1000, - undoPosition = -1, // The next history item to undo; the next history item to redo = undoIndex + 1. + undoPosition = -1, // The next history item to undo; the next history item to redo = undoIndex + 1. undoData = {}, redoData = {}; function doKick(entityID) { var properties, - NO_KICK_ENTITY_TYPES = ["Text", "Web", "PolyLine", "ParticleEffect"], // Don't respond to gravity so don't kick. - DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD + NO_KICK_ENTITY_TYPES = ["Text", "Web", "PolyLine", "ParticleEffect"], // Don't respond to gravity so don't kick. + DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.1, z: 0 }; properties = Entities.getEntityProperties(entityID, ["type", "dynamic", "gravity", "velocity"]); @@ -65,10 +65,12 @@ History = (function () { function kickPhysics(entityID) { // Gives entities a small kick to start off physics, if necessary. - var KICK_DELAY = 500; // ms + var KICK_DELAY = 500; // ms // Give physics a chance to catch up. Avoids some erratic behavior. - Script.setTimeout(function () { doKick(entityID); }, KICK_DELAY); + Script.setTimeout(function () { + doKick(entityID); + }, KICK_DELAY); } function prePush(undo, redo) { diff --git a/scripts/shapes/modules/laser.js b/scripts/shapes/modules/laser.js index 39ce0b713b..29434a17f9 100644 --- a/scripts/shapes/modules/laser.js +++ b/scripts/shapes/modules/laser.js @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Laser */ +/* global Laser:true */ Laser = function (side) { // Draws hand lasers. @@ -25,18 +25,18 @@ Laser = function (side) { searchDistance = 0.0, - SEARCH_SPHERE_SIZE = 0.013, // Per handControllerGrab.js multiplied by 1.2 per handControllerGrab.js. + SEARCH_SPHERE_SIZE = 0.013, // Per handControllerGrab.js multiplied by 1.2 per handControllerGrab.js. MINUMUM_SEARCH_SPHERE_SIZE = 0.006, - SEARCH_SPHERE_FOLLOW_RATE = 0.5, // Per handControllerGrab.js. - COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 }, // Per handControllgerGrab.js. - COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }, // Per handControllgerGrab.js. + SEARCH_SPHERE_FOLLOW_RATE = 0.5, // Per handControllerGrab.js. + COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 }, // Per handControllgerGrab.js. + COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }, // Per handControllgerGrab.js. COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT, COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT, - BRIGHT_POW = 0.06, // Per handControllerGrab.js. + BRIGHT_POW = 0.06, // Per handControllerGrab.js. - GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. + GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. - PICK_MAX_DISTANCE = 500, // Per handControllerGrab.js. + PICK_MAX_DISTANCE = 500, // Per handControllerGrab.js. PRECISION_PICKING = true, NO_INCLUDE_IDS = [], NO_EXCLUDE_IDS = [], @@ -52,11 +52,11 @@ Laser = function (side) { intersection; - if (!this instanceof Laser) { + if (!(this instanceof Laser)) { return new Laser(side); } - function colorPow(color, power) { // Per handControllerGrab.js. + function colorPow(color, power) { // Per handControllerGrab.js. return { red: Math.pow(color.red / 255, power) * 255, green: Math.pow(color.green / 255, power) * 255, diff --git a/scripts/shapes/modules/selection.js b/scripts/shapes/modules/selection.js index 371d424cd7..faa514d641 100644 --- a/scripts/shapes/modules/selection.js +++ b/scripts/shapes/modules/selection.js @@ -8,24 +8,22 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global SelectionManager */ +/* global SelectionManager:true, App, History */ SelectionManager = function (side) { // Manages set of selected entities. Currently supports just one set of linked entities. "use strict"; - var selection = [], // Subset of properties to provide externally. + var selection = [], // Subset of properties to provide externally. selectionIDs = [], - selectionProperties = [], // Full set of properties for history. + selectionProperties = [], // Full set of properties for history. intersectedEntityID = null, intersectedEntityIndex, rootEntityID = null, rootPosition, rootOrientation, scaleFactor, - scalePosition, - scaleOrientation, scaleRootOffset, scaleRootOrientation, startPosition, @@ -35,11 +33,9 @@ SelectionManager = function (side) { ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "PolyLine"], ENTITY_TYPES_2D = ["Text", "Web"], MIN_HISTORY_MOVE_DISTANCE = 0.005, - MIN_HISTORY_ROTATE_ANGLE = 0.017453; // Radians = 1 degree. + MIN_HISTORY_ROTATE_ANGLE = 0.017453; // Radians = 1 degree. - - - if (!this instanceof SelectionManager) { + if (!(this instanceof SelectionManager)) { return new SelectionManager(side); } @@ -217,8 +213,8 @@ SelectionManager = function (side) { function doKick(entityID) { var properties, - NO_KICK_ENTITY_TYPES = ["Text", "Web", "PolyLine", "ParticleEffect"], // Don't respond to gravity so don't kick. - DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD + NO_KICK_ENTITY_TYPES = ["Text", "Web", "PolyLine", "ParticleEffect"], // Don't respond to gravity so don't kick. + DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.1, z: 0 }; if (entityID === rootEntityID && isEditing) { @@ -235,10 +231,12 @@ SelectionManager = function (side) { function kickPhysics(entityID) { // Gives entities a small kick to start off physics, if necessary. - var KICK_DELAY = 500; // ms + var KICK_DELAY = 500; // ms // Give physics a chance to catch up. Avoids some erratic behavior. - Script.setTimeout(function () { doKick(entityID); }, KICK_DELAY); + Script.setTimeout(function () { + doKick(entityID); + }, KICK_DELAY); } function startEditing() { @@ -249,13 +247,12 @@ SelectionManager = function (side) { startOrientation = selection[0].rotation; // Disable entity set's physics. - //for (i = 0, count = selection.length; i < count; i += 1) { for (i = selection.length - 1; i >= 0; i -= 1) { Entities.editEntity(selection[i].id, { - dynamic: false, // So that gravity doesn't fight with us trying to hold the entity in place. - collisionless: true, // So that entity doesn't bump us about as we resize the entity. - velocity: Vec3.ZERO, // So that entity doesn't drift if we've grabbed a set while it was moving. - angularVelocity: Vec3.ZERO // "" + dynamic: false, // So that gravity doesn't fight with us trying to hold the entity in place. + collisionless: true, // So that entity doesn't bump us about as we resize the entity. + velocity: Vec3.ZERO, // So that entity doesn't drift if we've grabbed a set while it was moving. + angularVelocity: Vec3.ZERO // "" }); } @@ -483,10 +480,8 @@ SelectionManager = function (side) { }); } - // Save most recent scale parameters. + // Save most recent scale factor. scaleFactor = factor; - scalePosition = position; - scaleOrientation = orientation; } function finishHandleScaling() { @@ -756,7 +751,7 @@ SelectionManager = function (side) { function deleteEntities() { if (rootEntityID) { History.push({ createEntities: selectionProperties }, { deleteEntities: [{ entityID: rootEntityID }] }); - Entities.deleteEntity(rootEntityID); // Children are automatically deleted. + Entities.deleteEntity(rootEntityID); // Children are automatically deleted. clear(); } } diff --git a/scripts/shapes/modules/toolIcon.js b/scripts/shapes/modules/toolIcon.js index a71dfdc156..1571a6b037 100644 --- a/scripts/shapes/modules/toolIcon.js +++ b/scripts/shapes/modules/toolIcon.js @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global App, ToolIcon */ +/* global ToolIcon:true, App, UIT */ ToolIcon = function (side) { // Tool icon displayed on non-dominant hand. @@ -17,10 +17,10 @@ ToolIcon = function (side) { var LEFT_HAND = 0, - MODEL_DIMENSIONS = { x: 0.1944, y: 0.1928, z: 0.1928 }, // Raw FBX dimensions. - MODEL_SCALE = 0.7, // Adjust icon dimensions so that the green bar matches that of the Tools header. - MODEL_POSITION_LEFT_HAND = { x: -0.025, y: 0.03, z: 0 }, // x raises in thumb direction; y moves in fingers direction. - MODEL_POSITION_RIGHT_HAND = { x: 0.025, y: 0.03, z: 0 }, // "" + MODEL_DIMENSIONS = { x: 0.1944, y: 0.1928, z: 0.1928 }, // Raw FBX dimensions. + MODEL_SCALE = 0.7, // Adjust icon dimensions so that the green bar matches that of the Tools header. + MODEL_POSITION_LEFT_HAND = { x: -0.025, y: 0.03, z: 0 }, // x raises in thumb direction; y moves in fingers direction. + MODEL_POSITION_RIGHT_HAND = { x: 0.025, y: 0.03, z: 0 }, // "" MODEL_ROTATION_LEFT_HAND = Quat.fromVec3Degrees({ x: 0, y: 0, z: 100 }), MODEL_ROTATION_RIGHT_HAND = Quat.fromVec3Degrees({ x: 0, y: 180, z: -100 }), @@ -46,8 +46,9 @@ ToolIcon = function (side) { }, ICON_PROPERTIES = { - localPosition: { x: 0.020, y: 0.069, z: 0 }, // Relative to model overlay. - color: UIT.colors.lightGrayText // x is in fingers direction; y is in thumb direction. + // Relative to model overlay. x is in fingers direction; y is in thumb direction. + localPosition: { x: 0.020, y: 0.069, z: 0 }, + color: UIT.colors.lightGrayText }, LABEL_PROPERTIES = { localPosition: { x: -0.040, y: 0.067, z: 0 }, @@ -67,7 +68,7 @@ ToolIcon = function (side) { modelOverlay = null; - if (!this instanceof ToolIcon) { + if (!(this instanceof ToolIcon)) { return new ToolIcon(); } @@ -89,7 +90,7 @@ ToolIcon = function (side) { function clear() { // Deletes current tool model. if (modelOverlay) { - Overlays.deleteOverlay(modelOverlay); // Child overlays are automatically deleted. + Overlays.deleteOverlay(modelOverlay); // Child overlays are automatically deleted. modelOverlay = null; } } diff --git a/scripts/shapes/modules/toolsMenu.js b/scripts/shapes/modules/toolsMenu.js index 182b53179f..6bd7d4a77b 100644 --- a/scripts/shapes/modules/toolsMenu.js +++ b/scripts/shapes/modules/toolsMenu.js @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global App, ToolsMenu */ +/* global ToolsMenu: true, App, Feedback, UIT */ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Tool menu displayed on top of forearm. @@ -36,11 +36,11 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { menuEnabled = [], optionsOverlays = [], - optionsOverlaysIDs = [], // Text ids (names) of options overlays. - optionsOverlaysLabels = [], // Overlay IDs of labels for optionsOverlays. - optionsOverlaysSublabels = [], // Overlay IDs of sublabels for optionsOverlays. - optionsSliderData = [], // Uses same index values as optionsOverlays. - optionsColorData = [], // Uses same index values as optionsOverlays. + optionsOverlaysIDs = [], // Text ids (names) of options overlays. + optionsOverlaysLabels = [], // Overlay IDs of labels for optionsOverlays. + optionsOverlaysSublabels = [], // Overlay IDs of sublabels for optionsOverlays. + optionsSliderData = [], // Uses same index values as optionsOverlays. + optionsColorData = [], // Uses same index values as optionsOverlays. optionsExtraOverlays = [], optionsEnabled = [], optionsSettings = { @@ -51,7 +51,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // For reliable access, use the values from optionsSettings rather than doing Settings.getValue() - Settings values // are not necessarily updated instantaneously. }, - optionsToggles = {}, // Cater for toggle buttons without a setting. + optionsToggles = {}, // Cater for toggle buttons without a setting. footerOverlays = [], footerHoverOverlays = [], @@ -92,23 +92,23 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { MENU_HEADER_PROPERTIES = { // Invisible box to catch laser intersections while menu heading and bar move inside. - dimensions: Vec3.sum(UIT.dimensions.header, MENU_HEADER_HOVER_OFFSET), // Keep the laser on top when hover. + dimensions: Vec3.sum(UIT.dimensions.header, MENU_HEADER_HOVER_OFFSET), // Keep the laser on top when hover. localPosition: { x: 0, y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.header.y / 2, z: UIT.dimensions.header.z / 2 + MENU_HEADER_HOVER_OFFSET.z / 2 }, localRotation: Quat.ZERO, - alpha: 0.0, // Invisible + alpha: 0.0, // Invisible solid: true, ignoreRayIntersection: false, - visible: true // Catch laser intersections. + visible: true // Catch laser intersections. }, MENU_HEADER_HEADING_PROPERTIES = { url: Script.resolvePath("../assets/gray-header.fbx"), highlightURL: Script.resolvePath("../assets/green-header.fbx"), - dimensions: UIT.dimensions.headerHeading, // Model is in rotated coordinate system but can override. + dimensions: UIT.dimensions.headerHeading, // Model is in rotated coordinate system but can override. localPosition: { x: 0, y: UIT.dimensions.headerBar.y / 2, z: -MENU_HEADER_HOVER_OFFSET.z / 2 }, localRotation: Quat.ZERO, alpha: 1.0, @@ -119,7 +119,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { MENU_HEADER_BAR_PROPERTIES = { url: Script.resolvePath("../assets/green-header-bar.fbx"), - dimensions: UIT.dimensions.headerBar, // Model is in rotated coordinate system but can override. + dimensions: UIT.dimensions.headerBar, // Model is in rotated coordinate system but can override. localPosition: { x: 0, y: -UIT.dimensions.headerHeading.y / 2 - UIT.dimensions.headerBar.y / 2, z: 0 }, localRotation: Quat.ZERO, alpha: 1.0, @@ -173,9 +173,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, MENU_HEADER_ICON_PROPERTIES = { - url: Script.resolvePath("../assets/tools/color-icon.svg"), // Initial value so that the overlay is initialized OK. - dimensions: { x: 0.01, y: 0.01 }, // "" - localPosition: Vec3.ZERO, // "" + url: Script.resolvePath("../assets/tools/color-icon.svg"), // Initial value so that the overlay is initialized OK. + dimensions: { x: 0.01, y: 0.01 }, // Ditto. + localPosition: Vec3.ZERO, // Ditto. localRotation: Quat.ZERO, color: UIT.colors.lightGrayText, alpha: 1.0, @@ -201,14 +201,14 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { UI_ELEMENTS = { "menuButton": { - overlay: "cube", // Invisible cube for hit area. + overlay: "cube", // Invisible cube for hit area. properties: { dimensions: UIT.dimensions.itemCollisionZone, localRotation: Quat.ZERO, - alpha: 0.0, // Invisible. + alpha: 0.0, // Invisible. solid: true, ignoreRayIntersection: false, - visible: true // So that laser intersects. + visible: true // So that laser intersects. }, hoverButton: { overlay: "shape", @@ -223,7 +223,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: -90 }), color: UIT.colors.greenHighlight, alpha: 1.0, - emissive: true, // TODO: This has no effect. + emissive: true, // TODO: This has no effect. solid: true, ignoreRayIntersection: true, visible: false @@ -334,7 +334,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.0294, y: UIT.dimensions.buttonDimensions.z, z: 0.0294 }, localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: -90 }), color: EMPTY_SWATCH_COLOR, - emissive: true, // TODO: This has no effect. + emissive: true, // TODO: This has no effect. alpha: 1.0, solid: true, ignoreRayIntersection: false, @@ -353,10 +353,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: UIT.dimensions.buttonDimensions.z, z: 0.0294 + SWATCH_HIGHLIGHT_DELTA }, - localRotation: Quat.ZERO, // Relative to swatch. + localRotation: Quat.ZERO, // Relative to swatch. localPosition: { x: 0, y: -0.0001, z: 0 }, color: UIT.colors.faintGray, - emissive: true, // TODO: This has no effect. + emissive: true, // TODO: This has no effect. alpha: 1.0, solid: true, ignoreRayIntersection: true, @@ -365,7 +365,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, "square": { - overlay: "cube", // Emulate a 2D square with a cube. + overlay: "cube", // Emulate a 2D square with a cube. properties: { localRotation: Quat.ZERO, color: UIT.colors.baseGrayShadow, @@ -420,10 +420,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { dimensions: { x: 0.02, y: 0.1, z: 0.01 }, localRotation: Quat.ZERO, - alpha: 0.0, // Invisible. + alpha: 0.0, // Invisible. solid: true, ignoreRayIntersection: false, - visible: true // Catch laser intersections. + visible: true // Catch laser intersections. }, label: { // Relative to barSlider. @@ -470,7 +470,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } }, - "imageSlider": { // Values range between 0.0 and 1.0. + "imageSlider": { // Values range between 0.0 and 1.0. overlay: "cube", properties: { dimensions: { x: 0.0160, y: 0.1229, z: UIT.dimensions.buttonDimensions.z }, @@ -542,7 +542,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: UIT.colors.white } }, - "picklistItem": { // Note: When using, declare before picklist item that it's being used in. + "picklistItem": { // Note: When using, declare before picklist item that it's being used in. overlay: "cube", properties: { dimensions: { x: 0.1416, y: 0.0280, z: UIT.dimensions.buttonDimensions.z }, @@ -571,21 +571,21 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { PICKLIST_HOVER_DELTA = { x: 0, y: 0, z: 0.006 }, BUTTON_PRESS_DELTA = { x: 0, y: 0, z: -0.002 }, - MIN_BAR_SLIDER_DIMENSION = 0.0001, // Avoid visual artifact for 0 slider values. + MIN_BAR_SLIDER_DIMENSION = 0.0001, // Avoid visual artifact for 0 slider values. PHYSICS_SLIDER_PRESETS = { // Slider values in the range 0.0 to 1.0. // Note: Friction values give the desired linear and angular damping values but friction values are a somewhat out, // especially for the balloon. - presetDefault: { gravity: 0.5, bounce: 0.5, friction: 0.5, density: 0.5 }, - presetLead: { gravity: 0.5, bounce: 0.0, friction: 0.5, density: 1.0 }, - presetWood: { gravity: 0.5, bounce: 0.4, friction: 0.5, density: 0.5 }, - presetIce: { gravity: 0.5, bounce: 0.99, friction: 0.151004, density: 0.349485 }, - presetRubber: { gravity: 0.5, bounce: 0.99, friction: 0.5, density: 0.5 }, - presetCotton: { gravity: 0.587303, bounce: 0.0, friction: 0.931878, density: 0.0 }, - presetTumbleweed: { gravity: 0.595893, bounce: 0.7, friction: 0.5, density: 0.0 }, - presetZeroG: { gravity: 0.596844, bounce: 0.5, friction: 0.5, density: 0.5 }, - presetBalloon: { gravity: 0.606313, bounce: 0.99, friction: 0.151004, density: 0.0 } + presetDefault: { gravity: 0.5, bounce: 0.5, friction: 0.5, density: 0.5 }, + presetLead: { gravity: 0.5, bounce: 0.0, friction: 0.5, density: 1.0 }, + presetWood: { gravity: 0.5, bounce: 0.4, friction: 0.5, density: 0.5 }, + presetIce: { gravity: 0.5, bounce: 0.99, friction: 0.151004, density: 0.349485 }, + presetRubber: { gravity: 0.5, bounce: 0.99, friction: 0.5, density: 0.5 }, + presetCotton: { gravity: 0.587303, bounce: 0.0, friction: 0.931878, density: 0.0 }, + presetTumbleweed: { gravity: 0.595893, bounce: 0.7, friction: 0.5, density: 0.0 }, + presetZeroG: { gravity: 0.596844, bounce: 0.5, friction: 0.5, density: 0.5 }, + presetBalloon: { gravity: 0.606313, bounce: 0.99, friction: 0.151004, density: 0.0 } }, OPTONS_PANELS = { @@ -893,7 +893,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2 + UIT.dimensions.footerHeight, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset }, - color: UIT.colors.white // Icon SVG is already lightGray color. + color: UIT.colors.white // Icon SVG is already lightGray color. } }, { @@ -902,7 +902,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { url: Script.resolvePath("../assets/tools/stretch/info-text.svg"), scale: 0.1340, - localPosition: { // Vertically center on info icon. + localPosition: { // Vertically center on info icon. x: -UIT.dimensions.panel.x / 2 + 0.0679 + 0.1340 / 2, y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2 + UIT.dimensions.footerHeight, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset @@ -1146,7 +1146,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, url: Script.resolvePath("../assets/tools/physics/buttons/off-label.svg"), scale: 0.0104, - color: UIT.colors.white // SVG has gray color. + color: UIT.colors.white // SVG has gray color. }, onSublabel: { url: Script.resolvePath("../assets/tools/physics/buttons/on-label.svg"), @@ -1194,7 +1194,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, url: Script.resolvePath("../assets/tools/physics/buttons/off-label.svg"), scale: 0.0104, - color: UIT.colors.white // SVG has gray color. + color: UIT.colors.white // SVG has gray color. }, onSublabel: { url: Script.resolvePath("../assets/tools/physics/buttons/on-label.svg"), @@ -1242,7 +1242,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, url: Script.resolvePath("../assets/tools/physics/buttons/off-label.svg"), scale: 0.0104, - color: UIT.colors.white // SVG has gray color. + color: UIT.colors.white // SVG has gray color. }, onSublabel: { url: Script.resolvePath("../assets/tools/physics/buttons/on-label.svg"), @@ -1445,7 +1445,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: 0, z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, - color: UIT.colors.white // SVG is colored baseGrayHighlight + color: UIT.colors.white // SVG is colored baseGrayHighlight }, customLabel: { url: Script.resolvePath("../assets/tools/physics/presets/custom-label.svg"), @@ -1659,7 +1659,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2 + UIT.dimensions.footerHeight, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset }, - color: UIT.colors.white // Icon SVG is already lightGray color. + color: UIT.colors.white // Icon SVG is already lightGray color. } }, { @@ -1668,7 +1668,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { url: Script.resolvePath("../assets/tools/delete/info-text.svg"), scale: 0.1416, - localPosition: { // Vertically off-center w.r.t. info icon. + localPosition: { // Vertically off-center w.r.t. info icon. x: -UIT.dimensions.panel.x / 2 + 0.0679 + 0.1340 / 2, y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0240 / 2 + 0.0063 / 2 + UIT.dimensions.footerHeight, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset @@ -1913,7 +1913,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } ], - COLOR_TOOL = 0, // Indexes of corresponding MENU_ITEMS item. + COLOR_TOOL = 0, // Indexes of corresponding MENU_ITEMS item. SCALE_TOOL = 1, CLONE_TOOL = 2, GROUP_TOOL = 3, @@ -1938,7 +1938,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: MENU_ITEM_XS[2], - y: MENU_ITEM_YS[3] - 0.008, // Allow space for horizontal rule and Line up with Create palette row. + y: MENU_ITEM_YS[3] - 0.008, // Allow space for horizontal rule and Line up with Create palette row. z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2 } }, @@ -2033,8 +2033,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Forward declarations. doCommand; - - if (!this instanceof ToolsMenu) { + if (!(this instanceof ToolsMenu)) { return new ToolsMenu(); } @@ -2232,7 +2231,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } } else if (optionsItems[i].type === "toggleButton") { - optionsToggles[optionsItems[i].id] = false; // Default to off. + optionsToggles[optionsItems[i].id] = false; // Default to off. } optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[optionsItems[i].type].overlay, properties)); @@ -2307,7 +2306,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { childProperties = Object.clone(UI_ELEMENTS.image.properties); childProperties.url = optionsItems[i].imageURL; childProperties.dimensions = { x: properties.dimensions.x, y: properties.dimensions.y }; - imageOffset += 2 * IMAGE_OFFSET; // Double offset to prevent clipping against background. + imageOffset += 2 * IMAGE_OFFSET; // Double offset to prevent clipping against background. if (optionsItems[i].useBaseColor) { childProperties.color = properties.color; } @@ -2358,7 +2357,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { childProperties = Object.clone(UI_ELEMENTS.image.properties); childProperties.url = optionsItems[i].imageURL; childProperties.scale = properties.dimensions.x; - imageOffset += 2 * IMAGE_OFFSET; // Double offset to prevent clipping against background. + imageOffset += 2 * IMAGE_OFFSET; // Double offset to prevent clipping against background. childProperties.localPosition = { x: 0, y: properties.dimensions.y / 2 + imageOffset, z: 0 }; childProperties.localRotation = Quat.fromVec3Degrees({ x: -90, y: 90, z: 0 }); childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; @@ -2690,252 +2689,252 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { switch (command) { - case "setPickColor": - setColorPicker(parameter); - break; + case "setPickColor": + setColorPicker(parameter); + break; - case "setColorPerCircle": - hsvControl.hsv.h = parameter.h; - hsvControl.hsv.s = parameter.s; - updateColorSlider(); - value = hsvToRGB(hsvControl.hsv); - setCurrentColor(value); - uiCommandCallback("setColor", value); - break; - - case "setColorPerSlider": - hsvControl.hsv.v = parameter; - updateColorCircle(); - value = hsvToRGB(hsvControl.hsv); - setCurrentColor(value); - uiCommandCallback("setColor", value); - break; - - case "setColorPerSwatch": - index = optionsOverlaysIDs.indexOf(parameter); - value = optionsSettings[parameter].value; - if (value !== "") { - // Set current color to swatch color. + case "setColorPerCircle": + hsvControl.hsv.h = parameter.h; + hsvControl.hsv.s = parameter.s; + updateColorSlider(); + value = hsvToRGB(hsvControl.hsv); setCurrentColor(value); - setColorPicker(value); uiCommandCallback("setColor", value); - } else { - // Swatch has no color; set swatch color to current color. - value = optionsSettings.currentColor.value; + break; + + case "setColorPerSlider": + hsvControl.hsv.v = parameter; + updateColorCircle(); + value = hsvToRGB(hsvControl.hsv); + setCurrentColor(value); + uiCommandCallback("setColor", value); + break; + + case "setColorPerSwatch": + index = optionsOverlaysIDs.indexOf(parameter); + value = optionsSettings[parameter].value; + if (value !== "") { + // Set current color to swatch color. + setCurrentColor(value); + setColorPicker(value); + uiCommandCallback("setColor", value); + } else { + // Swatch has no color; set swatch color to current color. + value = optionsSettings.currentColor.value; + Overlays.editOverlay(optionsOverlays[index], { + color: value + }); + optionsSettings[parameter].value = value; + Settings.setValue(optionsSettings[parameter].key, value); + } + break; + + case "togglePickColor": + optionsToggles.pickColor = !optionsToggles.pickColor; + index = optionsOverlaysIDs.indexOf("pickColor"); + Overlays.editOverlay(optionsOverlays[index], { + color: optionsToggles.pickColor + ? UI_ELEMENTS[optionsItems[index].type].onHoverColor + : UI_ELEMENTS[optionsItems[index].type].offHoverColor + }); + uiCommandCallback("pickColorTool", optionsToggles.pickColor); + break; + + case "setColorFromPick": + optionsToggles.pickColor = false; + index = optionsOverlaysIDs.indexOf("pickColor"); + Overlays.editOverlay(optionsOverlays[index], { + color: UI_ELEMENTS[optionsItems[index].type].offColor + }); + setCurrentColor(parameter); + setColorPicker(parameter); + break; + + case "toggleGroupSelectionBox": + optionsToggles.groupSelectionBoxButton = !optionsToggles.groupSelectionBoxButton; + index = optionsOverlaysIDs.indexOf("groupSelectionBoxButton"); + Overlays.editOverlay(optionsOverlays[index], { + color: optionsToggles.groupSelectionBoxButton + ? UI_ELEMENTS[optionsItems[index].type].onHoverColor + : UI_ELEMENTS[optionsItems[index].type].offHoverColor + }); + uiCommandCallback("toggleGroupSelectionBoxTool", optionsToggles.groupSelectionBoxButton); + break; + + case "clearGroupSelection": + index = clearGroupingButtonIndex; + Overlays.editOverlay(optionsOverlays[index], { + color: optionsItems[index].properties.color + }); + Overlays.editOverlay(optionsOverlaysLabels[index], { + color: optionsItems[index].label.color + }); + optionsEnabled[index] = false; + uiCommandCallback("clearGroupSelectionTool"); + break; + + case "setGravityOn": + case "setGrabOn": + case "setCollideOn": + value = !optionsSettings[parameter].value; + optionsSettings[parameter].value = value; + optionsToggles[parameter] = value; + Settings.setValue(optionsSettings[parameter].key, value); + index = optionsOverlaysIDs.indexOf(parameter); Overlays.editOverlay(optionsOverlays[index], { color: value + ? UI_ELEMENTS[optionsItems[index].type].onHoverColor + : UI_ELEMENTS[optionsItems[index].type].offHoverColor }); - optionsSettings[parameter].value = value; - Settings.setValue(optionsSettings[parameter].key, value); - } - break; + properties = Object.clone(value ? optionsItems[index].onSublabel : optionsItems[index].offSublabel); + Overlays.editOverlay(optionsOverlaysSublabels[index], properties); + uiCommandCallback(command, value); + break; - case "togglePickColor": - optionsToggles.pickColor = !optionsToggles.pickColor; - index = optionsOverlaysIDs.indexOf("pickColor"); - Overlays.editOverlay(optionsOverlays[index], { - color: optionsToggles.pickColor - ? UI_ELEMENTS[optionsItems[index].type].onHoverColor - : UI_ELEMENTS[optionsItems[index].type].offHoverColor - }); - uiCommandCallback("pickColorTool", optionsToggles.pickColor); - break; + case "togglePhysicsPresets": + if (isPicklistOpen) { + // Close picklist. + index = optionsOverlaysIDs.indexOf("physicsPresets"); - case "setColorFromPick": - optionsToggles.pickColor = false; - index = optionsOverlaysIDs.indexOf("pickColor"); - Overlays.editOverlay(optionsOverlays[index], { - color: UI_ELEMENTS[optionsItems[index].type].offColor - }); - setCurrentColor(parameter); - setColorPicker(parameter); - break; + // Lower picklist. + isHighlightingPicklist = hoveredElementType === "picklist"; + Overlays.editOverlay(optionsOverlays[index], { + localPosition: isHighlightingPicklist + ? Vec3.sum(optionsItems[index].properties.localPosition, OPTION_HOVER_DELTA) + : optionsItems[index].properties.localPosition, + color: isHighlightingPicklist + ? UIT.colors.highlightColor + : UI_ELEMENTS.picklist.properties.color + }); + Overlays.editOverlay(optionsOverlaysSublabels[index], { + url: PICKLIST_DOWN_ARROW + }); - case "toggleGroupSelectionBox": - optionsToggles.groupSelectionBoxButton = !optionsToggles.groupSelectionBoxButton; - index = optionsOverlaysIDs.indexOf("groupSelectionBoxButton"); - Overlays.editOverlay(optionsOverlays[index], { - color: optionsToggles.groupSelectionBoxButton - ? UI_ELEMENTS[optionsItems[index].type].onHoverColor - : UI_ELEMENTS[optionsItems[index].type].offHoverColor - }); - uiCommandCallback("toggleGroupSelectionBoxTool", optionsToggles.groupSelectionBoxButton); - break; + // Hide options. + items = optionsItems[index].items; + for (i = 0, length = items.length; i < length; i += 1) { + index = optionsOverlaysIDs.indexOf(items[i]); + Overlays.editOverlay(optionsOverlays[index], { + localPosition: Vec3.ZERO, + visible: false + }); + Overlays.editOverlay(optionsOverlaysLabels[index], { + visible: false + }); + } + } - case "clearGroupSelection": - index = clearGroupingButtonIndex; - Overlays.editOverlay(optionsOverlays[index], { - color: optionsItems[index].properties.color - }); - Overlays.editOverlay(optionsOverlaysLabels[index], { - color: optionsItems[index].label.color - }); - optionsEnabled[index] = false; - uiCommandCallback("clearGroupSelectionTool"); - break; + isPicklistOpen = !isPicklistOpen; - case "setGravityOn": - case "setGrabOn": - case "setCollideOn": - value = !optionsSettings[parameter].value; - optionsSettings[parameter].value = value; - optionsToggles[parameter] = value; - Settings.setValue(optionsSettings[parameter].key, value); - index = optionsOverlaysIDs.indexOf(parameter); - Overlays.editOverlay(optionsOverlays[index], { - color: value - ? UI_ELEMENTS[optionsItems[index].type].onHoverColor - : UI_ELEMENTS[optionsItems[index].type].offHoverColor - }); - properties = Object.clone(value ? optionsItems[index].onSublabel : optionsItems[index].offSublabel); - Overlays.editOverlay(optionsOverlaysSublabels[index], properties); - uiCommandCallback(command, value); - break; + if (isPicklistOpen) { + // Open picklist. + index = optionsOverlaysIDs.indexOf("physicsPresets"); + parentID = optionsOverlays[index]; - case "togglePhysicsPresets": - if (isPicklistOpen) { + // Raise picklist. + Overlays.editOverlay(parentID, { + localPosition: Vec3.sum(optionsItems[index].properties.localPosition, PICKLIST_HOVER_DELTA) + }); + Overlays.editOverlay(optionsOverlaysSublabels[index], { + url: PICKLIST_UP_ARROW + }); + + // Show options. + items = optionsItems[index].items; + for (i = 0, length = items.length; i < length; i += 1) { + index = optionsOverlaysIDs.indexOf(items[i]); + Overlays.editOverlay(optionsOverlays[index], { + parentID: parentID, + localPosition: { x: 0, y: (i + 1) * UI_ELEMENTS.picklistItem.properties.dimensions.y, z: 0 }, + visible: true + }); + Overlays.editOverlay(optionsOverlaysLabels[index], { + visible: true + }); + } + } + break; + + case "pickPhysicsPreset": // Close picklist. - index = optionsOverlaysIDs.indexOf("physicsPresets"); + doCommand("togglePhysicsPresets"); - // Lower picklist. - isHighlightingPicklist = hoveredElementType === "picklist"; - Overlays.editOverlay(optionsOverlays[index], { - localPosition: isHighlightingPicklist - ? Vec3.sum(optionsItems[index].properties.localPosition, OPTION_HOVER_DELTA) - : optionsItems[index].properties.localPosition, - color: isHighlightingPicklist - ? UIT.colors.highlightColor - : UI_ELEMENTS.picklist.properties.color - }); - Overlays.editOverlay(optionsOverlaysSublabels[index], { - url: PICKLIST_DOWN_ARROW + // Update picklist label. + label = optionsItems[optionsOverlaysIDs.indexOf(parameter)].label; + Overlays.editOverlay(optionsOverlaysLabels[optionsOverlaysIDs.indexOf("physicsPresets")], { + url: label.url, + scale: label.scale, + localPosition: label.localPosition }); + optionsSettings.physicsPresets.value = parameter; + Settings.setValue(optionsSettings.physicsPresets.key, parameter); - // Hide options. - items = optionsItems[index].items; - for (i = 0, length = items.length; i < length; i += 1) { - index = optionsOverlaysIDs.indexOf(items[i]); - Overlays.editOverlay(optionsOverlays[index], { - localPosition: Vec3.ZERO, - visible: false - }); - Overlays.editOverlay(optionsOverlaysLabels[index], { - visible: false - }); - } - } + // Update sliders. + values = PHYSICS_SLIDER_PRESETS[parameter]; + setBarSliderValue(optionsOverlaysIDs.indexOf("gravitySlider"), values.gravity); + Settings.setValue(optionsSettings.gravitySlider.key, values.gravity); + uiCommandCallback("setGravity", values.gravity); + setBarSliderValue(optionsOverlaysIDs.indexOf("bounceSlider"), values.bounce); + Settings.setValue(optionsSettings.bounceSlider.key, values.bounce); + uiCommandCallback("setBounce", values.bounce); + setBarSliderValue(optionsOverlaysIDs.indexOf("frictionSlider"), values.friction); + Settings.setValue(optionsSettings.frictionSlider.key, values.friction); + uiCommandCallback("setFriction", values.friction); + setBarSliderValue(optionsOverlaysIDs.indexOf("densitySlider"), values.density); + Settings.setValue(optionsSettings.densitySlider.key, values.density); + uiCommandCallback("setDensity", values.density); - isPicklistOpen = !isPicklistOpen; + break; - if (isPicklistOpen) { - // Open picklist. - index = optionsOverlaysIDs.indexOf("physicsPresets"); - parentID = optionsOverlays[index]; + case "setGravity": + setPresetsLabelToCustom(); + Settings.setValue(optionsSettings.gravitySlider.key, parameter); + uiCommandCallback("setGravity", parameter); + break; + case "setBounce": + setPresetsLabelToCustom(); + Settings.setValue(optionsSettings.bounceSlider.key, parameter); + uiCommandCallback("setBounce", parameter); + break; + case "setFriction": + setPresetsLabelToCustom(); + Settings.setValue(optionsSettings.frictionSlider.key, parameter); + uiCommandCallback("setFriction", parameter); + break; + case "setDensity": + setPresetsLabelToCustom(); + Settings.setValue(optionsSettings.densitySlider.key, parameter); + uiCommandCallback("setDensity", parameter); + break; - // Raise picklist. - Overlays.editOverlay(parentID, { - localPosition: Vec3.sum(optionsItems[index].properties.localPosition, PICKLIST_HOVER_DELTA) - }); - Overlays.editOverlay(optionsOverlaysSublabels[index], { - url: PICKLIST_UP_ARROW - }); + case "closeOptions": + closeOptions(); + openMenu(); + break; - // Show options. - items = optionsItems[index].items; - for (i = 0, length = items.length; i < length; i += 1) { - index = optionsOverlaysIDs.indexOf(items[i]); - Overlays.editOverlay(optionsOverlays[index], { - parentID: parentID, - localPosition: { x: 0, y: (i + 1) * UI_ELEMENTS.picklistItem.properties.dimensions.y, z: 0 }, - visible: true - }); - Overlays.editOverlay(optionsOverlaysLabels[index], { - visible: true - }); - } - } - break; + case "clearTool": + uiCommandCallback("clearTool"); + break; - case "pickPhysicsPreset": - // Close picklist. - doCommand("togglePhysicsPresets"); - - // Update picklist label. - label = optionsItems[optionsOverlaysIDs.indexOf(parameter)].label; - Overlays.editOverlay(optionsOverlaysLabels[optionsOverlaysIDs.indexOf("physicsPresets")], { - url: label.url, - scale: label.scale, - localPosition: label.localPosition - }); - optionsSettings.physicsPresets.value = parameter; - Settings.setValue(optionsSettings.physicsPresets.key, parameter); - - // Update sliders. - values = PHYSICS_SLIDER_PRESETS[parameter]; - setBarSliderValue(optionsOverlaysIDs.indexOf("gravitySlider"), values.gravity); - Settings.setValue(optionsSettings.gravitySlider.key, values.gravity); - uiCommandCallback("setGravity", values.gravity); - setBarSliderValue(optionsOverlaysIDs.indexOf("bounceSlider"), values.bounce); - Settings.setValue(optionsSettings.bounceSlider.key, values.bounce); - uiCommandCallback("setBounce", values.bounce); - setBarSliderValue(optionsOverlaysIDs.indexOf("frictionSlider"), values.friction); - Settings.setValue(optionsSettings.frictionSlider.key, values.friction); - uiCommandCallback("setFriction", values.friction); - setBarSliderValue(optionsOverlaysIDs.indexOf("densitySlider"), values.density); - Settings.setValue(optionsSettings.densitySlider.key, values.density); - uiCommandCallback("setDensity", values.density); - - break; - - case "setGravity": - setPresetsLabelToCustom(); - Settings.setValue(optionsSettings.gravitySlider.key, parameter); - uiCommandCallback("setGravity", parameter); - break; - case "setBounce": - setPresetsLabelToCustom(); - Settings.setValue(optionsSettings.bounceSlider.key, parameter); - uiCommandCallback("setBounce", parameter); - break; - case "setFriction": - setPresetsLabelToCustom(); - Settings.setValue(optionsSettings.frictionSlider.key, parameter); - uiCommandCallback("setFriction", parameter); - break; - case "setDensity": - setPresetsLabelToCustom(); - Settings.setValue(optionsSettings.densitySlider.key, parameter); - uiCommandCallback("setDensity", parameter); - break; - - case "closeOptions": - closeOptions(); - openMenu(); - break; - - case "clearTool": - uiCommandCallback("clearTool"); - break; - - default: - App.log(side, "ERROR: ToolsMenu: Unexpected command! " + command); + default: + App.log(side, "ERROR: ToolsMenu: Unexpected command! " + command); } }; function doGripClicked(command, parameter) { switch (command) { - case "clearSwatch": - // Remove highlight ring and change swatch to current color. - Overlays.editOverlay(swatchHighlightOverlay, { visible: false }); - Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf(parameter)], { - color: optionsSettings.currentColor.value, - dimensions: UI_ELEMENTS.swatch.properties.dimensions - }); - optionsSettings[parameter].value = ""; // Emulate Settings.getValue() returning "" for nonexistent setting. - Settings.setValue(optionsSettings[parameter].key, null); // Delete settings value. - break; - default: - App.log(side, "ERROR: ToolsMenu: Unexpected command! " + command); + case "clearSwatch": + // Remove highlight ring and change swatch to current color. + Overlays.editOverlay(swatchHighlightOverlay, { visible: false }); + Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf(parameter)], { + color: optionsSettings.currentColor.value, + dimensions: UI_ELEMENTS.swatch.properties.dimensions + }); + optionsSettings[parameter].value = ""; // Emulate Settings.getValue() returning "" for nonexistent setting. + Settings.setValue(optionsSettings[parameter].key, null); // Delete settings value. + break; + default: + App.log(side, "ERROR: ToolsMenu: Unexpected command! " + command); } } @@ -3044,7 +3043,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Overlays.editOverlay(menuHeaderHeadingOverlay, { url: MENU_HEADER_HEADING_PROPERTIES.highlightURL, localPosition: Vec3.sum(MENU_HEADER_HEADING_PROPERTIES.localPosition, MENU_HEADER_HOVER_OFFSET), - emissive: true // TODO: This has no effect. + emissive: true // TODO: This has no effect. }); Overlays.editOverlay(menuHeaderBackOverlay, { color: UIT.colors.white @@ -3112,83 +3111,83 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (hoveredItem !== NONE) { // Unhover old item. switch (hoveredElementType) { - case "menuButton": - if (hoveredSourceOverlays === menuOverlays) { - menuButtonHoverOverlays = menuHoverOverlays; - menuButtonIconOverlays = menuIconOverlays; - } else { - menuButtonHoverOverlays = footerHoverOverlays; - menuButtonIconOverlays = footerIconOverlays; - } - Overlays.editOverlay(menuButtonHoverOverlays[hoveredItem], { - localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, - visible: false - }); - Overlays.editOverlay(menuButtonIconOverlays[hoveredItem], { - color: UI_ELEMENTS.menuButton.icon.properties.color - }); - break; - case "button": - if (hoveredSourceItems[hoveredItem].enabledColor !== undefined && optionsEnabled[hoveredItem]) { - color = hoveredSourceItems[hoveredItem].enabledColor; - } else { - color = hoveredSourceItems[hoveredItem].properties.color !== undefined - ? hoveredSourceItems[hoveredItem].properties.color - : UI_ELEMENTS.button.properties.color; - } - Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { - color: color, - localPosition: hoveredSourceItems[hoveredItem].properties.localPosition - }); - break; - case "toggleButton": - color = optionsToggles[hoveredSourceItems[hoveredItem].id] - ? UI_ELEMENTS.toggleButton.onColor - : UI_ELEMENTS.toggleButton.offColor; - Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { - color: color, - localPosition: hoveredSourceItems[hoveredItem].properties.localPosition - }); - break; - case "swatch": - Overlays.editOverlay(swatchHighlightOverlay, { visible: false }); - color = optionsSettings[hoveredSourceItems[hoveredItem].id].value; - Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { - dimensions: UI_ELEMENTS.swatch.properties.dimensions, - color: color === "" ? EMPTY_SWATCH_COLOR : color, - localPosition: hoveredSourceItems[hoveredItem].properties.localPosition - }); - break; - case "barSlider": - case "imageSlider": - case "colorCircle": - // Lower old slider or color circle. - Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { - localPosition: hoveredSourceItems[hoveredItem].properties.localPosition - }); - break; - case "picklist": - if (hoveredSourceItems[hoveredItem].type !== "picklistItem" && !isPicklistOpen) { - Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { - localPosition: hoveredSourceItems[hoveredItem].properties.localPosition, - color: UI_ELEMENTS.picklist.properties.color + case "menuButton": + if (hoveredSourceOverlays === menuOverlays) { + menuButtonHoverOverlays = menuHoverOverlays; + menuButtonIconOverlays = menuIconOverlays; + } else { + menuButtonHoverOverlays = footerHoverOverlays; + menuButtonIconOverlays = footerIconOverlays; + } + Overlays.editOverlay(menuButtonHoverOverlays[hoveredItem], { + localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, + visible: false }); - } else { - Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { - color: UIT.colors.darkGray + Overlays.editOverlay(menuButtonIconOverlays[hoveredItem], { + color: UI_ELEMENTS.menuButton.icon.properties.color }); - } - break; - case "picklistItem": - Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { - color: UI_ELEMENTS.picklistItem.properties.color - }); - break; - case null: - // Nothing to do. - break; - default: - App.log(side, "ERROR: ToolsMenu: Unexpected hover item! " + hoveredElementType); + break; + case "button": + if (hoveredSourceItems[hoveredItem].enabledColor !== undefined && optionsEnabled[hoveredItem]) { + color = hoveredSourceItems[hoveredItem].enabledColor; + } else { + color = hoveredSourceItems[hoveredItem].properties.color !== undefined + ? hoveredSourceItems[hoveredItem].properties.color + : UI_ELEMENTS.button.properties.color; + } + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { + color: color, + localPosition: hoveredSourceItems[hoveredItem].properties.localPosition + }); + break; + case "toggleButton": + color = optionsToggles[hoveredSourceItems[hoveredItem].id] + ? UI_ELEMENTS.toggleButton.onColor + : UI_ELEMENTS.toggleButton.offColor; + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { + color: color, + localPosition: hoveredSourceItems[hoveredItem].properties.localPosition + }); + break; + case "swatch": + Overlays.editOverlay(swatchHighlightOverlay, { visible: false }); + color = optionsSettings[hoveredSourceItems[hoveredItem].id].value; + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { + dimensions: UI_ELEMENTS.swatch.properties.dimensions, + color: color === "" ? EMPTY_SWATCH_COLOR : color, + localPosition: hoveredSourceItems[hoveredItem].properties.localPosition + }); + break; + case "barSlider": + case "imageSlider": + case "colorCircle": + // Lower old slider or color circle. + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { + localPosition: hoveredSourceItems[hoveredItem].properties.localPosition + }); + break; + case "picklist": + if (hoveredSourceItems[hoveredItem].type !== "picklistItem" && !isPicklistOpen) { + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { + localPosition: hoveredSourceItems[hoveredItem].properties.localPosition, + color: UI_ELEMENTS.picklist.properties.color + }); + } else { + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { + color: UIT.colors.darkGray + }); + } + break; + case "picklistItem": + Overlays.editOverlay(hoveredSourceOverlays[hoveredItem], { + color: UI_ELEMENTS.picklistItem.properties.color + }); + break; + case null: + // Nothing to do. + break; + default: + App.log(side, "ERROR: ToolsMenu: Unexpected hover item! " + hoveredElementType); } // Update status variables. @@ -3208,105 +3207,105 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Hover new item. switch (hoveredElementType) { - case "menuButton": - Feedback.play(otherSide, Feedback.HOVER_MENU_ITEM); - if (intersectionOverlays === menuOverlays) { - menuButtonHoverOverlays = menuHoverOverlays; - menuButtonIconOverlays = menuIconOverlays; - } else { - menuButtonHoverOverlays = footerHoverOverlays; - menuButtonIconOverlays = footerIconOverlays; - } - Overlays.editOverlay(menuButtonHoverOverlays[hoveredItem], { - localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, - MENU_HOVER_DELTA), - visible: true - }); - Overlays.editOverlay(menuButtonIconOverlays[hoveredItem], { - color: UI_ELEMENTS.menuButton.icon.highlightColor - }); - break; - case "button": - if (intersectionEnabled[hoveredItem]) { - Feedback.play(otherSide, Feedback.HOVER_BUTTON); - localPosition = intersectionItems[hoveredItem].properties.localPosition; - Overlays.editOverlay(intersectionOverlays[hoveredItem], { - color: intersectionItems[hoveredItem].highlightColor !== undefined - ? intersectionItems[hoveredItem].highlightColor - : UIT.colors.greenHighlight, - localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) - }); - } - break; - case "toggleButton": - if (intersectionEnabled[hoveredItem]) { - Feedback.play(otherSide, Feedback.HOVER_BUTTON); - localPosition = intersectionItems[hoveredItem].properties.localPosition; - Overlays.editOverlay(intersectionOverlays[hoveredItem], { - color: optionsToggles[intersectionItems[hoveredItem].id] - ? UI_ELEMENTS.toggleButton.onHoverColor - : UI_ELEMENTS.toggleButton.offHoverColor, - localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) - }); - } - break; - case "swatch": - Feedback.play(otherSide, Feedback.HOVER_BUTTON); - localPosition = intersectionItems[hoveredItem].properties.localPosition; - if (optionsSettings[intersectionItems[hoveredItem].id].value === "") { - // Swatch is empty; highlight it with current color. - Overlays.editOverlay(intersectionOverlays[hoveredItem], { - color: optionsSettings.currentColor.value, - localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) - }); - } else { - // Swatch is full; highlight it with ring. - Overlays.editOverlay(intersectionOverlays[hoveredItem], { - dimensions: Vec3.subtract(UI_ELEMENTS.swatch.properties.dimensions, - { x: SWATCH_HIGHLIGHT_DELTA, y: 0, z: SWATCH_HIGHLIGHT_DELTA }), - localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) - }); - Overlays.editOverlay(swatchHighlightOverlay, { - parentID: intersectionOverlays[hoveredItem], - localPosition: UI_ELEMENTS.swatchHighlight.properties.localPosition, + case "menuButton": + Feedback.play(otherSide, Feedback.HOVER_MENU_ITEM); + if (intersectionOverlays === menuOverlays) { + menuButtonHoverOverlays = menuHoverOverlays; + menuButtonIconOverlays = menuIconOverlays; + } else { + menuButtonHoverOverlays = footerHoverOverlays; + menuButtonIconOverlays = footerIconOverlays; + } + Overlays.editOverlay(menuButtonHoverOverlays[hoveredItem], { + localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, + MENU_HOVER_DELTA), visible: true }); - } - break; - case "barSlider": - case "imageSlider": - case "colorCircle": - Feedback.play(otherSide, Feedback.HOVER_BUTTON); - localPosition = intersectionItems[hoveredItem].properties.localPosition; - Overlays.editOverlay(intersectionOverlays[hoveredItem], { - localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) - }); - break; - case "picklist": - Feedback.play(otherSide, Feedback.HOVER_BUTTON); - if (!isPicklistOpen) { + Overlays.editOverlay(menuButtonIconOverlays[hoveredItem], { + color: UI_ELEMENTS.menuButton.icon.highlightColor + }); + break; + case "button": + if (intersectionEnabled[hoveredItem]) { + Feedback.play(otherSide, Feedback.HOVER_BUTTON); + localPosition = intersectionItems[hoveredItem].properties.localPosition; + Overlays.editOverlay(intersectionOverlays[hoveredItem], { + color: intersectionItems[hoveredItem].highlightColor !== undefined + ? intersectionItems[hoveredItem].highlightColor + : UIT.colors.greenHighlight, + localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) + }); + } + break; + case "toggleButton": + if (intersectionEnabled[hoveredItem]) { + Feedback.play(otherSide, Feedback.HOVER_BUTTON); + localPosition = intersectionItems[hoveredItem].properties.localPosition; + Overlays.editOverlay(intersectionOverlays[hoveredItem], { + color: optionsToggles[intersectionItems[hoveredItem].id] + ? UI_ELEMENTS.toggleButton.onHoverColor + : UI_ELEMENTS.toggleButton.offHoverColor, + localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) + }); + } + break; + case "swatch": + Feedback.play(otherSide, Feedback.HOVER_BUTTON); + localPosition = intersectionItems[hoveredItem].properties.localPosition; + if (optionsSettings[intersectionItems[hoveredItem].id].value === "") { + // Swatch is empty; highlight it with current color. + Overlays.editOverlay(intersectionOverlays[hoveredItem], { + color: optionsSettings.currentColor.value, + localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) + }); + } else { + // Swatch is full; highlight it with ring. + Overlays.editOverlay(intersectionOverlays[hoveredItem], { + dimensions: Vec3.subtract(UI_ELEMENTS.swatch.properties.dimensions, + { x: SWATCH_HIGHLIGHT_DELTA, y: 0, z: SWATCH_HIGHLIGHT_DELTA }), + localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) + }); + Overlays.editOverlay(swatchHighlightOverlay, { + parentID: intersectionOverlays[hoveredItem], + localPosition: UI_ELEMENTS.swatchHighlight.properties.localPosition, + visible: true + }); + } + break; + case "barSlider": + case "imageSlider": + case "colorCircle": + Feedback.play(otherSide, Feedback.HOVER_BUTTON); localPosition = intersectionItems[hoveredItem].properties.localPosition; Overlays.editOverlay(intersectionOverlays[hoveredItem], { - localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA), - color: UIT.colors.greenHighlight + localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA) }); - } else { + break; + case "picklist": + Feedback.play(otherSide, Feedback.HOVER_BUTTON); + if (!isPicklistOpen) { + localPosition = intersectionItems[hoveredItem].properties.localPosition; + Overlays.editOverlay(intersectionOverlays[hoveredItem], { + localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA), + color: UIT.colors.greenHighlight + }); + } else { + Overlays.editOverlay(intersectionOverlays[hoveredItem], { + color: UIT.colors.greenHighlight + }); + } + break; + case "picklistItem": + Feedback.play(otherSide, Feedback.HOVER_BUTTON); Overlays.editOverlay(intersectionOverlays[hoveredItem], { color: UIT.colors.greenHighlight }); - } - break; - case "picklistItem": - Feedback.play(otherSide, Feedback.HOVER_BUTTON); - Overlays.editOverlay(intersectionOverlays[hoveredItem], { - color: UIT.colors.greenHighlight - }); - break; - case null: - // Nothing to do. - break; - default: - App.log(side, "ERROR: ToolsMenu: Unexpected hover item! " + hoveredElementType); + break; + case null: + // Nothing to do. + break; + default: + App.log(side, "ERROR: ToolsMenu: Unexpected hover item! " + hoveredElementType); } } @@ -3469,8 +3468,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }); if (intersectionItems[intersectedItem].command) { // Cartesian planar coordinates. - x = -delta.z; // Coordinates based on rotate cylinder entity. TODO: Use FBX model instead of cylinder entity. - y = -delta.x; // "" + x = -delta.z; // Coordinates based on rotate cylinder entity. TODO: Use FBX model instead of cylinder entity. + y = -delta.x; // "" s = Math.sqrt(x * x + y * y) / hsvControl.circle.radius; h = Math.atan2(y, x) / (2 * Math.PI); if (h < 0) { @@ -3648,7 +3647,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { closeOptions(); } - Overlays.deleteOverlay(menuOriginOverlay); // Automatically deletes all other overlays because they're children. + Overlays.deleteOverlay(menuOriginOverlay); // Automatically deletes all other overlays because they're children. menuOverlays = []; menuHoverOverlays = []; menuIconOverlays = []; diff --git a/scripts/shapes/modules/uit.js b/scripts/shapes/modules/uit.js index 7ad6e67c56..f79be9fc0c 100644 --- a/scripts/shapes/modules/uit.js +++ b/scripts/shapes/modules/uit.js @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global UIT */ +/* global UIT:true */ UIT = (function () { // User Interface Toolkit. Global object. @@ -32,10 +32,10 @@ UIT = (function () { // Coordinate system: UI lies in x-y plane with the front surface being +z. // Offsets are relative to parents' centers. dimensions: { - canvas: { x: 0.24, y: 0.296 }, // Overall UI size. - canvasSeparation: 0.004, // Gap between Tools menu and Create panel. - handOffset: 0.085, // Distance from hand (wrist) joint to center of canvas. - handLateralOffset: 0.01, // Offset of UI in direction of palm normal. + canvas: { x: 0.24, y: 0.296 }, // Overall UI size. + canvasSeparation: 0.004, // Gap between Tools menu and Create panel. + handOffset: 0.085, // Distance from hand (wrist) joint to center of canvas. + handLateralOffset: 0.01, // Offset of UI in direction of palm normal. topMargin: 0.010, leftMargin: 0.0118, @@ -47,24 +47,24 @@ UIT = (function () { panel: { x: 0.24, y: 0.236, z: 0.008 }, footerHeight: 0.056, - itemCollisionZone: { x: 0.0481, y: 0.0480, z: 0.0040 }, // Cursor intersection zone for Tools and Create items. + itemCollisionZone: { x: 0.0481, y: 0.0480, z: 0.0040 }, // Cursor intersection zone for Tools and Create items. - buttonDimensions: { x: 0.2164, y: 0.0840, z: 0.0040 }, // Default size of large single options button. + buttonDimensions: { x: 0.2164, y: 0.0840, z: 0.0040 }, // Default size of large single options button. menuButtonDimensions: { x: 0.0267, y: 0.0267, z: 0.0040 }, - menuButtonIconOffset: { x: 0, y: 0.00935, z: -0.0040 }, // Non-hovered position relative to itemCollisionZone. - menuButtonLabelYOffset: -0.00915, // Relative to itemCollisionZone. - menuButtonSublabelYOffset: -0.01775, // Relative to itemCollisionZone. + menuButtonIconOffset: { x: 0, y: 0.00935, z: -0.0040 }, // Non-hovered position relative to itemCollisionZone. + menuButtonLabelYOffset: -0.00915, // Relative to itemCollisionZone. + menuButtonSublabelYOffset: -0.01775, // Relative to itemCollisionZone. paletteItemButtonDimensions: { x: 0.0481, y: 0.0480, z: 0.0020 }, - paletteItemButtonOffset: { x: 0, y: 0, z: -0.0020 }, // Non-hovered position relative to itemCollisionZone. + paletteItemButtonOffset: { x: 0, y: 0, z: -0.0020 }, // Non-hovered position relative to itemCollisionZone. paletteItemButtonHoveredOffset: { x: 0, y: 0, z: -0.0010 }, paletteItemIconDimensions: { x: 0.024, y: 0.024, z: 0.024 }, - paletteItemIconOffset: { x: 0, y: 0, z: 0.015 }, // Non-hovered position relative to palette button. + paletteItemIconOffset: { x: 0, y: 0, z: 0.015 }, // Non-hovered position relative to palette button. horizontalRuleHeight : 0.0004, - imageOverlayOffset: 0.001 // Raise image above surface. + imageOverlayOffset: 0.001 // Raise image above surface. } }; }()); diff --git a/scripts/shapes/shapes.js b/scripts/shapes/shapes.js index 2154262229..e0a066b3c7 100644 --- a/scripts/shapes/shapes.js +++ b/scripts/shapes/shapes.js @@ -8,6 +8,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* global Feedback, History */ + (function () { "use strict"; @@ -18,7 +20,7 @@ APP_ICON_DISABLED = Script.resolvePath("./assets/shapes-d.svg"), ENABLED_CAPTION_COLOR_OVERRIDE = "", DISABLED_CAPTION_COLOR_OVERRIDE = "#888888", - START_DELAY = 2000, // ms + START_DELAY = 2000, // ms // Application state isAppActive, @@ -132,7 +134,7 @@ intersection = {}; - if (!this instanceof Inputs) { + if (!(this instanceof Inputs)) { return new Inputs(); } @@ -225,10 +227,9 @@ isDisplaying = false, - getIntersection; // Function. + getIntersection; // Function. - - if (!this instanceof UI) { + if (!(this instanceof UI)) { return new UI(); } @@ -343,17 +344,17 @@ handles, // References. - otherEditor, // Other hand's Editor object. + otherEditor, // Other hand's Editor object. hand, laser, // Editor states. EDITOR_IDLE = 0, EDITOR_SEARCHING = 1, - EDITOR_HIGHLIGHTING = 2, // Highlighting an entity (not hovering a handle). + EDITOR_HIGHLIGHTING = 2, // Highlighting an entity (not hovering a handle). EDITOR_GRABBING = 3, - EDITOR_DIRECT_SCALING = 4, // Scaling data are sent to other editor's EDITOR_GRABBING state. - EDITOR_HANDLE_SCALING = 5, // "" + EDITOR_DIRECT_SCALING = 4, // Scaling data are sent to other editor's EDITOR_GRABBING state. + EDITOR_HANDLE_SCALING = 5, // "" EDITOR_CLONING = 6, EDITOR_GROUPING = 7, EDITOR_STATE_STRINGS = ["EDITOR_IDLE", "EDITOR_SEARCHING", "EDITOR_HIGHLIGHTING", "EDITOR_GRABBING", @@ -362,8 +363,8 @@ // State machine. STATE_MACHINE, - intersectedEntityID = null, // Intersected entity of highlighted entity set. - rootEntityID = null, // Root entity of highlighted entity set. + intersectedEntityID = null, // Intersected entity of highlighted entity set. + rootEntityID = null, // Root entity of highlighted entity set. wasScaleTool = false, isOtherEditorEditingEntityID = false, isTriggerClicked = false, @@ -380,8 +381,8 @@ // Scaling values. isScalingWithHand = false, - isDirectScaling = false, // Modifies EDITOR_GRABBING state. - isHandleScaling = false, // "" + isDirectScaling = false, // Modifies EDITOR_GRABBING state. + isHandleScaling = false, // "" initialTargetsSeparation, initialtargetsDirection, otherTargetPosition, @@ -394,12 +395,12 @@ MIN_SCALE = 0.001, MIN_SCALE_HANDLE_DISTANCE = 0.0001, - getIntersection, // Function. + getIntersection, // Function. intersection, isUIVisible = true; - if (!this instanceof Editor) { + if (!(this instanceof Editor)) { return new Editor(); } @@ -408,12 +409,12 @@ handles = new Handles(side); function setReferences(inputs, editor) { - hand = inputs.hand(); // Object. - laser = inputs.laser(); // Object. - getIntersection = inputs.intersection; // Function. - otherEditor = editor; // Object. + hand = inputs.hand(); // Object. + laser = inputs.laser(); // Object. + getIntersection = inputs.intersection; // Function. + otherEditor = editor; // Object. - laserOffset = laser.handOffset(); // Value. + laserOffset = laser.handOffset(); // Value. highlights.setHandHighlightRadius(hand.getNearGrabRadius()); } @@ -546,8 +547,8 @@ // Initial handle offset from center of selection. selectionPositionAndOrientation = selection.getPositionAndOrientation(); - handleUnitScaleAxis = handles.scalingAxis(overlayID); // Unit vector in direction of scaling. - handleScaleDirections = handles.scalingDirections(overlayID); // Which axes to scale the selection on. + handleUnitScaleAxis = handles.scalingAxis(overlayID); // Unit vector in direction of scaling. + handleScaleDirections = handles.scalingDirections(overlayID); // Which axes to scale the selection on. scaleAxis = Vec3.multiplyQbyV(selectionPositionAndOrientation.orientation, handleUnitScaleAxis); handleTargetOffset = handles.handleOffset(overlayID) + Vec3.dot(Vec3.subtract(otherTargetPosition, Overlays.getProperty(overlayID, "position")), scaleAxis); @@ -571,7 +572,7 @@ if (isHandleScaling) { handles.finishScaling(); selection.finishHandleScaling(); - handles.grab(null); // Stop highlighting grabbed handle and resume displaying all handles. + handles.grab(null); // Stop highlighting grabbed handle and resume displaying all handles. isHandleScaling = false; } } @@ -733,7 +734,7 @@ } function enterEditorGrabbing() { - selection.select(intersectedEntityID); // For when transitioning from EDITOR_SEARCHING. + selection.select(intersectedEntityID); // For when transitioning from EDITOR_SEARCHING. if (intersection.laserIntersected) { laser.setLength(laser.length()); } else { @@ -769,7 +770,7 @@ } function enterEditorDirectScaling() { - selection.select(intersectedEntityID); // In case need to transition to EDITOR_GRABBING. + selection.select(intersectedEntityID); // In case need to transition to EDITOR_GRABBING. isScalingWithHand = intersection.handIntersected; if (intersection.laserIntersected) { laser.setLength(laser.length()); @@ -790,7 +791,7 @@ } function enterEditorHandleScaling() { - selection.select(intersectedEntityID); // In case need to transition to EDITOR_GRABBING. + selection.select(intersectedEntityID); // In case need to transition to EDITOR_GRABBING. isScalingWithHand = intersection.handIntersected; if (intersection.laserIntersected) { laser.setLength(laser.length()); @@ -812,7 +813,7 @@ function enterEditorCloning() { Feedback.play(side, Feedback.CLONE_ENTITY); - selection.select(intersectedEntityID); // For when transitioning from EDITOR_SEARCHING. + selection.select(intersectedEntityID); // For when transitioning from EDITOR_SEARCHING. selection.cloneEntities(); intersectedEntityID = selection.intersectedEntityID(); rootEntityID = selection.rootEntityID(); @@ -938,290 +939,290 @@ // State update. switch (editorState) { - case EDITOR_IDLE: - if (!hand.valid()) { - // No transition. + case EDITOR_IDLE: + if (!hand.valid()) { + // No transition. + break; + } + setState(EDITOR_SEARCHING); break; - } - setState(EDITOR_SEARCHING); - break; - case EDITOR_SEARCHING: - if (hand.valid() - && !(intersection.overlayID && !wasTriggerClicked && isTriggerClicked - && otherEditor.isHandle(intersection.overlayID)) - && !(intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) + case EDITOR_SEARCHING: + if (hand.valid() + && !(intersection.overlayID && !wasTriggerClicked && isTriggerClicked + && otherEditor.isHandle(intersection.overlayID)) + && !(intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) + && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab + && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, hand.palmPosition()))) + && !(intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) + && (!wasTriggerClicked || isAutoGrab) && isTriggerClicked)) { + // No transition. + updateState(); + updateTool(); + break; + } + if (!hand.valid()) { + setState(EDITOR_IDLE); + } else if (intersection.overlayID && !wasTriggerClicked && isTriggerClicked + && otherEditor.isHandle(intersection.overlayID)) { + intersectedEntityID = otherEditor.intersectedEntityID(); + rootEntityID = otherEditor.rootEntityID(); + setState(EDITOR_HANDLE_SCALING); + } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab - && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, hand.palmPosition()))) - && !(intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) - && (!wasTriggerClicked || isAutoGrab) && isTriggerClicked)) { - // No transition. - updateState(); - updateTool(); - break; - } - if (!hand.valid()) { - setState(EDITOR_IDLE); - } else if (intersection.overlayID && !wasTriggerClicked && isTriggerClicked - && otherEditor.isHandle(intersection.overlayID)) { - intersectedEntityID = otherEditor.intersectedEntityID(); - rootEntityID = otherEditor.rootEntityID(); - setState(EDITOR_HANDLE_SCALING); - } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) - && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab - && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, hand.palmPosition()))) { - intersectedEntityID = intersection.entityID; - rootEntityID = Entities.rootOf(intersectedEntityID); - setState(EDITOR_HIGHLIGHTING); - } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) - && (!wasTriggerClicked || isAutoGrab) && isTriggerClicked) { - intersectedEntityID = intersection.entityID; - rootEntityID = Entities.rootOf(intersectedEntityID); - if (otherEditor.isEditing(rootEntityID)) { - if (toolSelected !== TOOL_SCALE) { - setState(EDITOR_DIRECT_SCALING); - } - } else if (toolSelected === TOOL_CLONE) { - setState(EDITOR_CLONING); - } else if (toolSelected === TOOL_GROUP || toolSelected === TOOL_GROUP_BOX) { - setState(EDITOR_GROUPING); - } else if (toolSelected === TOOL_COLOR) { - setState(EDITOR_HIGHLIGHTING); - if (selection.applyColor(colorToolColor, false)) { - Feedback.play(side, Feedback.APPLY_PROPERTY); - } else { - Feedback.play(side, Feedback.APPLY_ERROR); - } - } else if (toolSelected === TOOL_PICK_COLOR) { - color = selection.getColor(intersection.entityID); - if (color) { - colorToolColor = color; - ui.doPickColor(colorToolColor); - } else { - Feedback.play(side, Feedback.APPLY_ERROR); - } - toolSelected = TOOL_COLOR; - ui.setToolIcon(ui.COLOR_TOOL); - } else if (toolSelected === TOOL_PHYSICS) { - setState(EDITOR_HIGHLIGHTING); - selection.applyPhysics(physicsToolPhysics); - } else if (toolSelected === TOOL_DELETE) { - setState(EDITOR_HIGHLIGHTING); - Feedback.play(side, Feedback.DELETE_ENTITY); - selection.deleteEntities(); - setState(EDITOR_SEARCHING); - } else { - setState(EDITOR_GRABBING); - } - } else { - log(side, "ERROR: Editor: Unexpected condition in EDITOR_SEARCHING!"); - } - break; - case EDITOR_HIGHLIGHTING: - if (hand.valid() - && intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) - && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, hand.palmPosition())) - && !(!wasTriggerClicked && isTriggerClicked - && (!otherEditor.isEditing(rootEntityID) || toolSelected !== TOOL_SCALE)) - && !(!wasTriggerClicked && isTriggerClicked && intersection.overlayID - && otherEditor.isHandle(intersection.overlayID))) { - // No transition. - doUpdateState = false; - if (otherEditor.isEditing(rootEntityID) !== isOtherEditorEditingEntityID) { - doUpdateState = true; - } - if (Entities.rootOf(intersection.entityID) !== rootEntityID) { + && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, hand.palmPosition()))) { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); - doUpdateState = true; - } - if ((toolSelected === TOOL_SCALE) !== wasScaleTool) { - wasScaleTool = toolSelected === TOOL_SCALE; - doUpdateState = true; - } - if (toolSelected === TOOL_COLOR && intersection.entityID !== intersectedEntityID) { + setState(EDITOR_HIGHLIGHTING); + } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) + && (!wasTriggerClicked || isAutoGrab) && isTriggerClicked) { intersectedEntityID = intersection.entityID; - doUpdateState = true; - } - if ((toolSelected === TOOL_COLOR || toolSelected === TOOL_PHYSICS) - && intersection.entityID !== intersectedEntityID) { - intersectedEntityID = intersection.entityID; - doUpdateState = true; - } - if (doUpdateState) { - updateState(); - } - updateTool(); - break; - } - if (!hand.valid()) { - setState(EDITOR_IDLE); - } else if (intersection.overlayID && !wasTriggerClicked && isTriggerClicked - && otherEditor.isHandle(intersection.overlayID)) { - intersectedEntityID = otherEditor.intersectedEntityID(); - rootEntityID = otherEditor.rootEntityID(); - setState(EDITOR_HANDLE_SCALING); - } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) - && !wasTriggerClicked && isTriggerClicked) { - intersectedEntityID = intersection.entityID; // May be a different entityID. - rootEntityID = Entities.rootOf(intersectedEntityID); - if (otherEditor.isEditing(rootEntityID)) { - if (toolSelected !== TOOL_SCALE) { - setState(EDITOR_DIRECT_SCALING); - } else { - log(side, "ERROR: Editor: Unexpected condition A in EDITOR_HIGHLIGHTING!"); - } - } else if (toolSelected === TOOL_CLONE) { - setState(EDITOR_CLONING); - } else if (toolSelected === TOOL_GROUP || toolSelected === TOOL_GROUP_BOX) { - setState(EDITOR_GROUPING); - } else if (toolSelected === TOOL_COLOR) { - if (selection.applyColor(colorToolColor, false)) { - Feedback.play(side, Feedback.APPLY_PROPERTY); - } else { - Feedback.play(side, Feedback.APPLY_ERROR); - } - } else if (toolSelected === TOOL_PICK_COLOR) { - color = selection.getColor(intersection.entityID); - if (color) { - colorToolColor = color; - ui.doPickColor(colorToolColor); + rootEntityID = Entities.rootOf(intersectedEntityID); + if (otherEditor.isEditing(rootEntityID)) { + if (toolSelected !== TOOL_SCALE) { + setState(EDITOR_DIRECT_SCALING); + } + } else if (toolSelected === TOOL_CLONE) { + setState(EDITOR_CLONING); + } else if (toolSelected === TOOL_GROUP || toolSelected === TOOL_GROUP_BOX) { + setState(EDITOR_GROUPING); + } else if (toolSelected === TOOL_COLOR) { + setState(EDITOR_HIGHLIGHTING); + if (selection.applyColor(colorToolColor, false)) { + Feedback.play(side, Feedback.APPLY_PROPERTY); + } else { + Feedback.play(side, Feedback.APPLY_ERROR); + } + } else if (toolSelected === TOOL_PICK_COLOR) { + color = selection.getColor(intersection.entityID); + if (color) { + colorToolColor = color; + ui.doPickColor(colorToolColor); + } else { + Feedback.play(side, Feedback.APPLY_ERROR); + } toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); + } else if (toolSelected === TOOL_PHYSICS) { + setState(EDITOR_HIGHLIGHTING); + selection.applyPhysics(physicsToolPhysics); + } else if (toolSelected === TOOL_DELETE) { + setState(EDITOR_HIGHLIGHTING); + Feedback.play(side, Feedback.DELETE_ENTITY); + selection.deleteEntities(); + setState(EDITOR_SEARCHING); } else { - Feedback.play(side, Feedback.APPLY_ERROR); + setState(EDITOR_GRABBING); } - } else if (toolSelected === TOOL_PHYSICS) { - selection.applyPhysics(physicsToolPhysics); - } else if (toolSelected === TOOL_DELETE) { - Feedback.play(side, Feedback.DELETE_ENTITY); - selection.deleteEntities(); + } else { + log(side, "ERROR: Editor: Unexpected condition in EDITOR_SEARCHING!"); + } + break; + case EDITOR_HIGHLIGHTING: + if (hand.valid() + && intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) + && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, hand.palmPosition())) + && !(!wasTriggerClicked && isTriggerClicked + && (!otherEditor.isEditing(rootEntityID) || toolSelected !== TOOL_SCALE)) + && !(!wasTriggerClicked && isTriggerClicked && intersection.overlayID + && otherEditor.isHandle(intersection.overlayID))) { + // No transition. + doUpdateState = false; + if (otherEditor.isEditing(rootEntityID) !== isOtherEditorEditingEntityID) { + doUpdateState = true; + } + if (Entities.rootOf(intersection.entityID) !== rootEntityID) { + intersectedEntityID = intersection.entityID; + rootEntityID = Entities.rootOf(intersectedEntityID); + doUpdateState = true; + } + if ((toolSelected === TOOL_SCALE) !== wasScaleTool) { + wasScaleTool = toolSelected === TOOL_SCALE; + doUpdateState = true; + } + if (toolSelected === TOOL_COLOR && intersection.entityID !== intersectedEntityID) { + intersectedEntityID = intersection.entityID; + doUpdateState = true; + } + if ((toolSelected === TOOL_COLOR || toolSelected === TOOL_PHYSICS) + && intersection.entityID !== intersectedEntityID) { + intersectedEntityID = intersection.entityID; + doUpdateState = true; + } + if (doUpdateState) { + updateState(); + } + updateTool(); + break; + } + if (!hand.valid()) { + setState(EDITOR_IDLE); + } else if (intersection.overlayID && !wasTriggerClicked && isTriggerClicked + && otherEditor.isHandle(intersection.overlayID)) { + intersectedEntityID = otherEditor.intersectedEntityID(); + rootEntityID = otherEditor.rootEntityID(); + setState(EDITOR_HANDLE_SCALING); + } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) + && !wasTriggerClicked && isTriggerClicked) { + intersectedEntityID = intersection.entityID; // May be a different entityID. + rootEntityID = Entities.rootOf(intersectedEntityID); + if (otherEditor.isEditing(rootEntityID)) { + if (toolSelected !== TOOL_SCALE) { + setState(EDITOR_DIRECT_SCALING); + } else { + log(side, "ERROR: Editor: Unexpected condition A in EDITOR_HIGHLIGHTING!"); + } + } else if (toolSelected === TOOL_CLONE) { + setState(EDITOR_CLONING); + } else if (toolSelected === TOOL_GROUP || toolSelected === TOOL_GROUP_BOX) { + setState(EDITOR_GROUPING); + } else if (toolSelected === TOOL_COLOR) { + if (selection.applyColor(colorToolColor, false)) { + Feedback.play(side, Feedback.APPLY_PROPERTY); + } else { + Feedback.play(side, Feedback.APPLY_ERROR); + } + } else if (toolSelected === TOOL_PICK_COLOR) { + color = selection.getColor(intersection.entityID); + if (color) { + colorToolColor = color; + ui.doPickColor(colorToolColor); + toolSelected = TOOL_COLOR; + ui.setToolIcon(ui.COLOR_TOOL); + } else { + Feedback.play(side, Feedback.APPLY_ERROR); + } + } else if (toolSelected === TOOL_PHYSICS) { + selection.applyPhysics(physicsToolPhysics); + } else if (toolSelected === TOOL_DELETE) { + Feedback.play(side, Feedback.DELETE_ENTITY); + selection.deleteEntities(); + setState(EDITOR_SEARCHING); + } else { + setState(EDITOR_GRABBING); + } + } else if (!intersection.entityID || !intersection.editableEntity + || (!isTriggerPressed && !isCameraOutsideEntity(intersection.entityID, hand.palmPosition()))) { setState(EDITOR_SEARCHING); } else { - setState(EDITOR_GRABBING); + log(side, "ERROR: Editor: Unexpected condition B in EDITOR_HIGHLIGHTING!"); } - } else if (!intersection.entityID || !intersection.editableEntity - || (!isTriggerPressed && !isCameraOutsideEntity(intersection.entityID, hand.palmPosition()))) { - setState(EDITOR_SEARCHING); - } else { - log(side, "ERROR: Editor: Unexpected condition B in EDITOR_HIGHLIGHTING!"); - } - break; - case EDITOR_GRABBING: - if (hand.valid() && isTriggerClicked && !isGripClicked) { - // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. - // No transition. - if ((toolSelected === TOOL_SCALE) !== wasScaleTool) { + break; + case EDITOR_GRABBING: + if (hand.valid() && isTriggerClicked && !isGripClicked) { + // Don't test intersection.intersected because when scaling with handles intersection may lag behind. + // No transition. + if ((toolSelected === TOOL_SCALE) !== wasScaleTool) { + updateState(); + wasScaleTool = toolSelected === TOOL_SCALE; + } + // updateTool(); Don't updateTool() because grip button is used to delete grabbed entity. + break; + } + if (!hand.valid()) { + setState(EDITOR_IDLE); + } else if (!isTriggerClicked) { + if (intersection.entityID && intersection.editableEntity) { + intersectedEntityID = intersection.entityID; + rootEntityID = Entities.rootOf(intersectedEntityID); + setState(EDITOR_HIGHLIGHTING); + } else { + setState(EDITOR_SEARCHING); + } + } else if (isGripClicked) { + if (!wasGripClicked) { + Feedback.play(side, Feedback.DELETE_ENTITY); + selection.deleteEntities(); + setState(EDITOR_SEARCHING); + } + } else { + log(side, "ERROR: Editor: Unexpected condition in EDITOR_GRABBING!"); + } + break; + case EDITOR_DIRECT_SCALING: + if (hand.valid() && isTriggerClicked + && (otherEditor.isEditing(rootEntityID) || otherEditor.isHandle(intersection.overlayID))) { + // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. + // Don't test toolSelected === TOOL_SCALE because this is a UI element and so not able to be changed while + // scaling with two hands. + // No transition. updateState(); - wasScaleTool = toolSelected === TOOL_SCALE; + // updateTool(); Don't updateTool() because this hand is currently using the scaling tool. + break; + } + if (!hand.valid()) { + setState(EDITOR_IDLE); + } else if (!isTriggerClicked) { + if (!intersection.entityID || !intersection.editableEntity) { + setState(EDITOR_SEARCHING); + } else { + intersectedEntityID = intersection.entityID; + rootEntityID = Entities.rootOf(intersectedEntityID); + setState(EDITOR_HIGHLIGHTING); + } + } else if (!otherEditor.isEditing(rootEntityID)) { + // Grab highlightEntityID that was scaling and has already been set. + setState(EDITOR_GRABBING); + } else { + log(side, "ERROR: Editor: Unexpected condition in EDITOR_DIRECT_SCALING!"); } - //updateTool(); Don't updateTool() because grip button is used to delete grabbed entity. break; - } - if (!hand.valid()) { - setState(EDITOR_IDLE); - } else if (!isTriggerClicked) { - if (intersection.entityID && intersection.editableEntity) { - intersectedEntityID = intersection.entityID; - rootEntityID = Entities.rootOf(intersectedEntityID); - setState(EDITOR_HIGHLIGHTING); + case EDITOR_HANDLE_SCALING: + if (hand.valid() && isTriggerClicked && otherEditor.isEditing(rootEntityID)) { + // Don't test intersection.intersected because when scaling with handles intersection may lag behind. + // Don't test toolSelected === TOOL_SCALE because this is a UI element and so not able to be changed + // while scaling with two hands. + // No transition. + updateState(); + updateTool(); + break; + } + if (!hand.valid()) { + setState(EDITOR_IDLE); + } else if (!isTriggerClicked) { + if (!intersection.entityID || !intersection.editableEntity) { + setState(EDITOR_SEARCHING); + } else { + intersectedEntityID = intersection.entityID; + rootEntityID = Entities.rootOf(intersectedEntityID); + setState(EDITOR_HIGHLIGHTING); + } + } else if (!otherEditor.isEditing(rootEntityID)) { + // Grab highlightEntityID that was scaling and has already been set. + setState(EDITOR_GRABBING); + } else { + log(side, "ERROR: Editor: Unexpected condition in EDITOR_HANDLE_SCALING!"); + } + break; + case EDITOR_CLONING: + // Immediate transition out of state after cloning entities during state entry. + if (hand.valid() && isTriggerClicked) { + setState(EDITOR_GRABBING); + } else if (!hand.valid()) { + setState(EDITOR_IDLE); + } else if (!isTriggerClicked) { + if (intersection.entityID && intersection.editableEntity) { + intersectedEntityID = intersection.entityID; + rootEntityID = Entities.rootOf(intersectedEntityID); + setState(EDITOR_HIGHLIGHTING); + } else { + setState(EDITOR_SEARCHING); + } + } else { + log(side, "ERROR: Editor: Unexpected condition in EDITOR_CLONING!"); + } + break; + case EDITOR_GROUPING: + // Immediate transition out of state after updating group data during state entry. + if (hand.valid() && isTriggerClicked) { + // No transition. + break; + } + if (!hand.valid()) { + setState(EDITOR_IDLE); } else { setState(EDITOR_SEARCHING); } - } else if (isGripClicked) { - if (!wasGripClicked) { - Feedback.play(side, Feedback.DELETE_ENTITY); - selection.deleteEntities(); - setState(EDITOR_SEARCHING); - } - } else { - log(side, "ERROR: Editor: Unexpected condition in EDITOR_GRABBING!"); - } - break; - case EDITOR_DIRECT_SCALING: - if (hand.valid() && isTriggerClicked - && (otherEditor.isEditing(rootEntityID) || otherEditor.isHandle(intersection.overlayID))) { - // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. - // Don't test toolSelected === TOOL_SCALE because this is a UI element and so not able to be changed while - // scaling with two hands. - // No transition. - updateState(); - // updateTool(); Don't updateTool() because this hand is currently using the scaling tool. break; - } - if (!hand.valid()) { - setState(EDITOR_IDLE); - } else if (!isTriggerClicked) { - if (!intersection.entityID || !intersection.editableEntity) { - setState(EDITOR_SEARCHING); - } else { - intersectedEntityID = intersection.entityID; - rootEntityID = Entities.rootOf(intersectedEntityID); - setState(EDITOR_HIGHLIGHTING); - } - } else if (!otherEditor.isEditing(rootEntityID)) { - // Grab highlightEntityID that was scaling and has already been set. - setState(EDITOR_GRABBING); - } else { - log(side, "ERROR: Editor: Unexpected condition in EDITOR_DIRECT_SCALING!"); - } - break; - case EDITOR_HANDLE_SCALING: - if (hand.valid() && isTriggerClicked && otherEditor.isEditing(rootEntityID)) { - // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. - // Don't test toolSelected === TOOL_SCALE because this is a UI element and so not able to be changed while - // scaling with two hands. - // No transition. - updateState(); - updateTool(); - break; - } - if (!hand.valid()) { - setState(EDITOR_IDLE); - } else if (!isTriggerClicked) { - if (!intersection.entityID || !intersection.editableEntity) { - setState(EDITOR_SEARCHING); - } else { - intersectedEntityID = intersection.entityID; - rootEntityID = Entities.rootOf(intersectedEntityID); - setState(EDITOR_HIGHLIGHTING); - } - } else if (!otherEditor.isEditing(rootEntityID)) { - // Grab highlightEntityID that was scaling and has already been set. - setState(EDITOR_GRABBING); - } else { - log(side, "ERROR: Editor: Unexpected condition in EDITOR_HANDLE_SCALING!"); - } - break; - case EDITOR_CLONING: - // Immediate transition out of state after cloning entities during state entry. - if (hand.valid() && isTriggerClicked) { - setState(EDITOR_GRABBING); - } else if (!hand.valid()) { - setState(EDITOR_IDLE); - } else if (!isTriggerClicked) { - if (intersection.entityID && intersection.editableEntity) { - intersectedEntityID = intersection.entityID; - rootEntityID = Entities.rootOf(intersectedEntityID); - setState(EDITOR_HIGHLIGHTING); - } else { - setState(EDITOR_SEARCHING); - } - } else { - log(side, "ERROR: Editor: Unexpected condition in EDITOR_CLONING!"); - } - break; - case EDITOR_GROUPING: - // Immediate transition out of state after updating group data during state entry. - if (hand.valid() && isTriggerClicked) { - // No transition. - break; - } - if (!hand.valid()) { - setState(EDITOR_IDLE); - } else { - setState(EDITOR_SEARCHING); - } - break; } wasTriggerClicked = isTriggerClicked; @@ -1235,15 +1236,15 @@ function apply() { switch (editorState) { - case EDITOR_GRABBING: - if (isDirectScaling) { - applyDirectScale(); - } else if (isHandleScaling) { - applyHandleScale(); - } else { - applyGrab(); - } - break; + case EDITOR_GRABBING: + if (isDirectScaling) { + applyDirectScale(); + } else if (isHandleScaling) { + applyHandleScale(); + } else { + applyGrab(); + } + break; } } @@ -1302,8 +1303,8 @@ var groups, highlights, - selectInBoxSelection, // Selection of all entities selected. - groupSelection, // New group to add to selection. + selectInBoxSelection, // Selection of all entities selected. + groupSelection, // New group to add to selection. exludedLeftRootEntityID = null, exludedrightRootEntityID = null, excludedRootEntityIDs = [], @@ -1311,7 +1312,7 @@ hasSelectionChanged = false, isSelectInBox = false; - if (!this instanceof Grouping) { + if (!(this instanceof Grouping)) { return new Grouping(); } @@ -1611,201 +1612,201 @@ function onUICommand(command, parameter) { switch (command) { - case "scaleTool": - Feedback.play(dominantHand, Feedback.EQUIP_TOOL); - grouping.clear(); - toolSelected = TOOL_SCALE; - ui.setToolIcon(ui.SCALE_TOOL); - ui.updateUIOverlays(); - break; - case "cloneTool": - Feedback.play(dominantHand, Feedback.EQUIP_TOOL); - grouping.clear(); - toolSelected = TOOL_CLONE; - ui.setToolIcon(ui.CLONE_TOOL); - ui.updateUIOverlays(); - break; - case "groupTool": - Feedback.play(dominantHand, Feedback.EQUIP_TOOL); - toolSelected = TOOL_GROUP; - ui.setToolIcon(ui.GROUP_TOOL); - ui.updateUIOverlays(); - break; - case "colorTool": - Feedback.play(dominantHand, Feedback.EQUIP_TOOL); - grouping.clear(); - toolSelected = TOOL_COLOR; - ui.setToolIcon(ui.COLOR_TOOL); - colorToolColor = parameter; - ui.updateUIOverlays(); - break; - case "pickColorTool": - if (parameter) { + case "scaleTool": + Feedback.play(dominantHand, Feedback.EQUIP_TOOL); grouping.clear(); - toolSelected = TOOL_PICK_COLOR; + toolSelected = TOOL_SCALE; + ui.setToolIcon(ui.SCALE_TOOL); ui.updateUIOverlays(); - } else { + break; + case "cloneTool": + Feedback.play(dominantHand, Feedback.EQUIP_TOOL); + grouping.clear(); + toolSelected = TOOL_CLONE; + ui.setToolIcon(ui.CLONE_TOOL); + ui.updateUIOverlays(); + break; + case "groupTool": + Feedback.play(dominantHand, Feedback.EQUIP_TOOL); + toolSelected = TOOL_GROUP; + ui.setToolIcon(ui.GROUP_TOOL); + ui.updateUIOverlays(); + break; + case "colorTool": Feedback.play(dominantHand, Feedback.EQUIP_TOOL); grouping.clear(); - toolSelected = TOOL_COLOR; - ui.updateUIOverlays(); - } - break; - case "physicsTool": - Feedback.play(dominantHand, Feedback.EQUIP_TOOL); - grouping.clear(); - toolSelected = TOOL_PHYSICS; - ui.setToolIcon(ui.PHYSICS_TOOL); - ui.updateUIOverlays(); - break; - case "deleteTool": - Feedback.play(dominantHand, Feedback.EQUIP_TOOL); - grouping.clear(); - toolSelected = TOOL_DELETE; - ui.setToolIcon(ui.DELETE_TOOL); - ui.updateUIOverlays(); - break; - case "clearTool": - Feedback.play(dominantHand, Feedback.DROP_TOOL); - grouping.clear(); - toolSelected = TOOL_NONE; - ui.clearTool(); - ui.updateUIOverlays(); - break; - - case "groupButton": - Feedback.play(dominantHand, Feedback.APPLY_PROPERTY); - grouping.group(); - grouping.clear(); - toolSelected = TOOL_NONE; - ui.clearTool(); - ui.updateUIOverlays(); - break; - case "ungroupButton": - Feedback.play(dominantHand, Feedback.APPLY_PROPERTY); - grouping.ungroup(); - grouping.clear(); - toolSelected = TOOL_NONE; - ui.clearTool(); - ui.updateUIOverlays(); - break; - case "toggleGroupSelectionBoxTool": - toolSelected = parameter ? TOOL_GROUP_BOX : TOOL_GROUP; - if (toolSelected === TOOL_GROUP_BOX) { - grouping.startSelectInBox(); - } else { - grouping.stopSelectInBox(); - } - break; - case "clearGroupSelectionTool": - if (grouping.groupsCount() > 0) { - Feedback.play(dominantHand, Feedback.SELECT_ENTITY); - } - grouping.clear(); - if (toolSelected === TOOL_GROUP_BOX) { - grouping.startSelectInBox(); - } - break; - - case "setColor": - if (toolSelected === TOOL_PICK_COLOR) { toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); - } - colorToolColor = parameter; - break; - - case "setGravityOn": - // Dynamic is true if the entity has gravity or is grabbable. - if (parameter) { - physicsToolPhysics.gravity = { x: 0, y: physicsToolGravity, z: 0 }; - physicsToolPhysics.dynamic = true; - } else { - physicsToolPhysics.gravity = Vec3.ZERO; - physicsToolPhysics.dynamic = physicsToolPhysics.userData.grabbableKey.grabbable === true; - } - break; - case "setGrabOn": - // Dynamic is true if the entity has gravity or is grabbable. - physicsToolPhysics.userData.grabbableKey.grabbable = parameter; - physicsToolPhysics.dynamic = parameter - || (physicsToolPhysics.gravity && Vec3.length(physicsToolPhysics.gravity) > 0); - break; - case "setCollideOn": - if (parameter) { - physicsToolPhysics.collisionless = false; - physicsToolPhysics.collidesWith = "static,dynamic,kinematic,myAvatar,otherAvatar"; - } else { - physicsToolPhysics.collisionless = true; - physicsToolPhysics.collidesWith = ""; - } - break; - - case "setGravity": - if (parameter !== undefined) { - // Power range 0.0, 0.5, 1.0 maps to -50.0, -9.80665, 50.0. - physicsToolGravity = 82.36785162 * Math.pow(2.214065901, parameter) - 132.36785; - if (physicsToolPhysics.dynamic === true) { // Only apply if gravity is turned on. - physicsToolPhysics.gravity = { x: 0, y: physicsToolGravity, z: 0 }; + colorToolColor = parameter; + ui.updateUIOverlays(); + break; + case "pickColorTool": + if (parameter) { + grouping.clear(); + toolSelected = TOOL_PICK_COLOR; + ui.updateUIOverlays(); + } else { + Feedback.play(dominantHand, Feedback.EQUIP_TOOL); + grouping.clear(); + toolSelected = TOOL_COLOR; + ui.updateUIOverlays(); } - } - break; - case "setBounce": - if (parameter !== undefined) { - // Linear range from 0.0, 0.5, 1.0 maps to 0.0, 0.5, 1.0; - physicsToolPhysics.restitution = parameter; - } - break; - case "setFriction": - if (parameter !== undefined) { - // Power range 0.0, 0.5, 1.0 maps to 0, 0.39, 1.0. - physicsToolPhysics.damping = 0.69136364 * Math.pow(2.446416831, parameter) - 0.691364; - // Power range 0.0, 0.5, 1.0 maps to 0, 0.3935, 1.0. - physicsToolPhysics.angularDamping = 0.72695892 * Math.pow(2.375594, parameter) - 0.726959; - // Linear range from 0.0, 0.5, 1.0 maps to 0.0, 0.5, 1.0; - physicsToolPhysics.friction = parameter; - } - break; - case "setDensity": - if (parameter !== undefined) { - // Power range 0.0, 0.5, 1.0 maps to 100, 1000, 10000. - physicsToolPhysics.density = Math.pow(10, 2 + 2 * parameter); - } - break; + break; + case "physicsTool": + Feedback.play(dominantHand, Feedback.EQUIP_TOOL); + grouping.clear(); + toolSelected = TOOL_PHYSICS; + ui.setToolIcon(ui.PHYSICS_TOOL); + ui.updateUIOverlays(); + break; + case "deleteTool": + Feedback.play(dominantHand, Feedback.EQUIP_TOOL); + grouping.clear(); + toolSelected = TOOL_DELETE; + ui.setToolIcon(ui.DELETE_TOOL); + ui.updateUIOverlays(); + break; + case "clearTool": + Feedback.play(dominantHand, Feedback.DROP_TOOL); + grouping.clear(); + toolSelected = TOOL_NONE; + ui.clearTool(); + ui.updateUIOverlays(); + break; - case "autoGrab": - if (dominantHand === LEFT_HAND) { - editors[LEFT_HAND].enableAutoGrab(); - } else { - editors[RIGHT_HAND].enableAutoGrab(); - } - break; + case "groupButton": + Feedback.play(dominantHand, Feedback.APPLY_PROPERTY); + grouping.group(); + grouping.clear(); + toolSelected = TOOL_NONE; + ui.clearTool(); + ui.updateUIOverlays(); + break; + case "ungroupButton": + Feedback.play(dominantHand, Feedback.APPLY_PROPERTY); + grouping.ungroup(); + grouping.clear(); + toolSelected = TOOL_NONE; + ui.clearTool(); + ui.updateUIOverlays(); + break; + case "toggleGroupSelectionBoxTool": + toolSelected = parameter ? TOOL_GROUP_BOX : TOOL_GROUP; + if (toolSelected === TOOL_GROUP_BOX) { + grouping.startSelectInBox(); + } else { + grouping.stopSelectInBox(); + } + break; + case "clearGroupSelectionTool": + if (grouping.groupsCount() > 0) { + Feedback.play(dominantHand, Feedback.SELECT_ENTITY); + } + grouping.clear(); + if (toolSelected === TOOL_GROUP_BOX) { + grouping.startSelectInBox(); + } + break; - case "undoAction": - if (History.hasUndo()) { - Feedback.play(dominantHand, Feedback.UNDO_ACTION); - History.undo(); - } else { - Feedback.play(dominantHand, Feedback.GENERAL_ERROR); - } - break; - case "redoAction": - if (History.hasRedo()) { - Feedback.play(dominantHand, Feedback.REDO_ACTION); - History.redo(); - } else { - Feedback.play(dominantHand, Feedback.GENERAL_ERROR); - } - break; + case "setColor": + if (toolSelected === TOOL_PICK_COLOR) { + toolSelected = TOOL_COLOR; + ui.setToolIcon(ui.COLOR_TOOL); + } + colorToolColor = parameter; + break; - default: - log("ERROR: Unexpected command in onUICommand(): " + command + ", " + parameter); + case "setGravityOn": + // Dynamic is true if the entity has gravity or is grabbable. + if (parameter) { + physicsToolPhysics.gravity = { x: 0, y: physicsToolGravity, z: 0 }; + physicsToolPhysics.dynamic = true; + } else { + physicsToolPhysics.gravity = Vec3.ZERO; + physicsToolPhysics.dynamic = physicsToolPhysics.userData.grabbableKey.grabbable === true; + } + break; + case "setGrabOn": + // Dynamic is true if the entity has gravity or is grabbable. + physicsToolPhysics.userData.grabbableKey.grabbable = parameter; + physicsToolPhysics.dynamic = parameter + || (physicsToolPhysics.gravity && Vec3.length(physicsToolPhysics.gravity) > 0); + break; + case "setCollideOn": + if (parameter) { + physicsToolPhysics.collisionless = false; + physicsToolPhysics.collidesWith = "static,dynamic,kinematic,myAvatar,otherAvatar"; + } else { + physicsToolPhysics.collisionless = true; + physicsToolPhysics.collidesWith = ""; + } + break; + + case "setGravity": + if (parameter !== undefined) { + // Power range 0.0, 0.5, 1.0 maps to -50.0, -9.80665, 50.0. + physicsToolGravity = 82.36785162 * Math.pow(2.214065901, parameter) - 132.36785; + if (physicsToolPhysics.dynamic === true) { // Only apply if gravity is turned on. + physicsToolPhysics.gravity = { x: 0, y: physicsToolGravity, z: 0 }; + } + } + break; + case "setBounce": + if (parameter !== undefined) { + // Linear range from 0.0, 0.5, 1.0 maps to 0.0, 0.5, 1.0; + physicsToolPhysics.restitution = parameter; + } + break; + case "setFriction": + if (parameter !== undefined) { + // Power range 0.0, 0.5, 1.0 maps to 0, 0.39, 1.0. + physicsToolPhysics.damping = 0.69136364 * Math.pow(2.446416831, parameter) - 0.691364; + // Power range 0.0, 0.5, 1.0 maps to 0, 0.3935, 1.0. + physicsToolPhysics.angularDamping = 0.72695892 * Math.pow(2.375594, parameter) - 0.726959; + // Linear range from 0.0, 0.5, 1.0 maps to 0.0, 0.5, 1.0; + physicsToolPhysics.friction = parameter; + } + break; + case "setDensity": + if (parameter !== undefined) { + // Power range 0.0, 0.5, 1.0 maps to 100, 1000, 10000. + physicsToolPhysics.density = Math.pow(10, 2 + 2 * parameter); + } + break; + + case "autoGrab": + if (dominantHand === LEFT_HAND) { + editors[LEFT_HAND].enableAutoGrab(); + } else { + editors[RIGHT_HAND].enableAutoGrab(); + } + break; + + case "undoAction": + if (History.hasUndo()) { + Feedback.play(dominantHand, Feedback.UNDO_ACTION); + History.undo(); + } else { + Feedback.play(dominantHand, Feedback.GENERAL_ERROR); + } + break; + case "redoAction": + if (History.hasRedo()) { + Feedback.play(dominantHand, Feedback.REDO_ACTION); + History.redo(); + } else { + Feedback.play(dominantHand, Feedback.GENERAL_ERROR); + } + break; + + default: + log("ERROR: Unexpected command in onUICommand(): " + command + ", " + parameter); } } function startApp() { ui.display(); - update(); // Start main update loop. + update(); // Start main update loop. } function stopApp() { @@ -1823,9 +1824,9 @@ function onAppButtonClicked() { var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications", - EDIT_ERROR = 4, // Per notifications.js. + EDIT_ERROR = 4, // Per notifications.js. INSUFFICIENT_PERMISSIONS_ERROR_MSG = - "You do not have the necessary permissions to edit on this domain."; // Same as edit.js. + "You do not have the necessary permissions to edit on this domain."; // Same as edit.js. // Application tablet/toolbar button clicked. if (!isAppActive && !(Entities.canRez() || Entities.canRezTmp())) { @@ -2030,6 +2031,6 @@ tablet = null; } - Script.setTimeout(setUp, START_DELAY); // Delay start so that Entities.canRez() work; button is enabled correctly. + Script.setTimeout(setUp, START_DELAY); // Delay start so that Entities.canRez() work; button is enabled correctly. Script.scriptEnding.connect(tearDown); }()); diff --git a/scripts/shapes/utilities/utilities.js b/scripts/shapes/utilities/utilities.js index 198e45c256..71986455d4 100644 --- a/scripts/shapes/utilities/utilities.js +++ b/scripts/shapes/utilities/utilities.js @@ -40,7 +40,7 @@ if (typeof Uuid.SELF !== "string") { if (typeof Entities.rootOf !== "function") { Entities.rootOfCache = { - CACHE_ENTRY_EXPIRY_TIME: 1000 // ms + CACHE_ENTRY_EXPIRY_TIME: 1000 // ms }; Entities.rootOf = function (entityID) { @@ -72,7 +72,7 @@ if (typeof Entities.rootOf !== "function") { if (typeof Entities.hasEditableRoot !== "function") { Entities.hasEditableRootCache = { - CACHE_ENTRY_EXPIRY_TIME: 5000 // ms + CACHE_ENTRY_EXPIRY_TIME: 5000 // ms }; Entities.hasEditableRoot = function (entityID) { @@ -114,10 +114,10 @@ if (typeof Object.merge !== "function") { var a = JSON.stringify(objectA), b = JSON.stringify(objectB); if (a === "{}") { - return JSON.parse(b); // Always return a new object. + return JSON.parse(b); // Always return a new object. } if (b === "{}") { - return JSON.parse(a); // "" + return JSON.parse(a); // "" } return JSON.parse(a.slice(0, -1) + "," + b.slice(1)); }; From 680235a7b1f4c8ac0b0407de2320ab5dee8c1e8f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 1 Oct 2017 13:22:48 +1300 Subject: [PATCH 618/722] += 1 ==> ++ --- scripts/shapes/modules/createPalette.js | 6 ++-- scripts/shapes/modules/groups.js | 20 ++++++------ scripts/shapes/modules/hand.js | 4 +-- scripts/shapes/modules/handles.js | 18 +++++------ scripts/shapes/modules/highlights.js | 6 ++-- scripts/shapes/modules/history.js | 22 ++++++------- scripts/shapes/modules/selection.js | 28 ++++++++--------- scripts/shapes/modules/toolsMenu.js | 42 ++++++++++++------------- scripts/shapes/shapes.js | 10 +++--- 9 files changed, 78 insertions(+), 78 deletions(-) diff --git a/scripts/shapes/modules/createPalette.js b/scripts/shapes/modules/createPalette.js index 505853f482..af22c4e4ff 100644 --- a/scripts/shapes/modules/createPalette.js +++ b/scripts/shapes/modules/createPalette.js @@ -351,12 +351,12 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { var i, length; - for (i = 0, length = staticOverlays.length; i < length; i += 1) { + for (i = 0, length = staticOverlays.length; i < length; i++) { Overlays.editOverlay(staticOverlays[i], { visible: visible }); } if (!visible) { - for (i = 0, length = paletteItemHoverOverlays.length; i < length; i += 1) { + for (i = 0, length = paletteItemHoverOverlays.length; i < length; i++) { Overlays.editOverlay(paletteItemHoverOverlays[i], { visible: false }); } } @@ -486,7 +486,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { palettePanelOverlay = Overlays.addOverlay("cube", properties); // Palette items. - for (i = 0, length = PALETTE_ITEMS.length; i < length; i += 1) { + for (i = 0, length = PALETTE_ITEMS.length; i < length; i++) { // Collision overlay. properties = Object.clone(PALETTE_ITEM.properties); properties.parentID = palettePanelOverlay; diff --git a/scripts/shapes/modules/groups.js b/scripts/shapes/modules/groups.js index 6147df98af..223992d646 100644 --- a/scripts/shapes/modules/groups.js +++ b/scripts/shapes/modules/groups.js @@ -56,9 +56,9 @@ Groups = function () { j, lengthJ; - for (i = 0, lengthI = rootEntityIDs.length; i < lengthI; i += 1) { + for (i = 0, lengthI = rootEntityIDs.length; i < lengthI; i++) { if (excludes.indexOf(rootEntityIDs[i]) === -1) { - for (j = 0, lengthJ = selections[i].length; j < lengthJ; j += 1) { + for (j = 0, lengthJ = selections[i].length; j < lengthJ; j++) { result.push(selections[i][j]); } } @@ -98,8 +98,8 @@ Groups = function () { // collisionless. (Don't need to worry about other groups physics properties because only those of the first entity in // the linkset are used by High Fidelity.) See Selection.applyPhysics(). if (selections[0][0].dynamic) { - for (i = 1, lengthI = selections.length; i < lengthI; i += 1) { - for (j = 0, lengthJ = selections[i].length; j < lengthJ; j += 1) { + for (i = 1, lengthI = selections.length; i < lengthI; i++) { + for (j = 0, lengthJ = selections[i].length; j < lengthJ; j++) { undoData.push({ entityID: selections[i][j].id, properties: { @@ -120,7 +120,7 @@ Groups = function () { // Make the first entity in the first group the root and link the first entities of all other groups to it. rootID = rootEntityIDs[0]; - for (i = 1, lengthI = rootEntityIDs.length; i < lengthI; i += 1) { + for (i = 1, lengthI = rootEntityIDs.length; i < lengthI; i++) { undoData.push({ entityID: rootEntityIDs[i], properties: { parentID: Uuid.NULL } @@ -136,7 +136,7 @@ Groups = function () { // Update selection. rootEntityIDs.splice(1, rootEntityIDs.length - 1); - for (i = 1, lengthI = selections.length; i < lengthI; i += 1) { + for (i = 1, lengthI = selections.length; i < lengthI; i++) { selections[i][0].parentID = rootID; selections[0] = selections[0].concat(selections[i]); } @@ -193,7 +193,7 @@ Groups = function () { // Compile information on immediate children. rootID = rootEntityIDs[0]; - for (i = 1, lengthI = selections[0].length; i < lengthI; i += 1) { + for (i = 1, lengthI = selections[0].length; i < lengthI; i++) { if (selections[0][i].parentID === rootID) { childrenIDs.push(selections[0][i].id); childrenIndexes.push(i); @@ -205,7 +205,7 @@ Groups = function () { // Unlink children. isUngroupAll = hasSoloChildren !== hasGroupChildren; - for (i = childrenIDs.length - 1; i >= 0; i -= 1) { + for (i = childrenIDs.length - 1; i >= 0; i--) { if (isUngroupAll || childrenIndexIsGroup[i]) { undoData.push({ entityID: childrenIDs[i], @@ -227,8 +227,8 @@ Groups = function () { // If root group has physics, reset child groups to defaults for dynamic and collisionless. See // Selection.applyPhysics(). if (selections[0][0].dynamic) { - for (i = 1, lengthI = selections.length; i < lengthI; i += 1) { - for (j = 0, lengthJ = selections[i].length; j < lengthJ; j += 1) { + for (i = 1, lengthI = selections.length; i < lengthI; i++) { + for (j = 0, lengthJ = selections[i].length; j < lengthJ; j++) { undoData.push({ entityID: selections[i][j].id, properties: { diff --git a/scripts/shapes/modules/hand.js b/scripts/shapes/modules/hand.js index 1e8af70e57..29ed6bfd96 100644 --- a/scripts/shapes/modules/hand.js +++ b/scripts/shapes/modules/hand.js @@ -160,7 +160,7 @@ Hand = function (side) { if (overlayIDs.length > 1) { // Find closest overlay. overlayDistance = Vec3.length(Vec3.subtract(Overlays.getProperty(overlayID, "position"), palmPosition)); - for (i = 1, length = overlayIDs.length; i < length; i += 1) { + for (i = 1, length = overlayIDs.length; i < length; i++) { distance = Vec3.length(Vec3.subtract(Overlays.getProperty(overlayIDs[i], "position"), palmPosition)); if (distance > overlayDistance) { @@ -191,7 +191,7 @@ Hand = function (side) { if (entityIDs.length > 1) { // Find smallest, editable entity. entitySize = HALF_TREE_SCALE; - for (i = 0, length = entityIDs.length; i < length; i += 1) { + for (i = 0, length = entityIDs.length; i < length; i++) { if (Entities.hasEditableRoot(entityIDs[i])) { size = Vec3.length(Entities.getEntityProperties(entityIDs[i], "dimensions").dimensions); if (size < entitySize) { diff --git a/scripts/shapes/modules/handles.js b/scripts/shapes/modules/handles.js index c1d41b6066..d7627e8626 100644 --- a/scripts/shapes/modules/handles.js +++ b/scripts/shapes/modules/handles.js @@ -187,7 +187,7 @@ Handles = function (side) { // At right-most and opposite corners of bounding box. cameraUp = Quat.getUp(Camera.orientation); maxCrossProductScale = 0; - for (i = 0; i < NUM_CORNERS; i += 1) { + for (i = 0; i < NUM_CORNERS; i++) { cornerPosition = Vec3.sum(boundingBoxCenter, Vec3.multiplyQbyV(boundingBoxOrientation, Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[i], boundingBoxDimensions))); @@ -202,7 +202,7 @@ Handles = function (side) { cornerIndexes[0] = leftCornerIndex; cornerIndexes[1] = rightCornerIndex; cornerHandleDimensions = Vec3.multiply(distanceMultiplier, CORNER_HANDLE_OVERLAY_DIMENSIONS); - for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { + for (i = 0; i < NUM_CORNER_HANDLES; i++) { cornerHandleOverlays[i] = Overlays.addOverlay("sphere", { parentID: rootEntityID, localPosition: Vec3.sum(boundingBoxLocalCenter, @@ -224,7 +224,7 @@ Handles = function (side) { if (!isMultipleEntities) { faceHandleDimensions = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_DIMENSIONS); faceHandleOffsets = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_OFFSETS); - for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + for (i = 0; i < NUM_FACE_HANDLES; i++) { if (!isSuppressZAxis || FACE_HANDLE_OVERLAY_AXES[i].z === 0) { faceHandleOverlays[i] = Overlays.addOverlay("shape", { parentID: rootEntityID, @@ -263,7 +263,7 @@ Handles = function (side) { }); // Corner scale handles. - for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { + for (i = 0; i < NUM_CORNER_HANDLES; i++) { Overlays.editOverlay(cornerHandleOverlays[i], { localPosition: Vec3.sum(scalingBoundingBoxDimensions, Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[i]], scalingBoundingBoxLocalCenter)) @@ -272,7 +272,7 @@ Handles = function (side) { // Face scale handles. if (faceHandleOverlays.length > 0) { - for (i = 0; i < NUM_FACE_HANDLES; i += 1) { + for (i = 0; i < NUM_FACE_HANDLES; i++) { Overlays.editOverlay(faceHandleOverlays[i], { localPosition: Vec3.sum(scalingBoundingBoxDimensions, Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], @@ -314,7 +314,7 @@ Handles = function (side) { i, length; - for (i = 0, length = cornerHandleOverlays.length; i < length; i += 1) { + for (i = 0, length = cornerHandleOverlays.length; i < length; i++) { overlay = cornerHandleOverlays[i]; Overlays.editOverlay(overlay, { visible: isVisible && (isShowAll || overlay === overlayID), @@ -323,7 +323,7 @@ Handles = function (side) { }); } - for (i = 0, length = faceHandleOverlays.length; i < length; i += 1) { + for (i = 0, length = faceHandleOverlays.length; i < length; i++) { overlay = faceHandleOverlays[i]; Overlays.editOverlay(overlay, { visible: isVisible && (isShowAll || overlay === overlayID), @@ -338,10 +338,10 @@ Handles = function (side) { length; Overlays.deleteOverlay(boundingBoxOverlay); - for (i = 0; i < NUM_CORNER_HANDLES; i += 1) { + for (i = 0; i < NUM_CORNER_HANDLES; i++) { Overlays.deleteOverlay(cornerHandleOverlays[i]); } - for (i = 0, length = faceHandleOverlays.length; i < length; i += 1) { + for (i = 0, length = faceHandleOverlays.length; i < length; i++) { Overlays.deleteOverlay(faceHandleOverlays[i]); } diff --git a/scripts/shapes/modules/highlights.js b/scripts/shapes/modules/highlights.js index 1af309a38e..98c200a808 100644 --- a/scripts/shapes/modules/highlights.js +++ b/scripts/shapes/modules/highlights.js @@ -104,14 +104,14 @@ Highlights = function (side) { editEntityOverlay(0, selection[entityIndex], overlayColor); } else { // Add/edit entity overlays for all entities in selection. - for (i = 0, length = selection.length; i < length; i += 1) { + for (i = 0, length = selection.length; i < length; i++) { maybeAddEntityOverlay(i); editEntityOverlay(i, selection[i], overlayColor); } } // Delete extra entity overlays. - for (i = entityOverlays.length - 1, length = selection.length; i >= length; i -= 1) { + for (i = entityOverlays.length - 1, length = selection.length; i >= length; i--) { Overlays.deleteOverlay(entityOverlays[i]); entityOverlays.splice(i, 1); } @@ -141,7 +141,7 @@ Highlights = function (side) { Overlays.editOverlay(boundingBoxOverlay, { visible: false }); // Delete entity overlays. - for (i = 0, length = entityOverlays.length; i < length; i += 1) { + for (i = 0, length = entityOverlays.length; i < length; i++) { Overlays.deleteOverlay(entityOverlays[i]); } entityOverlays = []; diff --git a/scripts/shapes/modules/history.js b/scripts/shapes/modules/history.js index 6451000f04..4c1b7857b8 100644 --- a/scripts/shapes/modules/history.js +++ b/scripts/shapes/modules/history.js @@ -96,7 +96,7 @@ History = (function () { } history.push({ undoData: undoData, redoData: redoData }); - undoPosition += 1; + undoPosition++; undoData = {}; redoData = {}; @@ -112,7 +112,7 @@ History = (function () { length; if (properties) { - for (i = 0, length = properties.length; i < length; i += 1) { + for (i = 0, length = properties.length; i < length; i++) { if (properties[i].entityID === oldEntityID) { properties[i].entityID = newEntityID; } @@ -123,7 +123,7 @@ History = (function () { } } - for (i = 0, length = history.length; i < length; i += 1) { + for (i = 0, length = history.length; i < length; i++) { if (history[i].undoData) { updateEntityIDsInProperty(history[i].undoData.setProperties); updateEntityIDsInProperty(history[i].undoData.createEntities); @@ -155,14 +155,14 @@ History = (function () { undoData = history[undoPosition].undoData; if (undoData.createEntities) { - for (i = 0, length = undoData.createEntities.length; i < length; i += 1) { + for (i = 0, length = undoData.createEntities.length; i < length; i++) { entityID = Entities.addEntity(undoData.createEntities[i].properties); updateEntityIDs(undoData.createEntities[i].entityID, entityID); } } if (undoData.setProperties) { - for (i = 0, length = undoData.setProperties.length; i < length; i += 1) { + for (i = 0, length = undoData.setProperties.length; i < length; i++) { Entities.editEntity(undoData.setProperties[i].entityID, undoData.setProperties[i].properties); if (undoData.setProperties[i].properties.gravity) { kickPhysics(undoData.setProperties[i].entityID); @@ -171,12 +171,12 @@ History = (function () { } if (undoData.deleteEntities) { - for (i = 0, length = undoData.deleteEntities.length; i < length; i += 1) { + for (i = 0, length = undoData.deleteEntities.length; i < length; i++) { Entities.deleteEntity(undoData.deleteEntities[i].entityID); } } - undoPosition -= 1; + undoPosition--; } } @@ -191,14 +191,14 @@ History = (function () { redoData = history[undoPosition + 1].redoData; if (redoData.createEntities) { - for (i = 0, length = redoData.createEntities.length; i < length; i += 1) { + for (i = 0, length = redoData.createEntities.length; i < length; i++) { entityID = Entities.addEntity(redoData.createEntities[i].properties); updateEntityIDs(redoData.createEntities[i].entityID, entityID); } } if (redoData.setProperties) { - for (i = 0, length = redoData.setProperties.length; i < length; i += 1) { + for (i = 0, length = redoData.setProperties.length; i < length; i++) { Entities.editEntity(redoData.setProperties[i].entityID, redoData.setProperties[i].properties); if (redoData.setProperties[i].properties.gravity) { kickPhysics(redoData.setProperties[i].entityID); @@ -207,12 +207,12 @@ History = (function () { } if (redoData.deleteEntities) { - for (i = 0, length = redoData.deleteEntities.length; i < length; i += 1) { + for (i = 0, length = redoData.deleteEntities.length; i < length; i++) { Entities.deleteEntity(redoData.deleteEntities[i].entityID); } } - undoPosition += 1; + undoPosition++; } } diff --git a/scripts/shapes/modules/selection.js b/scripts/shapes/modules/selection.js index faa514d641..5c59c20aec 100644 --- a/scripts/shapes/modules/selection.js +++ b/scripts/shapes/modules/selection.js @@ -70,7 +70,7 @@ SelectionManager = function (side) { } children = Entities.getChildrenIDs(id); - for (i = 0, length = children.length; i < length; i += 1) { + for (i = 0, length = children.length; i < length; i++) { if (Entities.getNestableType(children[i]) === ENTITY_TYPE) { traverseEntityTree(children[i], selection, selectionIDs, selectionProperties); } @@ -164,7 +164,7 @@ SelectionManager = function (side) { min = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ZERO, selection[0].registrationPoint), selection[0].dimensions); max = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ONE, selection[0].registrationPoint), selection[0].dimensions); inverseOrientation = Quat.inverse(rootOrientation); - for (i = 1, length = selection.length; i < length; i += 1) { + for (i = 1, length = selection.length; i < length; i++) { registration = selection[i].registrationPoint; corners[0] = { x: -registration.x, y: -registration.y, z: -registration.z }; @@ -180,7 +180,7 @@ SelectionManager = function (side) { rotation = selection[i].rotation; dimensions = selection[i].dimensions; - for (j = 0; j < NUM_CORNERS; j += 1) { + for (j = 0; j < NUM_CORNERS; j++) { // Corner position in world coordinates. corners[j] = Vec3.sum(position, Vec3.multiplyQbyV(rotation, Vec3.multiplyVbyV(corners[j], dimensions))); // Corner position in root entity coordinates. @@ -247,7 +247,7 @@ SelectionManager = function (side) { startOrientation = selection[0].rotation; // Disable entity set's physics. - for (i = selection.length - 1; i >= 0; i -= 1) { + for (i = selection.length - 1; i >= 0; i--) { Entities.editEntity(selection[i].id, { dynamic: false, // So that gravity doesn't fight with us trying to hold the entity in place. collisionless: true, // So that entity doesn't bump us about as we resize the entity. @@ -267,7 +267,7 @@ SelectionManager = function (side) { // Restore entity set's physics. // Note: Need to apply children-first in order to avoid children's relative positions sometimes drifting. - for (i = selection.length - 1; i >= 0; i -= 1) { + for (i = selection.length - 1; i >= 0; i--) { Entities.editEntity(selection[i].id, { dynamic: selection[i].dynamic, collisionless: selection[i].collisionless @@ -360,7 +360,7 @@ SelectionManager = function (side) { }); // Scale and position children. - for (i = 1, length = selection.length; i < length; i += 1) { + for (i = 1, length = selection.length; i < length; i++) { Entities.editEntity(selection[i].id, { dimensions: Vec3.multiply(factor, selection[i].dimensions), localPosition: Vec3.multiply(factor, selection[i].localPosition) @@ -400,7 +400,7 @@ SelectionManager = function (side) { }); // Final scale and position of children. - for (i = 1, length = selection.length; i < length; i += 1) { + for (i = 1, length = selection.length; i < length; i++) { undoData.push({ entityID: selection[i].id, properties: { @@ -473,7 +473,7 @@ SelectionManager = function (side) { // Scale and position children. // Only corner handles are used for scaling multiple entities so scale factor is the same in all dimensions. // Therefore don't need to take into account orientation relative to parent when scaling local position. - for (i = 1, length = selection.length; i < length; i += 1) { + for (i = 1, length = selection.length; i < length; i++) { Entities.editEntity(selection[i].id, { dimensions: Vec3.multiplyVbyV(factor, selection[i].dimensions), localPosition: Vec3.multiplyVbyV(factor, selection[i].localPosition) @@ -513,7 +513,7 @@ SelectionManager = function (side) { }); // Final scale and position of children. - for (i = 1, length = selection.length; i < length; i += 1) { + for (i = 1, length = selection.length; i < length; i++) { undoData.push({ entityID: selection[i].id, properties: { @@ -555,12 +555,12 @@ SelectionManager = function (side) { length; // Map parent IDs; find intersectedEntityID's index. - for (i = 1, length = selection.length; i < length; i += 1) { + for (i = 1, length = selection.length; i < length; i++) { if (selection[i].id === intersectedEntityID) { intersectedEntityIndex = i; } parentID = selection[i].parentID; - for (j = 0; j < i; j += 1) { + for (j = 0; j < i; j++) { if (parentID === selection[j].id) { parentIDIndexes[i] = j; break; @@ -569,7 +569,7 @@ SelectionManager = function (side) { } // Clone entities. - for (i = 0, length = selection.length; i < length; i += 1) { + for (i = 0, length = selection.length; i < length; i++) { properties = Entities.getEntityProperties(selection[i].id); if (i > 0) { properties.parentID = selection[parentIDIndexes[i]].id; @@ -600,7 +600,7 @@ SelectionManager = function (side) { length; if (isApplyToAll) { - for (i = 0, length = selection.length; i < length; i += 1) { + for (i = 0, length = selection.length; i < length; i++) { properties = Entities.getEntityProperties(selection[i].id, ["type", "color"]); if (ENTITY_TYPES_WITH_COLOR.indexOf(properties.type) !== -1) { Entities.editEntity(selection[i].id, { @@ -680,7 +680,7 @@ SelectionManager = function (side) { dynamic: physicsProperties.dynamic, collisionless: physicsProperties.dynamic || physicsProperties.collisionless }; - for (i = 1, length = selection.length; i < length; i += 1) { + for (i = 1, length = selection.length; i < length; i++) { undoData.push({ entityID: selection[i].id, properties: { diff --git a/scripts/shapes/modules/toolsMenu.js b/scripts/shapes/modules/toolsMenu.js index 6bd7d4a77b..08476f9251 100644 --- a/scripts/shapes/modules/toolsMenu.js +++ b/scripts/shapes/modules/toolsMenu.js @@ -2094,7 +2094,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { Overlays.editOverlay(menuHeaderIconOverlay, { visible: false }); // Display menu items. - for (i = 0, length = MENU_ITEMS.length; i < length; i += 1) { + for (i = 0, length = MENU_ITEMS.length; i < length; i++) { properties = Object.clone(UI_ELEMENTS[MENU_ITEMS[i].type].properties); properties = Object.merge(properties, MENU_ITEMS[i].properties); properties.visible = isVisible; @@ -2144,7 +2144,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { var i, length; - for (i = 0, length = menuOverlays.length; i < length; i += 1) { + for (i = 0, length = menuOverlays.length; i < length; i++) { Overlays.deleteOverlay(menuOverlays[i]); } @@ -2192,7 +2192,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Open specified options panel. optionsItems = OPTONS_PANELS[menuItem.toolOptions]; parentID = menuPanelOverlay; - for (i = 0, length = optionsItems.length; i < length; i += 1) { + for (i = 0, length = optionsItems.length; i < length; i++) { properties = Object.clone(UI_ELEMENTS[optionsItems[i].type].properties); if (optionsItems[i].properties) { properties = Object.merge(properties, optionsItems[i].properties); @@ -2447,7 +2447,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { swatchHighlightOverlay = null; } - for (i = 0, length = optionsOverlays.length; i < length; i += 1) { + for (i = 0, length = optionsOverlays.length; i < length; i++) { Overlays.deleteOverlay(optionsOverlays[i]); } optionsOverlays = []; @@ -2477,7 +2477,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { length; // Display footer items. - for (i = 0, length = FOOTER_ITEMS.length; i < length; i += 1) { + for (i = 0, length = FOOTER_ITEMS.length; i < length; i++) { properties = Object.clone(UI_ELEMENTS[FOOTER_ITEMS[i].type].properties); properties = Object.merge(properties, FOOTER_ITEMS[i].properties); properties.visible = isVisible; @@ -2812,7 +2812,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Hide options. items = optionsItems[index].items; - for (i = 0, length = items.length; i < length; i += 1) { + for (i = 0, length = items.length; i < length; i++) { index = optionsOverlaysIDs.indexOf(items[i]); Overlays.editOverlay(optionsOverlays[index], { localPosition: Vec3.ZERO, @@ -2841,7 +2841,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Show options. items = optionsItems[index].items; - for (i = 0, length = items.length; i < length; i += 1) { + for (i = 0, length = items.length; i < length; i++) { index = optionsOverlaysIDs.indexOf(items[i]); Overlays.editOverlay(optionsOverlays[index], { parentID: parentID, @@ -2947,53 +2947,53 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { var i, length; - for (i = 0, length = staticOverlays.length; i < length; i += 1) { + for (i = 0, length = staticOverlays.length; i < length; i++) { Overlays.editOverlay(staticOverlays[i], { visible: visible }); } if (isOptionsOpen) { Overlays.editOverlay(menuHeaderBackOverlay, { visible: visible }); Overlays.editOverlay(menuHeaderIconOverlay, { visible: visible }); - for (i = 0, length = optionsOverlays.length; i < length; i += 1) { + for (i = 0, length = optionsOverlays.length; i < length; i++) { Overlays.editOverlay(optionsOverlays[i], { visible: visible }); } - for (i = 0, length = optionsOverlaysLabels.length; i < length; i += 1) { + for (i = 0, length = optionsOverlaysLabels.length; i < length; i++) { Overlays.editOverlay(optionsOverlaysLabels[i], { visible: visible }); } - for (i = 0, length = optionsOverlaysSublabels.length; i < length; i += 1) { + for (i = 0, length = optionsOverlaysSublabels.length; i < length; i++) { Overlays.editOverlay(optionsOverlaysSublabels[i], { visible: visible }); } - for (i = 0, length = optionsExtraOverlays.length; i < length; i += 1) { + for (i = 0, length = optionsExtraOverlays.length; i < length; i++) { Overlays.editOverlay(optionsExtraOverlays[i], { visible: visible }); } } else { - for (i = 0, length = menuOverlays.length; i < length; i += 1) { + for (i = 0, length = menuOverlays.length; i < length; i++) { Overlays.editOverlay(menuOverlays[i], { visible: visible }); } - for (i = 0, length = menuIconOverlays.length; i < length; i += 1) { + for (i = 0, length = menuIconOverlays.length; i < length; i++) { Overlays.editOverlay(menuIconOverlays[i], { visible: visible }); } - for (i = 0, length = menuLabelOverlays.length; i < length; i += 1) { + for (i = 0, length = menuLabelOverlays.length; i < length; i++) { Overlays.editOverlay(menuLabelOverlays[i], { visible: visible }); } if (!visible) { - for (i = 0, length = menuHoverOverlays.length; i < length; i += 1) { + for (i = 0, length = menuHoverOverlays.length; i < length; i++) { Overlays.editOverlay(menuHoverOverlays[i], { visible: false }); } } } - for (i = 0, length = footerOverlays.length; i < length; i += 1) { + for (i = 0, length = footerOverlays.length; i < length; i++) { Overlays.editOverlay(footerOverlays[i], { visible: visible }); } - for (i = 0, length = footerIconOverlays.length; i < length; i += 1) { + for (i = 0, length = footerIconOverlays.length; i < length; i++) { Overlays.editOverlay(footerIconOverlays[i], { visible: visible }); } - for (i = 0, length = footerLabelOverlays.length; i < length; i += 1) { + for (i = 0, length = footerLabelOverlays.length; i < length; i++) { Overlays.editOverlay(footerLabelOverlays[i], { visible: visible }); } if (!visible) { - for (i = 0, length = footerHoverOverlays.length; i < length; i += 1) { + for (i = 0, length = footerHoverOverlays.length; i < length; i++) { Overlays.editOverlay(footerHoverOverlays[i], { visible: false }); } } @@ -3621,7 +3621,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isGroupButtonEnabled = false; isUngroupButtonEnabled = false; isClearGroupingButtonEnabled = false; - for (i = 0, length = OPTONS_PANELS.groupOptions.length; i < length; i += 1) { + for (i = 0, length = OPTONS_PANELS.groupOptions.length; i < length; i++) { id = OPTONS_PANELS.groupOptions[i].id; if (id === "groupButton") { groupButtonIndex = i; diff --git a/scripts/shapes/shapes.js b/scripts/shapes/shapes.js index e0a066b3c7..573a9445e7 100644 --- a/scripts/shapes/shapes.js +++ b/scripts/shapes/shapes.js @@ -1330,7 +1330,7 @@ i, length; children = Entities.getChildrenIDs(id); - for (i = 0, length = children.length; i < length; i += 1) { + for (i = 0, length = children.length; i < length; i++) { if (Entities.getNestableType(children[i]) === ENTITY_TYPE) { childrenIDs.push(children[i]); traverseEntityTree(children[i]); @@ -1381,7 +1381,7 @@ isInside = Math.abs(cornerPosition.x) <= boundingBoxHalfDimensions.x && Math.abs(cornerPosition.y) <= boundingBoxHalfDimensions.y && Math.abs(cornerPosition.z) <= boundingBoxHalfDimensions.z; - i += 1; + i++; } return isInside; @@ -1424,7 +1424,7 @@ if (selectInBoxSelection.count() > 1) { boundingBox = selectInBoxSelection.boundingBox(); entityIDs = Entities.findEntities(boundingBox.center, Vec3.length(boundingBox.dimensions) / 2); - for (i = 0, lengthI = entityIDs.length; i < lengthI; i += 1) { + for (i = 0, lengthI = entityIDs.length; i < lengthI; i++) { entityID = entityIDs[i]; if (checkedEntityIDs.indexOf(entityID) === -1) { rootID = Entities.rootOf(entityID); @@ -1435,7 +1435,7 @@ lengthJ = groupIDs.length; while (doIncludeGroup && j < lengthJ) { doIncludeGroup = isInsideBoundingBox(groupIDs[j], boundingBox); - j += 1; + j++; } checkedEntityIDs = checkedEntityIDs.concat(groupIDs); if (doIncludeGroup) { @@ -1465,7 +1465,7 @@ rootEntityIDs = groups.rootEntityIDs(); if (rootEntityIDs.length > 0) { selectInBoxSelection.select(rootEntityIDs[0]); - for (i = 1, length = rootEntityIDs.length; i < length; i += 1) { + for (i = 1, length = rootEntityIDs.length; i < length; i++) { selectInBoxSelection.append(rootEntityIDs[i]); } } From feafd441b1ddf3be3b930ab5f9f85e9e6ebdcb36 Mon Sep 17 00:00:00 2001 From: vladest Date: Sun, 1 Oct 2017 18:54:07 +0200 Subject: [PATCH 619/722] Make sure keyboard will be rised after lowering on another click --- .../commerce/wallet/PassphraseSelection.qml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml index 93341d74cd..af05f16f15 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -87,6 +87,14 @@ Item { } } + MouseArea { + anchors.fill: parent + onPressed: { + sendSignalToWallet({method: 'walletSetup_raiseKeyboard'}); + mouse.accepted = false + } + } + onAccepted: { passphraseField.focus = true; } @@ -106,6 +114,14 @@ Item { activeFocusOnPress: true; activeFocusOnTab: true; + MouseArea { + anchors.fill: parent + onPressed: { + sendSignalToWallet({method: 'walletSetup_raiseKeyboard'}); + mouse.accepted = false + } + } + onFocusChanged: { if (focus) { sendMessageToLightbox({method: 'walletSetup_raiseKeyboard'}); @@ -118,6 +134,7 @@ Item { passphraseFieldAgain.focus = true; } } + HifiControlsUit.TextField { id: passphraseFieldAgain; colorScheme: hifi.colorSchemes.dark; @@ -131,6 +148,14 @@ Item { activeFocusOnPress: true; activeFocusOnTab: true; + MouseArea { + anchors.fill: parent + onPressed: { + sendSignalToWallet({method: 'walletSetup_raiseKeyboard'}); + mouse.accepted = false + } + } + onFocusChanged: { if (focus) { sendMessageToLightbox({method: 'walletSetup_raiseKeyboard'}); From 9a00468ea9414e635fa99c072faab2e80720cb53 Mon Sep 17 00:00:00 2001 From: vladest Date: Sun, 1 Oct 2017 19:21:30 +0200 Subject: [PATCH 620/722] semicolons, yes --- .../qml/hifi/commerce/wallet/PassphraseSelection.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml index af05f16f15..f0338bf1f1 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -88,10 +88,10 @@ Item { } MouseArea { - anchors.fill: parent + anchors.fill: parent; onPressed: { sendSignalToWallet({method: 'walletSetup_raiseKeyboard'}); - mouse.accepted = false + mouse.accepted = false; } } @@ -115,10 +115,10 @@ Item { activeFocusOnTab: true; MouseArea { - anchors.fill: parent + anchors.fill: parent; onPressed: { sendSignalToWallet({method: 'walletSetup_raiseKeyboard'}); - mouse.accepted = false + mouse.accepted = false; } } @@ -149,10 +149,10 @@ Item { activeFocusOnTab: true; MouseArea { - anchors.fill: parent + anchors.fill: parent; onPressed: { sendSignalToWallet({method: 'walletSetup_raiseKeyboard'}); - mouse.accepted = false + mouse.accepted = false; } } From 44cc6deac227b738fa91ae56418004723421b8f6 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Sun, 1 Oct 2017 12:55:44 -0700 Subject: [PATCH 621/722] moving render specific code from Application.cpp to Application_render.cpp --- interface/src/Application.cpp | 678 +++++++++++++-------------- interface/src/Application_render.cpp | 388 ++++++++------- 2 files changed, 553 insertions(+), 513 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5bf5f52a53..5ce63441aa 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2483,249 +2483,249 @@ void Application::updateCamera(RenderArgs& renderArgs) { renderArgs._cameraMode = (int8_t)_myCamera.getMode(); } - -void Application::editRenderArgs(RenderArgsEditor editor) { - QMutexLocker renderLocker(&_renderArgsMutex); - editor(_appRenderArgs); - -} - -void Application::paintGL() { - // Some plugins process message events, allowing paintGL to be called reentrantly. - if (_aboutToQuit || _window->isMinimized()) { - return; - } - - _frameCount++; - _lastTimeRendered.start(); - - auto lastPaintBegin = usecTimestampNow(); - PROFILE_RANGE_EX(render, __FUNCTION__, 0xff0000ff, (uint64_t)_frameCount); - PerformanceTimer perfTimer("paintGL"); - - if (nullptr == _displayPlugin) { - return; - } - - DisplayPluginPointer displayPlugin; - { - PROFILE_RANGE(render, "/getActiveDisplayPlugin"); - displayPlugin = getActiveDisplayPlugin(); - } - - { - PROFILE_RANGE(render, "/pluginBeginFrameRender"); - // If a display plugin loses it's underlying support, it - // needs to be able to signal us to not use it - if (!displayPlugin->beginFrameRender(_frameCount)) { - updateDisplayMode(); - return; - } - } - - // update the avatar with a fresh HMD pose - // { - // PROFILE_RANGE(render, "/updateAvatar"); - // getMyAvatar()->updateFromHMDSensorMatrix(getHMDSensorPose()); - // } - - // auto lodManager = DependencyManager::get(); - - RenderArgs renderArgs; - float sensorToWorldScale; - glm::mat4 HMDSensorPose; - glm::mat4 eyeToWorld; - glm::mat4 sensorToWorld; - - bool isStereo; - glm::mat4 stereoEyeOffsets[2]; - glm::mat4 stereoEyeProjections[2]; - - { - QMutexLocker viewLocker(&_renderArgsMutex); - renderArgs = _appRenderArgs._renderArgs; - HMDSensorPose = _appRenderArgs._headPose; - eyeToWorld = _appRenderArgs._eyeToWorld; - sensorToWorld = _appRenderArgs._sensorToWorld; - sensorToWorldScale = _appRenderArgs._sensorToWorldScale; - isStereo = _appRenderArgs._isStereo; - for_each_eye([&](Eye eye) { - stereoEyeOffsets[eye] = _appRenderArgs._eyeOffsets[eye]; - stereoEyeProjections[eye] = _appRenderArgs._eyeProjections[eye]; - }); - } - - //float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); - //{ - // PROFILE_RANGE(render, "/buildFrustrumAndArgs"); - // { - // QMutexLocker viewLocker(&_viewMutex); - // // adjust near clip plane to account for sensor scaling. - // auto adjustedProjection = glm::perspective(_viewFrustum.getFieldOfView(), - // _viewFrustum.getAspectRatio(), - // DEFAULT_NEAR_CLIP * sensorToWorldScale, - // _viewFrustum.getFarClip()); - // _viewFrustum.setProjection(adjustedProjection); - // _viewFrustum.calculate(); - // } - // renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(), - // lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, - // RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); - // { - // QMutexLocker viewLocker(&_viewMutex); - // renderArgs.setViewFrustum(_viewFrustum); - // } - //} - - //{ - // PROFILE_RANGE(render, "/resizeGL"); - // PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); - // bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - // PerformanceWarning warn(showWarnings, "Application::paintGL()"); - // resizeGL(); - //} - - { - PROFILE_RANGE(render, "/gpuContextReset"); - // _gpuContext->beginFrame(getHMDSensorPose()); - _gpuContext->beginFrame(HMDSensorPose); - // Reset the gpu::Context Stages - // Back to the default framebuffer; - gpu::doInBatch(_gpuContext, [&](gpu::Batch& batch) { - batch.resetStages(); - }); - } - - - { - PROFILE_RANGE(render, "/renderOverlay"); - PerformanceTimer perfTimer("renderOverlay"); - // NOTE: There is no batch associated with this renderArgs - // the ApplicationOverlay class assumes it's viewport is setup to be the device size - QSize size = getDeviceSize(); - renderArgs._viewport = glm::ivec4(0, 0, size.width(), size.height()); - _applicationOverlay.renderOverlay(&renderArgs); - } - - // updateCamera(renderArgs); - { - PROFILE_RANGE(render, "/updateCompositor"); - // getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform(), getMyAvatar()->getSensorToWorldMatrix()); - getApplicationCompositor().setFrameInfo(_frameCount, eyeToWorld, sensorToWorld); - } - - gpu::FramebufferPointer finalFramebuffer; - QSize finalFramebufferSize; - { - PROFILE_RANGE(render, "/getOutputFramebuffer"); - // Primary rendering pass - auto framebufferCache = DependencyManager::get(); - finalFramebufferSize = framebufferCache->getFrameBufferSize(); - // Final framebuffer that will be handled to the display-plugin - finalFramebuffer = framebufferCache->getFramebuffer(); - } - - //auto hmdInterface = DependencyManager::get(); - //float ipdScale = hmdInterface->getIPDScale(); - - //// scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. - //ipdScale *= sensorToWorldScale; - - { - //PROFILE_RANGE(render, "/mainRender"); - //PerformanceTimer perfTimer("mainRender"); - //// FIXME is this ever going to be different from the size previously set in the render args - //// in the overlay render? - //// Viewport is assigned to the size of the framebuffer - //renderArgs._viewport = ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height()); - //auto baseProjection = renderArgs.getViewFrustum().getProjection(); - //if (displayPlugin->isStereo()) { - // // Stereo modes will typically have a larger projection matrix overall, - // // so we ask for the 'mono' projection matrix, which for stereo and HMD - // // plugins will imply the combined projection for both eyes. - // // - // // This is properly implemented for the Oculus plugins, but for OpenVR - // // and Stereo displays I'm not sure how to get / calculate it, so we're - // // just relying on the left FOV in each case and hoping that the - // // overall culling margin of error doesn't cause popping in the - // // right eye. There are FIXMEs in the relevant plugins - // _myCamera.setProjection(displayPlugin->getCullingProjection(baseProjection)); - // renderArgs._context->enableStereo(true); - // mat4 eyeOffsets[2]; - // mat4 eyeProjections[2]; - - // // FIXME we probably don't need to set the projection matrix every frame, - // // only when the display plugin changes (or in non-HMD modes when the user - // // changes the FOV manually, which right now I don't think they can. - // for_each_eye([&](Eye eye) { - // // For providing the stereo eye views, the HMD head pose has already been - // // applied to the avatar, so we need to get the difference between the head - // // pose applied to the avatar and the per eye pose, and use THAT as - // // the per-eye stereo matrix adjustment. - // mat4 eyeToHead = displayPlugin->getEyeToHeadTransform(eye); - // // Grab the translation - // vec3 eyeOffset = glm::vec3(eyeToHead[3]); - // // Apply IPD scaling - // mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); - // eyeOffsets[eye] = eyeOffsetTransform; - // eyeProjections[eye] = displayPlugin->getEyeProjection(eye, baseProjection); - // }); - // renderArgs._context->setStereoProjections(eyeProjections); - // renderArgs._context->setStereoViews(eyeOffsets); - - // // Configure the type of display / stereo - // renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); - //} - - if (isStereo) { - renderArgs._context->enableStereo(true); - renderArgs._context->setStereoProjections(stereoEyeProjections); - renderArgs._context->setStereoViews(stereoEyeOffsets); - // renderArgs._displayMode - } - - renderArgs._blitFramebuffer = finalFramebuffer; - // displaySide(&renderArgs, _myCamera); - runRenderFrame(&renderArgs); - } - - gpu::Batch postCompositeBatch; - { - PROFILE_RANGE(render, "/postComposite"); - PerformanceTimer perfTimer("postComposite"); - renderArgs._batch = &postCompositeBatch; - renderArgs._batch->setViewportTransform(ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height())); - renderArgs._batch->setViewTransform(renderArgs.getViewFrustum().getView()); - _overlays.render3DHUDOverlays(&renderArgs); - } - - auto frame = _gpuContext->endFrame(); - frame->frameIndex = _frameCount; - frame->framebuffer = finalFramebuffer; - frame->framebufferRecycler = [](const gpu::FramebufferPointer& framebuffer){ - DependencyManager::get()->releaseFramebuffer(framebuffer); - }; - frame->overlay = _applicationOverlay.getOverlayTexture(); - frame->postCompositeBatch = postCompositeBatch; - // deliver final scene rendering commands to the display plugin - { - PROFILE_RANGE(render, "/pluginOutput"); - PerformanceTimer perfTimer("pluginOutput"); - _frameCounter.increment(); - displayPlugin->submitFrame(frame); - } - - // Reset the framebuffer and stereo state - renderArgs._blitFramebuffer.reset(); - renderArgs._context->enableStereo(false); - - { - Stats::getInstance()->setRenderDetails(renderArgs._details); - } - - uint64_t lastPaintDuration = usecTimestampNow() - lastPaintBegin; - _frameTimingsScriptingInterface.addValue(lastPaintDuration); -} +// +//void Application::editRenderArgs(RenderArgsEditor editor) { +// QMutexLocker renderLocker(&_renderArgsMutex); +// editor(_appRenderArgs); +// +//} +// +//void Application::paintGL() { +// // Some plugins process message events, allowing paintGL to be called reentrantly. +// if (_aboutToQuit || _window->isMinimized()) { +// return; +// } +// +// _frameCount++; +// _lastTimeRendered.start(); +// +// auto lastPaintBegin = usecTimestampNow(); +// PROFILE_RANGE_EX(render, __FUNCTION__, 0xff0000ff, (uint64_t)_frameCount); +// PerformanceTimer perfTimer("paintGL"); +// +// if (nullptr == _displayPlugin) { +// return; +// } +// +// DisplayPluginPointer displayPlugin; +// { +// PROFILE_RANGE(render, "/getActiveDisplayPlugin"); +// displayPlugin = getActiveDisplayPlugin(); +// } +// +// { +// PROFILE_RANGE(render, "/pluginBeginFrameRender"); +// // If a display plugin loses it's underlying support, it +// // needs to be able to signal us to not use it +// if (!displayPlugin->beginFrameRender(_frameCount)) { +// updateDisplayMode(); +// return; +// } +// } +// +// // update the avatar with a fresh HMD pose +// // { +// // PROFILE_RANGE(render, "/updateAvatar"); +// // getMyAvatar()->updateFromHMDSensorMatrix(getHMDSensorPose()); +// // } +// +// // auto lodManager = DependencyManager::get(); +// +// RenderArgs renderArgs; +// float sensorToWorldScale; +// glm::mat4 HMDSensorPose; +// glm::mat4 eyeToWorld; +// glm::mat4 sensorToWorld; +// +// bool isStereo; +// glm::mat4 stereoEyeOffsets[2]; +// glm::mat4 stereoEyeProjections[2]; +// +// { +// QMutexLocker viewLocker(&_renderArgsMutex); +// renderArgs = _appRenderArgs._renderArgs; +// HMDSensorPose = _appRenderArgs._headPose; +// eyeToWorld = _appRenderArgs._eyeToWorld; +// sensorToWorld = _appRenderArgs._sensorToWorld; +// sensorToWorldScale = _appRenderArgs._sensorToWorldScale; +// isStereo = _appRenderArgs._isStereo; +// for_each_eye([&](Eye eye) { +// stereoEyeOffsets[eye] = _appRenderArgs._eyeOffsets[eye]; +// stereoEyeProjections[eye] = _appRenderArgs._eyeProjections[eye]; +// }); +// } +// +// //float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); +// //{ +// // PROFILE_RANGE(render, "/buildFrustrumAndArgs"); +// // { +// // QMutexLocker viewLocker(&_viewMutex); +// // // adjust near clip plane to account for sensor scaling. +// // auto adjustedProjection = glm::perspective(_viewFrustum.getFieldOfView(), +// // _viewFrustum.getAspectRatio(), +// // DEFAULT_NEAR_CLIP * sensorToWorldScale, +// // _viewFrustum.getFarClip()); +// // _viewFrustum.setProjection(adjustedProjection); +// // _viewFrustum.calculate(); +// // } +// // renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(), +// // lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, +// // RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); +// // { +// // QMutexLocker viewLocker(&_viewMutex); +// // renderArgs.setViewFrustum(_viewFrustum); +// // } +// //} +// +// //{ +// // PROFILE_RANGE(render, "/resizeGL"); +// // PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); +// // bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); +// // PerformanceWarning warn(showWarnings, "Application::paintGL()"); +// // resizeGL(); +// //} +// +// { +// PROFILE_RANGE(render, "/gpuContextReset"); +// // _gpuContext->beginFrame(getHMDSensorPose()); +// _gpuContext->beginFrame(HMDSensorPose); +// // Reset the gpu::Context Stages +// // Back to the default framebuffer; +// gpu::doInBatch(_gpuContext, [&](gpu::Batch& batch) { +// batch.resetStages(); +// }); +// } +// +// +// { +// PROFILE_RANGE(render, "/renderOverlay"); +// PerformanceTimer perfTimer("renderOverlay"); +// // NOTE: There is no batch associated with this renderArgs +// // the ApplicationOverlay class assumes it's viewport is setup to be the device size +// QSize size = getDeviceSize(); +// renderArgs._viewport = glm::ivec4(0, 0, size.width(), size.height()); +// _applicationOverlay.renderOverlay(&renderArgs); +// } +// +// // updateCamera(renderArgs); +// { +// PROFILE_RANGE(render, "/updateCompositor"); +// // getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform(), getMyAvatar()->getSensorToWorldMatrix()); +// getApplicationCompositor().setFrameInfo(_frameCount, eyeToWorld, sensorToWorld); +// } +// +// gpu::FramebufferPointer finalFramebuffer; +// QSize finalFramebufferSize; +// { +// PROFILE_RANGE(render, "/getOutputFramebuffer"); +// // Primary rendering pass +// auto framebufferCache = DependencyManager::get(); +// finalFramebufferSize = framebufferCache->getFrameBufferSize(); +// // Final framebuffer that will be handled to the display-plugin +// finalFramebuffer = framebufferCache->getFramebuffer(); +// } +// +// //auto hmdInterface = DependencyManager::get(); +// //float ipdScale = hmdInterface->getIPDScale(); +// +// //// scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. +// //ipdScale *= sensorToWorldScale; +// +// { +// //PROFILE_RANGE(render, "/mainRender"); +// //PerformanceTimer perfTimer("mainRender"); +// //// FIXME is this ever going to be different from the size previously set in the render args +// //// in the overlay render? +// //// Viewport is assigned to the size of the framebuffer +// //renderArgs._viewport = ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height()); +// //auto baseProjection = renderArgs.getViewFrustum().getProjection(); +// //if (displayPlugin->isStereo()) { +// // // Stereo modes will typically have a larger projection matrix overall, +// // // so we ask for the 'mono' projection matrix, which for stereo and HMD +// // // plugins will imply the combined projection for both eyes. +// // // +// // // This is properly implemented for the Oculus plugins, but for OpenVR +// // // and Stereo displays I'm not sure how to get / calculate it, so we're +// // // just relying on the left FOV in each case and hoping that the +// // // overall culling margin of error doesn't cause popping in the +// // // right eye. There are FIXMEs in the relevant plugins +// // _myCamera.setProjection(displayPlugin->getCullingProjection(baseProjection)); +// // renderArgs._context->enableStereo(true); +// // mat4 eyeOffsets[2]; +// // mat4 eyeProjections[2]; +// +// // // FIXME we probably don't need to set the projection matrix every frame, +// // // only when the display plugin changes (or in non-HMD modes when the user +// // // changes the FOV manually, which right now I don't think they can. +// // for_each_eye([&](Eye eye) { +// // // For providing the stereo eye views, the HMD head pose has already been +// // // applied to the avatar, so we need to get the difference between the head +// // // pose applied to the avatar and the per eye pose, and use THAT as +// // // the per-eye stereo matrix adjustment. +// // mat4 eyeToHead = displayPlugin->getEyeToHeadTransform(eye); +// // // Grab the translation +// // vec3 eyeOffset = glm::vec3(eyeToHead[3]); +// // // Apply IPD scaling +// // mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); +// // eyeOffsets[eye] = eyeOffsetTransform; +// // eyeProjections[eye] = displayPlugin->getEyeProjection(eye, baseProjection); +// // }); +// // renderArgs._context->setStereoProjections(eyeProjections); +// // renderArgs._context->setStereoViews(eyeOffsets); +// +// // // Configure the type of display / stereo +// // renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); +// //} +// +// if (isStereo) { +// renderArgs._context->enableStereo(true); +// renderArgs._context->setStereoProjections(stereoEyeProjections); +// renderArgs._context->setStereoViews(stereoEyeOffsets); +// // renderArgs._displayMode +// } +// +// renderArgs._blitFramebuffer = finalFramebuffer; +// // displaySide(&renderArgs, _myCamera); +// runRenderFrame(&renderArgs); +// } +// +// gpu::Batch postCompositeBatch; +// { +// PROFILE_RANGE(render, "/postComposite"); +// PerformanceTimer perfTimer("postComposite"); +// renderArgs._batch = &postCompositeBatch; +// renderArgs._batch->setViewportTransform(ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height())); +// renderArgs._batch->setViewTransform(renderArgs.getViewFrustum().getView()); +// _overlays.render3DHUDOverlays(&renderArgs); +// } +// +// auto frame = _gpuContext->endFrame(); +// frame->frameIndex = _frameCount; +// frame->framebuffer = finalFramebuffer; +// frame->framebufferRecycler = [](const gpu::FramebufferPointer& framebuffer){ +// DependencyManager::get()->releaseFramebuffer(framebuffer); +// }; +// frame->overlay = _applicationOverlay.getOverlayTexture(); +// frame->postCompositeBatch = postCompositeBatch; +// // deliver final scene rendering commands to the display plugin +// { +// PROFILE_RANGE(render, "/pluginOutput"); +// PerformanceTimer perfTimer("pluginOutput"); +// _frameCounter.increment(); +// displayPlugin->submitFrame(frame); +// } +// +// // Reset the framebuffer and stereo state +// renderArgs._blitFramebuffer.reset(); +// renderArgs._context->enableStereo(false); +// +// { +// Stats::getInstance()->setRenderDetails(renderArgs._details); +// } +// +// uint64_t lastPaintDuration = usecTimestampNow() - lastPaintBegin; +// _frameTimingsScriptingInterface.addValue(lastPaintDuration); +//} void Application::runTests() { runTimingTests(); @@ -5401,7 +5401,7 @@ void Application::update(float deltaTime) { DependencyManager::get()->update(); - // Game loopis done, mark the end of the frame for the scene transactions + // Game loop is done, mark the end of the frame for the scene transactions and the render loop to take over getMain3DScene()->enqueueFrame(); } @@ -5692,101 +5692,101 @@ void Application::copyDisplayViewFrustum(ViewFrustum& viewOut) const { viewOut = _displayViewFrustum; } -// WorldBox Render Data & rendering functions - -class WorldBoxRenderData { -public: - typedef render::Payload Payload; - typedef Payload::DataPointer Pointer; - - int _val = 0; - static render::ItemID _item; // unique WorldBoxRenderData -}; - -render::ItemID WorldBoxRenderData::_item { render::Item::INVALID_ITEM_ID }; - -namespace render { - template <> const ItemKey payloadGetKey(const WorldBoxRenderData::Pointer& stuff) { return ItemKey::Builder::opaqueShape(); } - template <> const Item::Bound payloadGetBound(const WorldBoxRenderData::Pointer& stuff) { return Item::Bound(); } - template <> void payloadRender(const WorldBoxRenderData::Pointer& stuff, RenderArgs* args) { - if (Menu::getInstance()->isOptionChecked(MenuOption::WorldAxes)) { - PerformanceTimer perfTimer("worldBox"); - - auto& batch = *args->_batch; - DependencyManager::get()->bindSimpleProgram(batch); - renderWorldBox(args, batch); - } - } -} - -void Application::runRenderFrame(RenderArgs* renderArgs) { - - // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. - // Then we can move this logic into the Avatar::simulate call. - // auto myAvatar = getMyAvatar(); - // myAvatar->preDisplaySide(renderArgs); - - PROFILE_RANGE(render, __FUNCTION__); - PerformanceTimer perfTimer("display"); - PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::runRenderFrame()"); - - // load the view frustum - // { - // QMutexLocker viewLocker(&_viewMutex); - // theCamera.loadViewFrustum(_displayViewFrustum); - // } - - // The pending changes collecting the changes here - render::Transaction transaction; - - // Assuming nothing gets rendered through that - //if (!selfAvatarOnly) { - { - if (DependencyManager::get()->shouldRenderEntities()) { - // render models... - PerformanceTimer perfTimer("entities"); - PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), - "Application::runRenderFrame() ... entities..."); - - RenderArgs::DebugFlags renderDebugFlags = RenderArgs::RENDER_DEBUG_NONE; - - if (Menu::getInstance()->isOptionChecked(MenuOption::PhysicsShowHulls)) { - renderDebugFlags = static_cast(renderDebugFlags | - static_cast(RenderArgs::RENDER_DEBUG_HULLS)); - } - renderArgs->_debugFlags = renderDebugFlags; - } - } - - // FIXME: Move this out of here!, WorldBox should be driven by the entity content just like the other entities - // Make sure the WorldBox is in the scene - if (!render::Item::isValidID(WorldBoxRenderData::_item)) { - auto worldBoxRenderData = make_shared(); - auto worldBoxRenderPayload = make_shared(worldBoxRenderData); - - WorldBoxRenderData::_item = _main3DScene->allocateID(); - - transaction.resetItem(WorldBoxRenderData::_item, worldBoxRenderPayload); - _main3DScene->enqueueTransaction(transaction); - } - - - // For now every frame pass the renderContext - { - PerformanceTimer perfTimer("EngineRun"); - - /* { - QMutexLocker viewLocker(&_viewMutex); - renderArgs->setViewFrustum(_displayViewFrustum); - }*/ - // renderArgs->_cameraMode = (int8_t)theCamera.getMode(); // HACK - renderArgs->_scene = getMain3DScene(); - _renderEngine->getRenderContext()->args = renderArgs; - - // Before the deferred pass, let's try to use the render engine - _renderEngine->run(); - } -} +//// WorldBox Render Data & rendering functions +// +//class WorldBoxRenderData { +//public: +// typedef render::Payload Payload; +// typedef Payload::DataPointer Pointer; +// +// int _val = 0; +// static render::ItemID _item; // unique WorldBoxRenderData +//}; +// +//render::ItemID WorldBoxRenderData::_item { render::Item::INVALID_ITEM_ID }; +// +//namespace render { +// template <> const ItemKey payloadGetKey(const WorldBoxRenderData::Pointer& stuff) { return ItemKey::Builder::opaqueShape(); } +// template <> const Item::Bound payloadGetBound(const WorldBoxRenderData::Pointer& stuff) { return Item::Bound(); } +// template <> void payloadRender(const WorldBoxRenderData::Pointer& stuff, RenderArgs* args) { +// if (Menu::getInstance()->isOptionChecked(MenuOption::WorldAxes)) { +// PerformanceTimer perfTimer("worldBox"); +// +// auto& batch = *args->_batch; +// DependencyManager::get()->bindSimpleProgram(batch); +// renderWorldBox(args, batch); +// } +// } +//} +// +//void Application::runRenderFrame(RenderArgs* renderArgs) { +// +// // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. +// // Then we can move this logic into the Avatar::simulate call. +// // auto myAvatar = getMyAvatar(); +// // myAvatar->preDisplaySide(renderArgs); +// +// PROFILE_RANGE(render, __FUNCTION__); +// PerformanceTimer perfTimer("display"); +// PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::runRenderFrame()"); +// +// // load the view frustum +// // { +// // QMutexLocker viewLocker(&_viewMutex); +// // theCamera.loadViewFrustum(_displayViewFrustum); +// // } +// +// // The pending changes collecting the changes here +// render::Transaction transaction; +// +// // Assuming nothing gets rendered through that +// //if (!selfAvatarOnly) { +// { +// if (DependencyManager::get()->shouldRenderEntities()) { +// // render models... +// PerformanceTimer perfTimer("entities"); +// PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), +// "Application::runRenderFrame() ... entities..."); +// +// RenderArgs::DebugFlags renderDebugFlags = RenderArgs::RENDER_DEBUG_NONE; +// +// if (Menu::getInstance()->isOptionChecked(MenuOption::PhysicsShowHulls)) { +// renderDebugFlags = static_cast(renderDebugFlags | +// static_cast(RenderArgs::RENDER_DEBUG_HULLS)); +// } +// renderArgs->_debugFlags = renderDebugFlags; +// } +// } +// +// // FIXME: Move this out of here!, WorldBox should be driven by the entity content just like the other entities +// // Make sure the WorldBox is in the scene +// if (!render::Item::isValidID(WorldBoxRenderData::_item)) { +// auto worldBoxRenderData = make_shared(); +// auto worldBoxRenderPayload = make_shared(worldBoxRenderData); +// +// WorldBoxRenderData::_item = _main3DScene->allocateID(); +// +// transaction.resetItem(WorldBoxRenderData::_item, worldBoxRenderPayload); +// _main3DScene->enqueueTransaction(transaction); +// } +// +// +// // For now every frame pass the renderContext +// { +// PerformanceTimer perfTimer("EngineRun"); +// +// /* { +// QMutexLocker viewLocker(&_viewMutex); +// renderArgs->setViewFrustum(_displayViewFrustum); +// }*/ +// // renderArgs->_cameraMode = (int8_t)theCamera.getMode(); // HACK +// renderArgs->_scene = getMain3DScene(); +// _renderEngine->getRenderContext()->args = renderArgs; +// +// // Before the deferred pass, let's try to use the render engine +// _renderEngine->run(); +// } +//} void Application::resetSensors(bool andReload) { DependencyManager::get()->reset(); diff --git a/interface/src/Application_render.cpp b/interface/src/Application_render.cpp index ee3267284a..51d55a2bd8 100644 --- a/interface/src/Application_render.cpp +++ b/interface/src/Application_render.cpp @@ -8,7 +8,22 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifdef tryingSOmething +#include "Application.h" +#include + +#include +#include +#include "ui/Stats.h" +#include "FrameTimingsScriptingInterface.h" + +// Statically provided display and input plugins +extern DisplayPluginList getDisplayPlugins(); + +void Application::editRenderArgs(RenderArgsEditor editor) { + QMutexLocker renderLocker(&_renderArgsMutex); + editor(_appRenderArgs); + +} void Application::paintGL() { // Some plugins process message events, allowing paintGL to be called reentrantly. @@ -54,42 +69,56 @@ void Application::paintGL() { RenderArgs renderArgs; float sensorToWorldScale; glm::mat4 HMDSensorPose; + glm::mat4 eyeToWorld; + glm::mat4 sensorToWorld; + + bool isStereo; + glm::mat4 stereoEyeOffsets[2]; + glm::mat4 stereoEyeProjections[2]; + { QMutexLocker viewLocker(&_renderArgsMutex); renderArgs = _appRenderArgs._renderArgs; - HMDSensorPose = _appRenderArgs._eyeToWorld; + HMDSensorPose = _appRenderArgs._headPose; + eyeToWorld = _appRenderArgs._eyeToWorld; + sensorToWorld = _appRenderArgs._sensorToWorld; sensorToWorldScale = _appRenderArgs._sensorToWorldScale; + isStereo = _appRenderArgs._isStereo; + for_each_eye([&](Eye eye) { + stereoEyeOffsets[eye] = _appRenderArgs._eyeOffsets[eye]; + stereoEyeProjections[eye] = _appRenderArgs._eyeProjections[eye]; + }); } - /* - float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); - { - PROFILE_RANGE(render, "/buildFrustrumAndArgs"); - { - QMutexLocker viewLocker(&_viewMutex); - // adjust near clip plane to account for sensor scaling. - auto adjustedProjection = glm::perspective(_viewFrustum.getFieldOfView(), - _viewFrustum.getAspectRatio(), - DEFAULT_NEAR_CLIP * sensorToWorldScale, - _viewFrustum.getFarClip()); - _viewFrustum.setProjection(adjustedProjection); - _viewFrustum.calculate(); - } - renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(), - lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, - RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); - { - QMutexLocker viewLocker(&_viewMutex); - renderArgs.setViewFrustum(_viewFrustum); - } - } - */ - { - PROFILE_RANGE(render, "/resizeGL"); - PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::paintGL()"); - resizeGL(); - } + + //float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); + //{ + // PROFILE_RANGE(render, "/buildFrustrumAndArgs"); + // { + // QMutexLocker viewLocker(&_viewMutex); + // // adjust near clip plane to account for sensor scaling. + // auto adjustedProjection = glm::perspective(_viewFrustum.getFieldOfView(), + // _viewFrustum.getAspectRatio(), + // DEFAULT_NEAR_CLIP * sensorToWorldScale, + // _viewFrustum.getFarClip()); + // _viewFrustum.setProjection(adjustedProjection); + // _viewFrustum.calculate(); + // } + // renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(), + // lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, + // RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); + // { + // QMutexLocker viewLocker(&_viewMutex); + // renderArgs.setViewFrustum(_viewFrustum); + // } + //} + + //{ + // PROFILE_RANGE(render, "/resizeGL"); + // PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); + // bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + // PerformanceWarning warn(showWarnings, "Application::paintGL()"); + // resizeGL(); + //} { PROFILE_RANGE(render, "/gpuContextReset"); @@ -114,103 +143,10 @@ void Application::paintGL() { } // updateCamera(renderArgs); - - /* glm::vec3 boomOffset; - { - PROFILE_RANGE(render, "/updateCamera"); - { - PerformanceTimer perfTimer("CameraUpdates"); - - auto myAvatar = getMyAvatar(); - boomOffset = myAvatar->getModelScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; - - // The render mode is default or mirror if the camera is in mirror mode, assigned further below - renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - - // Always use the default eye position, not the actual head eye position. - // Using the latter will cause the camera to wobble with idle animations, - // or with changes from the face tracker - if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { - if (isHMDMode()) { - mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - _myCamera.setPosition(extractTranslation(camMat)); - _myCamera.setOrientation(glmExtractRotation(camMat)); - } else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition()); - _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); - } - } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { - if (isHMDMode()) { - auto hmdWorldMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - _myCamera.setOrientation(glm::normalize(glmExtractRotation(hmdWorldMat))); - _myCamera.setPosition(extractTranslation(hmdWorldMat) + - myAvatar->getOrientation() * boomOffset); - } else { - _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); - if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + _myCamera.getOrientation() * boomOffset); - } else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + myAvatar->getOrientation() * boomOffset); - } - } - } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - if (isHMDMode()) { - auto mirrorBodyOrientation = myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)); - - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD yaw and roll - glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); - mirrorHmdEulers.y = -mirrorHmdEulers.y; - mirrorHmdEulers.z = -mirrorHmdEulers.z; - glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); - - glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; - - _myCamera.setOrientation(worldMirrorRotation); - - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD lateral offsets - hmdOffset.x = -hmdOffset.x; - - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) - + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror - + mirrorBodyOrientation * hmdOffset); - } else { - _myCamera.setOrientation(myAvatar->getOrientation() - * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) - + (myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * - glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); - } - renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; - } else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { - EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); - if (cameraEntity != nullptr) { - if (isHMDMode()) { - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - _myCamera.setOrientation(cameraEntity->getRotation() * hmdRotation); - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - _myCamera.setPosition(cameraEntity->getPosition() + (hmdRotation * hmdOffset)); - } else { - _myCamera.setOrientation(cameraEntity->getRotation()); - _myCamera.setPosition(cameraEntity->getPosition()); - } - } - } - // Update camera position - if (!isHMDMode()) { - _myCamera.update(1.0f / _frameCounter.rate()); - } - } - } - */ { PROFILE_RANGE(render, "/updateCompositor"); - getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform(), getMyAvatar()->getSensorToWorldMatrix()); + // getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform(), getMyAvatar()->getSensorToWorldMatrix()); + getApplicationCompositor().setFrameInfo(_frameCount, eyeToWorld, sensorToWorld); } gpu::FramebufferPointer finalFramebuffer; @@ -224,57 +160,65 @@ void Application::paintGL() { finalFramebuffer = framebufferCache->getFramebuffer(); } - auto hmdInterface = DependencyManager::get(); - float ipdScale = hmdInterface->getIPDScale(); + //auto hmdInterface = DependencyManager::get(); + //float ipdScale = hmdInterface->getIPDScale(); - // scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. - ipdScale *= sensorToWorldScale; + //// scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. + //ipdScale *= sensorToWorldScale; { - PROFILE_RANGE(render, "/mainRender"); - PerformanceTimer perfTimer("mainRender"); - // FIXME is this ever going to be different from the size previously set in the render args - // in the overlay render? - // Viewport is assigned to the size of the framebuffer - renderArgs._viewport = ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height()); - auto baseProjection = renderArgs.getViewFrustum().getProjection(); - if (displayPlugin->isStereo()) { - // Stereo modes will typically have a larger projection matrix overall, - // so we ask for the 'mono' projection matrix, which for stereo and HMD - // plugins will imply the combined projection for both eyes. - // - // This is properly implemented for the Oculus plugins, but for OpenVR - // and Stereo displays I'm not sure how to get / calculate it, so we're - // just relying on the left FOV in each case and hoping that the - // overall culling margin of error doesn't cause popping in the - // right eye. There are FIXMEs in the relevant plugins - _myCamera.setProjection(displayPlugin->getCullingProjection(baseProjection)); + //PROFILE_RANGE(render, "/mainRender"); + //PerformanceTimer perfTimer("mainRender"); + //// FIXME is this ever going to be different from the size previously set in the render args + //// in the overlay render? + //// Viewport is assigned to the size of the framebuffer + //renderArgs._viewport = ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height()); + //auto baseProjection = renderArgs.getViewFrustum().getProjection(); + //if (displayPlugin->isStereo()) { + // // Stereo modes will typically have a larger projection matrix overall, + // // so we ask for the 'mono' projection matrix, which for stereo and HMD + // // plugins will imply the combined projection for both eyes. + // // + // // This is properly implemented for the Oculus plugins, but for OpenVR + // // and Stereo displays I'm not sure how to get / calculate it, so we're + // // just relying on the left FOV in each case and hoping that the + // // overall culling margin of error doesn't cause popping in the + // // right eye. There are FIXMEs in the relevant plugins + // _myCamera.setProjection(displayPlugin->getCullingProjection(baseProjection)); + // renderArgs._context->enableStereo(true); + // mat4 eyeOffsets[2]; + // mat4 eyeProjections[2]; + + // // FIXME we probably don't need to set the projection matrix every frame, + // // only when the display plugin changes (or in non-HMD modes when the user + // // changes the FOV manually, which right now I don't think they can. + // for_each_eye([&](Eye eye) { + // // For providing the stereo eye views, the HMD head pose has already been + // // applied to the avatar, so we need to get the difference between the head + // // pose applied to the avatar and the per eye pose, and use THAT as + // // the per-eye stereo matrix adjustment. + // mat4 eyeToHead = displayPlugin->getEyeToHeadTransform(eye); + // // Grab the translation + // vec3 eyeOffset = glm::vec3(eyeToHead[3]); + // // Apply IPD scaling + // mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); + // eyeOffsets[eye] = eyeOffsetTransform; + // eyeProjections[eye] = displayPlugin->getEyeProjection(eye, baseProjection); + // }); + // renderArgs._context->setStereoProjections(eyeProjections); + // renderArgs._context->setStereoViews(eyeOffsets); + + // // Configure the type of display / stereo + // renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); + //} + + if (isStereo) { renderArgs._context->enableStereo(true); - mat4 eyeOffsets[2]; - mat4 eyeProjections[2]; - - // FIXME we probably don't need to set the projection matrix every frame, - // only when the display plugin changes (or in non-HMD modes when the user - // changes the FOV manually, which right now I don't think they can. - for_each_eye([&](Eye eye) { - // For providing the stereo eye views, the HMD head pose has already been - // applied to the avatar, so we need to get the difference between the head - // pose applied to the avatar and the per eye pose, and use THAT as - // the per-eye stereo matrix adjustment. - mat4 eyeToHead = displayPlugin->getEyeToHeadTransform(eye); - // Grab the translation - vec3 eyeOffset = glm::vec3(eyeToHead[3]); - // Apply IPD scaling - mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); - eyeOffsets[eye] = eyeOffsetTransform; - eyeProjections[eye] = displayPlugin->getEyeProjection(eye, baseProjection); - }); - renderArgs._context->setStereoProjections(eyeProjections); - renderArgs._context->setStereoViews(eyeOffsets); - - // Configure the type of display / stereo - renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); + renderArgs._context->setStereoProjections(stereoEyeProjections); + renderArgs._context->setStereoViews(stereoEyeOffsets); + // renderArgs._displayMode } + renderArgs._blitFramebuffer = finalFramebuffer; // displaySide(&renderArgs, _myCamera); runRenderFrame(&renderArgs); @@ -317,4 +261,100 @@ void Application::paintGL() { uint64_t lastPaintDuration = usecTimestampNow() - lastPaintBegin; _frameTimingsScriptingInterface.addValue(lastPaintDuration); } -#endif \ No newline at end of file + + +// WorldBox Render Data & rendering functions + +class WorldBoxRenderData { +public: + typedef render::Payload Payload; + typedef Payload::DataPointer Pointer; + + int _val = 0; + static render::ItemID _item; // unique WorldBoxRenderData +}; + +render::ItemID WorldBoxRenderData::_item{ render::Item::INVALID_ITEM_ID }; + +namespace render { + template <> const ItemKey payloadGetKey(const WorldBoxRenderData::Pointer& stuff) { return ItemKey::Builder::opaqueShape(); } + template <> const Item::Bound payloadGetBound(const WorldBoxRenderData::Pointer& stuff) { return Item::Bound(); } + template <> void payloadRender(const WorldBoxRenderData::Pointer& stuff, RenderArgs* args) { + if (Menu::getInstance()->isOptionChecked(MenuOption::WorldAxes)) { + PerformanceTimer perfTimer("worldBox"); + + auto& batch = *args->_batch; + DependencyManager::get()->bindSimpleProgram(batch); + renderWorldBox(args, batch); + } + } +} + +void Application::runRenderFrame(RenderArgs* renderArgs) { + + // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. + // Then we can move this logic into the Avatar::simulate call. + // auto myAvatar = getMyAvatar(); + // myAvatar->preDisplaySide(renderArgs); + + PROFILE_RANGE(render, __FUNCTION__); + PerformanceTimer perfTimer("display"); + PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::runRenderFrame()"); + + // load the view frustum + // { + // QMutexLocker viewLocker(&_viewMutex); + // theCamera.loadViewFrustum(_displayViewFrustum); + // } + + // The pending changes collecting the changes here + render::Transaction transaction; + + // Assuming nothing gets rendered through that + //if (!selfAvatarOnly) { + { + if (DependencyManager::get()->shouldRenderEntities()) { + // render models... + PerformanceTimer perfTimer("entities"); + PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), + "Application::runRenderFrame() ... entities..."); + + RenderArgs::DebugFlags renderDebugFlags = RenderArgs::RENDER_DEBUG_NONE; + + if (Menu::getInstance()->isOptionChecked(MenuOption::PhysicsShowHulls)) { + renderDebugFlags = static_cast(renderDebugFlags | + static_cast(RenderArgs::RENDER_DEBUG_HULLS)); + } + renderArgs->_debugFlags = renderDebugFlags; + } + } + + // FIXME: Move this out of here!, WorldBox should be driven by the entity content just like the other entities + // Make sure the WorldBox is in the scene + if (!render::Item::isValidID(WorldBoxRenderData::_item)) { + auto worldBoxRenderData = make_shared(); + auto worldBoxRenderPayload = make_shared(worldBoxRenderData); + + WorldBoxRenderData::_item = _main3DScene->allocateID(); + + transaction.resetItem(WorldBoxRenderData::_item, worldBoxRenderPayload); + _main3DScene->enqueueTransaction(transaction); + } + + + // For now every frame pass the renderContext + { + PerformanceTimer perfTimer("EngineRun"); + + /* { + QMutexLocker viewLocker(&_viewMutex); + renderArgs->setViewFrustum(_displayViewFrustum); + }*/ + // renderArgs->_cameraMode = (int8_t)theCamera.getMode(); // HACK + renderArgs->_scene = getMain3DScene(); + _renderEngine->getRenderContext()->args = renderArgs; + + // Before the deferred pass, let's try to use the render engine + _renderEngine->run(); + } +} From 572986bfad264e63ff25481ce0cc3decbe128cf2 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 29 Sep 2017 17:48:59 -0700 Subject: [PATCH 622/722] fix modeloverlay visible change (cherry picked from commit 0bb27a7165a874766b7625928fd00eed444d87af) --- interface/src/ui/overlays/Base3DOverlay.h | 2 +- interface/src/ui/overlays/ModelOverlay.cpp | 39 ++++++++++++++-------- interface/src/ui/overlays/ModelOverlay.h | 8 ++++- interface/src/ui/overlays/Overlay.h | 2 +- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index 93a973e60a..3e65f163e2 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -47,7 +47,7 @@ public: void setIsSolid(bool isSolid) { _isSolid = isSolid; } void setIsDashedLine(bool isDashedLine) { _isDashedLine = isDashedLine; } void setIgnoreRayIntersection(bool value) { _ignoreRayIntersection = value; } - void setDrawInFront(bool value) { _drawInFront = value; } + virtual void setDrawInFront(bool value) { _drawInFront = value; } void setIsGrabbable(bool value) { _isGrabbable = value; } virtual AABox getBounds() const override = 0; diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index ca5ca54144..d19668af37 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -60,6 +60,24 @@ void ModelOverlay::update(float deltatime) { _model->simulate(deltatime); } _isLoaded = _model->isActive(); + + // check to see if when we added our model to the scene they were ready, if they were not ready, then + // fix them up in the scene + render::ScenePointer scene = qApp->getMain3DScene(); + render::Transaction transaction; + if (_model->needsFixupInScene()) { + _model->removeFromScene(scene, transaction); + _model->addToScene(scene, transaction); + } + if (_visibleDirty) { + _visibleDirty = false; + _model->setVisibleInScene(getVisible(), scene); + } + if (_drawInFrontDirty) { + _drawInFrontDirty = false; + _model->setLayeredInFront(getDrawInFront(), scene); + } + scene->enqueueTransaction(transaction); } bool ModelOverlay::addToScene(Overlay::Pointer overlay, const render::ScenePointer& scene, render::Transaction& transaction) { @@ -73,21 +91,14 @@ void ModelOverlay::removeFromScene(Overlay::Pointer overlay, const render::Scene _model->removeFromScene(scene, transaction); } -void ModelOverlay::render(RenderArgs* args) { +void ModelOverlay::setVisible(bool visible) { + Overlay::setVisible(visible); + _visibleDirty = true; +} - // check to see if when we added our model to the scene they were ready, if they were not ready, then - // fix them up in the scene - render::ScenePointer scene = qApp->getMain3DScene(); - render::Transaction transaction; - if (_model->needsFixupInScene()) { - _model->removeFromScene(scene, transaction); - _model->addToScene(scene, transaction); - } - - _model->setVisibleInScene(_visible, scene); - _model->setLayeredInFront(getDrawInFront(), scene); - - scene->enqueueTransaction(transaction); +void ModelOverlay::setDrawInFront(bool drawInFront) { + Base3DOverlay::setDrawInFront(drawInFront); + _drawInFrontDirty = true; } void ModelOverlay::setProperties(const QVariantMap& properties) { diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index 8d8429b29e..c1403cadbe 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -28,7 +28,7 @@ public: ModelOverlay(const ModelOverlay* modelOverlay); virtual void update(float deltatime) override; - virtual void render(RenderArgs* args) override; + virtual void render(RenderArgs* args) override {}; void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, @@ -45,6 +45,9 @@ public: float getLoadPriority() const { return _loadPriority; } + void setVisible(bool visible) override; + void setDrawInFront(bool drawInFront) override; + protected: Transform evalRenderTransform() override; @@ -62,6 +65,9 @@ private: bool _updateModel = { false }; bool _scaleToFit = { false }; float _loadPriority { 0.0f }; + + bool _visibleDirty { false }; + bool _drawInFrontDirty { false }; }; #endif // hifi_ModelOverlay_h diff --git a/interface/src/ui/overlays/Overlay.h b/interface/src/ui/overlays/Overlay.h index db2979b4d5..775c597397 100644 --- a/interface/src/ui/overlays/Overlay.h +++ b/interface/src/ui/overlays/Overlay.h @@ -73,7 +73,7 @@ public: float getAlphaPulse() const { return _alphaPulse; } // setters - void setVisible(bool visible) { _visible = visible; } + virtual void setVisible(bool visible) { _visible = visible; } void setDrawHUDLayer(bool drawHUDLayer); void setColor(const xColor& color) { _color = color; } void setAlpha(float alpha) { _alpha = alpha; } From aa10c29609d90942915791e9bae1514d888fa7d1 Mon Sep 17 00:00:00 2001 From: samcake Date: Sun, 1 Oct 2017 17:16:10 -0700 Subject: [PATCH 623/722] Cleaning up the changed code --- interface/src/Application.cpp | 535 +++++---------------------- interface/src/Application_render.cpp | 150 +------- 2 files changed, 112 insertions(+), 573 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5ce63441aa..3f87d72c88 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -188,7 +188,7 @@ #include "InterfaceParentFinder.h" #include "ui/OctreeStatsProvider.h" -#include "FrameTimingsScriptingInterface.h" +//#include "FrameTimingsScriptingInterface.h" #include #include #include @@ -2018,7 +2018,7 @@ void Application::cleanupBeforeQuit() { // The cleanup process enqueues the transactions but does not process them. Calling this here will force the actual // removal of the items. // See https://highfidelity.fogbugz.com/f/cases/5328 - _main3DScene->processTransactionQueue(); + // _main3DScene->processTransactionQueue(); // first stop all timers directly or by invokeMethod // depending on what thread they run in @@ -2216,7 +2216,7 @@ void Application::initializeGL() { update(0); } -FrameTimingsScriptingInterface _frameTimingsScriptingInterface; +//FrameTimingsScriptingInterface _frameTimingsScriptingInterface; extern void setupPreferences(); @@ -2272,7 +2272,7 @@ void Application::initializeUi() { surfaceContext->setContextProperty("Recording", DependencyManager::get().data()); surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("FrameTimings", &_frameTimingsScriptingInterface); + //surfaceContext->setContextProperty("FrameTimings", &_frameTimingsScriptingInterface); // TODO: Remove this Context Property ? i don;t see anywhere surfaceContext->setContextProperty("Rates", new RatesScriptingInterface(this)); surfaceContext->setContextProperty("TREE_SCALE", TREE_SCALE); @@ -2378,354 +2378,105 @@ void Application::initializeUi() { } void Application::updateCamera(RenderArgs& renderArgs) { - + PROFILE_RANGE(render, "/updateCamera"); + PerformanceTimer perfTimer("CameraUpdates"); glm::vec3 boomOffset; - { - PROFILE_RANGE(render, "/updateCamera"); - { - PerformanceTimer perfTimer("CameraUpdates"); + auto myAvatar = getMyAvatar(); + boomOffset = myAvatar->getModelScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; - auto myAvatar = getMyAvatar(); - boomOffset = myAvatar->getModelScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; + // The render mode is default or mirror if the camera is in mirror mode, assigned further below + renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - // The render mode is default or mirror if the camera is in mirror mode, assigned further below - renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - - // Always use the default eye position, not the actual head eye position. - // Using the latter will cause the camera to wobble with idle animations, - // or with changes from the face tracker - if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { - if (isHMDMode()) { - mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - _myCamera.setPosition(extractTranslation(camMat)); - _myCamera.setOrientation(glmExtractRotation(camMat)); - } - else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition()); - _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); - } + // Always use the default eye position, not the actual head eye position. + // Using the latter will cause the camera to wobble with idle animations, + // or with changes from the face tracker + if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { + if (isHMDMode()) { + mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); + _myCamera.setPosition(extractTranslation(camMat)); + _myCamera.setOrientation(glmExtractRotation(camMat)); + } + else { + _myCamera.setPosition(myAvatar->getDefaultEyePosition()); + _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); + } + } + else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { + if (isHMDMode()) { + auto hmdWorldMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); + _myCamera.setOrientation(glm::normalize(glmExtractRotation(hmdWorldMat))); + _myCamera.setPosition(extractTranslation(hmdWorldMat) + + myAvatar->getOrientation() * boomOffset); + } + else { + _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); + if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + _myCamera.getOrientation() * boomOffset); } - else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { - if (isHMDMode()) { - auto hmdWorldMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - _myCamera.setOrientation(glm::normalize(glmExtractRotation(hmdWorldMat))); - _myCamera.setPosition(extractTranslation(hmdWorldMat) + - myAvatar->getOrientation() * boomOffset); - } - else { - _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); - if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + _myCamera.getOrientation() * boomOffset); - } - else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + myAvatar->getOrientation() * boomOffset); - } - } - } - else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - if (isHMDMode()) { - auto mirrorBodyOrientation = myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)); - - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD yaw and roll - glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); - mirrorHmdEulers.y = -mirrorHmdEulers.y; - mirrorHmdEulers.z = -mirrorHmdEulers.z; - glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); - - glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; - - _myCamera.setOrientation(worldMirrorRotation); - - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD lateral offsets - hmdOffset.x = -hmdOffset.x; - - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) - + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror - + mirrorBodyOrientation * hmdOffset); - } - else { - _myCamera.setOrientation(myAvatar->getOrientation() - * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) - + (myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * - glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); - } - renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; - } - else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { - EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); - if (cameraEntity != nullptr) { - if (isHMDMode()) { - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - _myCamera.setOrientation(cameraEntity->getRotation() * hmdRotation); - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - _myCamera.setPosition(cameraEntity->getPosition() + (hmdRotation * hmdOffset)); - } - else { - _myCamera.setOrientation(cameraEntity->getRotation()); - _myCamera.setPosition(cameraEntity->getPosition()); - } - } - } - // Update camera position - if (!isHMDMode()) { - _myCamera.update(1.0f / _frameCounter.rate()); + else { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + myAvatar->getOrientation() * boomOffset); } } } + else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { + if (isHMDMode()) { + auto mirrorBodyOrientation = myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)); + + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD yaw and roll + glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); + mirrorHmdEulers.y = -mirrorHmdEulers.y; + mirrorHmdEulers.z = -mirrorHmdEulers.z; + glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); + + glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; + + _myCamera.setOrientation(worldMirrorRotation); + + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD lateral offsets + hmdOffset.x = -hmdOffset.x; + + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) + + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + + mirrorBodyOrientation * hmdOffset); + } + else { + _myCamera.setOrientation(myAvatar->getOrientation() + * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) + + (myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * + glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); + } + renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; + } + else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { + EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); + if (cameraEntity != nullptr) { + if (isHMDMode()) { + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + _myCamera.setOrientation(cameraEntity->getRotation() * hmdRotation); + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + _myCamera.setPosition(cameraEntity->getPosition() + (hmdRotation * hmdOffset)); + } + else { + _myCamera.setOrientation(cameraEntity->getRotation()); + _myCamera.setPosition(cameraEntity->getPosition()); + } + } + } + // Update camera position + if (!isHMDMode()) { + _myCamera.update(1.0f / _frameCounter.rate()); + } renderArgs._cameraMode = (int8_t)_myCamera.getMode(); } -// -//void Application::editRenderArgs(RenderArgsEditor editor) { -// QMutexLocker renderLocker(&_renderArgsMutex); -// editor(_appRenderArgs); -// -//} -// -//void Application::paintGL() { -// // Some plugins process message events, allowing paintGL to be called reentrantly. -// if (_aboutToQuit || _window->isMinimized()) { -// return; -// } -// -// _frameCount++; -// _lastTimeRendered.start(); -// -// auto lastPaintBegin = usecTimestampNow(); -// PROFILE_RANGE_EX(render, __FUNCTION__, 0xff0000ff, (uint64_t)_frameCount); -// PerformanceTimer perfTimer("paintGL"); -// -// if (nullptr == _displayPlugin) { -// return; -// } -// -// DisplayPluginPointer displayPlugin; -// { -// PROFILE_RANGE(render, "/getActiveDisplayPlugin"); -// displayPlugin = getActiveDisplayPlugin(); -// } -// -// { -// PROFILE_RANGE(render, "/pluginBeginFrameRender"); -// // If a display plugin loses it's underlying support, it -// // needs to be able to signal us to not use it -// if (!displayPlugin->beginFrameRender(_frameCount)) { -// updateDisplayMode(); -// return; -// } -// } -// -// // update the avatar with a fresh HMD pose -// // { -// // PROFILE_RANGE(render, "/updateAvatar"); -// // getMyAvatar()->updateFromHMDSensorMatrix(getHMDSensorPose()); -// // } -// -// // auto lodManager = DependencyManager::get(); -// -// RenderArgs renderArgs; -// float sensorToWorldScale; -// glm::mat4 HMDSensorPose; -// glm::mat4 eyeToWorld; -// glm::mat4 sensorToWorld; -// -// bool isStereo; -// glm::mat4 stereoEyeOffsets[2]; -// glm::mat4 stereoEyeProjections[2]; -// -// { -// QMutexLocker viewLocker(&_renderArgsMutex); -// renderArgs = _appRenderArgs._renderArgs; -// HMDSensorPose = _appRenderArgs._headPose; -// eyeToWorld = _appRenderArgs._eyeToWorld; -// sensorToWorld = _appRenderArgs._sensorToWorld; -// sensorToWorldScale = _appRenderArgs._sensorToWorldScale; -// isStereo = _appRenderArgs._isStereo; -// for_each_eye([&](Eye eye) { -// stereoEyeOffsets[eye] = _appRenderArgs._eyeOffsets[eye]; -// stereoEyeProjections[eye] = _appRenderArgs._eyeProjections[eye]; -// }); -// } -// -// //float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); -// //{ -// // PROFILE_RANGE(render, "/buildFrustrumAndArgs"); -// // { -// // QMutexLocker viewLocker(&_viewMutex); -// // // adjust near clip plane to account for sensor scaling. -// // auto adjustedProjection = glm::perspective(_viewFrustum.getFieldOfView(), -// // _viewFrustum.getAspectRatio(), -// // DEFAULT_NEAR_CLIP * sensorToWorldScale, -// // _viewFrustum.getFarClip()); -// // _viewFrustum.setProjection(adjustedProjection); -// // _viewFrustum.calculate(); -// // } -// // renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(), -// // lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, -// // RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); -// // { -// // QMutexLocker viewLocker(&_viewMutex); -// // renderArgs.setViewFrustum(_viewFrustum); -// // } -// //} -// -// //{ -// // PROFILE_RANGE(render, "/resizeGL"); -// // PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); -// // bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); -// // PerformanceWarning warn(showWarnings, "Application::paintGL()"); -// // resizeGL(); -// //} -// -// { -// PROFILE_RANGE(render, "/gpuContextReset"); -// // _gpuContext->beginFrame(getHMDSensorPose()); -// _gpuContext->beginFrame(HMDSensorPose); -// // Reset the gpu::Context Stages -// // Back to the default framebuffer; -// gpu::doInBatch(_gpuContext, [&](gpu::Batch& batch) { -// batch.resetStages(); -// }); -// } -// -// -// { -// PROFILE_RANGE(render, "/renderOverlay"); -// PerformanceTimer perfTimer("renderOverlay"); -// // NOTE: There is no batch associated with this renderArgs -// // the ApplicationOverlay class assumes it's viewport is setup to be the device size -// QSize size = getDeviceSize(); -// renderArgs._viewport = glm::ivec4(0, 0, size.width(), size.height()); -// _applicationOverlay.renderOverlay(&renderArgs); -// } -// -// // updateCamera(renderArgs); -// { -// PROFILE_RANGE(render, "/updateCompositor"); -// // getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform(), getMyAvatar()->getSensorToWorldMatrix()); -// getApplicationCompositor().setFrameInfo(_frameCount, eyeToWorld, sensorToWorld); -// } -// -// gpu::FramebufferPointer finalFramebuffer; -// QSize finalFramebufferSize; -// { -// PROFILE_RANGE(render, "/getOutputFramebuffer"); -// // Primary rendering pass -// auto framebufferCache = DependencyManager::get(); -// finalFramebufferSize = framebufferCache->getFrameBufferSize(); -// // Final framebuffer that will be handled to the display-plugin -// finalFramebuffer = framebufferCache->getFramebuffer(); -// } -// -// //auto hmdInterface = DependencyManager::get(); -// //float ipdScale = hmdInterface->getIPDScale(); -// -// //// scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. -// //ipdScale *= sensorToWorldScale; -// -// { -// //PROFILE_RANGE(render, "/mainRender"); -// //PerformanceTimer perfTimer("mainRender"); -// //// FIXME is this ever going to be different from the size previously set in the render args -// //// in the overlay render? -// //// Viewport is assigned to the size of the framebuffer -// //renderArgs._viewport = ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height()); -// //auto baseProjection = renderArgs.getViewFrustum().getProjection(); -// //if (displayPlugin->isStereo()) { -// // // Stereo modes will typically have a larger projection matrix overall, -// // // so we ask for the 'mono' projection matrix, which for stereo and HMD -// // // plugins will imply the combined projection for both eyes. -// // // -// // // This is properly implemented for the Oculus plugins, but for OpenVR -// // // and Stereo displays I'm not sure how to get / calculate it, so we're -// // // just relying on the left FOV in each case and hoping that the -// // // overall culling margin of error doesn't cause popping in the -// // // right eye. There are FIXMEs in the relevant plugins -// // _myCamera.setProjection(displayPlugin->getCullingProjection(baseProjection)); -// // renderArgs._context->enableStereo(true); -// // mat4 eyeOffsets[2]; -// // mat4 eyeProjections[2]; -// -// // // FIXME we probably don't need to set the projection matrix every frame, -// // // only when the display plugin changes (or in non-HMD modes when the user -// // // changes the FOV manually, which right now I don't think they can. -// // for_each_eye([&](Eye eye) { -// // // For providing the stereo eye views, the HMD head pose has already been -// // // applied to the avatar, so we need to get the difference between the head -// // // pose applied to the avatar and the per eye pose, and use THAT as -// // // the per-eye stereo matrix adjustment. -// // mat4 eyeToHead = displayPlugin->getEyeToHeadTransform(eye); -// // // Grab the translation -// // vec3 eyeOffset = glm::vec3(eyeToHead[3]); -// // // Apply IPD scaling -// // mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); -// // eyeOffsets[eye] = eyeOffsetTransform; -// // eyeProjections[eye] = displayPlugin->getEyeProjection(eye, baseProjection); -// // }); -// // renderArgs._context->setStereoProjections(eyeProjections); -// // renderArgs._context->setStereoViews(eyeOffsets); -// -// // // Configure the type of display / stereo -// // renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); -// //} -// -// if (isStereo) { -// renderArgs._context->enableStereo(true); -// renderArgs._context->setStereoProjections(stereoEyeProjections); -// renderArgs._context->setStereoViews(stereoEyeOffsets); -// // renderArgs._displayMode -// } -// -// renderArgs._blitFramebuffer = finalFramebuffer; -// // displaySide(&renderArgs, _myCamera); -// runRenderFrame(&renderArgs); -// } -// -// gpu::Batch postCompositeBatch; -// { -// PROFILE_RANGE(render, "/postComposite"); -// PerformanceTimer perfTimer("postComposite"); -// renderArgs._batch = &postCompositeBatch; -// renderArgs._batch->setViewportTransform(ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height())); -// renderArgs._batch->setViewTransform(renderArgs.getViewFrustum().getView()); -// _overlays.render3DHUDOverlays(&renderArgs); -// } -// -// auto frame = _gpuContext->endFrame(); -// frame->frameIndex = _frameCount; -// frame->framebuffer = finalFramebuffer; -// frame->framebufferRecycler = [](const gpu::FramebufferPointer& framebuffer){ -// DependencyManager::get()->releaseFramebuffer(framebuffer); -// }; -// frame->overlay = _applicationOverlay.getOverlayTexture(); -// frame->postCompositeBatch = postCompositeBatch; -// // deliver final scene rendering commands to the display plugin -// { -// PROFILE_RANGE(render, "/pluginOutput"); -// PerformanceTimer perfTimer("pluginOutput"); -// _frameCounter.increment(); -// displayPlugin->submitFrame(frame); -// } -// -// // Reset the framebuffer and stereo state -// renderArgs._blitFramebuffer.reset(); -// renderArgs._context->enableStereo(false); -// -// { -// Stats::getInstance()->setRenderDetails(renderArgs._details); -// } -// -// uint64_t lastPaintDuration = usecTimestampNow() - lastPaintBegin; -// _frameTimingsScriptingInterface.addValue(lastPaintDuration); -//} void Application::runTests() { runTimingTests(); @@ -5286,11 +5037,12 @@ void Application::update(float deltaTime) { editRenderArgs([this](AppRenderArgs& appRenderArgs) { + appRenderArgs._renderArgs._scene = getMain3DScene(); + appRenderArgs._headPose= getHMDSensorPose(); auto myAvatar = getMyAvatar(); - // update the avatar with a fresh HMD pose { PROFILE_RANGE(render, "/updateAvatar"); @@ -5372,8 +5124,6 @@ void Application::update(float deltaTime) { eyeOffsets[eye] = eyeOffsetTransform; eyeProjections[eye] = getActiveDisplayPlugin()->getEyeProjection(eye, baseProjection); }); - //renderArgs._context->setStereoProjections(eyeProjections); - //renderArgs._context->setStereoViews(eyeOffsets); // Configure the type of display / stereo appRenderArgs._renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); @@ -5461,7 +5211,6 @@ int Application::sendNackPackets() { } }); - return packetsSent; } @@ -5692,102 +5441,6 @@ void Application::copyDisplayViewFrustum(ViewFrustum& viewOut) const { viewOut = _displayViewFrustum; } -//// WorldBox Render Data & rendering functions -// -//class WorldBoxRenderData { -//public: -// typedef render::Payload Payload; -// typedef Payload::DataPointer Pointer; -// -// int _val = 0; -// static render::ItemID _item; // unique WorldBoxRenderData -//}; -// -//render::ItemID WorldBoxRenderData::_item { render::Item::INVALID_ITEM_ID }; -// -//namespace render { -// template <> const ItemKey payloadGetKey(const WorldBoxRenderData::Pointer& stuff) { return ItemKey::Builder::opaqueShape(); } -// template <> const Item::Bound payloadGetBound(const WorldBoxRenderData::Pointer& stuff) { return Item::Bound(); } -// template <> void payloadRender(const WorldBoxRenderData::Pointer& stuff, RenderArgs* args) { -// if (Menu::getInstance()->isOptionChecked(MenuOption::WorldAxes)) { -// PerformanceTimer perfTimer("worldBox"); -// -// auto& batch = *args->_batch; -// DependencyManager::get()->bindSimpleProgram(batch); -// renderWorldBox(args, batch); -// } -// } -//} -// -//void Application::runRenderFrame(RenderArgs* renderArgs) { -// -// // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. -// // Then we can move this logic into the Avatar::simulate call. -// // auto myAvatar = getMyAvatar(); -// // myAvatar->preDisplaySide(renderArgs); -// -// PROFILE_RANGE(render, __FUNCTION__); -// PerformanceTimer perfTimer("display"); -// PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::runRenderFrame()"); -// -// // load the view frustum -// // { -// // QMutexLocker viewLocker(&_viewMutex); -// // theCamera.loadViewFrustum(_displayViewFrustum); -// // } -// -// // The pending changes collecting the changes here -// render::Transaction transaction; -// -// // Assuming nothing gets rendered through that -// //if (!selfAvatarOnly) { -// { -// if (DependencyManager::get()->shouldRenderEntities()) { -// // render models... -// PerformanceTimer perfTimer("entities"); -// PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), -// "Application::runRenderFrame() ... entities..."); -// -// RenderArgs::DebugFlags renderDebugFlags = RenderArgs::RENDER_DEBUG_NONE; -// -// if (Menu::getInstance()->isOptionChecked(MenuOption::PhysicsShowHulls)) { -// renderDebugFlags = static_cast(renderDebugFlags | -// static_cast(RenderArgs::RENDER_DEBUG_HULLS)); -// } -// renderArgs->_debugFlags = renderDebugFlags; -// } -// } -// -// // FIXME: Move this out of here!, WorldBox should be driven by the entity content just like the other entities -// // Make sure the WorldBox is in the scene -// if (!render::Item::isValidID(WorldBoxRenderData::_item)) { -// auto worldBoxRenderData = make_shared(); -// auto worldBoxRenderPayload = make_shared(worldBoxRenderData); -// -// WorldBoxRenderData::_item = _main3DScene->allocateID(); -// -// transaction.resetItem(WorldBoxRenderData::_item, worldBoxRenderPayload); -// _main3DScene->enqueueTransaction(transaction); -// } -// -// -// // For now every frame pass the renderContext -// { -// PerformanceTimer perfTimer("EngineRun"); -// -// /* { -// QMutexLocker viewLocker(&_viewMutex); -// renderArgs->setViewFrustum(_displayViewFrustum); -// }*/ -// // renderArgs->_cameraMode = (int8_t)theCamera.getMode(); // HACK -// renderArgs->_scene = getMain3DScene(); -// _renderEngine->getRenderContext()->args = renderArgs; -// -// // Before the deferred pass, let's try to use the render engine -// _renderEngine->run(); -// } -//} - void Application::resetSensors(bool andReload) { DependencyManager::get()->reset(); DependencyManager::get()->reset(); diff --git a/interface/src/Application_render.cpp b/interface/src/Application_render.cpp index 51d55a2bd8..43e1d7a9e1 100644 --- a/interface/src/Application_render.cpp +++ b/interface/src/Application_render.cpp @@ -15,6 +15,10 @@ #include #include "ui/Stats.h" #include "FrameTimingsScriptingInterface.h" +#include +#include "Util.h" + +FrameTimingsScriptingInterface _frameTimingsScriptingInterface; // Statically provided display and input plugins extern DisplayPluginList getDisplayPlugins(); @@ -58,14 +62,6 @@ void Application::paintGL() { } } - // update the avatar with a fresh HMD pose - // { - // PROFILE_RANGE(render, "/updateAvatar"); - // getMyAvatar()->updateFromHMDSensorMatrix(getHMDSensorPose()); - // } - - // auto lodManager = DependencyManager::get(); - RenderArgs renderArgs; float sensorToWorldScale; glm::mat4 HMDSensorPose; @@ -90,39 +86,8 @@ void Application::paintGL() { }); } - //float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); - //{ - // PROFILE_RANGE(render, "/buildFrustrumAndArgs"); - // { - // QMutexLocker viewLocker(&_viewMutex); - // // adjust near clip plane to account for sensor scaling. - // auto adjustedProjection = glm::perspective(_viewFrustum.getFieldOfView(), - // _viewFrustum.getAspectRatio(), - // DEFAULT_NEAR_CLIP * sensorToWorldScale, - // _viewFrustum.getFarClip()); - // _viewFrustum.setProjection(adjustedProjection); - // _viewFrustum.calculate(); - // } - // renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(), - // lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, - // RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); - // { - // QMutexLocker viewLocker(&_viewMutex); - // renderArgs.setViewFrustum(_viewFrustum); - // } - //} - - //{ - // PROFILE_RANGE(render, "/resizeGL"); - // PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); - // bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - // PerformanceWarning warn(showWarnings, "Application::paintGL()"); - // resizeGL(); - //} - { PROFILE_RANGE(render, "/gpuContextReset"); - // _gpuContext->beginFrame(getHMDSensorPose()); _gpuContext->beginFrame(HMDSensorPose); // Reset the gpu::Context Stages // Back to the default framebuffer; @@ -160,67 +125,14 @@ void Application::paintGL() { finalFramebuffer = framebufferCache->getFramebuffer(); } - //auto hmdInterface = DependencyManager::get(); - //float ipdScale = hmdInterface->getIPDScale(); - - //// scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. - //ipdScale *= sensorToWorldScale; - { - //PROFILE_RANGE(render, "/mainRender"); - //PerformanceTimer perfTimer("mainRender"); - //// FIXME is this ever going to be different from the size previously set in the render args - //// in the overlay render? - //// Viewport is assigned to the size of the framebuffer - //renderArgs._viewport = ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height()); - //auto baseProjection = renderArgs.getViewFrustum().getProjection(); - //if (displayPlugin->isStereo()) { - // // Stereo modes will typically have a larger projection matrix overall, - // // so we ask for the 'mono' projection matrix, which for stereo and HMD - // // plugins will imply the combined projection for both eyes. - // // - // // This is properly implemented for the Oculus plugins, but for OpenVR - // // and Stereo displays I'm not sure how to get / calculate it, so we're - // // just relying on the left FOV in each case and hoping that the - // // overall culling margin of error doesn't cause popping in the - // // right eye. There are FIXMEs in the relevant plugins - // _myCamera.setProjection(displayPlugin->getCullingProjection(baseProjection)); - // renderArgs._context->enableStereo(true); - // mat4 eyeOffsets[2]; - // mat4 eyeProjections[2]; - - // // FIXME we probably don't need to set the projection matrix every frame, - // // only when the display plugin changes (or in non-HMD modes when the user - // // changes the FOV manually, which right now I don't think they can. - // for_each_eye([&](Eye eye) { - // // For providing the stereo eye views, the HMD head pose has already been - // // applied to the avatar, so we need to get the difference between the head - // // pose applied to the avatar and the per eye pose, and use THAT as - // // the per-eye stereo matrix adjustment. - // mat4 eyeToHead = displayPlugin->getEyeToHeadTransform(eye); - // // Grab the translation - // vec3 eyeOffset = glm::vec3(eyeToHead[3]); - // // Apply IPD scaling - // mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); - // eyeOffsets[eye] = eyeOffsetTransform; - // eyeProjections[eye] = displayPlugin->getEyeProjection(eye, baseProjection); - // }); - // renderArgs._context->setStereoProjections(eyeProjections); - // renderArgs._context->setStereoViews(eyeOffsets); - - // // Configure the type of display / stereo - // renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); - //} - if (isStereo) { renderArgs._context->enableStereo(true); renderArgs._context->setStereoProjections(stereoEyeProjections); renderArgs._context->setStereoViews(stereoEyeOffsets); - // renderArgs._displayMode } renderArgs._blitFramebuffer = finalFramebuffer; - // displaySide(&renderArgs, _myCamera); runRenderFrame(&renderArgs); } @@ -291,49 +203,34 @@ namespace render { } void Application::runRenderFrame(RenderArgs* renderArgs) { - - // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. - // Then we can move this logic into the Avatar::simulate call. - // auto myAvatar = getMyAvatar(); - // myAvatar->preDisplaySide(renderArgs); - PROFILE_RANGE(render, __FUNCTION__); PerformanceTimer perfTimer("display"); PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::runRenderFrame()"); - // load the view frustum - // { - // QMutexLocker viewLocker(&_viewMutex); - // theCamera.loadViewFrustum(_displayViewFrustum); - // } - // The pending changes collecting the changes here render::Transaction transaction; - // Assuming nothing gets rendered through that - //if (!selfAvatarOnly) { - { - if (DependencyManager::get()->shouldRenderEntities()) { - // render models... - PerformanceTimer perfTimer("entities"); - PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), - "Application::runRenderFrame() ... entities..."); + if (DependencyManager::get()->shouldRenderEntities()) { + // render models... + PerformanceTimer perfTimer("entities"); + PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), + "Application::runRenderFrame() ... entities..."); - RenderArgs::DebugFlags renderDebugFlags = RenderArgs::RENDER_DEBUG_NONE; + RenderArgs::DebugFlags renderDebugFlags = RenderArgs::RENDER_DEBUG_NONE; - if (Menu::getInstance()->isOptionChecked(MenuOption::PhysicsShowHulls)) { - renderDebugFlags = static_cast(renderDebugFlags | - static_cast(RenderArgs::RENDER_DEBUG_HULLS)); - } - renderArgs->_debugFlags = renderDebugFlags; + if (Menu::getInstance()->isOptionChecked(MenuOption::PhysicsShowHulls)) { + renderDebugFlags = static_cast(renderDebugFlags | + static_cast(RenderArgs::RENDER_DEBUG_HULLS)); } + renderArgs->_debugFlags = renderDebugFlags; } - // FIXME: Move this out of here!, WorldBox should be driven by the entity content just like the other entities // Make sure the WorldBox is in the scene + // For the record, this one RenderItem is the first one we created and added to the scene. + // We could meoee that code elsewhere but you know... if (!render::Item::isValidID(WorldBoxRenderData::_item)) { - auto worldBoxRenderData = make_shared(); - auto worldBoxRenderPayload = make_shared(worldBoxRenderData); + auto worldBoxRenderData = std::make_shared(); + auto worldBoxRenderPayload = std::make_shared(worldBoxRenderData); WorldBoxRenderData::_item = _main3DScene->allocateID(); @@ -341,20 +238,9 @@ void Application::runRenderFrame(RenderArgs* renderArgs) { _main3DScene->enqueueTransaction(transaction); } - - // For now every frame pass the renderContext { PerformanceTimer perfTimer("EngineRun"); - - /* { - QMutexLocker viewLocker(&_viewMutex); - renderArgs->setViewFrustum(_displayViewFrustum); - }*/ - // renderArgs->_cameraMode = (int8_t)theCamera.getMode(); // HACK - renderArgs->_scene = getMain3DScene(); _renderEngine->getRenderContext()->args = renderArgs; - - // Before the deferred pass, let's try to use the render engine _renderEngine->run(); } } From 0cbf19bcf141765c153befda56705359272b7c58 Mon Sep 17 00:00:00 2001 From: samcake Date: Sun, 1 Oct 2017 17:47:35 -0700 Subject: [PATCH 624/722] Removing comments --- interface/src/Application.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3f87d72c88..efa18f36df 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -429,7 +429,7 @@ public: // Don't actually crash in debug builds, in case this apparent deadlock is simply from // the developer actively debugging code #ifdef NDEBUG - // deadlockDetectionCrash(); + deadlockDetectionCrash(); #endif } } @@ -5036,9 +5036,6 @@ void Application::update(float deltaTime) { } editRenderArgs([this](AppRenderArgs& appRenderArgs) { - - appRenderArgs._renderArgs._scene = getMain3DScene(); - appRenderArgs._headPose= getHMDSensorPose(); auto myAvatar = getMyAvatar(); @@ -5068,6 +5065,8 @@ void Application::update(float deltaTime) { appRenderArgs._renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(), lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); + appRenderArgs._renderArgs._scene = getMain3DScene(); + { QMutexLocker viewLocker(&_viewMutex); appRenderArgs._renderArgs.setViewFrustum(_viewFrustum); From 7dfa80f666a00a31d3973fff12233725e41b93fd Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 28 Sep 2017 18:03:22 -0700 Subject: [PATCH 625/722] Bug fix for offset of animated parts of oculus touch controller display Ensure position/rotations are updated with a consistent scale and are animated correctly as the controller values change. --- .../system/controllers/controllerDisplay.js | 153 +++++++++--------- 1 file changed, 74 insertions(+), 79 deletions(-) diff --git a/scripts/system/controllers/controllerDisplay.js b/scripts/system/controllers/controllerDisplay.js index af8cfa74f4..3c2794cf96 100644 --- a/scripts/system/controllers/controllerDisplay.js +++ b/scripts/system/controllers/controllerDisplay.js @@ -49,6 +49,7 @@ createControllerDisplay = function(config) { partOverlays: {}, parts: {}, mappingName: "mapping-display-" + Math.random(), + partValues: {}, setVisible: function(visible) { for (var i = 0; i < this.overlays.length; ++i) { @@ -109,12 +110,53 @@ createControllerDisplay = function(config) { for (var partName in controller.parts) { overlayID = this.overlays[i++]; var part = controller.parts[partName]; - var partPosition = Vec3.multiply(sensorScaleFactor, Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, part.naturalPosition))); - var partDimensions = Vec3.multiply(sensorScaleFactor, part.naturalDimensions); - Overlays.editOverlay(overlayID, { - dimensions: partDimensions, - localPosition: partPosition - }); + localPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, part.naturalPosition)); + var localRotation; + var value = this.partValues[partName]; + var offset, rotation; + if (value !== undefined) { + if (part.type === "linear") { + var axis = Vec3.multiplyQbyV(controller.rotation, part.axis); + offset = Vec3.multiply(part.maxTranslation * value, axis); + localPosition = Vec3.sum(localPosition, offset); + localRotation = undefined; + } else if (part.type === "joystick") { + rotation = Quat.fromPitchYawRollDegrees(value.y * part.xHalfAngle, 0, value.x * part.yHalfAngle); + if (part.originOffset) { + offset = Vec3.multiplyQbyV(rotation, part.originOffset); + offset = Vec3.subtract(part.originOffset, offset); + } else { + offset = { x: 0, y: 0, z: 0 }; + } + localPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, Vec3.sum(offset, part.naturalPosition))); + localRotation = Quat.multiply(controller.rotation, rotation); + } else if (part.type === "rotational") { + value = clamp(value, part.minValue, part.maxValue); + var pct = (value - part.minValue) / part.maxValue; + var angle = pct * part.maxAngle; + rotation = Quat.angleAxis(angle, part.axis); + if (part.origin) { + offset = Vec3.multiplyQbyV(rotation, part.origin); + offset = Vec3.subtract(offset, part.origin); + } else { + offset = { x: 0, y: 0, z: 0 }; + } + localPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, Vec3.sum(offset, part.naturalPosition))); + localRotation = Quat.multiply(controller.rotation, rotation); + } + } + if (localRotation !== undefined) { + Overlays.editOverlay(overlayID, { + dimensions: Vec3.multiply(sensorScaleFactor, part.naturalDimensions), + localPosition: Vec3.multiply(sensorScaleFactor, localPosition), + localRotation: localRotation + }); + } else { + Overlays.editOverlay(overlayID, { + dimensions: Vec3.multiply(sensorScaleFactor, part.naturalDimensions), + localPosition: Vec3.multiply(sensorScaleFactor, localPosition) + }); + } } } } @@ -172,29 +214,13 @@ createControllerDisplay = function(config) { if (part.type === "rotational") { var input = resolveHardware(part.input); print("Mapping to: ", part.input, input); - mapping.from([input]).peek().to(function(controller, overlayID, part) { + mapping.from([input]).peek().to(function(partName) { return function(value) { - value = clamp(value, part.minValue, part.maxValue); - - var pct = (value - part.minValue) / part.maxValue; - var angle = pct * part.maxAngle; - var rotation = Quat.angleAxis(angle, part.axis); - - var offset = { x: 0, y: 0, z: 0 }; - if (part.origin) { - offset = Vec3.multiplyQbyV(rotation, part.origin); - offset = Vec3.subtract(offset, part.origin); - } - - var partPosition = Vec3.sum(controller.position, - Vec3.multiplyQbyV(controller.rotation, Vec3.sum(offset, part.naturalPosition))); - - Overlays.editOverlay(overlayID, { - localPosition: partPosition, - localRotation: Quat.multiply(controller.rotation, rotation) - }); + // insert the most recent controller value into controllerDisplay.partValues. + controllerDisplay.partValues[partName] = value; + controllerDisplay.resize(MyAvatar.sensorToWorldScale); }; - }(controller, overlayID, part)); + }(partName)); } else if (part.type === "touchpad") { var visibleInput = resolveHardware(part.visibleInput); var xInput = resolveHardware(part.xInput); @@ -210,69 +236,38 @@ createControllerDisplay = function(config) { mapping.from([yInput]).peek().invert().to(function(value) { }); } else if (part.type === "joystick") { - (function(controller, overlayID, part) { + (function(part, partName) { var xInput = resolveHardware(part.xInput); var yInput = resolveHardware(part.yInput); - - var xvalue = 0; - var yvalue = 0; - - function calculatePositionAndRotation(xValue, yValue) { - var rotation = Quat.fromPitchYawRollDegrees(yValue * part.xHalfAngle, 0, xValue * part.yHalfAngle); - - var offset = { x: 0, y: 0, z: 0 }; - if (part.originOffset) { - offset = Vec3.multiplyQbyV(rotation, part.originOffset); - offset = Vec3.subtract(part.originOffset, offset); - } - - var partPosition = Vec3.sum(controller.position, - Vec3.multiplyQbyV(controller.rotation, Vec3.sum(offset, part.naturalPosition))); - - var partRotation = Quat.multiply(controller.rotation, rotation); - - return { - position: partPosition, - rotation: partRotation - }; - } - mapping.from([xInput]).peek().to(function(value) { - xvalue = value; - var posRot = calculatePositionAndRotation(xvalue, yvalue); - Overlays.editOverlay(overlayID, { - localPosition: posRot.position, - localRotation: posRot.rotation - }); + // insert the most recent controller value into controllerDisplay.partValues. + if (controllerDisplay.partValues[partName]) { + controllerDisplay.partValues[partName].x = value; + } else { + controllerDisplay.partValues[partName] = {x: value, y: 0}; + } + controllerDisplay.resize(MyAvatar.sensorToWorldScale); }); - mapping.from([yInput]).peek().to(function(value) { - yvalue = value; - var posRot = calculatePositionAndRotation(xvalue, yvalue); - Overlays.editOverlay(overlayID, { - localPosition: posRot.position, - localRotation: posRot.rotation - }); + // insert the most recent controller value into controllerDisplay.partValues. + if (controllerDisplay.partValues[partName]) { + controllerDisplay.partValues[partName].y = value; + } else { + controllerDisplay.partValues[partName] = {x: 0, y: value}; + } + controllerDisplay.resize(MyAvatar.sensorToWorldScale); }); - })(controller, overlayID, part); + })(part, partName); } else if (part.type === "linear") { - (function(controller, overlayID, part) { + (function(part, partName) { var input = resolveHardware(part.input); - mapping.from([input]).peek().to(function(value) { - var axis = Vec3.multiplyQbyV(controller.rotation, part.axis); - var offset = Vec3.multiply(part.maxTranslation * value, axis); - - var partPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, part.naturalPosition)); - var position = Vec3.sum(partPosition, offset); - - Overlays.editOverlay(overlayID, { - localPosition: position - }); + // insert the most recent controller value into controllerDisplay.partValues. + controllerDisplay.partValues[partName] = value; + controllerDisplay.resize(MyAvatar.sensorToWorldScale); }); - - })(controller, overlayID, part); + })(part, partName); } else if (part.type === "static") { // do nothing From 6c510bc18f297842fac4c4c3e77dc20041a3b65c Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Sun, 1 Oct 2017 20:05:48 -0700 Subject: [PATCH 626/722] fine tuning the changes to respect clean up --- interface/src/Application.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index efa18f36df..35fc0049a8 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2018,7 +2018,8 @@ void Application::cleanupBeforeQuit() { // The cleanup process enqueues the transactions but does not process them. Calling this here will force the actual // removal of the items. // See https://highfidelity.fogbugz.com/f/cases/5328 - // _main3DScene->processTransactionQueue(); + _main3DScene->enqueueFrame(); // flush all the transactions + _main3DScene->processTransactionQueue(); // process and apply deletions // first stop all timers directly or by invokeMethod // depending on what thread they run in From ec10d38983f813e37ff123486602f0ea14fa2add Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Sun, 1 Oct 2017 22:28:04 -0700 Subject: [PATCH 627/722] more cleaning --- interface/src/Application_render.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/interface/src/Application_render.cpp b/interface/src/Application_render.cpp index 43e1d7a9e1..1c82c95a8a 100644 --- a/interface/src/Application_render.cpp +++ b/interface/src/Application_render.cpp @@ -63,7 +63,6 @@ void Application::paintGL() { } RenderArgs renderArgs; - float sensorToWorldScale; glm::mat4 HMDSensorPose; glm::mat4 eyeToWorld; glm::mat4 sensorToWorld; @@ -78,7 +77,6 @@ void Application::paintGL() { HMDSensorPose = _appRenderArgs._headPose; eyeToWorld = _appRenderArgs._eyeToWorld; sensorToWorld = _appRenderArgs._sensorToWorld; - sensorToWorldScale = _appRenderArgs._sensorToWorldScale; isStereo = _appRenderArgs._isStereo; for_each_eye([&](Eye eye) { stereoEyeOffsets[eye] = _appRenderArgs._eyeOffsets[eye]; @@ -110,7 +108,6 @@ void Application::paintGL() { // updateCamera(renderArgs); { PROFILE_RANGE(render, "/updateCompositor"); - // getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform(), getMyAvatar()->getSensorToWorldMatrix()); getApplicationCompositor().setFrameInfo(_frameCount, eyeToWorld, sensorToWorld); } From 0bd67d494d60623dce8438f11e3a862fa581cb37 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 2 Oct 2017 19:27:26 +1300 Subject: [PATCH 628/722] Fix UI sometimes turning off/on/off as hand enters entity --- scripts/shapes/modules/hand.js | 4 ++++ scripts/shapes/shapes.js | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/scripts/shapes/modules/hand.js b/scripts/shapes/modules/hand.js index 29ed6bfd96..73efed8017 100644 --- a/scripts/shapes/modules/hand.js +++ b/scripts/shapes/modules/hand.js @@ -204,6 +204,10 @@ Hand = function (side) { } if (entityID) { intersectionPosition = Entities.getEntityProperties(entityID, "position").position; + if (Vec3.distance(palmPosition, intersectionPosition) > NEAR_GRAB_RADIUS) { + intersectionPosition = Vec3.sum(palmPosition, + Vec3.multiply(NEAR_GRAB_RADIUS, Vec3.normalize(Vec3.subtract(intersectionPosition, palmPosition)))); + } } } diff --git a/scripts/shapes/shapes.js b/scripts/shapes/shapes.js index 573a9445e7..b4e822c233 100644 --- a/scripts/shapes/shapes.js +++ b/scripts/shapes/shapes.js @@ -930,7 +930,7 @@ // Hide UI if hand is intersecting entity and camera is outside entity, or if hand is intersecting stretch handle. if (side !== dominantHand) { showUI = !intersection.handIntersected || (intersection.entityID !== null - && !isCameraOutsideEntity(intersection.entityID, hand.palmPosition())); + && !isCameraOutsideEntity(intersection.entityID, intersection.intersection)); if (showUI !== isUIVisible) { isUIVisible = !isUIVisible; ui.setVisible(isUIVisible); @@ -952,7 +952,8 @@ && otherEditor.isHandle(intersection.overlayID)) && !(intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab - && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, hand.palmPosition()))) + && (isTriggerPressed + || isCameraOutsideEntity(intersection.entityID, intersection.intersection))) && !(intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (!wasTriggerClicked || isAutoGrab) && isTriggerClicked)) { // No transition. @@ -969,7 +970,7 @@ setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab - && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, hand.palmPosition()))) { + && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, intersection.intersection))) { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); @@ -1020,7 +1021,7 @@ case EDITOR_HIGHLIGHTING: if (hand.valid() && intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) - && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, hand.palmPosition())) + && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, intersection.intersection)) && !(!wasTriggerClicked && isTriggerClicked && (!otherEditor.isEditing(rootEntityID) || toolSelected !== TOOL_SCALE)) && !(!wasTriggerClicked && isTriggerClicked && intersection.overlayID @@ -1100,8 +1101,9 @@ } else { setState(EDITOR_GRABBING); } + } else if (!intersection.entityID || !intersection.editableEntity - || (!isTriggerPressed && !isCameraOutsideEntity(intersection.entityID, hand.palmPosition()))) { + || (!isTriggerPressed && !isCameraOutsideEntity(intersection.entityID, intersection.intersection))) { setState(EDITOR_SEARCHING); } else { log(side, "ERROR: Editor: Unexpected condition B in EDITOR_HIGHLIGHTING!"); From 0f6884dd80037e2ff3da6532a375f9cb2cf1a524 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 2 Oct 2017 09:44:10 -0700 Subject: [PATCH 629/722] fix mac/linux compiler error --- libraries/entities/src/EntityItem.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 12c1624366..c26f1694a9 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -332,7 +332,7 @@ public: QByteArray getStaticCertificateHash() const; bool verifyStaticCertificateProperties(); #ifdef DEBUG_CERT - QString EntityItem::computeCertificateID(); + QString computeCertificateID(); #endif // TODO: get rid of users of getRadius()... From 2b03e08c968fc2dcfd60dd7e90878f8aa9cc335d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 2 Oct 2017 10:20:24 -0700 Subject: [PATCH 630/722] Fix errant status when requesting it for the first time --- interface/src/commerce/QmlCommerce.cpp | 31 ++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 255d6ef516..b735cfcd17 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -29,34 +29,37 @@ QmlCommerce::QmlCommerce(QQuickItem* parent) : OffscreenQmlDialog(parent) { connect(ledger.data(), &Ledger::historyResult, this, &QmlCommerce::historyResult); connect(wallet.data(), &Wallet::keyFilePathIfExistsResult, this, &QmlCommerce::keyFilePathIfExistsResult); connect(ledger.data(), &Ledger::accountResult, this, &QmlCommerce::accountResult); + connect(ledger.data(), &Ledger::accountResult, this, [&]() { + auto wallet = DependencyManager::get(); + auto walletScriptingInterface = DependencyManager::get(); + uint status; + + if (wallet->getKeyFilePath() == "" || !wallet->getSecurityImage()) { + status = (uint)WalletStatus::WALLET_STATUS_NOT_SET_UP; + } else if (!wallet->walletIsAuthenticatedWithPassphrase()) { + status = (uint)WalletStatus::WALLET_STATUS_NOT_AUTHENTICATED; + } else { + status = (uint)WalletStatus::WALLET_STATUS_READY; + } + + walletScriptingInterface->setWalletStatus(status); + emit walletStatusResult(status); + }); } void QmlCommerce::getWalletStatus() { - auto wallet = DependencyManager::get(); - auto ledger = DependencyManager::get(); auto walletScriptingInterface = DependencyManager::get(); uint status; if (DependencyManager::get()->isLoggedIn()) { // This will set account info for the wallet, allowing us to decrypt and display the security image. - ledger->account(); + account(); } else { status = (uint)WalletStatus::WALLET_STATUS_NOT_LOGGED_IN; emit walletStatusResult(status); walletScriptingInterface->setWalletStatus(status); return; } - - if (wallet->getKeyFilePath() == "" || !wallet->getSecurityImage()) { - status = (uint)WalletStatus::WALLET_STATUS_NOT_SET_UP; - } else if (!wallet->walletIsAuthenticatedWithPassphrase()) { - status = (uint)WalletStatus::WALLET_STATUS_NOT_AUTHENTICATED; - } else { - status = (uint)WalletStatus::WALLET_STATUS_READY; - } - - walletScriptingInterface->setWalletStatus(status); - emit walletStatusResult(status); } void QmlCommerce::getLoginStatus() { From 5c5f052bc27067800a7bf37d918459d0e892078b Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 2 Oct 2017 10:28:47 -0700 Subject: [PATCH 631/722] explicit free, and remove header that isn't available on mac/linux --- libraries/entities/src/EntityItem.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index dbdfbdb0e6..ebe7c53bd4 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -14,8 +14,7 @@ #include #include #include -#include // see comments for DEBUG_CERT -#include +#include // see comments for DEBUG_CERT #include #include @@ -1637,6 +1636,7 @@ KxHof+LmYSYZAiB4U+yEh9SsXdq40W/3fpLMPuNq1PRezJ5jGidGMcvF+wIgUNec\n\ unsigned int signatureLength = 0; const int signOK = RSA_sign(NID_sha256, text, textLength, reinterpret_cast(signature.data()), &signatureLength, rsa); BIO_free(bio); + RSA_free(rsa); if (!signOK) { qCWarning(entities) << "Unable to compute signature for" << getName() << getEntityItemID(); return ""; @@ -1670,6 +1670,8 @@ AqbcNnrV/TcG6LPI7ZbiMjdUixmTNvYMRZH3Wlqtl2IKG1W68y3stKECAwEAAQ==\n\ RSA* rsa = EVP_PKEY_get1_RSA(evp_key); bool answer = RSA_verify(NID_sha256, text, textLength, signature, signatureLength, rsa); BIO_free(bio); + RSA_free(rsa); + EVP_PKEY_free(evp_key); return answer; } From bf39eb9a77dadd889f6593b9ae39c97ba2235c83 Mon Sep 17 00:00:00 2001 From: vladest Date: Mon, 2 Oct 2017 20:02:55 +0200 Subject: [PATCH 632/722] Test Tablet keyboard specific --- interface/resources/qml/LoginDialog.qml | 2 ++ .../qml/LoginDialog/LinkAccountBody.qml | 11 ++++++--- .../qml/dialogs/TabletLoginDialog.qml | 24 +++++++++++++++---- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index 315cda3551..e21e8b7354 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -27,6 +27,8 @@ ModalWindow { destroyOnHidden: true visible: true + readonly property bool isTablet: false + property string iconText: "" property int iconSize: 50 diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 13462804c3..dd38b641bb 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -244,12 +244,17 @@ Item { } } - - Component.onCompleted: { root.title = qsTr("Sign Into High Fidelity") root.iconText = "<" - keyboardEnabled = HMD.active; + + //dont rise local keyboard + keyboardEnabled = !root.isTablet && HMD.active; + //but rise Tablet's one instead for Tablet interface + if (root.isTablet) { + root.keyboardEnabled = HMD.active; + root.keyboardRaised = Qt.binding( function() { return keyboardRaised; }) + } //d.resize(); if (failAfterSignUp) { diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index f4f7a5848c..334cb9304f 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -32,8 +32,14 @@ TabletModalWindow { //fake root for shared components expecting root here property var root: QtObject { id: root - property alias title: realRoot.title + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + readonly property bool isTablet: true + + property alias title: realRoot.title property real width: realRoot.width property real height: realRoot.height @@ -65,10 +71,6 @@ TabletModalWindow { //onTitleWidthChanged: d.resize(); - property bool keyboardEnabled: false - property bool keyboardRaised: false - property bool punctuationMode: false - //onKeyboardRaisedChanged: d.resize(); signal canceled(); @@ -123,6 +125,18 @@ TabletModalWindow { } } + Keyboard { + raised: root.keyboardEnabled && root.keyboardRaised + numeric: root.punctuationMode + enabled: true + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + bottomMargin: root.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } + } + Keys.onPressed: { if (!visible) { return From f150d77d318517975fa32996cb162eca8f9158e5 Mon Sep 17 00:00:00 2001 From: Liv Date: Mon, 2 Oct 2017 11:08:07 -0700 Subject: [PATCH 633/722] Updating placeholder.js to use a url that points to example scripts --- domain-server/resources/web/assignment/placeholder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/resources/web/assignment/placeholder.js b/domain-server/resources/web/assignment/placeholder.js index 2c1d8253aa..bf64539bea 100644 --- a/domain-server/resources/web/assignment/placeholder.js +++ b/domain-server/resources/web/assignment/placeholder.js @@ -1,3 +1,3 @@ // Here you can put a script that will be run by an assignment-client (AC) -// For examples, please go to http://public.highfidelity.io/scripts +// For examples, please go to https://github.com/highfidelity/hifi/tree/master/script-archive/acScripts // The directory named acScripts contains assignment-client specific scripts you can try. From cc8f1352da3993b81cd8b9bdbc9c052ef6a83692 Mon Sep 17 00:00:00 2001 From: beholder Date: Mon, 2 Oct 2017 22:18:03 +0300 Subject: [PATCH 634/722] 7951 Reload button in Running Scripts dialog stops script --- libraries/script-engine/src/ScriptEngine.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 2260cea616..a2dac7ed43 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -97,6 +97,9 @@ static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) int functionSignatureMetaID = qRegisterMetaType(); +Q_DECLARE_METATYPE(ScriptEnginePointer) +int scriptEnginePointerMetaID = qRegisterMetaType(); + Q_LOGGING_CATEGORY(scriptengineScript, "hifi.scriptengine.script") static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) { From 15c0a21e0e996a3b5898d766c8303c91e02ead6f Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 2 Oct 2017 12:19:24 -0700 Subject: [PATCH 635/722] Varied commit - styling changes, beginnings of new Wallet endpoint, etc --- .../qml/hifi/commerce/checkout/Checkout.qml | 80 +++++++++++-------- .../scripting/WalletScriptingInterface.cpp | 25 ++++++ .../src/scripting/WalletScriptingInterface.h | 5 ++ scripts/system/html/js/marketplacesInject.js | 2 +- scripts/system/marketplaces/marketplaces.js | 3 +- 5 files changed, 78 insertions(+), 37 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index f337e55dc9..2163407e0b 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -75,7 +75,7 @@ Rectangle { onBuyResult: { if (result.status !== 'success') { - failureErrorText.text = "Here's some more info about the error:

    " + (result.message); + failureErrorText.text = result.message; root.activeView = "checkoutFailure"; } else { root.activeView = "checkoutSuccess"; @@ -312,12 +312,12 @@ Rectangle { id: itemPriceTextLabel; text: hifi.glyphs.hfc; // Size - size: 36; + size: 30; // Anchors anchors.right: itemPriceText.left; anchors.rightMargin: 4; anchors.top: parent.top; - anchors.topMargin: -4; + anchors.topMargin: 0; width: paintedWidth; height: paintedHeight; // Style @@ -395,7 +395,7 @@ Rectangle { verticalAlignment: Text.AlignTop; } - RalewaySemiBold { + RalewayRegular { id: buyText; // Text size size: 18; @@ -699,7 +699,9 @@ Rectangle { anchors.top: titleBarContainer.bottom; anchors.bottom: root.bottom; anchors.left: parent.left; + anchors.leftMargin: 16; anchors.right: parent.right; + anchors.rightMargin: 16; RalewayRegular { id: failureHeaderText; @@ -708,57 +710,65 @@ Rectangle { size: 24; // Anchors anchors.top: parent.top; - anchors.topMargin: 80; + anchors.topMargin: 40; height: paintedHeight; anchors.left: parent.left; anchors.right: parent.right; // Style color: hifi.colors.black; wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; } - RalewayRegular { - id: failureErrorText; - // Text size - size: 16; - // Anchors + Rectangle { + id: failureErrorTextContainer; anchors.top: failureHeaderText.bottom; anchors.topMargin: 35; - height: paintedHeight; anchors.left: parent.left; anchors.right: parent.right; - // Style - color: hifi.colors.black; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + height: failureErrorText.height + 30; + radius: 4; + border.width: 2; + border.color: "#F3808F"; + color: "#FFC3CD"; + + AnonymousProRegular { + id: failureErrorText; + // Text size + size: 16; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 15; + anchors.left: parent.left; + anchors.leftMargin: 8; + anchors.right: parent.right; + anchors.rightMargin: 8; + height: paintedHeight; + // Style + color: hifi.colors.black; + wrapMode: Text.Wrap; + verticalAlignment: Text.AlignVCenter; + } } Item { id: backToMarketplaceButtonContainer; // Size width: root.width; - height: 130; + height: 50; // Anchors anchors.left: parent.left; anchors.bottom: parent.bottom; - anchors.bottomMargin: 8; + anchors.bottomMargin: 16; // "Back to Marketplace" button HifiControlsUit.Button { id: backToMarketplaceButton; - color: hifi.buttons.black; + color: hifi.buttons.noneBorderlessGray; colorScheme: hifi.colorSchemes.light; anchors.top: parent.top; - anchors.topMargin: 3; anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: parent.width/2 - anchors.rightMargin*2; + anchors.left: parent.left; + anchors.leftMargin: 16; + width: parent.width/2 - anchors.leftMargin*2; text: "Back to Marketplace"; onClicked: { sendToScript({method: 'checkout_continueShopping', itemId: itemId}); @@ -806,7 +816,7 @@ Rectangle { itemId = message.params.itemId; itemNameText.text = message.params.itemName; root.itemPrice = message.params.itemPrice; - itemPriceText.text = root.itemPrice === 0 ? "Free" : root.itemPrice; + itemPriceText.text = root.itemPrice; itemHref = message.params.itemHref; itemPreviewImageUrl = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg"; if (itemHref.indexOf('.json') === -1) { @@ -834,10 +844,10 @@ Rectangle { if (root.purchasesReceived && root.balanceReceived) { if (root.balanceAfterPurchase < 0) { if (root.alreadyOwned) { - buyText.text = "Your Wallet does not have sufficient funds to purchase this item again.
    " + - 'View the copy you own in My Purchases'; + buyText.text = "Your Wallet does not have sufficient funds to purchase this item again.
    " + + 'View the copy you own in My Purchases
    '; } else { - buyText.text = "Your Wallet does not have sufficient funds to purchase this item."; + buyText.text = "Your Wallet does not have sufficient funds to purchase this item."; } buyTextContainer.color = "#FFC3CD"; buyTextContainer.border.color = "#F3808F"; @@ -845,8 +855,8 @@ Rectangle { buyGlyph.size = 54; } else { if (root.alreadyOwned) { - buyText.text = 'You already own this item.
    Purchasing it will buy another copy.
    View this item in My Purchases'; + buyText.text = 'You already own this item.
    Purchasing it will buy another copy.
    View this item in My Purchases
    '; buyTextContainer.color = "#FFD6AD"; buyTextContainer.border.color = "#FAC07D"; buyGlyph.text = hifi.glyphs.alert; @@ -859,7 +869,7 @@ Rectangle { buyText.text = ""; } } else { - buyText.text = "This Marketplace item isn't an entity. It will not be added to your Purchases."; + buyText.text = "This free item will not be added to your Purchases. Non-entities can't yet be purchased for HFC."; buyTextContainer.color = "#FFD6AD"; buyTextContainer.border.color = "#FAC07D"; buyGlyph.text = hifi.glyphs.alert; diff --git a/interface/src/scripting/WalletScriptingInterface.cpp b/interface/src/scripting/WalletScriptingInterface.cpp index 94f8030fa2..2d94bf46cb 100644 --- a/interface/src/scripting/WalletScriptingInterface.cpp +++ b/interface/src/scripting/WalletScriptingInterface.cpp @@ -13,3 +13,28 @@ WalletScriptingInterface::WalletScriptingInterface() { } + +static const QString CHECKOUT_QML_PATH = qApp->applicationDirPath() + "../../../qml/hifi/commerce/checkout/Checkout.qml"; +void WalletScriptingInterface::buy() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + + if (!tablet->isPathLoaded(CHECKOUT_QML_PATH)) { + tablet->loadQMLSource(CHECKOUT_QML_PATH); + } + DependencyManager::get()->openTablet(); + + QVariant message = {}; + + + tablet->sendToQml(message); .sendToQml({ + method: 'updateCheckoutQML', params : { + itemId: '0d90d21c-ce7a-4990-ad18-e9d2cf991027', + itemName : 'Test Flaregun', + itemAuthor : 'hifiDave', + itemPrice : 17, + itemHref : 'http://mpassets.highfidelity.com/0d90d21c-ce7a-4990-ad18-e9d2cf991027-v1/flaregun.json', + }, + canRezCertifiedItems: Entities.canRezCertified || Entities.canRezTmpCertified + }); +} \ No newline at end of file diff --git a/interface/src/scripting/WalletScriptingInterface.h b/interface/src/scripting/WalletScriptingInterface.h index 111a6eea3d..9ff3a0319e 100644 --- a/interface/src/scripting/WalletScriptingInterface.h +++ b/interface/src/scripting/WalletScriptingInterface.h @@ -15,6 +15,9 @@ #include #include +#include "scripting/HMDScriptingInterface.h" +#include + class WalletScriptingInterface : public QObject, public Dependency { Q_OBJECT @@ -27,6 +30,8 @@ public: Q_INVOKABLE uint getWalletStatus() { return _walletStatus; } void setWalletStatus(const uint& status) { _walletStatus = status; } + Q_INVOKABLE void buy(); + signals: void walletStatusChanged(); diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 082e3ab4c0..326866ee7a 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -192,7 +192,7 @@ var dropDownElement = document.getElementById('user-dropdown'); purchasesElement.id = "purchasesButton"; purchasesElement.setAttribute('href', "#"); - purchasesElement.innerHTML = "MY PURCHASES"; + purchasesElement.innerHTML = "My Purchases"; // FRONTEND WEBDEV RANT: The username dropdown should REALLY not be programmed to be on the same // line as the search bar, overlaid on top of the search bar, floated right, and then relatively bumped up using "top:-50px". purchasesElement.style = "height:100%;margin-top:18px;font-weight:bold;float:right;margin-right:" + (dropDownElement.offsetWidth + 30) + diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 711f94d53e..3e54b22f75 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -60,6 +60,7 @@ var onCommerceScreen = false; var debugCheckout = false; + var debugError = false; function showMarketplace() { if (!debugCheckout) { UserActivityLogger.openedMarketplace(); @@ -71,7 +72,7 @@ itemId: '0d90d21c-ce7a-4990-ad18-e9d2cf991027', itemName: 'Test Flaregun', itemAuthor: 'hifiDave', - itemPrice: 17, + itemPrice: (debugError ? 10 : 17), itemHref: 'http://mpassets.highfidelity.com/0d90d21c-ce7a-4990-ad18-e9d2cf991027-v1/flaregun.json', }, canRezCertifiedItems: Entities.canRezCertified || Entities.canRezTmpCertified From 7b0321c1e199290aaa28a48921e6b5512d238696 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 2 Oct 2017 12:30:25 -0700 Subject: [PATCH 636/722] guess for mac/linux openssl --- libraries/entities/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/entities/CMakeLists.txt b/libraries/entities/CMakeLists.txt index 19341ec3e2..322d69da16 100644 --- a/libraries/entities/CMakeLists.txt +++ b/libraries/entities/CMakeLists.txt @@ -1,3 +1,4 @@ set(TARGET_NAME entities) setup_hifi_library(Network Script) +include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}") link_hifi_libraries(shared networking octree avatars) From 1050d2851d11553bc048643bd7e815d73a46db48 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 2 Oct 2017 12:37:49 -0700 Subject: [PATCH 637/722] Modify injected code to add to dropdown menu --- scripts/system/html/js/marketplacesInject.js | 25 ++++++++++++++++++++ scripts/system/marketplaces/marketplaces.js | 8 +++++++ 2 files changed, 33 insertions(+) diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 326866ee7a..5e15e1c23e 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -207,6 +207,29 @@ } } + function changeDropdownMenu() { + var logInOrOutButton = document.createElement('a'); + logInOrOutButton.id = "logInOrOutButton"; + logInOrOutButton.setAttribute('href', "#"); + logInOrOutButton.innerHTML = userIsLoggedIn ? "Log Out" : "Log In"; + logInOrOutButton.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "LOGIN" + })); + }; + + $($('.dropdown-menu').find('li')[0]).append(logInOrOutButton); + + $('a[href="/marketplace?view=mine"]').each(function () { + $(this).attr('href', '#'); + $(this).on('click', function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "MY_ITEMS" + })); + }); + }); + } + function buyButtonClicked(id, name, author, price, href) { EventBridge.emitWebEvent(JSON.stringify({ type: "CHECKOUT", @@ -284,6 +307,7 @@ maybeAddLogInButton(); maybeAddSetupWalletButton(); + changeDropdownMenu(); var target = document.getElementById('templated-items'); // MutationObserver is necessary because the DOM is populated after the page is loaded. @@ -312,6 +336,7 @@ maybeAddLogInButton(); maybeAddSetupWalletButton(); + changeDropdownMenu(); var purchaseButton = $('#side-info').find('.btn').first(); diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 3e54b22f75..c9e6eca922 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -215,6 +215,14 @@ openLoginWindow(); } else if (parsedJsonMessage.type === "WALLET_SETUP") { tablet.pushOntoStack(MARKETPLACE_WALLET_QML_PATH); + } else if (parsedJsonMessage.type === "MY_ITEMS") { + referrerURL = MARKETPLACE_URL_INITIAL; + filterText = ""; + tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); + wireEventBridge(true); + tablet.sendToQml({ + method: 'purchases_showMyItems' + }); } } } From 1d1b846f39b26cb424e9bc3fc4ff92fdd03fe9b8 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 2 Oct 2017 12:44:51 -0700 Subject: [PATCH 638/722] Better 'view backup instructions' behavior --- .../qml/hifi/commerce/wallet/Security.qml | 31 +++++++++++++++++++ .../qml/hifi/commerce/wallet/WalletSetup.qml | 1 + 2 files changed, 32 insertions(+) diff --git a/interface/resources/qml/hifi/commerce/wallet/Security.qml b/interface/resources/qml/hifi/commerce/wallet/Security.qml index fcce798646..9b70bb1f71 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Security.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Security.qml @@ -280,6 +280,34 @@ Item { verticalAlignment: Text.AlignVCenter; } + Rectangle { + id: removeHmdContainer; + z: 998; + visible: false; + color: hifi.colors.blueHighlight; + anchors.fill: backupInstructionsButton; + radius: 5; + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + } + + RalewayBold { + anchors.fill: parent; + text: "INSTRUCTIONS OPEN ON DESKTOP"; + size: 15; + color: hifi.colors.white; + verticalAlignment: Text.AlignVCenter; + horizontalAlignment: Text.AlignHCenter; + } + + Timer { + id: removeHmdContainerTimer; + interval: 5000; + onTriggered: removeHmdContainer.visible = false + } + } + HifiControlsUit.Button { id: backupInstructionsButton; text: "View Backup Instructions"; @@ -293,6 +321,9 @@ Item { onClicked: { Qt.openUrlExternally("https://www.highfidelity.com/"); + Qt.openUrlExternally("file:///" + root.keyFilePath.substring(0, root.keyFilePath.lastIndexOf('/'))); + removeHmdContainer.visible = true; + removeHmdContainerTimer.start(); } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml index bc3ebb258e..898cdf0ef2 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml @@ -683,6 +683,7 @@ Item { instructions02Container.visible = true; keysReadyPageFinishButton.visible = true; Qt.openUrlExternally("https://www.highfidelity.com/"); + Qt.openUrlExternally("file:///" + root.keyFilePath.substring(0, root.keyFilePath.lastIndexOf('/'))); } } } From 7df8816a8122e9a748519084676274abb1494811 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 2 Oct 2017 12:56:04 -0700 Subject: [PATCH 639/722] faster rate counting --- libraries/shared/src/shared/RateCounter.h | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/shared/src/shared/RateCounter.h b/libraries/shared/src/shared/RateCounter.h index 3cf509b6bf..208446e9b1 100644 --- a/libraries/shared/src/shared/RateCounter.h +++ b/libraries/shared/src/shared/RateCounter.h @@ -35,19 +35,19 @@ public: uint32_t interval() const { return INTERVAL; } private: - mutable uint64_t _start { usecTimestampNow() }; + mutable uint64_t _expiry { usecTimestampNow() + INTERVAL * USECS_PER_MSEC}; mutable size_t _count { 0 }; const float _scale { powf(10, PRECISION) }; mutable std::atomic _rate; void checkRate() const { auto now = usecTimestampNow(); - float currentIntervalMs = (now - _start) / (float)USECS_PER_MSEC; - if (currentIntervalMs > (float)INTERVAL) { - float currentCount = _count; - float intervalSeconds = currentIntervalMs / (float)MSECS_PER_SECOND; - _rate = roundf(currentCount / intervalSeconds * _scale) / _scale; - _start = now; + if (now > _expiry) { + float MSECS_PER_USEC = 0.001f; + float SECS_PER_MSEC = 0.001f; + float intervalSeconds = ((float)INTERVAL + (float)(now - _expiry) * MSECS_PER_USEC) * SECS_PER_MSEC; + _rate = roundf((float)_count / intervalSeconds * _scale) / _scale; + _expiry = now + INTERVAL * USECS_PER_MSEC; _count = 0; }; } From e9d48a27137050de2932ddc7d1b138f608567ce4 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 2 Oct 2017 12:56:33 -0700 Subject: [PATCH 640/722] fix warning about double-to-float conversion --- libraries/render/src/render/DrawSceneOctree.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render/src/render/DrawSceneOctree.cpp b/libraries/render/src/render/DrawSceneOctree.cpp index de5ff1f74c..36663a454a 100644 --- a/libraries/render/src/render/DrawSceneOctree.cpp +++ b/libraries/render/src/render/DrawSceneOctree.cpp @@ -151,7 +151,7 @@ void DrawSceneOctree::run(const RenderContextPointer& renderContext, const ItemS float angle = glm::degrees(getAccuracyAngle(args->_sizeScale, args->_boundaryLevelAdjust)); Transform crosshairModel; crosshairModel.setTranslation(glm::vec3(0.0, 0.0, -1000.0)); - crosshairModel.setScale(1000.0 * tan(glm::radians(angle))); // Scaling at the actual tan of the lod angle => Multiplied by TWO + crosshairModel.setScale(1000.0f * tanf(glm::radians(angle))); // Scaling at the actual tan of the lod angle => Multiplied by TWO batch.resetViewTransform(); batch.setModelTransform(crosshairModel); batch.setPipeline(getDrawLODReticlePipeline()); From 8d2153d2f3a03a3238e2e18afab6648629bd2c75 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 2 Oct 2017 12:57:16 -0700 Subject: [PATCH 641/722] cleanup debug stats, use more correct names --- interface/resources/qml/Stats.qml | 21 ++++----- interface/src/Application.cpp | 41 ++++++++--------- interface/src/Application.h | 17 +++---- interface/src/avatar/AvatarManager.cpp | 3 +- .../src/scripting/RatesScriptingInterface.h | 6 +-- interface/src/ui/Stats.cpp | 45 +++++++++++++++++-- interface/src/ui/Stats.h | 11 ++--- .../src/display-plugins/OpenGLDisplayPlugin.h | 8 ++-- libraries/shared/src/shared/Camera.cpp | 2 +- libraries/shared/src/shared/Camera.h | 2 +- 10 files changed, 90 insertions(+), 66 deletions(-) diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 96e267f67f..8737e422cb 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -55,7 +55,11 @@ Item { text: "Avatars: " + root.avatarCount } StatText { - text: "Frame Rate: " + root.framerate.toFixed(2); + text: "Game Rate: " + root.gameLoopRate + } + StatText { + visible: root.expanded + text: root.gameUpdateStats } StatText { text: "Render Rate: " + root.renderrate.toFixed(2); @@ -64,21 +68,17 @@ Item { text: "Present Rate: " + root.presentrate.toFixed(2); } StatText { - text: "Present New Rate: " + root.presentnewrate.toFixed(2); + visible: root.expanded + text: " Present New Rate: " + root.presentnewrate.toFixed(2); } StatText { - text: "Present Drop Rate: " + root.presentdroprate.toFixed(2); + visible: root.expanded + text: " Present Drop Rate: " + root.presentdroprate.toFixed(2); } StatText { text: "Stutter Rate: " + root.stutterrate.toFixed(3); visible: root.stutterrate != -1; } - StatText { - text: "Simrate: " + root.simrate - } - StatText { - text: "Avatar Simrate: " + root.avatarSimrate - } StatText { text: "Missed Frame Count: " + root.appdropped; visible: root.appdropped > 0; @@ -261,9 +261,6 @@ Item { StatText { text: "GPU: " + root.gpuFrameTime.toFixed(1) + " ms" } - StatText { - text: "Avatar: " + root.avatarSimulationTime.toFixed(1) + " ms" - } StatText { text: "Triangles: " + root.triangles + " / Material Switches: " + root.materialSwitches diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a61c149b9c..4106e0c84d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1545,15 +1545,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo auto displayPlugin = qApp->getActiveDisplayPlugin(); - properties["fps"] = _frameCounter.rate(); - properties["target_frame_rate"] = getTargetFrameRate(); - properties["render_rate"] = displayPlugin->renderRate(); + properties["render_rate"] = _renderLoopCounter.rate(); + properties["target_render_rate"] = getTargetRenderFrameRate(); properties["present_rate"] = displayPlugin->presentRate(); properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate(); properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); properties["stutter_rate"] = displayPlugin->stutterRate(); - properties["sim_rate"] = getAverageSimsPerSecond(); - properties["avatar_sim_rate"] = getAvatarSimrate(); + properties["game_rate"] = getGameLoopRate(); properties["has_async_reprojection"] = displayPlugin->hasAsyncReprojection(); properties["hardware_stats"] = displayPlugin->getHardwareStats(); @@ -2388,11 +2386,11 @@ void Application::paintGL() { return; } - _frameCount++; + _renderFrameCount++; _lastTimeRendered.start(); auto lastPaintBegin = usecTimestampNow(); - PROFILE_RANGE_EX(render, __FUNCTION__, 0xff0000ff, (uint64_t)_frameCount); + PROFILE_RANGE_EX(render, __FUNCTION__, 0xff0000ff, (uint64_t)_renderFrameCount); PerformanceTimer perfTimer("paintGL"); if (nullptr == _displayPlugin) { @@ -2409,7 +2407,7 @@ void Application::paintGL() { PROFILE_RANGE(render, "/pluginBeginFrameRender"); // If a display plugin loses it's underlying support, it // needs to be able to signal us to not use it - if (!displayPlugin->beginFrameRender(_frameCount)) { + if (!displayPlugin->beginFrameRender(_renderFrameCount)) { updateDisplayMode(); return; } @@ -2564,14 +2562,14 @@ void Application::paintGL() { } // Update camera position if (!isHMDMode()) { - _myCamera.update(1.0f / _frameCounter.rate()); + _myCamera.update(); } } } { PROFILE_RANGE(render, "/updateCompositor"); - getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform(), getMyAvatar()->getSensorToWorldMatrix()); + getApplicationCompositor().setFrameInfo(_renderFrameCount, _myCamera.getTransform(), getMyAvatar()->getSensorToWorldMatrix()); } gpu::FramebufferPointer finalFramebuffer; @@ -2652,7 +2650,7 @@ void Application::paintGL() { } auto frame = _gpuContext->endFrame(); - frame->frameIndex = _frameCount; + frame->frameIndex = _renderFrameCount; frame->framebuffer = finalFramebuffer; frame->framebufferRecycler = [](const gpu::FramebufferPointer& framebuffer){ DependencyManager::get()->releaseFramebuffer(framebuffer); @@ -2663,7 +2661,7 @@ void Application::paintGL() { { PROFILE_RANGE(render, "/pluginOutput"); PerformanceTimer perfTimer("pluginOutput"); - _frameCounter.increment(); + _renderLoopCounter.increment(); displayPlugin->submitFrame(frame); } @@ -4041,7 +4039,7 @@ void Application::idle() { if (displayPlugin) { PROFILE_COUNTER_IF_CHANGED(app, "present", float, displayPlugin->presentRate()); } - PROFILE_COUNTER_IF_CHANGED(app, "fps", float, _frameCounter.rate()); + PROFILE_COUNTER_IF_CHANGED(app, "renderLoopRate", float, _renderLoopCounter.rate()); PROFILE_COUNTER_IF_CHANGED(app, "currentDownloads", int, ResourceCache::getLoadingRequests().length()); PROFILE_COUNTER_IF_CHANGED(app, "pendingDownloads", int, ResourceCache::getPendingRequestCount()); PROFILE_COUNTER_IF_CHANGED(app, "currentProcessing", int, DependencyManager::get()->getStat("Processing").toInt()); @@ -4077,8 +4075,6 @@ void Application::idle() { Stats::getInstance()->updateStats(); - _simCounter.increment(); - // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing // details if we're in ExtraDebugging mode. However, the ::update() and its subcomponents will show their timing // details normally. @@ -4158,6 +4154,7 @@ void Application::idle() { Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !(myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN)); cameraMenuChanged(); } + _gameLoopCounter.increment(); } ivec2 Application::getMouse() const { @@ -4844,7 +4841,7 @@ static bool domainLoadingInProgress = false; void Application::update(float deltaTime) { - PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_frameCount + 1); + PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_renderFrameCount + 1); bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::update()"); @@ -5138,22 +5135,22 @@ void Application::update(float deltaTime) { // AvatarManager update { - PerformanceTimer perfTimer("AvatarManager"); - _avatarSimCounter.increment(); - { + PerformanceTimer perfTimer("otherAvatars"); PROFILE_RANGE_EX(simulation, "OtherAvatars", 0xffff00ff, (uint64_t)getActiveDisplayPlugin()->presentCount()); avatarManager->updateOtherAvatars(deltaTime); } - qApp->updateMyAvatarLookAtPosition(); - { PROFILE_RANGE_EX(simulation, "MyAvatar", 0xffff00ff, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("MyAvatar"); + qApp->updateMyAvatarLookAtPosition(); avatarManager->updateMyAvatar(deltaTime); } } + PerformanceTimer perfTimer("misc"); + // TODO: break these out into distinct perfTimers when they prove interesting { PROFILE_RANGE(app, "RayPickManager"); _rayPickManager.update(); @@ -5471,7 +5468,7 @@ bool Application::isHMDMode() const { return getActiveDisplayPlugin()->isHmd(); } -float Application::getTargetFrameRate() const { return getActiveDisplayPlugin()->getTargetFrameRate(); } +float Application::getTargetRenderFrameRate() const { return getActiveDisplayPlugin()->getTargetFrameRate(); } QRect Application::getDesirableApplicationGeometry() const { QRect applicationGeometry = getWindow()->geometry(); diff --git a/interface/src/Application.h b/interface/src/Application.h index b0cac6bb87..f3c7f82b4b 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -192,10 +192,9 @@ public: Overlays& getOverlays() { return _overlays; } - - size_t getFrameCount() const { return _frameCount; } - float getFps() const { return _frameCounter.rate(); } - float getTargetFrameRate() const; // frames/second + size_t getRenderFrameCount() const { return _renderFrameCount; } + float getRenderLoopRate() const { return _renderLoopCounter.rate(); } + float getTargetRenderFrameRate() const; // frames/second float getFieldOfView() { return _fieldOfView.get(); } void setFieldOfView(float fov); @@ -266,8 +265,7 @@ public: void updateMyAvatarLookAtPosition(); - float getAvatarSimrate() const { return _avatarSimCounter.rate(); } - float getAverageSimsPerSecond() const { return _simCounter.rate(); } + float getGameLoopRate() const { return _gameLoopCounter.rate(); } void takeSnapshot(bool notify, bool includeAnimated = false, float aspectRatio = 0.0f); void takeSecondaryCameraSnapshot(); @@ -531,12 +529,11 @@ private: QUndoStack _undoStack; UndoStackScriptingInterface _undoStackScriptingInterface; - uint32_t _frameCount { 0 }; + uint32_t _renderFrameCount { 0 }; // Frame Rate Measurement - RateCounter<> _frameCounter; - RateCounter<> _avatarSimCounter; - RateCounter<> _simCounter; + RateCounter<500> _renderLoopCounter; + RateCounter<500> _gameLoopCounter; QTimer _minimizedWindowTimer; QElapsedTimer _timerStart; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index a6d77c8d03..5d8393ba7a 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -252,11 +252,12 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { qApp->getMain3DScene()->enqueueTransaction(transaction); } - _avatarSimulationTime = (float)(usecTimestampNow() - startTime) / (float)USECS_PER_MSEC; _numAvatarsUpdated = numAvatarsUpdated; _numAvatarsNotUpdated = numAVatarsNotUpdated; simulateAvatarFades(deltaTime); + + _avatarSimulationTime = (float)(usecTimestampNow() - startTime) / (float)USECS_PER_MSEC; } void AvatarManager::postUpdate(float deltaTime, const render::ScenePointer& scene) { diff --git a/interface/src/scripting/RatesScriptingInterface.h b/interface/src/scripting/RatesScriptingInterface.h index 7bcab0ea70..5658ed99a0 100644 --- a/interface/src/scripting/RatesScriptingInterface.h +++ b/interface/src/scripting/RatesScriptingInterface.h @@ -22,16 +22,14 @@ class RatesScriptingInterface : public QObject { Q_PROPERTY(float newFrame READ getNewFrameRate) Q_PROPERTY(float dropped READ getDropRate) Q_PROPERTY(float simulation READ getSimulationRate) - Q_PROPERTY(float avatar READ getAvatarRate) public: RatesScriptingInterface(QObject* parent) : QObject(parent) {} - float getRenderRate() { return qApp->getFps(); } + float getRenderRate() { return qApp->getRenderLoopRate(); } float getPresentRate() { return qApp->getActiveDisplayPlugin()->presentRate(); } float getNewFrameRate() { return qApp->getActiveDisplayPlugin()->newFramePresentRate(); } float getDropRate() { return qApp->getActiveDisplayPlugin()->droppedFrameRate(); } - float getSimulationRate() { return qApp->getAverageSimsPerSecond(); } - float getAvatarRate() { return qApp->getAvatarSimrate(); } + float getSimulationRate() { return qApp->getGameLoopRate(); } }; #endif // HIFI_INTERFACE_RATES_SCRIPTING_INTERFACE_H diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 767e499503..358b877840 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -8,6 +8,7 @@ #include "Stats.h" +#include #include #include @@ -129,7 +130,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(updatedAvatarCount, avatarManager->getNumAvatarsUpdated()); STAT_UPDATE(notUpdatedAvatarCount, avatarManager->getNumAvatarsNotUpdated()); STAT_UPDATE(serverCount, (int)nodeList->size()); - STAT_UPDATE_FLOAT(framerate, qApp->getFps(), 0.1f); + STAT_UPDATE_FLOAT(renderrate, qApp->getRenderLoopRate(), 0.1f); if (qApp->getActiveDisplayPlugin()) { auto displayPlugin = qApp->getActiveDisplayPlugin(); auto stats = displayPlugin->getHardwareStats(); @@ -137,7 +138,6 @@ void Stats::updateStats(bool force) { STAT_UPDATE(longrenders, stats["long_render_count"].toInt()); STAT_UPDATE(longsubmits, stats["long_submit_count"].toInt()); STAT_UPDATE(longframes, stats["long_frame_count"].toInt()); - STAT_UPDATE_FLOAT(renderrate, displayPlugin->renderRate(), 0.1f); STAT_UPDATE_FLOAT(presentrate, displayPlugin->presentRate(), 0.1f); STAT_UPDATE_FLOAT(presentnewrate, displayPlugin->newFramePresentRate(), 0.1f); STAT_UPDATE_FLOAT(presentdroprate, displayPlugin->droppedFrameRate(), 0.1f); @@ -150,8 +150,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(presentnewrate, -1); STAT_UPDATE(presentdroprate, -1); } - STAT_UPDATE(simrate, (int)qApp->getAverageSimsPerSecond()); - STAT_UPDATE(avatarSimrate, (int)qApp->getAvatarSimrate()); + STAT_UPDATE(gameLoopRate, (int)qApp->getGameLoopRate()); auto bandwidthRecorder = DependencyManager::get(); STAT_UPDATE(packetInCount, (int)bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond()); @@ -453,9 +452,47 @@ void Stats::updateStats(bool force) { } _timingStats = perfLines; emit timingStatsChanged(); + + // build _gameUpdateStats + class SortableStat { + public: + SortableStat(QString a, float p) : message(a), priority(p) {} + QString message; + float priority; + bool operator<(const SortableStat& other) const { return priority < other.priority; } + }; + + std::priority_queue idleUpdateStats; + auto itr = allRecords.find("/idle/update"); + if (itr != allRecords.end()) { + uint64_t dt = (float)itr.value().getMovingAverage() / (float)USECS_PER_MSEC; + _gameUpdateStats = QString("/idle/update = %1 ms").arg(dt); + + QVector categories = { "devices", "physics", "otherAvatars", "MyAvatar", "misc" }; + for (int32_t j = 0; j < categories.size(); ++j) { + QString recordKey = "/idle/update/" + categories[j]; + itr = allRecords.find(recordKey); + if (itr != allRecords.end()) { + uint64_t dt = (float)itr.value().getMovingAverage() / (float)USECS_PER_MSEC; + QString message = QString("\n %1 = %2").arg(categories[j]).arg(dt); + idleUpdateStats.push(SortableStat(message, dt)); + } + } + while (!idleUpdateStats.empty()) { + SortableStat stat = idleUpdateStats.top(); + _gameUpdateStats += stat.message; + idleUpdateStats.pop(); + } + emit gameUpdateStatsChanged(); + } else if (_gameUpdateStats != "") { + _gameUpdateStats = ""; + emit gameUpdateStatsChanged(); + } } else if (_timingExpanded) { _timingExpanded = false; emit timingExpandedChanged(); + _gameUpdateStats = ""; + emit gameUpdateStatsChanged(); } } diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index b3c920d4ef..0b10e7fdfe 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -32,8 +32,6 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, serverCount, 0) // How often the app is creating new gpu::Frames - STATS_PROPERTY(float, framerate, 0) - // How often the display plugin is executing a given frame STATS_PROPERTY(float, renderrate, 0) // How often the display plugin is presenting to the device STATS_PROPERTY(float, presentrate, 0) @@ -47,8 +45,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(float, presentnewrate, 0) STATS_PROPERTY(float, presentdroprate, 0) - STATS_PROPERTY(int, simrate, 0) - STATS_PROPERTY(int, avatarSimrate, 0) + STATS_PROPERTY(int, gameLoopRate, 0) STATS_PROPERTY(int, avatarCount, 0) STATS_PROPERTY(int, updatedAvatarCount, 0) STATS_PROPERTY(int, notUpdatedAvatarCount, 0) @@ -108,6 +105,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(QString, packetStats, QString()) STATS_PROPERTY(QString, lodStatus, QString()) STATS_PROPERTY(QString, timingStats, QString()) + STATS_PROPERTY(QString, gameUpdateStats, QString()) STATS_PROPERTY(int, serverElements, 0) STATS_PROPERTY(int, serverInternal, 0) STATS_PROPERTY(int, serverLeaves, 0) @@ -167,7 +165,6 @@ signals: void longrendersChanged(); void longframesChanged(); void appdroppedChanged(); - void framerateChanged(); void expandedChanged(); void timingExpandedChanged(); void serverCountChanged(); @@ -176,8 +173,7 @@ signals: void presentnewrateChanged(); void presentdroprateChanged(); void stutterrateChanged(); - void simrateChanged(); - void avatarSimrateChanged(); + void gameLoopRateChanged(); void avatarCountChanged(); void updatedAvatarCountChanged(); void notUpdatedAvatarCountChanged(); @@ -242,6 +238,7 @@ signals: void localInternalChanged(); void localLeavesChanged(); void timingStatsChanged(); + void gameUpdateStatsChanged(); void glContextSwapchainMemoryChanged(); void qmlTextureMemoryChanged(); void texturePendingTransfersChanged(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 2080fa5ea6..51f4a33bbd 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -129,10 +129,10 @@ protected: bool _vsyncEnabled { true }; QThread* _presentThread{ nullptr }; std::queue _newFrameQueue; - RateCounter<100> _droppedFrameRate; - RateCounter<100> _newFrameRate; - RateCounter<100> _presentRate; - RateCounter<100> _renderRate; + RateCounter<200> _droppedFrameRate; + RateCounter<200> _newFrameRate; + RateCounter<200> _presentRate; + RateCounter<200> _renderRate; gpu::FramePointer _currentFrame; gpu::Frame* _lastFrame { nullptr }; diff --git a/libraries/shared/src/shared/Camera.cpp b/libraries/shared/src/shared/Camera.cpp index 48fea9e835..ab841c4717 100644 --- a/libraries/shared/src/shared/Camera.cpp +++ b/libraries/shared/src/shared/Camera.cpp @@ -45,7 +45,7 @@ Camera::Camera() : { } -void Camera::update(float deltaTime) { +void Camera::update() { if (_isKeepLookingAt) { lookAt(_lookingAt); } diff --git a/libraries/shared/src/shared/Camera.h b/libraries/shared/src/shared/Camera.h index c7b943f0dd..3ad08bd719 100644 --- a/libraries/shared/src/shared/Camera.h +++ b/libraries/shared/src/shared/Camera.h @@ -53,7 +53,7 @@ public: void initialize(); // instantly put the camera at the ideal position and orientation. - void update( float deltaTime ); + void update(); CameraMode getMode() const { return _mode; } void setMode(CameraMode m); From 97b09d680ca7bbff215a7765d082f909deed5d73 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 2 Oct 2017 13:17:53 -0700 Subject: [PATCH 642/722] fix and improve laser pointer locking --- interface/src/raypick/LaserPointer.h | 3 + interface/src/raypick/LaserPointerManager.cpp | 105 ++++++++++-------- interface/src/raypick/LaserPointerManager.h | 2 - interface/src/raypick/RayPick.h | 5 + interface/src/raypick/RayPickManager.cpp | 76 +++++++------ interface/src/raypick/RayPickManager.h | 2 - 6 files changed, 108 insertions(+), 85 deletions(-) diff --git a/interface/src/raypick/LaserPointer.h b/interface/src/raypick/LaserPointer.h index 5467a8233e..fa7d396ae8 100644 --- a/interface/src/raypick/LaserPointer.h +++ b/interface/src/raypick/LaserPointer.h @@ -75,6 +75,8 @@ public: void setLockEndUUID(QUuid objectID, const bool isOverlay) { _objectLockEnd = std::pair(objectID, isOverlay); } + QReadWriteLock* getLock() { return &_lock; } + void update(); private: @@ -89,6 +91,7 @@ private: std::pair _objectLockEnd { std::pair(QUuid(), false)}; QUuid _rayPickUID; + QReadWriteLock _lock; void updateRenderStateOverlay(const OverlayID& id, const QVariant& props); void updateRenderState(const RenderState& renderState, const IntersectionType type, const float distance, const QUuid& objectID, const PickRay& pickRay, const bool defaultState); diff --git a/interface/src/raypick/LaserPointerManager.cpp b/interface/src/raypick/LaserPointerManager.cpp index b19ecc14f0..387f88724e 100644 --- a/interface/src/raypick/LaserPointerManager.cpp +++ b/interface/src/raypick/LaserPointerManager.cpp @@ -17,7 +17,6 @@ QUuid LaserPointerManager::createLaserPointer(const QVariant& rayProps, const La QWriteLocker containsLock(&_containsLock); QUuid id = QUuid::createUuid(); _laserPointers[id] = laserPointer; - _laserPointerLocks[id] = std::make_shared(); return id; } return QUuid(); @@ -26,46 +25,50 @@ QUuid LaserPointerManager::createLaserPointer(const QVariant& rayProps, const La void LaserPointerManager::removeLaserPointer(const QUuid uid) { QWriteLocker lock(&_containsLock); _laserPointers.remove(uid); - _laserPointerLocks.remove(uid); } void LaserPointerManager::enableLaserPointer(const QUuid uid) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QWriteLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->enable(); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QWriteLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->enable(); } } void LaserPointerManager::disableLaserPointer(const QUuid uid) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QWriteLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->disable(); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QWriteLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->disable(); } } void LaserPointerManager::setRenderState(QUuid uid, const std::string& renderState) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QWriteLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->setRenderState(renderState); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QWriteLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->setRenderState(renderState); } } void LaserPointerManager::editRenderState(QUuid uid, const std::string& state, const QVariant& startProps, const QVariant& pathProps, const QVariant& endProps) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QWriteLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->editRenderState(state, startProps, pathProps, endProps); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QWriteLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->editRenderState(state, startProps, pathProps, endProps); } } const RayPickResult LaserPointerManager::getPrevRayPickResult(const QUuid uid) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QReadLocker laserLock(_laserPointerLocks[uid].get()); - return _laserPointers[uid]->getPrevRayPickResult(); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QReadLocker laserLock(laserPointer.value()->getLock()); + return laserPointer.value()->getPrevRayPickResult(); } return RayPickResult(); } @@ -74,79 +77,89 @@ void LaserPointerManager::update() { QReadLocker lock(&_containsLock); for (QUuid& uid : _laserPointers.keys()) { // This only needs to be a read lock because update won't change any of the properties that can be modified from scripts - QReadLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->update(); + auto laserPointer = _laserPointers.find(uid); + QReadLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->update(); } } void LaserPointerManager::setPrecisionPicking(QUuid uid, const bool precisionPicking) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QWriteLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->setPrecisionPicking(precisionPicking); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QWriteLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->setPrecisionPicking(precisionPicking); } } void LaserPointerManager::setLaserLength(QUuid uid, const float laserLength) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QWriteLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->setLaserLength(laserLength); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QWriteLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->setLaserLength(laserLength); } } void LaserPointerManager::setIgnoreEntities(QUuid uid, const QScriptValue& ignoreEntities) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QWriteLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->setIgnoreEntities(ignoreEntities); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QWriteLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->setIgnoreEntities(ignoreEntities); } } void LaserPointerManager::setIncludeEntities(QUuid uid, const QScriptValue& includeEntities) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QWriteLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->setIncludeEntities(includeEntities); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QWriteLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->setIncludeEntities(includeEntities); } } void LaserPointerManager::setIgnoreOverlays(QUuid uid, const QScriptValue& ignoreOverlays) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QWriteLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->setIgnoreOverlays(ignoreOverlays); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QWriteLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->setIgnoreOverlays(ignoreOverlays); } } void LaserPointerManager::setIncludeOverlays(QUuid uid, const QScriptValue& includeOverlays) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QWriteLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->setIncludeOverlays(includeOverlays); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QWriteLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->setIncludeOverlays(includeOverlays); } } void LaserPointerManager::setIgnoreAvatars(QUuid uid, const QScriptValue& ignoreAvatars) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QWriteLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->setIgnoreAvatars(ignoreAvatars); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QWriteLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->setIgnoreAvatars(ignoreAvatars); } } void LaserPointerManager::setIncludeAvatars(QUuid uid, const QScriptValue& includeAvatars) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QWriteLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->setIncludeAvatars(includeAvatars); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QWriteLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->setIncludeAvatars(includeAvatars); } } void LaserPointerManager::setLockEndUUID(QUuid uid, QUuid objectID, const bool isOverlay) { QReadLocker lock(&_containsLock); - if (_laserPointers.contains(uid)) { - QWriteLocker laserLock(_laserPointerLocks[uid].get()); - _laserPointers[uid]->setLockEndUUID(objectID, isOverlay); + auto laserPointer = _laserPointers.find(uid); + if (laserPointer != _laserPointers.end()) { + QWriteLocker laserLock(laserPointer.value()->getLock()); + laserPointer.value()->setLockEndUUID(objectID, isOverlay); } } diff --git a/interface/src/raypick/LaserPointerManager.h b/interface/src/raypick/LaserPointerManager.h index 6494bb7056..b841877578 100644 --- a/interface/src/raypick/LaserPointerManager.h +++ b/interface/src/raypick/LaserPointerManager.h @@ -13,7 +13,6 @@ #include #include -#include #include "LaserPointer.h" @@ -46,7 +45,6 @@ public: private: QHash> _laserPointers; - QHash> _laserPointerLocks; QReadWriteLock _containsLock; }; diff --git a/interface/src/raypick/RayPick.h b/interface/src/raypick/RayPick.h index 0686a05718..428e44d670 100644 --- a/interface/src/raypick/RayPick.h +++ b/interface/src/raypick/RayPick.h @@ -16,6 +16,7 @@ #include "EntityItemID.h" #include "ui/overlays/Overlay.h" +#include class RayPickFilter { public: @@ -127,6 +128,8 @@ public: void setIgnoreAvatars(const QScriptValue& ignoreAvatars) { _ignoreAvatars = qVectorEntityItemIDFromScriptValue(ignoreAvatars); } void setIncludeAvatars(const QScriptValue& includeAvatars) { _includeAvatars = qVectorEntityItemIDFromScriptValue(includeAvatars); } + QReadWriteLock* getLock() { return &_lock; } + private: RayPickFilter _filter; float _maxDistance; @@ -139,6 +142,8 @@ private: QVector _includeOverlays; QVector _ignoreAvatars; QVector _includeAvatars; + + QReadWriteLock _lock; }; #endif // hifi_RayPick_h diff --git a/interface/src/raypick/RayPickManager.cpp b/interface/src/raypick/RayPickManager.cpp index bfc6e3fcb2..65f82dcd5f 100644 --- a/interface/src/raypick/RayPickManager.cpp +++ b/interface/src/raypick/RayPickManager.cpp @@ -47,6 +47,7 @@ void RayPickManager::update() { RayPickCache results; for (auto& uid : _rayPicks.keys()) { std::shared_ptr rayPick = _rayPicks[uid]; + QWriteLocker lock(rayPick->getLock()); if (!rayPick->isEnabled() || rayPick->getFilter().doesPickNothing() || rayPick->getMaxDistance() < 0.0f) { continue; } @@ -114,7 +115,6 @@ void RayPickManager::update() { } } - QWriteLocker lock(_rayPickLocks[uid].get()); if (rayPick->getMaxDistance() == 0.0f || (rayPick->getMaxDistance() > 0.0f && res.distance < rayPick->getMaxDistance())) { rayPick->setRayPickResult(res); } else { @@ -127,7 +127,6 @@ QUuid RayPickManager::createRayPick(const std::string& jointName, const glm::vec QWriteLocker lock(&_containsLock); QUuid id = QUuid::createUuid(); _rayPicks[id] = std::make_shared(jointName, posOffset, dirOffset, filter, maxDistance, enabled); - _rayPickLocks[id] = std::make_shared(); return id; } @@ -135,7 +134,6 @@ QUuid RayPickManager::createRayPick(const RayPickFilter& filter, const float max QWriteLocker lock(&_containsLock); QUuid id = QUuid::createUuid(); _rayPicks[id] = std::make_shared(filter, maxDistance, enabled); - _rayPickLocks[id] = std::make_shared(); return id; } @@ -143,93 +141,101 @@ QUuid RayPickManager::createRayPick(const glm::vec3& position, const glm::vec3& QWriteLocker lock(&_containsLock); QUuid id = QUuid::createUuid(); _rayPicks[id] = std::make_shared(position, direction, filter, maxDistance, enabled); - _rayPickLocks[id] = std::make_shared(); return id; } void RayPickManager::removeRayPick(const QUuid uid) { QWriteLocker lock(&_containsLock); _rayPicks.remove(uid); - _rayPickLocks.remove(uid); } void RayPickManager::enableRayPick(const QUuid uid) { QReadLocker containsLock(&_containsLock); - if (_rayPicks.contains(uid)) { - QWriteLocker rayPickLock(_rayPickLocks[uid].get()); - _rayPicks[uid]->enable(); + auto rayPick = _rayPicks.find(uid); + if (rayPick != _rayPicks.end()) { + QWriteLocker rayPickLock(rayPick.value()->getLock()); + rayPick.value()->enable(); } } void RayPickManager::disableRayPick(const QUuid uid) { QReadLocker containsLock(&_containsLock); - if (_rayPicks.contains(uid)) { - QWriteLocker rayPickLock(_rayPickLocks[uid].get()); - _rayPicks[uid]->disable(); + auto rayPick = _rayPicks.find(uid); + if (rayPick != _rayPicks.end()) { + QWriteLocker rayPickLock(rayPick.value()->getLock()); + rayPick.value()->disable(); } } const RayPickResult RayPickManager::getPrevRayPickResult(const QUuid uid) { QReadLocker containsLock(&_containsLock); - if (_rayPicks.contains(uid)) { - QReadLocker lock(_rayPickLocks[uid].get()); - return _rayPicks[uid]->getPrevRayPickResult(); + auto rayPick = _rayPicks.find(uid); + if (rayPick != _rayPicks.end()) { + QReadLocker lock(rayPick.value()->getLock()); + return rayPick.value()->getPrevRayPickResult(); } return RayPickResult(); } void RayPickManager::setPrecisionPicking(QUuid uid, const bool precisionPicking) { QReadLocker containsLock(&_containsLock); - if (_rayPicks.contains(uid)) { - QWriteLocker lock(_rayPickLocks[uid].get()); - _rayPicks[uid]->setPrecisionPicking(precisionPicking); + auto rayPick = _rayPicks.find(uid); + if (rayPick != _rayPicks.end()) { + QWriteLocker lock(rayPick.value()->getLock()); + rayPick.value()->setPrecisionPicking(precisionPicking); } } void RayPickManager::setIgnoreEntities(QUuid uid, const QScriptValue& ignoreEntities) { QReadLocker containsLock(&_containsLock); - if (_rayPicks.contains(uid)) { - QWriteLocker lock(_rayPickLocks[uid].get()); - _rayPicks[uid]->setIgnoreEntities(ignoreEntities); + auto rayPick = _rayPicks.find(uid); + if (rayPick != _rayPicks.end()) { + QWriteLocker lock(rayPick.value()->getLock()); + rayPick.value()->setIgnoreEntities(ignoreEntities); } } void RayPickManager::setIncludeEntities(QUuid uid, const QScriptValue& includeEntities) { QReadLocker containsLock(&_containsLock); - if (_rayPicks.contains(uid)) { - QWriteLocker lock(_rayPickLocks[uid].get()); - _rayPicks[uid]->setIncludeEntities(includeEntities); + auto rayPick = _rayPicks.find(uid); + if (rayPick != _rayPicks.end()) { + QWriteLocker lock(rayPick.value()->getLock()); + rayPick.value()->setIncludeEntities(includeEntities); } } void RayPickManager::setIgnoreOverlays(QUuid uid, const QScriptValue& ignoreOverlays) { QReadLocker containsLock(&_containsLock); - if (_rayPicks.contains(uid)) { - QWriteLocker lock(_rayPickLocks[uid].get()); - _rayPicks[uid]->setIgnoreOverlays(ignoreOverlays); + auto rayPick = _rayPicks.find(uid); + if (rayPick != _rayPicks.end()) { + QWriteLocker lock(rayPick.value()->getLock()); + rayPick.value()->setIgnoreOverlays(ignoreOverlays); } } void RayPickManager::setIncludeOverlays(QUuid uid, const QScriptValue& includeOverlays) { QReadLocker containsLock(&_containsLock); - if (_rayPicks.contains(uid)) { - QWriteLocker lock(_rayPickLocks[uid].get()); - _rayPicks[uid]->setIncludeOverlays(includeOverlays); + auto rayPick = _rayPicks.find(uid); + if (rayPick != _rayPicks.end()) { + QWriteLocker lock(rayPick.value()->getLock()); + rayPick.value()->setIncludeOverlays(includeOverlays); } } void RayPickManager::setIgnoreAvatars(QUuid uid, const QScriptValue& ignoreAvatars) { QReadLocker containsLock(&_containsLock); - if (_rayPicks.contains(uid)) { - QWriteLocker lock(_rayPickLocks[uid].get()); - _rayPicks[uid]->setIgnoreAvatars(ignoreAvatars); + auto rayPick = _rayPicks.find(uid); + if (rayPick != _rayPicks.end()) { + QWriteLocker lock(rayPick.value()->getLock()); + rayPick.value()->setIgnoreAvatars(ignoreAvatars); } } void RayPickManager::setIncludeAvatars(QUuid uid, const QScriptValue& includeAvatars) { QReadLocker containsLock(&_containsLock); - if (_rayPicks.contains(uid)) { - QWriteLocker lock(_rayPickLocks[uid].get()); - _rayPicks[uid]->setIncludeAvatars(includeAvatars); + auto rayPick = _rayPicks.find(uid); + if (rayPick != _rayPicks.end()) { + QWriteLocker lock(rayPick.value()->getLock()); + rayPick.value()->setIncludeAvatars(includeAvatars); } } \ No newline at end of file diff --git a/interface/src/raypick/RayPickManager.h b/interface/src/raypick/RayPickManager.h index 9717767f19..974022eb4d 100644 --- a/interface/src/raypick/RayPickManager.h +++ b/interface/src/raypick/RayPickManager.h @@ -15,7 +15,6 @@ #include #include -#include #include "RegisteredMetaTypes.h" @@ -47,7 +46,6 @@ public: private: QHash> _rayPicks; - QHash> _rayPickLocks; QReadWriteLock _containsLock; typedef QHash, std::unordered_map> RayPickCache; From 136381adb90e1fdb3c70d49d9714f23da7955578 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 2 Oct 2017 13:56:15 -0700 Subject: [PATCH 643/722] fatal compiler warning on mac/linux --- libraries/entities/src/EntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index ebe7c53bd4..6ea0cd0bc7 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -2938,7 +2938,7 @@ type EntityItem::get##accessor() const { \ } #define DEFINE_PROPERTY_SETTER(type, accessor, var) \ -void EntityItem::set##accessor(const type##& value) { \ +void EntityItem::set##accessor(const type & value) { \ withWriteLock([&] { \ _##var = value; \ }); \ From 0b7af66016ca3a8c8753447726bcc00f0b47aec4 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 2 Oct 2017 15:38:59 -0700 Subject: [PATCH 644/722] more linux warnings --- libraries/entities/src/EntityItem.cpp | 2 +- libraries/entities/src/EntityScriptingInterface.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 6ea0cd0bc7..a1b7ff54de 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1577,7 +1577,7 @@ float EntityItem::getRadius() const { // Checking Certifiable Properties #define ADD_STRING_PROPERTY(n, N) if (!propertySet.get##N().isEmpty()) json[#n] = propertySet.get##N() #define ADD_ENUM_PROPERTY(n, N) json[#n] = propertySet.get##N##AsString() -#define ADD_INT_PROPERTY(n, N) if (propertySet.get##N() != 0) json[#n] = (propertySet.get##N() == -1) ? -1.0 : ((double) propertySet.get##N()) +#define ADD_INT_PROPERTY(n, N) if (propertySet.get##N() != 0) json[#n] = (propertySet.get##N() == (quint32) -1) ? -1.0 : ((double) propertySet.get##N()) QByteArray EntityItem::getStaticCertificateJSON() const { // Produce a compact json of every non-default static certificate property, with the property names in alphabetical order. // The static certificate properties include all an only those properties that cannot be changed without altering the identity diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 06eea6c5ed..2ee2ce9b21 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1767,7 +1767,7 @@ glm::mat4 EntityScriptingInterface::getEntityLocalTransform(const QUuid& entityI } bool EntityScriptingInterface::verifyStaticCertificateProperties(const QUuid& entityID) { - bool result; + bool result = false; if (_entityTree) { _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); @@ -1781,7 +1781,7 @@ bool EntityScriptingInterface::verifyStaticCertificateProperties(const QUuid& en #ifdef DEBUG_CERT QString EntityScriptingInterface::computeCertificateID(const QUuid& entityID) { - QString result; + QString result = false; if (_entityTree) { _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); From ca11d19b3ea611e11345953d5a528a2e0978bbd8 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 2 Oct 2017 16:06:44 -0700 Subject: [PATCH 645/722] allow importing of avatar entities from json --- libraries/entities/src/EntityTree.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index bf37a08386..445f2ddb38 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1935,6 +1935,12 @@ bool EntityTree::readFromMap(QVariantMap& map) { entityItemID = EntityItemID(QUuid::createUuid()); } + if (properties.getClientOnly()) { + auto nodeList = DependencyManager::get(); + const QUuid myNodeID = nodeList->getSessionUUID(); + properties.setOwningAvatarID(myNodeID); + } + EntityItemPointer entity = addEntity(entityItemID, properties); if (!entity) { qCDebug(entities) << "adding Entity failed:" << entityItemID << properties.getType(); From 9b0ebf0e072a86cfd6e923dc3fe8e61d10c03e2d Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 2 Oct 2017 16:18:28 -0700 Subject: [PATCH 646/722] doh! string not boolean --- libraries/entities/src/EntityScriptingInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 2ee2ce9b21..f5117dddc0 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1781,7 +1781,7 @@ bool EntityScriptingInterface::verifyStaticCertificateProperties(const QUuid& en #ifdef DEBUG_CERT QString EntityScriptingInterface::computeCertificateID(const QUuid& entityID) { - QString result = false; + QString result { "" }; if (_entityTree) { _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); From c3167444a9d28f7007993edaf469254173fb4389 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 2 Oct 2017 16:31:20 -0700 Subject: [PATCH 647/722] decouple 'general timing' from 'game loop' details --- interface/src/ui/Stats.cpp | 31 +++++++++++++++++++------------ interface/src/ui/Stats.h | 5 +++-- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 358b877840..70cb74ed2f 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -117,10 +117,9 @@ void Stats::updateStats(bool force) { } } - bool shouldDisplayTimingDetail = Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && - Menu::getInstance()->isOptionChecked(MenuOption::Stats) && isExpanded(); - if (shouldDisplayTimingDetail != PerformanceTimer::isActive()) { - PerformanceTimer::setActive(shouldDisplayTimingDetail); + bool performanceTimerShouldBeActive = Menu::getInstance()->isOptionChecked(MenuOption::Stats) && _expanded; + if (performanceTimerShouldBeActive != PerformanceTimer::isActive()) { + PerformanceTimer::setActive(performanceTimerShouldBeActive); } auto nodeList = DependencyManager::get(); @@ -406,10 +405,11 @@ void Stats::updateStats(bool force) { } bool performanceTimerIsActive = PerformanceTimer::isActive(); - bool displayPerf = _expanded && Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails); - if (displayPerf && performanceTimerIsActive) { - if (!_timingExpanded) { - _timingExpanded = true; + + if (performanceTimerShouldBeActive && + Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails)) { + if (!_showTimingDetails) { + _showTimingDetails = true; emit timingExpandedChanged(); } PerformanceTimer::tallyAllTimerRecords(); // do this even if we're not displaying them, so they don't stack up @@ -452,8 +452,15 @@ void Stats::updateStats(bool force) { } _timingStats = perfLines; emit timingStatsChanged(); + } else if (_showTimingDetails) { + _showTimingDetails = false; + emit timingExpandedChanged(); + } - // build _gameUpdateStats + if (_expanded && performanceTimerIsActive) { + if (!_showGameUpdateStats) { + _showGameUpdateStats = true; + } class SortableStat { public: SortableStat(QString a, float p) : message(a), priority(p) {} @@ -462,6 +469,7 @@ void Stats::updateStats(bool force) { bool operator<(const SortableStat& other) const { return priority < other.priority; } }; + const QMap& allRecords = PerformanceTimer::getAllTimerRecords(); std::priority_queue idleUpdateStats; auto itr = allRecords.find("/idle/update"); if (itr != allRecords.end()) { @@ -488,9 +496,8 @@ void Stats::updateStats(bool force) { _gameUpdateStats = ""; emit gameUpdateStatsChanged(); } - } else if (_timingExpanded) { - _timingExpanded = false; - emit timingExpandedChanged(); + } else if (_showGameUpdateStats) { + _showGameUpdateStats = false; _gameUpdateStats = ""; emit gameUpdateStatsChanged(); } diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 0b10e7fdfe..af3189f20b 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -146,7 +146,7 @@ public: void updateStats(bool force = false); bool isExpanded() { return _expanded; } - bool isTimingExpanded() { return _timingExpanded; } + bool isTimingExpanded() { return _showTimingDetails; } void setExpanded(bool expanded) { if (_expanded != expanded) { @@ -264,7 +264,8 @@ private: int _recentMaxPackets{ 0 } ; // recent max incoming voxel packets to process bool _resetRecentMaxPacketsSoon{ true }; bool _expanded{ false }; - bool _timingExpanded{ false }; + bool _showTimingDetails{ false }; + bool _showGameUpdateStats{ false }; QString _monospaceFont; const AudioIOStats* _audioStats; QStringList _downloadUrls = QStringList(); From e7fa8131eaf3457596cfebeff5a5d50404d82ce3 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 2 Oct 2017 16:44:03 -0700 Subject: [PATCH 648/722] make json importer understand AVATAR_SELF_ID --- libraries/entities/src/EntityTree.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 445f2ddb38..e0187cd2b6 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1935,11 +1935,14 @@ bool EntityTree::readFromMap(QVariantMap& map) { entityItemID = EntityItemID(QUuid::createUuid()); } + auto nodeList = DependencyManager::get(); + const QUuid myNodeID = nodeList->getSessionUUID(); if (properties.getClientOnly()) { - auto nodeList = DependencyManager::get(); - const QUuid myNodeID = nodeList->getSessionUUID(); properties.setOwningAvatarID(myNodeID); } + if (properties.getParentID() == AVATAR_SELF_ID) { + properties.setParentID(myNodeID); + } EntityItemPointer entity = addEntity(entityItemID, properties); if (!entity) { From 8a0ecf4e6f5957488f824fcc4266446f89285a70 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Mon, 2 Oct 2017 16:46:43 -0700 Subject: [PATCH 649/722] propertly determine is laser intersects with HUD UI --- interface/resources/qml/desktop/Desktop.qml | 6 ++--- interface/resources/qml/windows/Frame.qml | 3 ++- libraries/ui/src/OffscreenUi.cpp | 1 - .../controllerModules/farActionGrabEntity.js | 24 +++++++++++++++++-- .../controllerModules/hudOverlayPointer.js | 2 +- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 6bf832865f..4e8090ce98 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -304,10 +304,10 @@ FocusScope { if (child.visible) { if (child.hasOwnProperty("modality")) { var mappedPoint = child.mapFromGlobal(point.x, point.y); - console.log(mappedPoint); - if (child.contains(mappedPoint)) { + var outLine = child.frame.children[2]; + var framePoint = outLine.mapFromGlobal(point.x, point.y); + if (child.contains(mappedPoint) || outLine.contains(framePoint)) { return true; - console.log(child); } } } diff --git a/interface/resources/qml/windows/Frame.qml b/interface/resources/qml/windows/Frame.qml index 030af974f6..c3b8399e01 100644 --- a/interface/resources/qml/windows/Frame.qml +++ b/interface/resources/qml/windows/Frame.qml @@ -26,6 +26,7 @@ Item { readonly property int frameMarginRight: frame.decoration ? frame.decoration.frameMarginRight : 0 readonly property int frameMarginTop: frame.decoration ? frame.decoration.frameMarginTop : 0 readonly property int frameMarginBottom: frame.decoration ? frame.decoration.frameMarginBottom : 0 + readonly property int offsetCorrection: 20 // Frames always fill their parents, but their decorations may extend // beyond the window via negative margin sizes @@ -73,7 +74,7 @@ Item { Rectangle { id: sizeOutline x: -frameMarginLeft - y: -frameMarginTop + y: -frameMarginTop - offsetCorrection width: window ? window.width + frameMarginLeft + frameMarginRight + 2 : 0 height: window ? window.height + frameMarginTop + frameMarginBottom + 2 : 0 color: hifi.colors.baseGrayHighlight15 diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 1cd30132ae..297ed9ca50 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -141,7 +141,6 @@ bool OffscreenUi::isPointOnDesktopWindow(QVariant point) { BLOCKING_INVOKE_METHOD(_desktop, "isPointOnWindow", Q_RETURN_ARG(QVariant, result), Q_ARG(QVariant, point)); - qDebug() << "------> invoke method isPointOnWindow <------"; return result.toBool(); } diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index c5b82f75f0..0ef0e67471 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -108,13 +108,17 @@ Script.include("/~/system/libraries/controllers.js"); "userData" ]; - + var MARGIN = 25; function FarActionGrabEntity(hand) { this.hand = hand; this.grabbedThingID = null; this.actionID = null; // action this script created... this.entityWithContextOverlay = false; this.contextOverlayTimer = false; + this.reticleMinX = MARGIN; + this.reticleMaxX; + this.reticleMinY = MARGIN; + this.reticleMaxY; var ACTION_TTL = 15; // seconds @@ -344,12 +348,28 @@ Script.include("/~/system/libraries/controllers.js"); this.grabbedThingID = null; }; + this.updateRecommendedArea = function() { + var dims = Controller.getViewportDimensions(); + this.reticleMaxX = dims.x - MARGIN; + this.reticleMaxY = dims.y - MARGIN; + }; + + this.calculateNewReticlePosition = function(intersection) { + this.updateRecommendedArea(); + var point2d = HMD.overlayFromWorldPoint(intersection); + point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX)); + point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY)); + return point2d; + }; + this.notPointingAtEntity = function(controllerData) { var intersection = controllerData.rayPicks[this.hand]; var entityProperty = Entities.getEntityProperties(intersection.objectID); var entityType = entityProperty.type; + var hudRayPick = controllerData.hudRayPicks[this.hand]; + var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); if ((intersection.type === RayPick.INTERSECTED_ENTITY && entityType === "Web") || - intersection.type === RayPick.INTERSECTED_OVERLAY) { + intersection.type === RayPick.INTERSECTED_OVERLAY || Window.isPointOnDesktopWindow(point2d)) { return true; } return false; diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js index d286f26108..add673df0c 100644 --- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js +++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js @@ -178,7 +178,7 @@ } var hudRayPick = controllerData.hudRayPicks[this.hand]; var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); - if (!Window.isPointOnDesktopWindow(point2d)) { + if (!Window.isPointOnDesktopWindow(point2d) && !controllerData.triggerClicks[this.hand]) { this.exitModule(); return false; } From 11d0062104698608d0ffe923e7d480b79b89e362 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 2 Oct 2017 16:37:14 -0700 Subject: [PATCH 650/722] Progress on buy endpoint --- .../qml/hifi/commerce/checkout/Checkout.qml | 41 ++++++++++--------- interface/src/commerce/Wallet.cpp | 2 +- .../scripting/WalletScriptingInterface.cpp | 39 ++++++++++-------- .../src/scripting/WalletScriptingInterface.h | 11 ++++- scripts/system/html/js/marketplacesInject.js | 1 - scripts/system/marketplaces/marketplaces.js | 6 +-- 6 files changed, 59 insertions(+), 41 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 2163407e0b..77180f7bab 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -26,15 +26,16 @@ Rectangle { HifiConstants { id: hifi; } id: root; + objectName: "checkout" property string activeView: "initialize"; property bool purchasesReceived: false; property bool balanceReceived: false; + property string itemName; property string itemId; - property string itemPreviewImageUrl; property string itemHref; property double balanceAfterPurchase; property bool alreadyOwned: false; - property int itemPrice: 0; + property int itemPrice; property bool itemIsJson: true; property bool shouldBuyWithControlledFailure: false; property bool debugCheckoutSuccess: false; @@ -107,6 +108,19 @@ Rectangle { } } + onItemIdChanged: { + commerce.inventory(); + itemPreviewImage.source = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg"; + } + + onItemHrefChanged: { + itemIsJson = root.itemHref.indexOf('.json') !== -1; + } + + onItemPriceChanged: { + commerce.balance(); + } + Timer { id: notSetUpTimer; interval: 200; @@ -271,7 +285,6 @@ Rectangle { Image { id: itemPreviewImage; - source: root.itemPreviewImageUrl; anchors.left: parent.left; anchors.top: parent.top; anchors.bottom: parent.bottom; @@ -281,6 +294,7 @@ Rectangle { RalewaySemiBold { id: itemNameText; + text: root.itemName; // Text size size: 26; // Anchors @@ -305,7 +319,7 @@ Rectangle { anchors.top: parent.top; anchors.right: parent.right; height: 30; - width: childrenRect.width; + width: itemPriceTextLabel.width + itemPriceText.width + 20; // "HFC" balance label HiFiGlyphs { @@ -325,7 +339,7 @@ Rectangle { } FiraSansSemiBold { id: itemPriceText; - text: "--"; + text: root.itemPrice; // Text size size: 26; // Anchors @@ -414,7 +428,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter; onLinkActivated: { - sendToScript({method: 'checkout_goToPurchases', filterText: itemNameText.text}); + sendToScript({method: 'checkout_goToPurchases', filterText: root.itemName}); } } } @@ -498,7 +512,7 @@ Rectangle { RalewaySemiBold { id: completeText2; - text: "The item " + '' + itemNameText.text + '' + + text: "The item " + '' + root.itemName + '' + " has been added to your Purchases and a receipt will appear in your Wallet's transaction history."; // Text size size: 20; @@ -814,14 +828,9 @@ Rectangle { switch (message.method) { case 'updateCheckoutQML': itemId = message.params.itemId; - itemNameText.text = message.params.itemName; + itemName = message.params.itemName; root.itemPrice = message.params.itemPrice; - itemPriceText.text = root.itemPrice; itemHref = message.params.itemHref; - itemPreviewImageUrl = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg"; - if (itemHref.indexOf('.json') === -1) { - root.itemIsJson = false; - } setBuyText(); break; default: @@ -883,12 +892,6 @@ Rectangle { } else { root.activeView = "checkoutSuccess"; } - if (!balanceReceived) { - commerce.balance(); - } - if (!purchasesReceived) { - commerce.inventory(); - } } // diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 5af3011f17..cc2039da48 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -284,7 +284,7 @@ Wallet::Wallet() { auto nodeList = DependencyManager::get(); auto& packetReceiver = nodeList->getPacketReceiver(); - packetReceiver.registerListener(PacketType::ChallengeOwnership, this, "verifyOwnerChallenge"); + packetReceiver.registerListener(PacketType::ChallengeOwnership, this, "handleChallengeOwnershipPacket"); } Wallet::~Wallet() { diff --git a/interface/src/scripting/WalletScriptingInterface.cpp b/interface/src/scripting/WalletScriptingInterface.cpp index 2d94bf46cb..555e9477b0 100644 --- a/interface/src/scripting/WalletScriptingInterface.cpp +++ b/interface/src/scripting/WalletScriptingInterface.cpp @@ -11,30 +11,37 @@ #include "WalletScriptingInterface.h" +CheckoutProxy::CheckoutProxy(QObject* qmlObject, QObject* parent) : QmlWrapper(qmlObject, parent) { + Q_ASSERT(QThread::currentThread() == qApp->thread()); +} + WalletScriptingInterface::WalletScriptingInterface() { } static const QString CHECKOUT_QML_PATH = qApp->applicationDirPath() + "../../../qml/hifi/commerce/checkout/Checkout.qml"; -void WalletScriptingInterface::buy() { +void WalletScriptingInterface::buy(const QString& name, const QString& id, const int& price, const QString& href) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "buy", Q_ARG(const QString&, name), Q_ARG(const QString&, id), Q_ARG(const int&, price), Q_ARG(const QString&, href)); + return; + } + auto tabletScriptingInterface = DependencyManager::get(); auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); - if (!tablet->isPathLoaded(CHECKOUT_QML_PATH)) { - tablet->loadQMLSource(CHECKOUT_QML_PATH); - } + tablet->loadQMLSource(CHECKOUT_QML_PATH); DependencyManager::get()->openTablet(); - QVariant message = {}; + QQuickItem* root = nullptr; + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !qApp->isHMDMode())) { + root = DependencyManager::get()->getRootItem(); + } else { + root = tablet->getTabletRoot(); + } + CheckoutProxy* checkout = new CheckoutProxy(root->findChild("checkout")); - - tablet->sendToQml(message); .sendToQml({ - method: 'updateCheckoutQML', params : { - itemId: '0d90d21c-ce7a-4990-ad18-e9d2cf991027', - itemName : 'Test Flaregun', - itemAuthor : 'hifiDave', - itemPrice : 17, - itemHref : 'http://mpassets.highfidelity.com/0d90d21c-ce7a-4990-ad18-e9d2cf991027-v1/flaregun.json', - }, - canRezCertifiedItems: Entities.canRezCertified || Entities.canRezTmpCertified - }); + // Example: Wallet.buy("Test Flaregun", "0d90d21c-ce7a-4990-ad18-e9d2cf991027", 17, "http://mpassets.highfidelity.com/0d90d21c-ce7a-4990-ad18-e9d2cf991027-v1/flaregun.json"); + checkout->writeProperty("itemName", name); + checkout->writeProperty("itemId", id); + checkout->writeProperty("itemPrice", price); + checkout->writeProperty("itemHref", href); } \ No newline at end of file diff --git a/interface/src/scripting/WalletScriptingInterface.h b/interface/src/scripting/WalletScriptingInterface.h index 9ff3a0319e..31b42094cf 100644 --- a/interface/src/scripting/WalletScriptingInterface.h +++ b/interface/src/scripting/WalletScriptingInterface.h @@ -17,6 +17,15 @@ #include "scripting/HMDScriptingInterface.h" #include +#include +#include +#include "Application.h" + +class CheckoutProxy : public QmlWrapper { + Q_OBJECT +public: + CheckoutProxy(QObject* qmlObject, QObject* parent = nullptr); +}; class WalletScriptingInterface : public QObject, public Dependency { @@ -30,7 +39,7 @@ public: Q_INVOKABLE uint getWalletStatus() { return _walletStatus; } void setWalletStatus(const uint& status) { _walletStatus = status; } - Q_INVOKABLE void buy(); + Q_INVOKABLE void buy(const QString& name, const QString& id, const int& price, const QString& href); signals: void walletStatusChanged(); diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 5e15e1c23e..ded4542c51 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -235,7 +235,6 @@ type: "CHECKOUT", itemId: id, itemName: name, - itemAuthor: author, itemPrice: price ? parseInt(price, 10) : 0, itemHref: href })); diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index c9e6eca922..e94b227a4a 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -19,7 +19,8 @@ var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); - var MARKETPLACE_CHECKOUT_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/checkout/Checkout.qml"; + var MARKETPLACE_CHECKOUT_QML_PATH_BASE = "qml/hifi/commerce/checkout/Checkout.qml"; + var MARKETPLACE_CHECKOUT_QML_PATH = Script.resourcesPath() + MARKETPLACE_CHECKOUT_QML_PATH_BASE; var MARKETPLACE_PURCHASES_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/purchases/Purchases.qml"; var MARKETPLACE_WALLET_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/wallet/Wallet.qml"; var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "commerce/inspectionCertificate/InspectionCertificate.qml"; @@ -71,7 +72,6 @@ method: 'updateCheckoutQML', params: { itemId: '0d90d21c-ce7a-4990-ad18-e9d2cf991027', itemName: 'Test Flaregun', - itemAuthor: 'hifiDave', itemPrice: (debugError ? 10 : 17), itemHref: 'http://mpassets.highfidelity.com/0d90d21c-ce7a-4990-ad18-e9d2cf991027-v1/flaregun.json', }, @@ -108,7 +108,7 @@ var filterText; // Used for updating Purchases QML function onScreenChanged(type, url) { onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; - onCommerceScreen = type === "QML" && (url === MARKETPLACE_CHECKOUT_QML_PATH || url === MARKETPLACE_PURCHASES_QML_PATH || url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); + onCommerceScreen = type === "QML" && (url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH_BASE) !== -1 || url === MARKETPLACE_PURCHASES_QML_PATH || url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); wireEventBridge(onCommerceScreen); if (url === MARKETPLACE_PURCHASES_QML_PATH) { From bb99f68d40ec7e0a67bb366067380d2638377a59 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 2 Oct 2017 17:04:00 -0700 Subject: [PATCH 651/722] Avoiding the need for the GLobal frameTiing Scriptiong interface and just a regular member of APplication instead --- interface/src/Application.cpp | 5 +---- interface/src/Application.h | 3 +++ interface/src/Application_render.cpp | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 35fc0049a8..97d621478d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -188,7 +188,6 @@ #include "InterfaceParentFinder.h" #include "ui/OctreeStatsProvider.h" -//#include "FrameTimingsScriptingInterface.h" #include #include #include @@ -2217,8 +2216,6 @@ void Application::initializeGL() { update(0); } -//FrameTimingsScriptingInterface _frameTimingsScriptingInterface; - extern void setupPreferences(); void Application::initializeUi() { @@ -2273,7 +2270,7 @@ void Application::initializeUi() { surfaceContext->setContextProperty("Recording", DependencyManager::get().data()); surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); - //surfaceContext->setContextProperty("FrameTimings", &_frameTimingsScriptingInterface); // TODO: Remove this Context Property ? i don;t see anywhere + surfaceContext->setContextProperty("FrameTimings", &_frameTimingsScriptingInterface); surfaceContext->setContextProperty("Rates", new RatesScriptingInterface(this)); surfaceContext->setContextProperty("TREE_SCALE", TREE_SCALE); diff --git a/interface/src/Application.h b/interface/src/Application.h index 77ae840274..3966d1c50d 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -76,6 +76,7 @@ #include #include #include +#include "FrameTimingsScriptingInterface.h" #include "Sound.h" @@ -537,6 +538,8 @@ private: RateCounter<> _avatarSimCounter; RateCounter<> _simCounter; + FrameTimingsScriptingInterface _frameTimingsScriptingInterface; + QTimer _minimizedWindowTimer; QElapsedTimer _timerStart; QElapsedTimer _lastTimeUpdated; diff --git a/interface/src/Application_render.cpp b/interface/src/Application_render.cpp index 1c82c95a8a..b09705c300 100644 --- a/interface/src/Application_render.cpp +++ b/interface/src/Application_render.cpp @@ -14,11 +14,9 @@ #include #include #include "ui/Stats.h" -#include "FrameTimingsScriptingInterface.h" #include #include "Util.h" -FrameTimingsScriptingInterface _frameTimingsScriptingInterface; // Statically provided display and input plugins extern DisplayPluginList getDisplayPlugins(); From 46ce68a893944e4cf3aef18c26765771764a2ff6 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 3 Oct 2017 13:33:11 +1300 Subject: [PATCH 652/722] Fix lasers --- scripts/system/libraries/utils.js | 4 ++-- scripts/tutorials/entity_scripts/ambientSound.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index 162edcaea0..fa9e60a4ef 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -6,8 +6,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -// note: this constant is currently duplicated in edit.js -EDIT_SETTING = "io.highfidelity.isEditting"; +// note: this constant is currently duplicated in edit.js and ambientSounds.js +EDIT_SETTING = "io.highfidelity.isEditing"; isInEditMode = function isInEditMode() { return Settings.getValue(EDIT_SETTING); }; diff --git a/scripts/tutorials/entity_scripts/ambientSound.js b/scripts/tutorials/entity_scripts/ambientSound.js index e0a7d0a3cf..dd964fb4e0 100644 --- a/scripts/tutorials/entity_scripts/ambientSound.js +++ b/scripts/tutorials/entity_scripts/ambientSound.js @@ -139,7 +139,7 @@ this._toggle = function(hint) { // Toggle between ON/OFF state, but only if not in edit mode - if (Settings.getValue("io.highfidelity.isEditting")) { + if (Settings.getValue("io.highfidelity.isEditing")) { return; } var props = Entities.getEntityProperties(entity, [ "userData" ]); From a59d590111f94527ae67dee26e9d2e21ab90aafa Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 3 Oct 2017 13:33:20 +1300 Subject: [PATCH 653/722] ESLint --- scripts/system/controllers/controllerModules/inVREditMode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/inVREditMode.js b/scripts/system/controllers/controllerModules/inVREditMode.js index c48955442b..f807dfd7ef 100644 --- a/scripts/system/controllers/controllerModules/inVREditMode.js +++ b/scripts/system/controllers/controllerModules/inVREditMode.js @@ -20,7 +20,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.hand = hand; this.disableModules = false; this.parameters = makeDispatcherModuleParameters( - 200, // Not too high otherwise the tablet laser doesn't work. + 200, // Not too high otherwise the tablet laser doesn't work. this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"], From 86f580919c5d42f1ebf3f3130b86aa75e48e190e Mon Sep 17 00:00:00 2001 From: druiz17 Date: Mon, 2 Oct 2017 17:43:43 -0700 Subject: [PATCH 654/722] remove what space --- interface/resources/qml/desktop/Desktop.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 4e8090ce98..000e310a66 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -223,7 +223,7 @@ FocusScope { //offscreenWindow.activeFocusItemChanged.connect(onWindowFocusChanged); focusHack.start(); } - + function onWindowFocusChanged() { //console.log("Focus item is " + offscreenWindow.activeFocusItem); From 31e72c55034b3c445e11995bc923a3e87548d0f2 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 3 Oct 2017 16:26:00 +1300 Subject: [PATCH 655/722] Work around bug undoing physics where entities can end at wrong position --- scripts/shapes/modules/history.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/shapes/modules/history.js b/scripts/shapes/modules/history.js index 4c1b7857b8..7065a3c52b 100644 --- a/scripts/shapes/modules/history.js +++ b/scripts/shapes/modules/history.js @@ -145,9 +145,17 @@ History = (function () { return undoPosition < history.length - 1; } + function undoSetProperties(entityID, properties) { + Entities.editEntity(entityID, properties); + if (properties.gravity) { + kickPhysics(entityID); + } + } + function undo() { var undoData, entityID, + REPEAT_UNDO_DELAY = 500, // ms i, length; @@ -163,9 +171,16 @@ History = (function () { if (undoData.setProperties) { for (i = 0, length = undoData.setProperties.length; i < length; i++) { - Entities.editEntity(undoData.setProperties[i].entityID, undoData.setProperties[i].properties); - if (undoData.setProperties[i].properties.gravity) { - kickPhysics(undoData.setProperties[i].entityID); + undoSetProperties(undoData.setProperties[i].entityID, undoData.setProperties[i].properties); + if (undoData.setProperties[i].properties.velocity + && Vec3.equal(undoData.setProperties[i].properties.velocity, Vec3.ZERO) + && undoData.setProperties[i].properties.angularVelocity + && Vec3.equal(undoData.setProperties[i].properties.angularVelocity, Vec3.ZERO)) { + // Work around physics bug wherein the entity doesn't always end up at the correct position. + Script.setTimeout( + undoSetProperties(undoData.setProperties[i].entityID, undoData.setProperties[i].properties), + REPEAT_UNDO_DELAY + ); } } } From 21cd986645e4e4f3bc2d1c30e6ec1ee3929e10c5 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 2 Oct 2017 20:57:20 -0700 Subject: [PATCH 656/722] tally PerformanceTimer records when it is active --- interface/src/ui/Stats.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 70cb74ed2f..fa5482f0d5 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -117,11 +117,6 @@ void Stats::updateStats(bool force) { } } - bool performanceTimerShouldBeActive = Menu::getInstance()->isOptionChecked(MenuOption::Stats) && _expanded; - if (performanceTimerShouldBeActive != PerformanceTimer::isActive()) { - PerformanceTimer::setActive(performanceTimerShouldBeActive); - } - auto nodeList = DependencyManager::get(); auto avatarManager = DependencyManager::get(); // we need to take one avatar out so we don't include ourselves @@ -404,7 +399,14 @@ void Stats::updateStats(bool force) { STAT_UPDATE(lodStatus, "You can see " + DependencyManager::get()->getLODFeedbackText()); } - bool performanceTimerIsActive = PerformanceTimer::isActive(); + + bool performanceTimerShouldBeActive = Menu::getInstance()->isOptionChecked(MenuOption::Stats) && _expanded; + if (performanceTimerShouldBeActive != PerformanceTimer::isActive()) { + PerformanceTimer::setActive(performanceTimerShouldBeActive); + } + if (performanceTimerShouldBeActive) { + PerformanceTimer::tallyAllTimerRecords(); // do this even if we're not displaying them, so they don't stack up + } if (performanceTimerShouldBeActive && Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails)) { @@ -412,7 +414,6 @@ void Stats::updateStats(bool force) { _showTimingDetails = true; emit timingExpandedChanged(); } - PerformanceTimer::tallyAllTimerRecords(); // do this even if we're not displaying them, so they don't stack up // we will also include room for 1 line per timing record and a header of 4 lines // Timing details... @@ -457,7 +458,7 @@ void Stats::updateStats(bool force) { emit timingExpandedChanged(); } - if (_expanded && performanceTimerIsActive) { + if (_expanded && performanceTimerShouldBeActive) { if (!_showGameUpdateStats) { _showGameUpdateStats = true; } From acb99592eeb266688c373a3395119d221c4fabe0 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 2 Oct 2017 21:31:30 -0700 Subject: [PATCH 657/722] fixing th e bug on exit due to the debug anim draw of the avatars --- libraries/render-utils/src/AnimDebugDraw.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/render-utils/src/AnimDebugDraw.cpp b/libraries/render-utils/src/AnimDebugDraw.cpp index 4f7f9ef5c4..c22e99cbbc 100644 --- a/libraries/render-utils/src/AnimDebugDraw.cpp +++ b/libraries/render-utils/src/AnimDebugDraw.cpp @@ -144,6 +144,7 @@ void AnimDebugDraw::shutdown() { if (scene && _itemID) { render::Transaction transaction; transaction.removeItem(_itemID); + render::Item::clearID(_itemID); scene->enqueueTransaction(transaction); } } @@ -316,7 +317,9 @@ void AnimDebugDraw::update() { if (!scene) { return; } - + if (!render::Item::isValidID(_itemID)) { + return; + } render::Transaction transaction; transaction.updateItem(_itemID, [&](AnimDebugDrawData& data) { From 895e3d0430017be666ddd4a6f81fd1b6c37b5e6c Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 2 Oct 2017 21:46:52 -0700 Subject: [PATCH 658/722] more cleanups --- interface/src/Application_render.cpp | 1 - interface/src/ui/overlays/Overlays.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/interface/src/Application_render.cpp b/interface/src/Application_render.cpp index b09705c300..ac9aecf66c 100644 --- a/interface/src/Application_render.cpp +++ b/interface/src/Application_render.cpp @@ -103,7 +103,6 @@ void Application::paintGL() { _applicationOverlay.renderOverlay(&renderArgs); } - // updateCamera(renderArgs); { PROFILE_RANGE(render, "/updateCompositor"); getApplicationCompositor().setFrameInfo(_frameCount, eyeToWorld, sensorToWorld); diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index e9cb1f2973..c93d225718 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -152,7 +152,7 @@ void Overlays::render3DHUDOverlays(RenderArgs* renderArgs) { foreach(Overlay::Pointer thisOverlay, _overlays3DHUD) { // Reset necessary batch pipeline settings between overlays batch.setResourceTexture(0, textureCache->getWhiteTexture()); // FIXME - do we really need to do this?? - // batch.setModelTransform(Transform()); + batch.setModelTransform(Transform()); renderArgs->_shapePipeline = _shapePlumber->pickPipeline(renderArgs, thisOverlay->getShapeKey()); thisOverlay->render(renderArgs); From 64f70f1ebbd3dcf69b772f5b2076466738ba2484 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 2 Oct 2017 22:00:41 -0700 Subject: [PATCH 659/722] more cleanups --- interface/src/ui/overlays/Base3DOverlay.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 4d4f2c3a1c..4210e76097 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -191,8 +191,6 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { // Communicate changes to the renderItem if needed if (needRenderItemUpdate) { - notifyRenderTransformChange(); - auto itemID = getRenderItemID(); if (render::Item::isValidID(itemID)) { render::ScenePointer scene = qApp->getMain3DScene(); @@ -267,7 +265,6 @@ void Base3DOverlay::parentDeleted() { } void Base3DOverlay::update(float duration) { - // In Base3DOverlay, if its location or bound changed, the renderTrasnformDirty flag is true. // then the correct transform used for rendering is computed in the update transaction and assigned. if (_renderTransformDirty) { From 5145107f8c002e8b9ac39fdcd5fc5641481a6d7a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 3 Oct 2017 21:36:40 +1300 Subject: [PATCH 660/722] 2D overlays can no longer be lasered --- scripts/system/controllers/controllerModules/inVREditMode.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/system/controllers/controllerModules/inVREditMode.js b/scripts/system/controllers/controllerModules/inVREditMode.js index f807dfd7ef..952bd14fd7 100644 --- a/scripts/system/controllers/controllerModules/inVREditMode.js +++ b/scripts/system/controllers/controllerModules/inVREditMode.js @@ -41,9 +41,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); return makeRunningValues(false, [], []); } - // 2D overlay lasers. - // These are automatically enabled. - // Tablet stylus. // Includes the tablet laser. var tabletStylusInput = getEnabledModuleByName(this.hand === RIGHT_HAND From 06f0e23bf9bbfc16c4dea90d564aeaa272a2c0b1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 3 Oct 2017 21:44:56 +1300 Subject: [PATCH 661/722] There no longer is a handControllerGrab.js --- scripts/shapes/modules/laser.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/shapes/modules/laser.js b/scripts/shapes/modules/laser.js index 29434a17f9..1efc38b65a 100644 --- a/scripts/shapes/modules/laser.js +++ b/scripts/shapes/modules/laser.js @@ -25,18 +25,18 @@ Laser = function (side) { searchDistance = 0.0, - SEARCH_SPHERE_SIZE = 0.013, // Per handControllerGrab.js multiplied by 1.2 per handControllerGrab.js. + SEARCH_SPHERE_SIZE = 0.013, // Per farActionGrabEntity.js multiplied by 1.2 per farActionGrabEntity.js. MINUMUM_SEARCH_SPHERE_SIZE = 0.006, - SEARCH_SPHERE_FOLLOW_RATE = 0.5, // Per handControllerGrab.js. - COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 }, // Per handControllgerGrab.js. - COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }, // Per handControllgerGrab.js. + SEARCH_SPHERE_FOLLOW_RATE = 0.5, + COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 }, // Per controllerDispatcherUtils.js. + COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }, // Per controllerDispatcherUtils.js. COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT, COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT, - BRIGHT_POW = 0.06, // Per handControllerGrab.js. + BRIGHT_POW = 0.06, // Per old handControllerGrab.js. GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. - PICK_MAX_DISTANCE = 500, // Per handControllerGrab.js. + PICK_MAX_DISTANCE = 500, // Per controllerDispatcherUtils.js. PRECISION_PICKING = true, NO_INCLUDE_IDS = [], NO_EXCLUDE_IDS = [], @@ -56,7 +56,7 @@ Laser = function (side) { return new Laser(side); } - function colorPow(color, power) { // Per handControllerGrab.js. + function colorPow(color, power) { // Per old handControllerGrab.js. return { red: Math.pow(color.red / 255, power) * 255, green: Math.pow(color.green / 255, power) * 255, From 773c48bbc95b9e9fd3b2c539aae6fabdf1708abb Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 3 Oct 2017 10:32:19 -0700 Subject: [PATCH 662/722] use floating point for game loop stats values --- interface/src/ui/Stats.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index fa5482f0d5..8140dfb8ae 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -474,7 +474,7 @@ void Stats::updateStats(bool force) { std::priority_queue idleUpdateStats; auto itr = allRecords.find("/idle/update"); if (itr != allRecords.end()) { - uint64_t dt = (float)itr.value().getMovingAverage() / (float)USECS_PER_MSEC; + float dt = (float)itr.value().getMovingAverage() / (float)USECS_PER_MSEC; _gameUpdateStats = QString("/idle/update = %1 ms").arg(dt); QVector categories = { "devices", "physics", "otherAvatars", "MyAvatar", "misc" }; @@ -482,7 +482,7 @@ void Stats::updateStats(bool force) { QString recordKey = "/idle/update/" + categories[j]; itr = allRecords.find(recordKey); if (itr != allRecords.end()) { - uint64_t dt = (float)itr.value().getMovingAverage() / (float)USECS_PER_MSEC; + float dt = (float)itr.value().getMovingAverage() / (float)USECS_PER_MSEC; QString message = QString("\n %1 = %2").arg(categories[j]).arg(dt); idleUpdateStats.push(SortableStat(message, dt)); } From cd4be1473f45864765e0c0dcae64e91453dfea23 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 3 Oct 2017 10:34:12 -0700 Subject: [PATCH 663/722] put more of Application::update() under time measurement --- interface/src/Application.cpp | 404 +++++++++++++++++----------------- 1 file changed, 202 insertions(+), 202 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4106e0c84d..39fcd76e29 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4843,11 +4843,6 @@ void Application::update(float deltaTime) { PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_renderFrameCount + 1); - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::update()"); - - updateLOD(deltaTime); - if (!_physicsEnabled) { if (!domainLoadingInProgress) { PROFILE_ASYNC_BEGIN(app, "Scene Loading", ""); @@ -4877,6 +4872,7 @@ void Application::update(float deltaTime) { PROFILE_ASYNC_END(app, "Scene Loading", ""); } + auto myAvatar = getMyAvatar(); { PerformanceTimer perfTimer("devices"); @@ -4910,129 +4906,127 @@ void Application::update(float deltaTime) { _lastFaceTrackerUpdate = 0; } - } + auto userInputMapper = DependencyManager::get(); - auto myAvatar = getMyAvatar(); - auto userInputMapper = DependencyManager::get(); + controller::InputCalibrationData calibrationData = { + myAvatar->getSensorToWorldMatrix(), + createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()), + myAvatar->getHMDSensorMatrix(), + myAvatar->getCenterEyeCalibrationMat(), + myAvatar->getHeadCalibrationMat(), + myAvatar->getSpine2CalibrationMat(), + myAvatar->getHipsCalibrationMat(), + myAvatar->getLeftFootCalibrationMat(), + myAvatar->getRightFootCalibrationMat(), + myAvatar->getRightArmCalibrationMat(), + myAvatar->getLeftArmCalibrationMat(), + myAvatar->getRightHandCalibrationMat(), + myAvatar->getLeftHandCalibrationMat() + }; - controller::InputCalibrationData calibrationData = { - myAvatar->getSensorToWorldMatrix(), - createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()), - myAvatar->getHMDSensorMatrix(), - myAvatar->getCenterEyeCalibrationMat(), - myAvatar->getHeadCalibrationMat(), - myAvatar->getSpine2CalibrationMat(), - myAvatar->getHipsCalibrationMat(), - myAvatar->getLeftFootCalibrationMat(), - myAvatar->getRightFootCalibrationMat(), - myAvatar->getRightArmCalibrationMat(), - myAvatar->getLeftArmCalibrationMat(), - myAvatar->getRightHandCalibrationMat(), - myAvatar->getLeftHandCalibrationMat() - }; - - InputPluginPointer keyboardMousePlugin; - for (auto inputPlugin : PluginManager::getInstance()->getInputPlugins()) { - if (inputPlugin->getName() == KeyboardMouseDevice::NAME) { - keyboardMousePlugin = inputPlugin; - } else if (inputPlugin->isActive()) { - inputPlugin->pluginUpdate(deltaTime, calibrationData); - } - } - - userInputMapper->setInputCalibrationData(calibrationData); - userInputMapper->update(deltaTime); - - if (keyboardMousePlugin && keyboardMousePlugin->isActive()) { - keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData); - } - - // Transfer the user inputs to the driveKeys - // FIXME can we drop drive keys and just have the avatar read the action states directly? - myAvatar->clearDriveKeys(); - if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { - if (!_controllerScriptingInterface->areActionsCaptured()) { - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); - if (deltaTime > FLT_EPSILON) { - myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); - myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); - myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); + InputPluginPointer keyboardMousePlugin; + for (auto inputPlugin : PluginManager::getInstance()->getInputPlugins()) { + if (inputPlugin->getName() == KeyboardMouseDevice::NAME) { + keyboardMousePlugin = inputPlugin; + } else if (inputPlugin->isActive()) { + inputPlugin->pluginUpdate(deltaTime, calibrationData); } } - myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); - } - static const std::vector avatarControllerActions = { - controller::Action::LEFT_HAND, - controller::Action::RIGHT_HAND, - controller::Action::LEFT_FOOT, - controller::Action::RIGHT_FOOT, - controller::Action::HIPS, - controller::Action::SPINE2, - controller::Action::HEAD, - controller::Action::LEFT_HAND_THUMB1, - controller::Action::LEFT_HAND_THUMB2, - controller::Action::LEFT_HAND_THUMB3, - controller::Action::LEFT_HAND_THUMB4, - controller::Action::LEFT_HAND_INDEX1, - controller::Action::LEFT_HAND_INDEX2, - controller::Action::LEFT_HAND_INDEX3, - controller::Action::LEFT_HAND_INDEX4, - controller::Action::LEFT_HAND_MIDDLE1, - controller::Action::LEFT_HAND_MIDDLE2, - controller::Action::LEFT_HAND_MIDDLE3, - controller::Action::LEFT_HAND_MIDDLE4, - controller::Action::LEFT_HAND_RING1, - controller::Action::LEFT_HAND_RING2, - controller::Action::LEFT_HAND_RING3, - controller::Action::LEFT_HAND_RING4, - controller::Action::LEFT_HAND_PINKY1, - controller::Action::LEFT_HAND_PINKY2, - controller::Action::LEFT_HAND_PINKY3, - controller::Action::LEFT_HAND_PINKY4, - controller::Action::RIGHT_HAND_THUMB1, - controller::Action::RIGHT_HAND_THUMB2, - controller::Action::RIGHT_HAND_THUMB3, - controller::Action::RIGHT_HAND_THUMB4, - controller::Action::RIGHT_HAND_INDEX1, - controller::Action::RIGHT_HAND_INDEX2, - controller::Action::RIGHT_HAND_INDEX3, - controller::Action::RIGHT_HAND_INDEX4, - controller::Action::RIGHT_HAND_MIDDLE1, - controller::Action::RIGHT_HAND_MIDDLE2, - controller::Action::RIGHT_HAND_MIDDLE3, - controller::Action::RIGHT_HAND_MIDDLE4, - controller::Action::RIGHT_HAND_RING1, - controller::Action::RIGHT_HAND_RING2, - controller::Action::RIGHT_HAND_RING3, - controller::Action::RIGHT_HAND_RING4, - controller::Action::RIGHT_HAND_PINKY1, - controller::Action::RIGHT_HAND_PINKY2, - controller::Action::RIGHT_HAND_PINKY3, - controller::Action::RIGHT_HAND_PINKY4, - controller::Action::LEFT_ARM, - controller::Action::RIGHT_ARM, - controller::Action::LEFT_SHOULDER, - controller::Action::RIGHT_SHOULDER, - controller::Action::LEFT_FORE_ARM, - controller::Action::RIGHT_FORE_ARM, - controller::Action::LEFT_LEG, - controller::Action::RIGHT_LEG, - controller::Action::LEFT_UP_LEG, - controller::Action::RIGHT_UP_LEG, - controller::Action::LEFT_TOE_BASE, - controller::Action::RIGHT_TOE_BASE - }; + userInputMapper->setInputCalibrationData(calibrationData); + userInputMapper->update(deltaTime); - // copy controller poses from userInputMapper to myAvatar. - glm::mat4 myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()); - glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix()); - glm::mat4 avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix; - for (auto& action : avatarControllerActions) { - controller::Pose pose = userInputMapper->getPoseState(action); - myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix)); + if (keyboardMousePlugin && keyboardMousePlugin->isActive()) { + keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData); + } + + // Transfer the user inputs to the driveKeys + // FIXME can we drop drive keys and just have the avatar read the action states directly? + myAvatar->clearDriveKeys(); + if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { + if (!_controllerScriptingInterface->areActionsCaptured()) { + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); + if (deltaTime > FLT_EPSILON) { + myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); + myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); + myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); + } + } + myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); + } + + static const std::vector avatarControllerActions = { + controller::Action::LEFT_HAND, + controller::Action::RIGHT_HAND, + controller::Action::LEFT_FOOT, + controller::Action::RIGHT_FOOT, + controller::Action::HIPS, + controller::Action::SPINE2, + controller::Action::HEAD, + controller::Action::LEFT_HAND_THUMB1, + controller::Action::LEFT_HAND_THUMB2, + controller::Action::LEFT_HAND_THUMB3, + controller::Action::LEFT_HAND_THUMB4, + controller::Action::LEFT_HAND_INDEX1, + controller::Action::LEFT_HAND_INDEX2, + controller::Action::LEFT_HAND_INDEX3, + controller::Action::LEFT_HAND_INDEX4, + controller::Action::LEFT_HAND_MIDDLE1, + controller::Action::LEFT_HAND_MIDDLE2, + controller::Action::LEFT_HAND_MIDDLE3, + controller::Action::LEFT_HAND_MIDDLE4, + controller::Action::LEFT_HAND_RING1, + controller::Action::LEFT_HAND_RING2, + controller::Action::LEFT_HAND_RING3, + controller::Action::LEFT_HAND_RING4, + controller::Action::LEFT_HAND_PINKY1, + controller::Action::LEFT_HAND_PINKY2, + controller::Action::LEFT_HAND_PINKY3, + controller::Action::LEFT_HAND_PINKY4, + controller::Action::RIGHT_HAND_THUMB1, + controller::Action::RIGHT_HAND_THUMB2, + controller::Action::RIGHT_HAND_THUMB3, + controller::Action::RIGHT_HAND_THUMB4, + controller::Action::RIGHT_HAND_INDEX1, + controller::Action::RIGHT_HAND_INDEX2, + controller::Action::RIGHT_HAND_INDEX3, + controller::Action::RIGHT_HAND_INDEX4, + controller::Action::RIGHT_HAND_MIDDLE1, + controller::Action::RIGHT_HAND_MIDDLE2, + controller::Action::RIGHT_HAND_MIDDLE3, + controller::Action::RIGHT_HAND_MIDDLE4, + controller::Action::RIGHT_HAND_RING1, + controller::Action::RIGHT_HAND_RING2, + controller::Action::RIGHT_HAND_RING3, + controller::Action::RIGHT_HAND_RING4, + controller::Action::RIGHT_HAND_PINKY1, + controller::Action::RIGHT_HAND_PINKY2, + controller::Action::RIGHT_HAND_PINKY3, + controller::Action::RIGHT_HAND_PINKY4, + controller::Action::LEFT_ARM, + controller::Action::RIGHT_ARM, + controller::Action::LEFT_SHOULDER, + controller::Action::RIGHT_SHOULDER, + controller::Action::LEFT_FORE_ARM, + controller::Action::RIGHT_FORE_ARM, + controller::Action::LEFT_LEG, + controller::Action::RIGHT_LEG, + controller::Action::LEFT_UP_LEG, + controller::Action::RIGHT_UP_LEG, + controller::Action::LEFT_TOE_BASE, + controller::Action::RIGHT_TOE_BASE + }; + + // copy controller poses from userInputMapper to myAvatar. + glm::mat4 myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()); + glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix()); + glm::mat4 avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix; + for (auto& action : avatarControllerActions) { + controller::Pose pose = userInputMapper->getPoseState(action); + myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix)); + } } updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... @@ -5040,97 +5034,97 @@ void Application::update(float deltaTime) { QSharedPointer avatarManager = DependencyManager::get(); - if (_physicsEnabled) { + { PROFILE_RANGE_EX(simulation_physics, "Physics", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); - PerformanceTimer perfTimer("physics"); + if (_physicsEnabled) { + { + PROFILE_RANGE_EX(simulation_physics, "UpdateStates", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); - { - PROFILE_RANGE_EX(simulation_physics, "UpdateStates", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("updateStates)"); + static VectorOfMotionStates motionStates; + _entitySimulation->getObjectsToRemoveFromPhysics(motionStates); + _physicsEngine->removeObjects(motionStates); + _entitySimulation->deleteObjectsRemovedFromPhysics(); - PerformanceTimer perfTimer("updateStates)"); - static VectorOfMotionStates motionStates; - _entitySimulation->getObjectsToRemoveFromPhysics(motionStates); - _physicsEngine->removeObjects(motionStates); - _entitySimulation->deleteObjectsRemovedFromPhysics(); + getEntities()->getTree()->withReadLock([&] { + _entitySimulation->getObjectsToAddToPhysics(motionStates); + _physicsEngine->addObjects(motionStates); - getEntities()->getTree()->withReadLock([&] { - _entitySimulation->getObjectsToAddToPhysics(motionStates); - _physicsEngine->addObjects(motionStates); - - }); - getEntities()->getTree()->withReadLock([&] { - _entitySimulation->getObjectsToChange(motionStates); - VectorOfMotionStates stillNeedChange = _physicsEngine->changeObjects(motionStates); - _entitySimulation->setObjectsToChange(stillNeedChange); - }); - - _entitySimulation->applyDynamicChanges(); - - avatarManager->getObjectsToRemoveFromPhysics(motionStates); - _physicsEngine->removeObjects(motionStates); - avatarManager->getObjectsToAddToPhysics(motionStates); - _physicsEngine->addObjects(motionStates); - avatarManager->getObjectsToChange(motionStates); - _physicsEngine->changeObjects(motionStates); - - myAvatar->prepareForPhysicsSimulation(); - _physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) { - dynamic->prepareForPhysicsSimulation(); - }); - } - { - PROFILE_RANGE_EX(simulation_physics, "StepSimulation", 0xffff8000, (uint64_t)getActiveDisplayPlugin()->presentCount()); - PerformanceTimer perfTimer("stepSimulation"); - getEntities()->getTree()->withWriteLock([&] { - _physicsEngine->stepSimulation(); - }); - } - { - PROFILE_RANGE_EX(simulation_physics, "HarvestChanges", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); - PerformanceTimer perfTimer("harvestChanges"); - if (_physicsEngine->hasOutgoingChanges()) { - // grab the collision events BEFORE handleOutgoingChanges() because at this point - // we have a better idea of which objects we own or should own. - auto& collisionEvents = _physicsEngine->getCollisionEvents(); - - getEntities()->getTree()->withWriteLock([&] { - PerformanceTimer perfTimer("handleOutgoingChanges"); - - const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); - _entitySimulation->handleChangedMotionStates(outgoingChanges); - avatarManager->handleChangedMotionStates(outgoingChanges); - - const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); - _entitySimulation->handleDeactivatedMotionStates(deactivations); + }); + getEntities()->getTree()->withReadLock([&] { + _entitySimulation->getObjectsToChange(motionStates); + VectorOfMotionStates stillNeedChange = _physicsEngine->changeObjects(motionStates); + _entitySimulation->setObjectsToChange(stillNeedChange); }); - if (!_aboutToQuit) { - // handleCollisionEvents() AFTER handleOutgoinChanges() - PerformanceTimer perfTimer("entities"); - avatarManager->handleCollisionEvents(collisionEvents); - // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk - // deadlock.) - _entitySimulation->handleCollisionEvents(collisionEvents); + _entitySimulation->applyDynamicChanges(); - // NOTE: the getEntities()->update() call below will wait for lock - // and will simulate entity motion (the EntityTree has been given an EntitySimulation). - getEntities()->update(true); // update the models... - } + avatarManager->getObjectsToRemoveFromPhysics(motionStates); + _physicsEngine->removeObjects(motionStates); + avatarManager->getObjectsToAddToPhysics(motionStates); + _physicsEngine->addObjects(motionStates); + avatarManager->getObjectsToChange(motionStates); + _physicsEngine->changeObjects(motionStates); - myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); - - if (Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && - Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsSimulationTiming)) { - _physicsEngine->harvestPerformanceStats(); - } - // NOTE: the PhysicsEngine stats are written to stdout NOT to Qt log framework - _physicsEngine->dumpStatsIfNecessary(); + myAvatar->prepareForPhysicsSimulation(); + _physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) { + dynamic->prepareForPhysicsSimulation(); + }); } + { + PROFILE_RANGE_EX(simulation_physics, "StepSimulation", 0xffff8000, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("stepSimulation"); + getEntities()->getTree()->withWriteLock([&] { + _physicsEngine->stepSimulation(); + }); + } + { + PROFILE_RANGE_EX(simulation_physics, "HarvestChanges", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("harvestChanges"); + if (_physicsEngine->hasOutgoingChanges()) { + // grab the collision events BEFORE handleOutgoingChanges() because at this point + // we have a better idea of which objects we own or should own. + auto& collisionEvents = _physicsEngine->getCollisionEvents(); + + getEntities()->getTree()->withWriteLock([&] { + PerformanceTimer perfTimer("handleOutgoingChanges"); + + const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); + _entitySimulation->handleChangedMotionStates(outgoingChanges); + avatarManager->handleChangedMotionStates(outgoingChanges); + + const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); + _entitySimulation->handleDeactivatedMotionStates(deactivations); + }); + + if (!_aboutToQuit) { + // handleCollisionEvents() AFTER handleOutgoinChanges() + PerformanceTimer perfTimer("entities"); + avatarManager->handleCollisionEvents(collisionEvents); + // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk + // deadlock.) + _entitySimulation->handleCollisionEvents(collisionEvents); + + // NOTE: the getEntities()->update() call below will wait for lock + // and will simulate entity motion (the EntityTree has been given an EntitySimulation). + getEntities()->update(true); // update the models... + } + + myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); + + if (Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && + Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsSimulationTiming)) { + _physicsEngine->harvestPerformanceStats(); + } + // NOTE: the PhysicsEngine stats are written to stdout NOT to Qt log framework + _physicsEngine->dumpStatsIfNecessary(); + } + } + } else { + // update the rendering without any simulation + getEntities()->update(false); } - } else { - // update the rendering without any simulation - getEntities()->update(false); } // AvatarManager update @@ -5150,6 +5144,12 @@ void Application::update(float deltaTime) { } PerformanceTimer perfTimer("misc"); + + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::update()"); + + updateLOD(deltaTime); + // TODO: break these out into distinct perfTimers when they prove interesting { PROFILE_RANGE(app, "RayPickManager"); From e64b0861a1aa9de93c974aec725dad562e90624a Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 3 Oct 2017 10:53:10 -0700 Subject: [PATCH 664/722] Change passphrase working --- .../qml/hifi/commerce/wallet/PassphraseChange.qml | 9 +++++---- .../hifi/commerce/wallet/PassphraseSelection.qml | 6 +++--- interface/src/commerce/QmlCommerce.cpp | 15 ++++++++++----- interface/src/commerce/QmlCommerce.h | 2 ++ 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml index e560384807..a75d511793 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml @@ -90,7 +90,7 @@ Item { } else { // Error submitting new passphrase resetSubmitButton(); - passphraseSelection.setErrorText("Backend error"); + passphraseSelection.setErrorText("Current passphrase incorrect - try again"); } } else { sendSignalToWallet(msg); @@ -137,9 +137,10 @@ Item { width: 150; text: "Submit"; onClicked: { - if (passphraseSelection.validateAndSubmitPassphrase()) { - passphraseSubmitButton.text = "Submitting..."; - passphraseSubmitButton.enabled = false; + passphraseSubmitButton.text = "Submitting..."; + passphraseSubmitButton.enabled = false; + if (!passphraseSelection.validateAndSubmitPassphrase()) { + resetSubmitButton(); } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml index 46c7e36eaf..7c0cecd98d 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -43,8 +43,8 @@ Item { passphrasePageSecurityImage.source = "image://security/securityImage"; } - onWalletAuthenticatedStatusResult: { - sendMessageToLightbox({method: 'statusResult', status: isAuthenticated}); + onChangePassphraseStatusResult: { + sendMessageToLightbox({method: 'statusResult', status: changeSuccess}); } } @@ -310,7 +310,7 @@ Item { passphraseFieldAgain.error = false; currentPassphraseField.error = false; setErrorText(""); - commerce.setPassphrase(passphraseField.text); + commerce.changePassphrase(currentPassphraseField.text, passphraseField.text); return true; } } diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index b735cfcd17..dbd84594bc 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -117,13 +117,18 @@ void QmlCommerce::history() { ledger->history(wallet->listPublicKeys()); } +void QmlCommerce::changePassphrase(const QString& oldPassphrase, const QString& newPassphrase) { + auto wallet = DependencyManager::get(); + if ((wallet->getPassphrase()->isEmpty() || wallet->getPassphrase() == oldPassphrase) && !newPassphrase.isEmpty()) { + emit changePassphraseStatusResult(wallet->changePassphrase(newPassphrase)); + } else { + emit changePassphraseStatusResult(false); + } +} + void QmlCommerce::setPassphrase(const QString& passphrase) { auto wallet = DependencyManager::get(); - if(wallet->getPassphrase() && !wallet->getPassphrase()->isEmpty() && !passphrase.isEmpty()) { - wallet->changePassphrase(passphrase); - } else { - wallet->setPassphrase(passphrase); - } + wallet->setPassphrase(passphrase); getWalletAuthenticatedStatus(); } diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 9323791a82..8e6af6da65 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -41,6 +41,7 @@ signals: void keyFilePathIfExistsResult(const QString& path); void securityImageResult(bool exists); void walletAuthenticatedStatusResult(bool isAuthenticated); + void changePassphraseStatusResult(bool changeSuccess); void buyResult(QJsonObject result); // Balance and Inventory are NOT properties, because QML can't change them (without risk of failure), and @@ -60,6 +61,7 @@ protected: Q_INVOKABLE void chooseSecurityImage(const QString& imageFile); Q_INVOKABLE void setPassphrase(const QString& passphrase); + Q_INVOKABLE void changePassphrase(const QString& oldPassphrase, const QString& newPassphrase); Q_INVOKABLE void buy(const QString& assetId, int cost, const bool controlledFailure = false); Q_INVOKABLE void balance(); From 1389c2e31d16969e3ea7d5173d52b8acf2c01d68 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 3 Oct 2017 12:15:23 -0700 Subject: [PATCH 665/722] Bug fix for deadlock in ModelEntityItem::setCompundShapeURL() The fix was to prevent ModelEntityItem::hasCompoundShapeURL() from taking a readlock on the entity, instead a finer grained lock (ThreadSafeValueCache) is made around the _compoundShapeURL QString. --- libraries/entities/src/ModelEntityItem.cpp | 16 ++++++---------- libraries/entities/src/ModelEntityItem.h | 4 +++- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index b50ed008a7..14813a68fe 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -217,7 +217,7 @@ void ModelEntityItem::debugDump() const { void ModelEntityItem::setShapeType(ShapeType type) { withWriteLock([&] { if (type != _shapeType) { - if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { + if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { // dynamic and STATIC_MESH are incompatible // since the shape is being set here we clear the dynamic bit _dynamic = false; @@ -260,9 +260,9 @@ void ModelEntityItem::setModelURL(const QString& url) { void ModelEntityItem::setCompoundShapeURL(const QString& url) { withWriteLock([&] { - if (_compoundShapeURL != url) { + if (_compoundShapeURL.get() != url) { ShapeType oldType = computeTrueShapeType(); - _compoundShapeURL = url; + _compoundShapeURL.set(url); if (oldType != computeTrueShapeType()) { _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; } @@ -496,10 +496,8 @@ bool ModelEntityItem::hasModel() const { return !_modelURL.isEmpty(); }); } -bool ModelEntityItem::hasCompoundShapeURL() const { - return resultWithReadLock([&] { - return !_compoundShapeURL.isEmpty(); - }); +bool ModelEntityItem::hasCompoundShapeURL() const { + return _compoundShapeURL.get().isEmpty(); } QString ModelEntityItem::getModelURL() const { @@ -509,9 +507,7 @@ QString ModelEntityItem::getModelURL() const { } QString ModelEntityItem::getCompoundShapeURL() const { - return resultWithReadLock([&] { - return _compoundShapeURL; - }); + return _compoundShapeURL.get(); } void ModelEntityItem::setColor(const rgbColor& value) { diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index c4b3e82f23..7efb493735 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -14,6 +14,7 @@ #include "EntityItem.h" #include +#include #include "AnimationPropertyGroup.h" class ModelEntityItem : public EntityItem { @@ -153,7 +154,8 @@ protected: rgbColor _color; QString _modelURL; - QString _compoundShapeURL; + + ThreadSafeValueCache _compoundShapeURL; AnimationPropertyGroup _animationProperties; From ef326a38518969dff000d550717b40027394cfc1 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 3 Oct 2017 11:59:25 -0700 Subject: [PATCH 666/722] expose performance hot spot in stats details --- .../src/EntityTreeRenderer.cpp | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 0cb25a2e2f..e42a307d96 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -260,12 +260,24 @@ void EntityTreeRenderer::update(bool simulate) { } } - auto scene = _viewState->getMain3DScene(); - if (scene) { - render::Transaction transaction; - addPendingEntities(scene, transaction); - updateChangedEntities(scene, transaction); - scene->enqueueTransaction(transaction); + { + PerformanceTimer sceneTimer("scene"); + auto scene = _viewState->getMain3DScene(); + if (scene) { + render::Transaction transaction; + { + PerformanceTimer foo("add"); + addPendingEntities(scene, transaction); + } + { + PerformanceTimer foo("change"); + updateChangedEntities(scene, transaction); + } + { + PerformanceTimer foo("enqueue"); + scene->enqueueTransaction(transaction); + } + } } } } @@ -1078,4 +1090,4 @@ void EntityTreeRenderer::onEntityChanged(const EntityItemID& id) { _changedEntitiesGuard.withWriteLock([&] { _changedEntities.insert(id); }); -} \ No newline at end of file +} From 46e809bbb25e64e912daf64ecd89b478c2645fec Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 3 Oct 2017 12:19:19 -0700 Subject: [PATCH 667/722] use a better variable name --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index e42a307d96..67fcc5cc69 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -266,15 +266,15 @@ void EntityTreeRenderer::update(bool simulate) { if (scene) { render::Transaction transaction; { - PerformanceTimer foo("add"); + PerformanceTimer pt("add"); addPendingEntities(scene, transaction); } { - PerformanceTimer foo("change"); + PerformanceTimer pt("change"); updateChangedEntities(scene, transaction); } { - PerformanceTimer foo("enqueue"); + PerformanceTimer pt("enqueue"); scene->enqueueTransaction(transaction); } } From a6f254551166707a3bd62a277f28bafd592c8710 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 3 Oct 2017 12:51:37 -0700 Subject: [PATCH 668/722] fix local t pose --- libraries/animation/src/Rig.cpp | 4 +++- libraries/animation/src/Rig.h | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 712c728dcb..0897c26a12 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1620,13 +1620,14 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo } void Rig::initAnimGraph(const QUrl& url) { - if (_animGraphURL != url || !_animNode) { + if (_animGraphURL != url || (!_animNode && !_animLoading)) { _animGraphURL = url; _animNode.reset(); // load the anim graph _animLoader.reset(new AnimNodeLoader(url)); + _animLoading = true; connect(_animLoader.get(), &AnimNodeLoader::success, [this](AnimNode::Pointer nodeIn) { _animNode = nodeIn; _animNode->setSkeleton(_animSkeleton); @@ -1637,6 +1638,7 @@ void Rig::initAnimGraph(const QUrl& url) { _userAnimState = { UserAnimState::None, "", 30.0f, false, 0.0f, 0.0f }; overrideAnimation(origState.url, origState.fps, origState.loop, origState.firstFrame, origState.lastFrame); } + _animLoading = false; emit onLoadComplete(); }); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index eabc62ab75..18d49c5f1e 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -303,6 +303,7 @@ protected: std::shared_ptr _animNode; std::shared_ptr _animSkeleton; std::unique_ptr _animLoader; + bool _animLoading { false }; AnimVariantMap _animVars; enum class RigRole { Idle = 0, From 33e9a71000c95d3ab4d8434023b3a9f8aa859b2c Mon Sep 17 00:00:00 2001 From: druiz17 Date: Tue, 3 Oct 2017 12:51:51 -0700 Subject: [PATCH 669/722] grab attachments --- .../controllers/controllerModules/nearParentGrabEntity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js index 9323f651a2..70d91bf1ec 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -223,7 +223,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); (distance > NEAR_GRAB_RADIUS * sensorScaleFactor)) { continue; } - if (entityIsGrabbable(props)) { + if (entityIsGrabbable(props) || entityIsCloneable(props)) { // give haptic feedback if (props.id !== this.hapticTargetID) { Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); From 6b3d89d18f23dc2b02b0099c5f3acf9c6d8e5355 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Tue, 3 Oct 2017 14:13:37 -0700 Subject: [PATCH 670/722] fix hudlaser module --- .../system/controllers/controllerModules/hudOverlayPointer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js index e13c8e2f20..912eeccadb 100644 --- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js +++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js @@ -178,7 +178,7 @@ } var hudRayPick = controllerData.hudRayPicks[this.hand]; var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); - if (!Window.isPointOnDesktopWindow(point2d) && !controllerData.triggerClicks[this.hand]) { + if (!Window.isPointOnDesktopWindow(point2d) && !this.triggerClicked) { this.exitModule(); return false; } From c9d2d40e1e0817e197ff385364e2c0d88eb0b23f Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 3 Oct 2017 14:44:11 -0700 Subject: [PATCH 671/722] possibly fix registration offset issue --- libraries/entities/src/EntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 107af837fe..cd00a7a211 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1368,7 +1368,7 @@ void EntityItem::recordCreationTime() { const Transform EntityItem::getTransformToCenter(bool& success) const { Transform result = getTransform(success); if (getRegistrationPoint() != ENTITY_ITEM_HALF_VEC3) { // If it is not already centered, translate to center - result.postTranslate(ENTITY_ITEM_HALF_VEC3 - getRegistrationPoint()); // Position to center + result.postTranslate((ENTITY_ITEM_HALF_VEC3 - getRegistrationPoint()) * getDimensions()); // Position to center } return result; } From fc6e5df2cbb52a105a9b8118126a9148fdadf145 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 3 Oct 2017 15:02:21 -0700 Subject: [PATCH 672/722] fix text/web registration point usage --- libraries/entities/src/TextEntityItem.cpp | 2 +- libraries/entities/src/WebEntityItem.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities/src/TextEntityItem.cpp b/libraries/entities/src/TextEntityItem.cpp index 1d8cb50a4b..3ade5879c5 100644 --- a/libraries/entities/src/TextEntityItem.cpp +++ b/libraries/entities/src/TextEntityItem.cpp @@ -136,7 +136,7 @@ bool TextEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec2 xyDimensions(dimensions.x, dimensions.y); glm::quat rotation = getRotation(); glm::vec3 position = getPosition() + rotation * - (dimensions * (getRegistrationPoint() - ENTITY_ITEM_DEFAULT_REGISTRATION_POINT)); + (dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint())); // FIXME - should set face and surfaceNormal return findRayRectangleIntersection(origin, direction, rotation, position, xyDimensions, distance); diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp index 61c6c8d80e..9595f2959c 100644 --- a/libraries/entities/src/WebEntityItem.cpp +++ b/libraries/entities/src/WebEntityItem.cpp @@ -112,7 +112,7 @@ bool WebEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const g glm::vec3 dimensions = getDimensions(); glm::vec2 xyDimensions(dimensions.x, dimensions.y); glm::quat rotation = getRotation(); - glm::vec3 position = getPosition() + rotation * (dimensions * (getRegistrationPoint() - ENTITY_ITEM_DEFAULT_REGISTRATION_POINT)); + glm::vec3 position = getPosition() + rotation * (dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint())); if (findRayRectangleIntersection(origin, direction, rotation, position, xyDimensions, distance)) { surfaceNormal = rotation * Vectors::UNIT_Z; From 0614fd9b4fafddfa60f8f3d4c367453d20865af0 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 3 Oct 2017 15:27:35 -0700 Subject: [PATCH 673/722] fix AvatarManger::findRayIntersection from script thread --- interface/src/avatar/AvatarManager.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index cef6bd4c9c..39a4f7a4af 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -73,9 +73,9 @@ public: Q_INVOKABLE RayToAvatarIntersectionResult findRayIntersection(const PickRay& ray, const QScriptValue& avatarIdsToInclude = QScriptValue(), const QScriptValue& avatarIdsToDiscard = QScriptValue()); - RayToAvatarIntersectionResult findRayIntersectionVector(const PickRay& ray, - const QVector& avatarsToInclude, - const QVector& avatarsToDiscard); + Q_INVOKABLE RayToAvatarIntersectionResult findRayIntersectionVector(const PickRay& ray, + const QVector& avatarsToInclude, + const QVector& avatarsToDiscard); // TODO: remove this HACK once we settle on optimal default sort coefficients Q_INVOKABLE float getAvatarSortCoefficient(const QString& name); From ad9677f63e3b7e3efdf89f4e443631f7f82c0321 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Thu, 28 Sep 2017 11:18:40 -0700 Subject: [PATCH 674/722] fixing grabbing and tablet bugs --- .../controllerModules/equipEntity.js | 16 +++++++++++++--- .../controllerModules/farActionGrabEntity.js | 4 ++-- .../controllerModules/tabletStylusInput.js | 17 ++++++++++++++++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index 29db02c6de..4978f225ce 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -255,6 +255,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa this.messageGrabEntity = false; this.grabEntityProps = null; this.shouldSendStart = false; + this.equipedWithSecondary = false; this.parameters = makeDispatcherModuleParameters( 300, @@ -370,6 +371,10 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa return this.rawSecondaryValue < BUMPER_ON_VALUE; }; + this.secondarySmoothedSqueezed = function() { + return this.rawSecondaryValue > BUMPER_ON_VALUE; + }; + this.chooseNearEquipHotspots = function(candidateEntityProps, controllerData) { var _this = this; var collectedHotspots = flatten(candidateEntityProps.map(function(props) { @@ -592,11 +597,13 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa // if the potentialHotspot os not cloneable and locked return null if (potentialEquipHotspot && - ((this.triggerSmoothedSqueezed() && !this.waitForTriggerRelease) || this.messageGrabEntity)) { + (((this.triggerSmoothedSqueezed() || this.secondarySmoothedSqueezed()) && !this.waitForTriggerRelease) || + this.messageGrabEntity)) { this.grabbedHotspot = potentialEquipHotspot; this.targetEntityID = this.grabbedHotspot.entityID; this.startEquipEntity(controllerData); this.messageGrabEnity = false; + this.equipedWithSecondary = this.secondarySmoothedSqueezed(); return makeRunningValues(true, [potentialEquipHotspot.entityID], []); } else { return makeRunningValues(false, [], []); @@ -627,7 +634,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa return this.checkNearbyHotspots(controllerData, deltaTime, timestamp); } - if (controllerData.secondaryValues[this.hand]) { + if (controllerData.secondaryValues[this.hand] && !this.equipedWithSecondary) { // this.secondaryReleased() will always be true when not depressed // so we cannot simply rely on that for release - ensure that the // trigger was first "prepared" by being pushed in before the release @@ -644,7 +651,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var dropDetected = this.dropGestureProcess(deltaTime); - if (this.triggerSmoothedReleased()) { + if (this.triggerSmoothedReleased() || this.secondaryReleased()) { if (this.shouldSendStart) { // we don't want to send startEquip message until the trigger is released. otherwise, // guns etc will fire right as they are equipped. @@ -653,6 +660,9 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa this.shouldSendStart = false; } this.waitForTriggerRelease = false; + if (this.secondaryReleased() && this.equipedWithSecondary) { + this.equipedWithSecondary = false; + } } if (dropDetected && this.prevDropDetected !== dropDetected) { diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index 5c31c859e9..c5b82f75f0 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -132,7 +132,7 @@ Script.include("/~/system/libraries/controllers.js"); this.updateLaserPointer = function(controllerData) { var SEARCH_SPHERE_SIZE = 0.011; var MIN_SPHERE_SIZE = 0.0005; - var radius = Math.max(1.2 * SEARCH_SPHERE_SIZE * this.intersectionDistance, MIN_SPHERE_SIZE); + var radius = Math.max(1.2 * SEARCH_SPHERE_SIZE * this.intersectionDistance, MIN_SPHERE_SIZE) * MyAvatar.sensorToWorldScale; var dim = {x: radius, y: radius, z: radius}; var mode = "hold"; if (!this.distanceHolding && !this.distanceRotating) { @@ -424,7 +424,7 @@ Script.include("/~/system/libraries/controllers.js"); this.laserPointerOff(); return makeRunningValues(false, [], []); } - + this.intersectionDistance = controllerData.rayPicks[this.hand].distance; this.updateLaserPointer(controllerData); var otherModuleName =this.hand === RIGHT_HAND ? "LeftFarActionGrabEntity" : "RightFarActionGrabEntity"; diff --git a/scripts/system/controllers/controllerModules/tabletStylusInput.js b/scripts/system/controllers/controllerModules/tabletStylusInput.js index def958b223..0a3b2b8adc 100644 --- a/scripts/system/controllers/controllerModules/tabletStylusInput.js +++ b/scripts/system/controllers/controllerModules/tabletStylusInput.js @@ -152,6 +152,20 @@ Script.include("/~/system/libraries/controllers.js"); } }; + this.updateStylus = function() { + if (this.stylus) { + var X_ROT_NEG_90 = { x: -0.70710678, y: 0, z: 0, w: 0.70710678 }; + var modelOrientation = Quat.multiply(this.stylusTip.orientation, X_ROT_NEG_90); + var modelPositionOffset = Vec3.multiplyQbyV(modelOrientation, { x: 0, y: 0, z: MyAvatar.sensorToWorldScale * -WEB_STYLUS_LENGTH / 2 }); + + var stylusProps = { + position: Vec3.sum(this.stylusTip.position, modelPositionOffset), + rotation: modelOrientation + }; + Overlays.editOverlay(this.stylus, stylusProps); + } + }; + this.showStylus = function() { if (this.stylus) { return; @@ -320,6 +334,7 @@ Script.include("/~/system/libraries/controllers.js"); if (this.isNearStylusTarget) { if (!this.useFingerInsteadOfStylus) { this.showStylus(); + this.updateStylus(); } else { this.pointFinger(true); } @@ -335,7 +350,7 @@ Script.include("/~/system/libraries/controllers.js"); var SCALED_TABLET_MAX_HOVER_DISTANCE = TABLET_MAX_HOVER_DISTANCE * sensorScaleFactor; if (nearestStylusTarget && nearestStylusTarget.distance > SCALED_TABLET_MIN_TOUCH_DISTANCE && - nearestStylusTarget.distance < SCALED_TABLET_MAX_HOVER_DISTANCE) { + nearestStylusTarget.distance < SCALED_TABLET_MAX_HOVER_DISTANCE && !this.getOtherHandController().stylusTouchingTarget) { this.requestTouchFocus(nearestStylusTarget); From 13feec89c272736c03b129b8ac1de0354d45f1e7 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Thu, 28 Sep 2017 13:23:28 -0700 Subject: [PATCH 675/722] remove update stylus --- .../controllerModules/tabletStylusInput.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/scripts/system/controllers/controllerModules/tabletStylusInput.js b/scripts/system/controllers/controllerModules/tabletStylusInput.js index 0a3b2b8adc..36ed7920dd 100644 --- a/scripts/system/controllers/controllerModules/tabletStylusInput.js +++ b/scripts/system/controllers/controllerModules/tabletStylusInput.js @@ -152,20 +152,6 @@ Script.include("/~/system/libraries/controllers.js"); } }; - this.updateStylus = function() { - if (this.stylus) { - var X_ROT_NEG_90 = { x: -0.70710678, y: 0, z: 0, w: 0.70710678 }; - var modelOrientation = Quat.multiply(this.stylusTip.orientation, X_ROT_NEG_90); - var modelPositionOffset = Vec3.multiplyQbyV(modelOrientation, { x: 0, y: 0, z: MyAvatar.sensorToWorldScale * -WEB_STYLUS_LENGTH / 2 }); - - var stylusProps = { - position: Vec3.sum(this.stylusTip.position, modelPositionOffset), - rotation: modelOrientation - }; - Overlays.editOverlay(this.stylus, stylusProps); - } - }; - this.showStylus = function() { if (this.stylus) { return; @@ -334,7 +320,6 @@ Script.include("/~/system/libraries/controllers.js"); if (this.isNearStylusTarget) { if (!this.useFingerInsteadOfStylus) { this.showStylus(); - this.updateStylus(); } else { this.pointFinger(true); } From fcafbef109892041f55c38d9e0ea1482ad97bded Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 4 Oct 2017 09:34:32 -0700 Subject: [PATCH 676/722] Fix buying free items using Commerce system --- interface/resources/qml/hifi/commerce/checkout/Checkout.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 77180f7bab..32f324aea9 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -35,7 +35,7 @@ Rectangle { property string itemHref; property double balanceAfterPurchase; property bool alreadyOwned: false; - property int itemPrice; + property int itemPrice: -1; property bool itemIsJson: true; property bool shouldBuyWithControlledFailure: false; property bool debugCheckoutSuccess: false; @@ -339,7 +339,7 @@ Rectangle { } FiraSansSemiBold { id: itemPriceText; - text: root.itemPrice; + text: (root.itemPrice === -1) ? "--" : root.itemPrice; // Text size size: 26; // Anchors From 0a505617df86a58ee95460e10c7bb40b716dbc44 Mon Sep 17 00:00:00 2001 From: vladest Date: Wed, 4 Oct 2017 19:33:36 +0200 Subject: [PATCH 677/722] Implemented single keyboard instance for all Tablet login dialog items --- .../qml/LoginDialog/CompleteProfileBody.qml | 24 ++++++++++--------- .../qml/LoginDialog/LinkAccountBody.qml | 13 +++++----- .../resources/qml/LoginDialog/SignInBody.qml | 6 +++-- .../resources/qml/LoginDialog/SignUpBody.qml | 23 ++++++++++++------ .../qml/LoginDialog/UsernameCollisionBody.qml | 13 +++++++--- .../qml/dialogs/TabletLoginDialog.qml | 5 ++-- 6 files changed, 52 insertions(+), 32 deletions(-) diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index 2c6bc1082a..fe4c511f1d 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -29,11 +29,12 @@ Item { readonly property int maxHeight: 720 function resize() { - var targetWidth = Math.max(titleWidth, Math.max(additionalTextContainer.contentWidth, - termsContainer.contentWidth)) + if (root.isTablet === false) { + var targetWidth = Math.max(titleWidth, Math.max(additionalTextContainer.contentWidth, + termsContainer.contentWidth)) + parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + } var targetHeight = 5 * hifi.dimensions.contentSpacing.y + buttons.height + additionalTextContainer.height + termsContainer.height - - parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) } } @@ -61,10 +62,7 @@ Item { Button { anchors.verticalCenter: parent.verticalCenter - text: qsTr("Cancel") - - onClicked: root.tryDestroy() } } @@ -96,17 +94,19 @@ Item { id: termsContainer anchors { top: additionalTextContainer.bottom - left: parent.left margins: 0 topMargin: 2 * hifi.dimensions.contentSpacing.y + horizontalCenter: parent.horizontalCenter } + width: parent.width text: qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") wrapMode: Text.WordWrap color: hifi.colors.baseGrayHighlight lineHeight: 1 lineHeightMode: Text.ProportionalHeight - horizontalAlignment: Text.AlignHCenter + fontSizeMode: Text.HorizontalFit + horizontalAlignment: Text.AlignVCenter onLinkActivated: loginDialog.openUrl(link) } @@ -128,8 +128,10 @@ Item { console.log("Create Failed: " + error) bodyLoader.source = "UsernameCollisionBody.qml" - bodyLoader.item.width = root.pane.width - bodyLoader.item.height = root.pane.height + if (!root.isTablet) { + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } } onHandleLoginCompleted: { console.log("Login Succeeded") diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index dd38b641bb..c73aab08c3 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -45,8 +45,7 @@ Item { function resize() { var targetWidth = Math.max(titleWidth, form.contentWidth); var targetHeight = hifi.dimensions.contentSpacing.y + mainTextContainer.height + - 4 * hifi.dimensions.contentSpacing.y + form.height/* + - hifi.dimensions.contentSpacing.y + buttons.height*/; + 4 * hifi.dimensions.contentSpacing.y + form.height; if (additionalInformation.visible) { targetWidth = Math.max(targetWidth, additionalInformation.width); @@ -118,7 +117,7 @@ Item { TextField { id: usernameField width: parent.width - + focus: true label: "Username or Email" ShortcutText { @@ -225,8 +224,10 @@ Item { onClicked: { bodyLoader.setSource("SignUpBody.qml") - bodyLoader.item.width = root.pane.width - bodyLoader.item.height = root.pane.height + if (!root.isTablet) { + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } } } } @@ -255,7 +256,7 @@ Item { root.keyboardEnabled = HMD.active; root.keyboardRaised = Qt.binding( function() { return keyboardRaised; }) } - //d.resize(); + d.resize(); if (failAfterSignUp) { mainTextContainer.text = "Account created successfully." diff --git a/interface/resources/qml/LoginDialog/SignInBody.qml b/interface/resources/qml/LoginDialog/SignInBody.qml index 71ec03f7ff..c4b6c2aee1 100644 --- a/interface/resources/qml/LoginDialog/SignInBody.qml +++ b/interface/resources/qml/LoginDialog/SignInBody.qml @@ -121,8 +121,10 @@ Item { console.log("Login Failed") bodyLoader.source = "CompleteProfileBody.qml" - bodyLoader.item.width = root.pane.width - bodyLoader.item.height = root.pane.height + if (!root.isTablet) { + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } } } } diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index c4056422dd..f6cf40db8e 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -44,8 +44,7 @@ Item { function resize() { var targetWidth = Math.max(titleWidth, form.contentWidth); var targetHeight = hifi.dimensions.contentSpacing.y + mainTextContainer.height + - 4 * hifi.dimensions.contentSpacing.y + form.height/* + - hifi.dimensions.contentSpacing.y + buttons.height*/; + 4 * hifi.dimensions.contentSpacing.y + form.height; parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)); parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) @@ -168,8 +167,10 @@ Item { onClicked: { bodyLoader.setSource("LinkAccountBody.qml") - bodyLoader.item.width = root.pane.width - bodyLoader.item.height = root.pane.height + if (!root.isTablet) { + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } } } } @@ -216,7 +217,13 @@ Item { Component.onCompleted: { root.title = qsTr("Create an Account") root.iconText = "<" - keyboardEnabled = HMD.active; + //dont rise local keyboard + keyboardEnabled = !root.isTablet && HMD.active; + //but rise Tablet's one instead for Tablet interface + if (root.isTablet) { + root.keyboardEnabled = HMD.active; + root.keyboardRaised = Qt.binding( function() { return keyboardRaised; }) + } d.resize(); emailField.forceActiveFocus(); @@ -247,8 +254,10 @@ Item { onHandleLoginFailed: { // we failed to login, show the LoginDialog so the user will try again bodyLoader.setSource("LinkAccountBody.qml", { "failAfterSignUp": true }) - bodyLoader.item.width = root.pane.width - bodyLoader.item.height = root.pane.height + if (!root.isTablet) { + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } } } diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index b049d7f8bb..5c212578b8 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -79,7 +79,7 @@ Item { margins: 0 topMargin: hifi.dimensions.contentSpacing.y } - width: 250 + width: parent.width placeholderText: "Choose your own" } @@ -102,7 +102,7 @@ Item { bottom: parent.bottom right: parent.right margins: 0 - topMargin: hifi.dimensions.contentSpacing.y + bottomMargin: hifi.dimensions.contentSpacing.y } spacing: hifi.dimensions.contentSpacing.x onHeightChanged: d.resize(); onWidthChanged: d.resize(); @@ -129,7 +129,14 @@ Item { Component.onCompleted: { root.title = qsTr("Complete Your Profile") root.iconText = "<" - keyboardEnabled = HMD.active; + //dont rise local keyboard + keyboardEnabled = !root.isTablet && HMD.active; + //but rise Tablet's one instead for Tablet interface + if (root.isTablet) { + root.keyboardEnabled = HMD.active; + root.keyboardRaised = Qt.binding( function() { return keyboardRaised; }) + } + d.resize(); } diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index 334cb9304f..29276a2ccf 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -93,7 +93,6 @@ TabletModalWindow { } } - TabletModalFrame { id: mfRoot @@ -103,6 +102,7 @@ TabletModalWindow { anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter + verticalCenterOffset: -loginKeyboard.height / 2 } LoginDialog { @@ -126,14 +126,13 @@ TabletModalWindow { } Keyboard { + id: loginKeyboard raised: root.keyboardEnabled && root.keyboardRaised numeric: root.punctuationMode - enabled: true anchors { left: parent.left right: parent.right bottom: parent.bottom - bottomMargin: root.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 } } From 1ed10ecf59ccb400eb43b508640d529ac0b00613 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 4 Oct 2017 11:05:17 -0700 Subject: [PATCH 678/722] Fix passphrase crash --- interface/src/commerce/QmlCommerce.cpp | 4 +++- interface/src/commerce/Wallet.cpp | 4 +++- interface/src/commerce/Wallet.h | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index dbd84594bc..9f8847e8c7 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -119,7 +119,9 @@ void QmlCommerce::history() { void QmlCommerce::changePassphrase(const QString& oldPassphrase, const QString& newPassphrase) { auto wallet = DependencyManager::get(); - if ((wallet->getPassphrase()->isEmpty() || wallet->getPassphrase() == oldPassphrase) && !newPassphrase.isEmpty()) { + if (wallet->getPassphrase()->isEmpty()) { + emit changePassphraseStatusResult(wallet->setPassphrase(newPassphrase)); + } else if (wallet->getPassphrase() == oldPassphrase && !newPassphrase.isEmpty()) { emit changePassphraseStatusResult(wallet->changePassphrase(newPassphrase)); } else { emit changePassphraseStatusResult(false); diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index cc2039da48..079e3a9479 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -293,13 +293,15 @@ Wallet::~Wallet() { } } -void Wallet::setPassphrase(const QString& passphrase) { +bool Wallet::setPassphrase(const QString& passphrase) { if (_passphrase) { delete _passphrase; } _passphrase = new QString(passphrase); _publicKeys.clear(); + + return true; } bool Wallet::writeSecurityImage(const QPixmap* pixmap, const QString& outputFilePath) { diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index 807080e6ea..acf9f8e45e 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -42,7 +42,7 @@ public: void setCKey(const QByteArray& ckey) { _ckey = ckey; } QByteArray getCKey() { return _ckey; } - void setPassphrase(const QString& passphrase); + bool setPassphrase(const QString& passphrase); QString* getPassphrase() { return _passphrase; } bool getPassphraseIsCached() { return !(_passphrase->isEmpty()); } bool walletIsAuthenticatedWithPassphrase(); From 86e8076362ce1f39547aa6845d14231c7c916df3 Mon Sep 17 00:00:00 2001 From: vladest Date: Wed, 4 Oct 2017 20:30:25 +0200 Subject: [PATCH 679/722] Fix puntuation mode --- interface/resources/qml/dialogs/TabletLoginDialog.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index 29276a2ccf..9722f31144 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -28,6 +28,7 @@ TabletModalWindow { color: hifi.colors.baseGray title: qsTr("Sign in to High Fidelity") property alias titleWidth: root.titleWidth + property alias punctuationMode: root.punctuationMode //fake root for shared components expecting root here property var root: QtObject { From 215193ad90425a980ac6cad229edbe9331b9c04c Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 4 Oct 2017 12:05:13 -0700 Subject: [PATCH 680/722] try to improve performance (cherry picked from commit a5f5f9fc5d7b3d4a6c2fddf54961b3c5070e441c) --- .../src/EntityTreeRenderer.cpp | 10 ------- .../src/RenderableEntityItem.cpp | 17 ------------ .../src/RenderableEntityItem.h | 26 ------------------- .../src/RenderableModelEntityItem.cpp | 9 ++++--- .../src/RenderableModelEntityItem.h | 6 ++--- 5 files changed, 8 insertions(+), 60 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 0cb25a2e2f..f8c40a5229 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -222,16 +222,6 @@ void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene _renderablesToUpdate.insert({ entityId, renderable }); } - // NOTE: Looping over all the entity renderers is likely to be a bottleneck in the future - // Currently, this is necessary because the model entity loading logic requires constant polling - // This was working fine because the entity server used to send repeated updates as your view changed, - // but with the improved entity server logic (PR 11141), updateInScene (below) would not be triggered enough - for (const auto& entry : _entitiesInScene) { - const auto& renderable = entry.second; - if (renderable) { - renderable->update(scene, transaction); - } - } if (!_renderablesToUpdate.empty()) { for (const auto& entry : _renderablesToUpdate) { const auto& renderable = entry.second; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index ea514d3181..3f1e89b86c 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -291,18 +291,6 @@ void EntityRenderer::updateInScene(const ScenePointer& scene, Transaction& trans }); } -void EntityRenderer::update(const ScenePointer& scene, Transaction& transaction) { - if (!isValidRenderItem()) { - return; - } - - if (!needsUpdate()) { - return; - } - - doUpdate(scene, transaction, _entity); -} - // // Internal methods // @@ -316,11 +304,6 @@ bool EntityRenderer::needsRenderUpdate() const { return needsRenderUpdateFromEntity(_entity); } -// Returns true if the item needs to have update called -bool EntityRenderer::needsUpdate() const { - return needsUpdateFromEntity(_entity); -} - // Returns true if the item in question needs to have updateInScene called because of changes in the entity bool EntityRenderer::needsRenderUpdateFromEntity(const EntityItemPointer& entity) const { bool success = false; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 56cb39252f..6b47ff8b1d 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -49,8 +49,6 @@ public: virtual bool addToScene(const ScenePointer& scene, Transaction& transaction) final; virtual void removeFromScene(const ScenePointer& scene, Transaction& transaction); - virtual void update(const ScenePointer& scene, Transaction& transaction); - protected: virtual bool needsRenderUpdateFromEntity() const final { return needsRenderUpdateFromEntity(_entity); } virtual void onAddToScene(const EntityItemPointer& entity); @@ -73,12 +71,6 @@ protected: // Returns true if the item in question needs to have updateInScene called because of changes in the entity virtual bool needsRenderUpdateFromEntity(const EntityItemPointer& entity) const; - // Returns true if the item in question needs to have update called - virtual bool needsUpdate() const; - - // Returns true if the item in question needs to have update called because of changes in the entity - virtual bool needsUpdateFromEntity(const EntityItemPointer& entity) const { return false; } - // Will be called on the main thread from updateInScene. This can be used to fetch things like // network textures or model geometry from resource caches virtual void doRenderUpdateSynchronous(const ScenePointer& scene, Transaction& transaction, const EntityItemPointer& entity) { } @@ -88,8 +80,6 @@ protected: // data in this method if using multi-threaded rendering virtual void doRenderUpdateAsynchronous(const EntityItemPointer& entity); - virtual void doUpdate(const ScenePointer& scene, Transaction& transaction, const EntityItemPointer& entity) { } - // Called by the `render` method after `needsRenderUpdate` virtual void doRender(RenderArgs* args) = 0; @@ -158,15 +148,6 @@ protected: onRemoveFromSceneTyped(_typedEntity); } - using Parent::needsUpdateFromEntity; - // Returns true if the item in question needs to have update called because of changes in the entity - virtual bool needsUpdateFromEntity(const EntityItemPointer& entity) const override final { - if (Parent::needsUpdateFromEntity(entity)) { - return true; - } - return needsUpdateFromTypedEntity(_typedEntity); - } - using Parent::needsRenderUpdateFromEntity; // Returns true if the item in question needs to have updateInScene called because of changes in the entity virtual bool needsRenderUpdateFromEntity(const EntityItemPointer& entity) const override final { @@ -181,11 +162,6 @@ protected: doRenderUpdateSynchronousTyped(scene, transaction, _typedEntity); } - virtual void doUpdate(const ScenePointer& scene, Transaction& transaction, const EntityItemPointer& entity) override final { - Parent::doUpdate(scene, transaction, entity); - doUpdateTyped(scene, transaction, _typedEntity); - } - virtual void doRenderUpdateAsynchronous(const EntityItemPointer& entity) override final { Parent::doRenderUpdateAsynchronous(entity); doRenderUpdateAsynchronousTyped(_typedEntity); @@ -194,8 +170,6 @@ protected: virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { return false; } virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { } virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { } - virtual bool needsUpdateFromTypedEntity(const TypedEntityPointer& entity) const { return false; } - virtual void doUpdateTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { } virtual void onAddToSceneTyped(const TypedEntityPointer& entity) { } virtual void onRemoveFromSceneTyped(const TypedEntityPointer& entity) { } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index d1e47fd906..19da8a77f4 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1026,7 +1026,7 @@ void ModelEntityRenderer::animate(const TypedEntityPointer& entity) { entity->copyAnimationJointDataToModel(); } -bool ModelEntityRenderer::needsUpdate() const { +bool ModelEntityRenderer::needsRenderUpdate() const { ModelPointer model; withReadLock([&] { model = _model; @@ -1061,10 +1061,10 @@ bool ModelEntityRenderer::needsUpdate() const { return true; } } - return Parent::needsUpdate(); + return Parent::needsRenderUpdate(); } -bool ModelEntityRenderer::needsUpdateFromTypedEntity(const TypedEntityPointer& entity) const { +bool ModelEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { if (resultWithReadLock([&] { if (entity->hasModel() != _hasModel) { return true; @@ -1126,7 +1126,7 @@ bool ModelEntityRenderer::needsUpdateFromTypedEntity(const TypedEntityPointer& e return false; } -void ModelEntityRenderer::doUpdateTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { +void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { if (_hasModel != entity->hasModel()) { _hasModel = entity->hasModel(); } @@ -1250,6 +1250,7 @@ void ModelEntityRenderer::doUpdateTyped(const ScenePointer& scene, Transaction& void ModelEntityRenderer::handleModelLoaded(bool success) { if (success) { _modelJustLoaded = true; + emit requestRenderUpdate(); } } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index ad0afeee0a..77121c30d9 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -138,10 +138,10 @@ protected: virtual ItemKey getKey() override; virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) override; - virtual bool needsUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; - virtual bool needsUpdate() const override; + virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; + virtual bool needsRenderUpdate() const override; virtual void doRender(RenderArgs* args) override; - virtual void doUpdateTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; + virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; private: void animate(const TypedEntityPointer& entity); From f8aa3d72de9a5020b67e8ca039259fe3deef978e Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 4 Oct 2017 15:20:47 -0700 Subject: [PATCH 681/722] Update macOS build guide --- BUILD_OSX.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/BUILD_OSX.md b/BUILD_OSX.md index 586f81def3..6b66863534 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -1,28 +1,28 @@ -Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only OS X specific instructions are found in this file. +Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only macOS specific instructions are found in this file. ### Homebrew -[Homebrew](https://brew.sh/) is an excellent package manager for OS X. It makes install of some High Fidelity dependencies very simple. +[Homebrew](https://brew.sh/) is an excellent package manager for macOS. It makes install of some High Fidelity dependencies very simple. - brew install cmake openssl + brew install cmake openssl qt ### OpenSSL Assuming you've installed OpenSSL using the homebrew instructions above, you'll need to set OPENSSL_ROOT_DIR so CMake can find your installations. For OpenSSL installed via homebrew, set OPENSSL_ROOT_DIR: - export OPENSSL_ROOT_DIR=/usr/local/Cellar/openssl/1.0.2h_1/ + export OPENSSL_ROOT_DIR=/usr/local/Cellar/openssl/1.0.2l Note that this uses the version from the homebrew formula at the time of this writing, and the version in the path will likely change. ### Qt -Download and install the [Qt 5.6.2 for macOS](http://download.qt.io/official_releases/qt/5.6/5.6.2/qt-opensource-mac-x64-clang-5.6.2.dmg). +Assuming you've installed Qt using the homebrew instructions above, you'll need to set QT_CMAKE_PREFIX_PATH so CMake can find your installations. +For Qt installed via homebrew, set QT_CMAKE_PREFIX_PATH: -Keep the default components checked when going through the installer. + export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt/5.9.1/lib/cmake -Once Qt is installed, you need to manually configure the following: -* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt5.6.2/5.6/clang_64/lib/cmake/` directory. +Note that this uses the version from the homebrew formula at the time of this writing, and the version in the path will likely change. ### Xcode From 3a6e84e681a070e2bf1f809c9ca151ebd1c15f8a Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 4 Oct 2017 17:22:21 -0700 Subject: [PATCH 682/722] trying to fix model issue --- .../src/RenderableModelEntityItem.cpp | 17 ++++------------- .../src/RenderableModelEntityItem.h | 3 --- libraries/render-utils/src/Model.cpp | 2 ++ libraries/render-utils/src/Model.h | 3 ++- 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 19da8a77f4..11c97f6716 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1032,10 +1032,6 @@ bool ModelEntityRenderer::needsRenderUpdate() const { model = _model; }); - if (_modelJustLoaded) { - return true; - } - if (model) { if (_needsJointSimulation || _moving || _animating) { return true; @@ -1149,14 +1145,15 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce model->removeFromScene(scene, transaction); withWriteLock([&] { _model.reset(); }); } + emit requestRenderUpdate(); return; } - _modelJustLoaded = false; // Check for addition if (_hasModel && !(bool)_model) { model = std::make_shared(nullptr, entity.get()); - connect(model.get(), &Model::setURLFinished, this, &ModelEntityRenderer::handleModelLoaded); + connect(model.get(), &Model::setURLFinished, this, &ModelEntityRenderer::requestRenderUpdate); + connect(model.get(), &Model::requestRenderUpdate, this, &ModelEntityRenderer::requestRenderUpdate); model->setLoadingPriority(EntityTreeRenderer::getEntityLoadingPriority(*entity)); model->init(); entity->setModel(model); @@ -1172,6 +1169,7 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce // Nothing else to do unless the model is loaded if (!model->isLoaded()) { + emit needsRenderUpdate(); return; } @@ -1247,13 +1245,6 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce } } -void ModelEntityRenderer::handleModelLoaded(bool success) { - if (success) { - _modelJustLoaded = true; - emit requestRenderUpdate(); - } -} - // NOTE: this only renders the "meta" portion of the Model, namely it renders debugging items void ModelEntityRenderer::doRender(RenderArgs* args) { PROFILE_RANGE(render_detail, "MetaModelRender"); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 77121c30d9..d1424316e9 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -151,7 +151,6 @@ private: // Transparency is handled in ModelMeshPartPayload virtual bool isTransparent() const override { return false; } - bool _modelJustLoaded { false }; bool _hasModel { false }; ::ModelPointer _model; GeometryResource::Pointer _compoundShapeResource; @@ -180,8 +179,6 @@ private: uint64_t _lastAnimated { 0 }; float _currentFrame { 0 }; -private slots: - void handleModelLoaded(bool success); }; } } // namespace diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index f25cad8a6e..837f485417 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -814,11 +814,13 @@ void Model::setTextures(const QVariantMap& textures) { _needsUpdateTextures = true; _needsFixupInScene = true; _renderGeometry->setTextures(textures); + emit requestRenderUpdate(); } else { // FIXME(Huffman): Disconnect previously connected lambdas so we don't set textures multiple // after the geometry has finished loading. connect(&_renderWatcher, &GeometryResourceWatcher::finished, this, [this, textures]() { _renderGeometry->setTextures(textures); + emit requestRenderUpdate(); }); } } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 6d338b4598..95dc171ff5 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -103,7 +103,7 @@ public: bool isLayeredInFront() const { return _isLayeredInFront; } virtual void updateRenderItems(); - void setRenderItemsNeedUpdate() { _renderItemsNeedUpdate = true; } + void setRenderItemsNeedUpdate() { _renderItemsNeedUpdate = true; emit requestRenderUpdate(); } bool getRenderItemsNeedUpdate() { return _renderItemsNeedUpdate; } AABox getRenderableMeshBound() const; const render::ItemIDs& fetchRenderItemIDs() const; @@ -265,6 +265,7 @@ public slots: signals: void setURLFinished(bool success); void setCollisionModelURLFinished(bool success); + void requestRenderUpdate(); protected: From 4855e0f528a55dbd58ee66609dbd16c22feb8a69 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 5 Oct 2017 14:36:25 +1300 Subject: [PATCH 683/722] Fix children's rotations drifting when scale group --- scripts/shapes/modules/selection.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/scripts/shapes/modules/selection.js b/scripts/shapes/modules/selection.js index 5c59c20aec..e88b0f8809 100644 --- a/scripts/shapes/modules/selection.js +++ b/scripts/shapes/modules/selection.js @@ -55,6 +55,7 @@ SelectionManager = function (side) { position: properties.position, parentID: properties.parentID, localPosition: properties.localPosition, + localRotation: properties.localRotation, registrationPoint: properties.registrationPoint, rotation: properties.rotation, dimensions: properties.dimensions, @@ -363,7 +364,8 @@ SelectionManager = function (side) { for (i = 1, length = selection.length; i < length; i++) { Entities.editEntity(selection[i].id, { dimensions: Vec3.multiply(factor, selection[i].dimensions), - localPosition: Vec3.multiply(factor, selection[i].localPosition) + localPosition: Vec3.multiply(factor, selection[i].localPosition), + localRotation: selection[i].localRotation // Always specify localRotation otherwise rotations can drift. }); } @@ -405,7 +407,8 @@ SelectionManager = function (side) { entityID: selection[i].id, properties: { dimensions: selection[i].dimensions, - localPosition: selection[i].localPosition + localPosition: selection[i].localPosition, + localRotation: selection[i].localRotation } }); selection[i].dimensions = Vec3.multiply(scaleFactor, selection[i].dimensions); @@ -414,7 +417,8 @@ SelectionManager = function (side) { entityID: selection[i].id, properties: { dimensions: selection[i].dimensions, - localPosition: selection[i].localPosition + localPosition: selection[i].localPosition, + localRotation: selection[i].localRotation } }); } @@ -476,7 +480,8 @@ SelectionManager = function (side) { for (i = 1, length = selection.length; i < length; i++) { Entities.editEntity(selection[i].id, { dimensions: Vec3.multiplyVbyV(factor, selection[i].dimensions), - localPosition: Vec3.multiplyVbyV(factor, selection[i].localPosition) + localPosition: Vec3.multiplyVbyV(factor, selection[i].localPosition), + localRotation: selection[i].localRotation // Always specify localRotation otherwise rotations can drift. }); } @@ -518,7 +523,8 @@ SelectionManager = function (side) { entityID: selection[i].id, properties: { dimensions: selection[i].dimensions, - localPosition: selection[i].localPosition + localPosition: selection[i].localPosition, + localRotation: selection[i].localRotation } }); selection[i].dimensions = Vec3.multiplyVbyV(scaleFactor, selection[i].dimensions); @@ -527,7 +533,8 @@ SelectionManager = function (side) { entityID: selection[i].id, properties: { dimensions: selection[i].dimensions, - localPosition: selection[i].localPosition + localPosition: selection[i].localPosition, + localRotation: selection[i].localRotation } }); } From b526ec0d9332e5505a4ea82d2f8163c7a4b1b153 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 5 Oct 2017 18:21:04 +1300 Subject: [PATCH 684/722] Code review --- scripts/shapes/modules/createPalette.js | 3 +- scripts/shapes/modules/selection.js | 18 +++++---- scripts/shapes/modules/toolsMenu.js | 40 +++++++++++-------- scripts/shapes/shapes.js | 6 ++- .../controllerModules/inVREditMode.js | 2 +- scripts/system/libraries/utils.js | 2 +- 6 files changed, 41 insertions(+), 30 deletions(-) diff --git a/scripts/shapes/modules/createPalette.js b/scripts/shapes/modules/createPalette.js index af22c4e4ff..694705cb2f 100644 --- a/scripts/shapes/modules/createPalette.js +++ b/scripts/shapes/modules/createPalette.js @@ -334,8 +334,9 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { function setHand(hand) { // Assumes UI is not displaying. + var NUMBER_OF_HANDS = 2; side = hand; - otherSide = (side + 1) % 2; + otherSide = (side + 1) % NUMBER_OF_HANDS; controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand(); controlJointName = side === LEFT_HAND ? "LeftHand" : "RightHand"; paletteLateralOffset = side === LEFT_HAND ? -UIT.dimensions.handLateralOffset : UIT.dimensions.handLateralOffset; diff --git a/scripts/shapes/modules/selection.js b/scripts/shapes/modules/selection.js index e88b0f8809..afa0da5fd2 100644 --- a/scripts/shapes/modules/selection.js +++ b/scripts/shapes/modules/selection.js @@ -168,14 +168,16 @@ SelectionManager = function (side) { for (i = 1, length = selection.length; i < length; i++) { registration = selection[i].registrationPoint; - corners[0] = { x: -registration.x, y: -registration.y, z: -registration.z }; - corners[1] = { x: -registration.x, y: -registration.y, z: 1.0 - registration.z }; - corners[2] = { x: -registration.x, y: 1.0 - registration.y, z: -registration.z }; - corners[3] = { x: -registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z }; - corners[4] = { x: 1.0 - registration.x, y: -registration.y, z: -registration.z }; - corners[5] = { x: 1.0 - registration.x, y: -registration.y, z: 1.0 - registration.z }; - corners[6] = { x: 1.0 - registration.x, y: 1.0 - registration.y, z: -registration.z }; - corners[7] = { x: 1.0 - registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z }; + corners = [ + { x: -registration.x, y: -registration.y, z: -registration.z }, + { x: -registration.x, y: -registration.y, z: 1.0 - registration.z }, + { x: -registration.x, y: 1.0 - registration.y, z: -registration.z }, + { x: -registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z }, + { x: 1.0 - registration.x, y: -registration.y, z: -registration.z }, + { x: 1.0 - registration.x, y: -registration.y, z: 1.0 - registration.z }, + { x: 1.0 - registration.x, y: 1.0 - registration.y, z: -registration.z }, + { x: 1.0 - registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z } + ]; position = selection[i].position; rotation = selection[i].rotation; diff --git a/scripts/shapes/modules/toolsMenu.js b/scripts/shapes/modules/toolsMenu.js index 08476f9251..19c114c8e9 100644 --- a/scripts/shapes/modules/toolsMenu.js +++ b/scripts/shapes/modules/toolsMenu.js @@ -129,6 +129,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, MENU_HEADER_BACK_PROPERTIES = { + // Magic numbers per UI design spec. url: Script.resolvePath("../assets/tools/back-icon.svg"), dimensions: { x: 0.0069, y: 0.0107 }, localPosition: { @@ -200,7 +201,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { SWATCH_HIGHLIGHT_DELTA = 0.0020, UI_ELEMENTS = { - "menuButton": { + // Magic numbers per UI design spec. + menuButton: { overlay: "cube", // Invisible cube for hit area. properties: { dimensions: UIT.dimensions.itemCollisionZone, @@ -268,7 +270,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } }, - "button": { + button: { overlay: "cube", properties: { dimensions: UIT.dimensions.buttonDimensions, @@ -289,7 +291,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: UIT.colors.white } }, - "toggleButton": { + toggleButton: { overlay: "cube", properties: { dimensions: UIT.dimensions.buttonDimensions, @@ -327,7 +329,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { offSublabel: { } // Optional properties to update sublabel with. */ }, - "swatch": { + swatch: { overlay: "shape", properties: { shape: "Cylinder", @@ -344,7 +346,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Setting property may optionally include a defaultValue. // A setting value of "" means that the swatch is unpopulated. }, - "swatchHighlight": { + swatchHighlight: { overlay: "shape", properties: { shape: "Cylinder", @@ -364,7 +366,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, - "square": { + square: { overlay: "cube", // Emulate a 2D square with a cube. properties: { localRotation: Quat.ZERO, @@ -375,7 +377,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, - "image": { + image: { overlay: "image3d", properties: { localPosition: { x: 0, y: 0, z: 0 }, @@ -387,7 +389,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, - "horizontalRule": { + horizontalRule: { overlay: "image3d", properties: { url: Script.resolvePath("../assets/horizontal-rule.svg"), @@ -401,7 +403,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, - "sphere": { + sphere: { overlay: "sphere", properties: { dimensions: { x: 0.01, y: 0.01, z: 0.01 }, @@ -413,7 +415,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, - "barSlider": { + barSlider: { // Invisible cube to catch laser intersections; value and remainder entities move inside. // Values range between 0.0 and 1.0. overlay: "cube", @@ -470,7 +472,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } }, - "imageSlider": { // Values range between 0.0 and 1.0. + imageSlider: { // Values range between 0.0 and 1.0. overlay: "cube", properties: { dimensions: { x: 0.0160, y: 0.1229, z: UIT.dimensions.buttonDimensions.z }, @@ -485,7 +487,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { imageURL: null, imageOverlayURL: null }, - "sliderPointer": { + sliderPointer: { overlay: "shape", properties: { shape: "Cone", @@ -498,7 +500,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, - "colorCircle": { + colorCircle: { overlay: "shape", properties: { shape: "Cylinder", @@ -513,7 +515,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { imageURL: null, imageOverlayURL: null }, - "circlePointer": { + circlePointer: { overlay: "shape", properties: { shape: "Cone", @@ -526,7 +528,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true } }, - "picklist": { + picklist: { overlay: "cube", properties: { dimensions: { x: 0.06, y: 0.02, z: 0.01 }, @@ -542,7 +544,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: UIT.colors.white } }, - "picklistItem": { // Note: When using, declare before picklist item that it's being used in. + picklistItem: { // Note: When using, declare before picklist item that it's being used in. overlay: "cube", properties: { dimensions: { x: 0.1416, y: 0.0280, z: UIT.dimensions.buttonDimensions.z }, @@ -589,6 +591,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, OPTONS_PANELS = { + // Magic numbers per UI design spec. colorOptions: [ { id: "colorSwatchesLabel", @@ -1683,6 +1686,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { MENU_ITEM_YS = [0.086, 0.030, -0.026, -0.082], MENU_ITEMS = [ + // Magic numbers per UI design spec. { id: "colorButton", type: "menuButton", @@ -1921,6 +1925,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { DELETE_TOOL = 5, FOOTER_ITEMS = [ + // Magic numbers per UI design spec. { id: "footerRule", type: "horizontalRule", @@ -2041,6 +2046,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { function setHand(hand) { // Assumes UI is not displaying. + var NUMBER_OF_HANDS = 2; side = hand; if (side === LEFT_HAND) { controlHand = rightInputs.hand(); @@ -2055,7 +2061,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { menuOriginLocalPosition = PANEL_ORIGIN_POSITION_RIGHT_HAND; menuOriginLocalRotation = PANEL_ORIGIN_ROTATION_RIGHT_HAND; } - otherSide = (side + 1) % 2; + otherSide = (side + 1) % NUMBER_OF_HANDS; } setHand(side); diff --git a/scripts/shapes/shapes.js b/scripts/shapes/shapes.js index b4e822c233..465ce6258f 100644 --- a/scripts/shapes/shapes.js +++ b/scripts/shapes/shapes.js @@ -113,7 +113,8 @@ } function otherHand(hand) { - return (hand + 1) % 2; + var NUMBER_OF_HANDS = 2; + return (hand + 1) % NUMBER_OF_HANDS; } App = { @@ -1989,7 +1990,8 @@ Entities.canRezChanged.disconnect(onCanRezChanged); Entities.canRezTmpChanged.disconnect(onCanRezChanged); Messages.messageReceived.disconnect(onMessageReceived); - Messages.unsubscribe(DOMAIN_CHANGED_MESSAGE); + // Messages.unsubscribe(DOMAIN_CHANGED_MESSAGE); Do NOT unsubscribe because edit.js also subscribes and + // Messages.subscribe works client-wide. MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged); MyAvatar.skeletonChanged.disconnect(onSkeletonChanged); diff --git a/scripts/system/controllers/controllerModules/inVREditMode.js b/scripts/system/controllers/controllerModules/inVREditMode.js index 952bd14fd7..e3035b26f2 100644 --- a/scripts/system/controllers/controllerModules/inVREditMode.js +++ b/scripts/system/controllers/controllerModules/inVREditMode.js @@ -105,4 +105,4 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); disableDispatcherModule("RightHandInVREditMode"); }; Script.scriptEnding.connect(this.cleanup); -}()); \ No newline at end of file +}()); diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index fa9e60a4ef..e6730b8826 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -6,7 +6,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -// note: this constant is currently duplicated in edit.js and ambientSounds.js +// note: this constant is currently duplicated in edit.js and ambientSound.js EDIT_SETTING = "io.highfidelity.isEditing"; isInEditMode = function isInEditMode() { return Settings.getValue(EDIT_SETTING); From 193dc83b99878a9aeea73b5688c78b0db13080bf Mon Sep 17 00:00:00 2001 From: vladest Date: Thu, 5 Oct 2017 13:59:42 +0200 Subject: [PATCH 685/722] Fix alignment of audio devices input header --- interface/resources/qml/hifi/audio/Audio.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index b1f80ac5e8..12c2ec1835 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -167,6 +167,9 @@ Rectangle { } RalewayRegular { anchors.verticalCenter: parent.verticalCenter; + width: margins.sizeText + margins.sizeLevel + anchors.left: parent.left + anchors.leftMargin: margins.sizeCheckBox size: 16; color: hifi.colors.lightGrayText; text: qsTr("CHOOSE INPUT DEVICE"); From 218de29356eb515774efb7e31cc7e9bf5467308d Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 5 Oct 2017 07:39:21 -0700 Subject: [PATCH 686/722] disable compiler optimization as temporary fix --- libraries/audio/src/AudioGate.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/audio/src/AudioGate.cpp b/libraries/audio/src/AudioGate.cpp index 5b2561da07..18884d008a 100644 --- a/libraries/audio/src/AudioGate.cpp +++ b/libraries/audio/src/AudioGate.cpp @@ -13,6 +13,10 @@ #include #include "AudioDynamics.h" +#ifdef __clang__ +#pragma clang optimize off +#endif + // log2 domain headroom bits above 0dB (int32_t) static const int LOG2_HEADROOM_Q30 = 1; From de2c1aabac887a96f303e2aa918c6e506f82fa7b Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 26 Sep 2017 15:37:14 -0700 Subject: [PATCH 687/722] Add removal of temporary files in FBXBaker --- libraries/baking/src/Baker.h | 2 ++ libraries/baking/src/FBXBaker.cpp | 11 +++++++++++ libraries/baking/src/FBXBaker.h | 1 + 3 files changed, 14 insertions(+) diff --git a/libraries/baking/src/Baker.h b/libraries/baking/src/Baker.h index 2da315c9fc..c1b2ddf959 100644 --- a/libraries/baking/src/Baker.h +++ b/libraries/baking/src/Baker.h @@ -18,6 +18,8 @@ class Baker : public QObject { Q_OBJECT public: + virtual ~Baker() = default; + bool shouldStop(); bool hasErrors() const { return !_errorList.isEmpty(); } diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index 3ef291af22..017f11c680 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -56,6 +56,17 @@ FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGet } +FBXBaker::~FBXBaker() { + if (_tempDir.exists()) { + if (!_tempDir.remove(_originalFBXFilePath)) { + qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalFBXFilePath; + } + if (!_tempDir.rmdir(".")) { + qCWarning(model_baking) << "Failed to remove temporary directory:" << _tempDir; + } + } +} + void FBXBaker::abort() { Baker::abort(); diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index ad8284bfa8..a6034ee2b7 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -35,6 +35,7 @@ class FBXBaker : public Baker { public: FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter, const QString& bakedOutputDir, const QString& originalOutputDir = ""); + ~FBXBaker() override; QUrl getFBXUrl() const { return _fbxURL; } QString getBakedFBXFilePath() const { return _bakedFBXFilePath; } From a6d148475bea71e881ccc785c7146ac831f905eb Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 27 Sep 2017 14:52:21 -0700 Subject: [PATCH 688/722] Add removal of temporary baked files in AssetServer --- assignment-client/src/assets/AssetServer.cpp | 13 ++++++++- assignment-client/src/assets/AssetServer.h | 3 +- .../src/assets/BakeAssetTask.cpp | 29 +++++++++++++++++-- assignment-client/src/assets/BakeAssetTask.h | 2 +- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 9df606c227..c03721d097 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -1162,7 +1162,8 @@ void AssetServer::handleFailedBake(QString originalAssetHash, QString assetPath, _pendingBakes.remove(originalAssetHash); } -void AssetServer::handleCompletedBake(QString originalAssetHash, QString originalAssetPath, QVector bakedFilePaths) { +void AssetServer::handleCompletedBake(QString originalAssetHash, QString originalAssetPath, + QString bakedTempOutputDir, QVector bakedFilePaths) { bool errorCompletingBake { false }; QString errorReason; @@ -1234,6 +1235,16 @@ void AssetServer::handleCompletedBake(QString originalAssetHash, QString origina } } + for (auto& filePath : bakedFilePaths) { + QFile file(filePath); + if (!file.remove()) { + qWarning() << "Failed to remove temporary file:" << filePath; + } + } + if (!QDir(bakedTempOutputDir).rmdir(".")) { + qWarning() << "Failed to remove temporary directory:" << bakedTempOutputDir; + } + if (!errorCompletingBake) { // create the meta file to store which version of the baking process we just completed writeMetaFile(originalAssetHash); diff --git a/assignment-client/src/assets/AssetServer.h b/assignment-client/src/assets/AssetServer.h index 94be560c9b..aeb40a416f 100644 --- a/assignment-client/src/assets/AssetServer.h +++ b/assignment-client/src/assets/AssetServer.h @@ -100,7 +100,8 @@ private: void bakeAsset(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath); /// Move baked content for asset to baked directory and update baked status - void handleCompletedBake(QString originalAssetHash, QString assetPath, QVector bakedFilePaths); + void handleCompletedBake(QString originalAssetHash, QString assetPath, QString bakedTempOutputDir, + QVector bakedFilePaths); void handleFailedBake(QString originalAssetHash, QString assetPath, QString errors); void handleAbortedBake(QString originalAssetHash, QString assetPath); diff --git a/assignment-client/src/assets/BakeAssetTask.cpp b/assignment-client/src/assets/BakeAssetTask.cpp index 9073510f79..94a0739612 100644 --- a/assignment-client/src/assets/BakeAssetTask.cpp +++ b/assignment-client/src/assets/BakeAssetTask.cpp @@ -24,20 +24,39 @@ BakeAssetTask::BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetP } +void cleanupTempFiles(QString tempOutputDir, std::vector files) { + for (const auto& filename : files) { + QFile f { filename }; + if (!f.remove()) { + qDebug() << "Failed to remove:" << filename; + } + } + if (!tempOutputDir.isEmpty()) { + QDir dir { tempOutputDir }; + if (!dir.rmdir(".")) { + qDebug() << "Failed to remove temporary directory:" << tempOutputDir; + } + } +}; + void BakeAssetTask::run() { _isBaking.store(true); qRegisterMetaType >("QVector"); TextureBakerThreadGetter fn = []() -> QThread* { return QThread::currentThread(); }; + QString tempOutputDir; + if (_assetPath.endsWith(".fbx")) { + tempOutputDir = PathUtils::generateTemporaryDir(); _baker = std::unique_ptr { - new FBXBaker(QUrl("file:///" + _filePath), fn, PathUtils::generateTemporaryDir()) + new FBXBaker(QUrl("file:///" + _filePath), fn, tempOutputDir) }; } else { + tempOutputDir = PathUtils::generateTemporaryDir(); _baker = std::unique_ptr { new TextureBaker(QUrl("file:///" + _filePath), image::TextureUsage::CUBE_TEXTURE, - PathUtils::generateTemporaryDir()) + tempOutputDir) }; } @@ -52,6 +71,8 @@ void BakeAssetTask::run() { _wasAborted.store(true); + cleanupTempFiles(tempOutputDir, _baker->getOutputFiles()); + emit bakeAborted(_assetHash, _assetPath); } else if (_baker->hasErrors()) { qDebug() << "Failed to bake: " << _assetHash << _assetPath << _baker->getErrors(); @@ -60,6 +81,8 @@ void BakeAssetTask::run() { _didFinish.store(true); + cleanupTempFiles(tempOutputDir, _baker->getOutputFiles()); + emit bakeFailed(_assetHash, _assetPath, errors); } else { auto vectorOutputFiles = QVector::fromStdVector(_baker->getOutputFiles()); @@ -68,7 +91,7 @@ void BakeAssetTask::run() { _didFinish.store(true); - emit bakeComplete(_assetHash, _assetPath, vectorOutputFiles); + emit bakeComplete(_assetHash, _assetPath, tempOutputDir, vectorOutputFiles); } } diff --git a/assignment-client/src/assets/BakeAssetTask.h b/assignment-client/src/assets/BakeAssetTask.h index 45e7ec8702..90458ac223 100644 --- a/assignment-client/src/assets/BakeAssetTask.h +++ b/assignment-client/src/assets/BakeAssetTask.h @@ -35,7 +35,7 @@ public: bool didFinish() const { return _didFinish.load(); } signals: - void bakeComplete(QString assetHash, QString assetPath, QVector outputFiles); + void bakeComplete(QString assetHash, QString assetPath, QString tempOutputDir, QVector outputFiles); void bakeFailed(QString assetHash, QString assetPath, QString errors); void bakeAborted(QString assetHash, QString assetPath); From f0c9badbd6a4912a1a4340db8945b5d39349b7d6 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 27 Sep 2017 15:56:04 -0700 Subject: [PATCH 689/722] Fix misspelling in lost-connection-to-atp error --- interface/resources/qml/hifi/AssetServer.qml | 2 +- interface/resources/qml/hifi/dialogs/TabletAssetServer.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index 6b60cbff7b..5358ad1adc 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -171,7 +171,7 @@ ScrollingWindow { } function handleGetMappingsError(errorString) { - errorMessageBox("There was a problem retreiving the list of assets from your Asset Server.\n" + errorString); + errorMessageBox("There was a problem retrieving the list of assets from your Asset Server.\n" + errorString); } function addToWorld() { diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 44cd700eac..a02496a252 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -172,7 +172,7 @@ Rectangle { } function handleGetMappingsError(errorString) { - errorMessageBox("There was a problem retreiving the list of assets from your Asset Server.\n" + errorString); + errorMessageBox("There was a problem retrieving the list of assets from your Asset Server.\n" + errorString); } function addToWorld() { From d8341a0929e6992bbdb670f9b684773a854e9a4c Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 5 Oct 2017 09:27:07 -0700 Subject: [PATCH 690/722] Work around compiler optimization bug --- libraries/audio/src/AudioDynamics.h | 19 +++++++++++-------- libraries/audio/src/AudioGate.cpp | 4 ---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/libraries/audio/src/AudioDynamics.h b/libraries/audio/src/AudioDynamics.h index 08daa21348..a759e1c63c 100644 --- a/libraries/audio/src/AudioDynamics.h +++ b/libraries/audio/src/AudioDynamics.h @@ -31,6 +31,9 @@ #define MULQ31(a,b) ((int32_t)(MUL64(a, b) >> 31)) #define MULDIV64(a,b,c) (int32_t)(MUL64(a, b) / (c)) +#define ADDMOD32(a,b) (int32_t)((uint32_t)(a) + (uint32_t)(b)) +#define SUBMOD32(a,b) (int32_t)((uint32_t)(a) - (uint32_t)(b)) + // // on x86 architecture, assume that SSE2 is present // @@ -399,14 +402,14 @@ public: x = MULHI(x, CICGAIN); _buffer[i] = _acc1; - _acc1 += x; // integrator + _acc1 = ADDMOD32(_acc1, x); // integrator i = (i + CIC1 - 1) & MASK; - x = _acc1 - _buffer[i]; // comb + x = SUBMOD32(_acc1, _buffer[i]); // comb _buffer[i] = _acc2; - _acc2 += x; // integrator + _acc2 = ADDMOD32(_acc2, x); // integrator i = (i + CIC2 - 1) & MASK; - x = _acc2 - _buffer[i]; // comb + x = SUBMOD32(_acc2, _buffer[i]); // comb _index = (i + 1) & MASK; // skip unused tap return x; @@ -464,14 +467,14 @@ public: x = MULHI(x, CICGAIN); _buffer[i] = _acc1; - _acc1 += x; // integrator + _acc1 = ADDMOD32(_acc1, x); // integrator i = (i + CIC1 - 1) & MASK; - x = _acc1 - _buffer[i]; // comb + x = SUBMOD32(_acc1, _buffer[i]); // comb _buffer[i] = _acc2; - _acc2 += x; // integrator + _acc2 = ADDMOD32(_acc2, x); // integrator i = (i + CIC2 - 1) & MASK; - x = _acc2 - _buffer[i]; // comb + x = SUBMOD32(_acc2, _buffer[i]); // comb _index = (i + 1) & MASK; // skip unused tap return x; diff --git a/libraries/audio/src/AudioGate.cpp b/libraries/audio/src/AudioGate.cpp index 18884d008a..5b2561da07 100644 --- a/libraries/audio/src/AudioGate.cpp +++ b/libraries/audio/src/AudioGate.cpp @@ -13,10 +13,6 @@ #include #include "AudioDynamics.h" -#ifdef __clang__ -#pragma clang optimize off -#endif - // log2 domain headroom bits above 0dB (int32_t) static const int LOG2_HEADROOM_Q30 = 1; From 82d13090f9a40e6a30372581658663942e6eea8c Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 5 Oct 2017 09:28:30 -0700 Subject: [PATCH 691/722] Better comments --- libraries/audio/src/AudioDynamics.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/audio/src/AudioDynamics.h b/libraries/audio/src/AudioDynamics.h index a759e1c63c..b7c168bfab 100644 --- a/libraries/audio/src/AudioDynamics.h +++ b/libraries/audio/src/AudioDynamics.h @@ -397,6 +397,8 @@ public: // Fast FIR attack/lowpass filter using a 2-stage CIC filter. // The step response reaches final value after N-1 samples. + // NOTE: CIC integrators intentionally overflow, using modulo arithmetic. + // See E. B. Hogenauer, "An economical class of digital filters for decimation and interpolation" const int32_t CICGAIN = 0xffffffff / (CIC1 * CIC2); // Q32 x = MULHI(x, CICGAIN); @@ -462,6 +464,8 @@ public: // Fast FIR attack/lowpass filter using a 2-stage CIC filter. // The step response reaches final value after N-1 samples. + // NOTE: CIC integrators intentionally overflow, using modulo arithmetic. + // See E. B. Hogenauer, "An economical class of digital filters for decimation and interpolation" const int32_t CICGAIN = 0xffffffff / (CIC1 * CIC2); // Q32 x = MULHI(x, CICGAIN); From 8822b1bfa425dc1434d363df542d094543589dc8 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 5 Oct 2017 09:49:38 -0700 Subject: [PATCH 692/722] Minor cleanup --- libraries/audio/src/AudioGate.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/audio/src/AudioGate.cpp b/libraries/audio/src/AudioGate.cpp index 5b2561da07..13c794b923 100644 --- a/libraries/audio/src/AudioGate.cpp +++ b/libraries/audio/src/AudioGate.cpp @@ -6,12 +6,12 @@ // Copyright 2017 High Fidelity, Inc. // -#include "AudioGate.h" - #include +#include #include -#include + #include "AudioDynamics.h" +#include "AudioGate.h" // log2 domain headroom bits above 0dB (int32_t) static const int LOG2_HEADROOM_Q30 = 1; @@ -418,7 +418,7 @@ void GateMono::process(int16_t* input, int16_t* output, int numFrames) { _dc.process(x); // peak detect - int32_t peak = std::abs(x); + int32_t peak = abs(x); // convert to log2 domain peak = fixlog2(peak); From d605bd9ef4f1da448ccc1561df1cb12d5ce5b480 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 5 Oct 2017 11:01:15 -0700 Subject: [PATCH 693/722] Preprocess inventory endpoint result --- .../qml/hifi/commerce/purchases/Purchases.qml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 0bb1515b69..1f7f2e6e53 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -81,8 +81,10 @@ Rectangle { if (result.status !== 'success') { console.log("Failed to get purchases", result.message); } else { + var inventoryResult = processInventoryResult(result.data.assets); + purchasesModel.clear(); - purchasesModel.append(result.data.assets); + purchasesModel.append(inventoryResult); if (previousPurchasesModel.count !== 0) { checkIfAnyItemStatusChanged(); @@ -93,7 +95,7 @@ Rectangle { purchasesModel.setProperty(i, "statusChanged", false); } } - previousPurchasesModel.append(result.data.assets); + previousPurchasesModel.append(inventoryResult); buildFilteredPurchasesModel(); @@ -590,6 +592,17 @@ Rectangle { // FUNCTION DEFINITIONS START // + function processInventoryResult(inventory) { + for (var i = 0; i < inventory.length; i++) { + if (inventory[i].status.length > 1) { + console.log("WARNING: Inventory result index " + i + " has a status of length >1!") + } + inventory[i].status = inventory[i].status[0]; + inventory[i].categories = inventory[i].categories.join(';'); + } + return inventory; + } + function populateDisplayedItemCounts() { var itemCountDictionary = {}; var currentItemId; From b3c3b2d34fbab47b720c8a6b1876568433ed8097 Mon Sep 17 00:00:00 2001 From: Cain Kilgore Date: Thu, 5 Oct 2017 19:42:31 +0100 Subject: [PATCH 694/722] Switched Time & Session ID Over --- libraries/shared/src/shared/FileLogger.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/shared/src/shared/FileLogger.cpp b/libraries/shared/src/shared/FileLogger.cpp index b019b69fb8..8ceb378574 100644 --- a/libraries/shared/src/shared/FileLogger.cpp +++ b/libraries/shared/src/shared/FileLogger.cpp @@ -41,7 +41,7 @@ private: -static const QString FILENAME_FORMAT = "hifi-log%1_%2.txt"; +static const QString FILENAME_FORMAT = "hifi-log_%1%2.txt"; static const QString DATETIME_FORMAT = "yyyy-MM-dd_hh.mm.ss"; static const QString LOGS_DIRECTORY = "Logs"; static const QString IPADDR_WILDCARD = "[0-9]*.[0-9]*.[0-9]*.[0-9]*"; @@ -69,7 +69,7 @@ QString getLogRollerFilename() { fileSessionID = "_" + SESSION_ID.toString().replace("{", "").replace("}", ""); } - result.append(QString(FILENAME_FORMAT).arg(fileSessionID, now.toString(DATETIME_FORMAT))); + result.append(QString(FILENAME_FORMAT).arg(now.toString(DATETIME_FORMAT), fileSessionID)); return result; } From 53a49272dc0e5f99ac8ea5dd8a67bb4b17ba4c63 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 5 Oct 2017 11:43:19 -0700 Subject: [PATCH 695/722] fix importing of avatar-entities --- interface/src/avatar/MyAvatar.cpp | 6 ++++++ interface/src/avatar/MyAvatar.h | 2 ++ .../entities/src/EntityEditPacketSender.cpp | 2 +- libraries/entities/src/EntityTree.cpp | 15 +++++++++------ libraries/shared/src/SpatiallyNestable.cpp | 18 ++++++++++++++---- .../libraries/controllerDispatcherUtils.js | 2 +- 6 files changed, 33 insertions(+), 12 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 10e2202553..5d82405aee 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3238,3 +3238,9 @@ void MyAvatar::setModelScale(float scale) { emit sensorToWorldScaleChanged(sensorToWorldScale); } } + +SpatialParentTree* MyAvatar::getParentTree() const { + auto entityTreeRenderer = qApp->getEntities(); + EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; + return entityTree.get(); +} diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 9620d61a49..952315a85f 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -544,6 +544,8 @@ public: float getUserHeight() const; float getUserEyeHeight() const; + virtual SpatialParentTree* getParentTree() const override; + public slots: void increaseSize(); void decreaseSize(); diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index ee0fcf8218..f93b6a49ec 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -45,7 +45,7 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type, } EntityItemPointer entity = entityTree->findEntityByEntityItemID(entityItemID); if (!entity) { - qCDebug(entities) << "EntityEditPacketSender::queueEditEntityMessage can't find entity."; + qCDebug(entities) << "EntityEditPacketSender::queueEditAvatarEntityMessage can't find entity: " << entityItemID; return; } diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index e0187cd2b6..ef1d27640c 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1842,7 +1842,13 @@ bool EntityTree::sendEntitiesOperation(const OctreeElementPointer& element, void QHash::iterator iter = args->map->find(oldID); if (iter == args->map->end()) { - EntityItemID newID = QUuid::createUuid(); + EntityItemID newID; + if (oldID == AVATAR_SELF_ID) { + auto nodeList = DependencyManager::get(); + newID = EntityItemID(nodeList->getSessionUUID()); + } else { + newID = QUuid::createUuid(); + } args->map->insert(oldID, newID); return newID; } @@ -1859,8 +1865,8 @@ bool EntityTree::sendEntitiesOperation(const OctreeElementPointer& element, void properties.setPosition(properties.getPosition() + args->root); } else { EntityItemPointer parentEntity = args->ourTree->findEntityByEntityItemID(oldParentID); - if (parentEntity) { // map the parent - properties.setParentID(getMapped(parentEntity->getID())); + if (parentEntity || oldParentID == AVATAR_SELF_ID) { // map the parent + properties.setParentID(getMapped(oldParentID)); // But do not add root offset in this case. } else { // Should not happen, but let's try to be helpful... item->globalizeProperties(properties, "Cannot find %3 parent of %2 %1", args->root); @@ -1940,9 +1946,6 @@ bool EntityTree::readFromMap(QVariantMap& map) { if (properties.getClientOnly()) { properties.setOwningAvatarID(myNodeID); } - if (properties.getParentID() == AVATAR_SELF_ID) { - properties.setParentID(myNodeID); - } EntityItemPointer entity = addEntity(entityItemID, properties); if (!entity) { diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 8ea38f5f13..8c43632456 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -102,8 +102,11 @@ SpatiallyNestablePointer SpatiallyNestable::getParentPointer(bool& success) cons if (parent && parent->getID() == parentID) { // parent pointer is up-to-date if (!_parentKnowsMe) { - parent->beParentOfChild(getThisPointer()); - _parentKnowsMe = true; + SpatialParentTree* parentTree = parent->getParentTree(); + if (!parentTree || parentTree == getParentTree()) { + parent->beParentOfChild(getThisPointer()); + _parentKnowsMe = true; + } } success = true; return parent; @@ -129,8 +132,15 @@ SpatiallyNestablePointer SpatiallyNestable::getParentPointer(bool& success) cons parent = _parent.lock(); if (parent) { - parent->beParentOfChild(getThisPointer()); - _parentKnowsMe = true; + + // it's possible for an entity with a parent of AVATAR_SELF_ID can be imported into a side-tree + // such as the clipboard's. if this is the case, we don't want the parent to consider this a + // child. + SpatialParentTree* parentTree = parent->getParentTree(); + if (!parentTree || parentTree == getParentTree()) { + parent->beParentOfChild(getThisPointer()); + _parentKnowsMe = true; + } } success = (parent || parentID.isNull()); diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index e9e25b058b..d6d80541ee 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -222,7 +222,7 @@ getControllerJointIndex = function (hand) { return controllerJointIndex; } - return MyAvatar.getJointIndex("Head"); + return -1; }; propsArePhysical = function (props) { From a6b7578c3c923578aa60133b4a1695320edce081 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 5 Oct 2017 11:53:16 -0700 Subject: [PATCH 696/722] start fixing asynch issue, fixes model loading! --- .../src/RenderableEntityItem.cpp | 36 +++---- .../src/RenderableEntityItem.h | 4 +- .../src/RenderableModelEntityItem.cpp | 2 - .../src/RenderableShapeEntityItem.cpp | 53 +++++----- .../src/RenderableWebEntityItem.cpp | 98 +++++++++---------- .../src/RenderableWebEntityItem.h | 1 - libraries/render-utils/src/Model.cpp | 7 +- libraries/render-utils/src/Model.h | 2 +- 8 files changed, 102 insertions(+), 101 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index 3f1e89b86c..c38d106bfa 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -329,25 +329,27 @@ bool EntityRenderer::needsRenderUpdateFromEntity(const EntityItemPointer& entity return false; } -void EntityRenderer::doRenderUpdateAsynchronous(const EntityItemPointer& entity) { - auto transparent = isTransparent(); - if (_prevIsTransparent && !transparent) { - _isFading = false; - } - _prevIsTransparent = transparent; +void EntityRenderer::doRenderUpdateSynchronous(const ScenePointer& scene, Transaction& transaction, const EntityItemPointer& entity) { + withWriteLock([&] { + auto transparent = isTransparent(); + if (_prevIsTransparent && !transparent) { + _isFading = false; + } + _prevIsTransparent = transparent; - bool success = false; - auto bound = entity->getAABox(success); - if (success) { - _bound = bound; - } - auto newModelTransform = entity->getTransformToCenter(success); - if (success) { - _modelTransform = newModelTransform; - } + bool success = false; + auto bound = entity->getAABox(success); + if (success) { + _bound = bound; + } + auto newModelTransform = entity->getTransformToCenter(success); + if (success) { + _modelTransform = newModelTransform; + } - _moving = entity->isMovingRelativeToParent(); - _visible = entity->getVisible(); + _moving = entity->isMovingRelativeToParent(); + _visible = entity->getVisible(); + }); } void EntityRenderer::onAddToScene(const EntityItemPointer& entity) { diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 6b47ff8b1d..6581e923bd 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -73,12 +73,12 @@ protected: // Will be called on the main thread from updateInScene. This can be used to fetch things like // network textures or model geometry from resource caches - virtual void doRenderUpdateSynchronous(const ScenePointer& scene, Transaction& transaction, const EntityItemPointer& entity) { } + virtual void doRenderUpdateSynchronous(const ScenePointer& scene, Transaction& transaction, const EntityItemPointer& entity); // Will be called by the lambda posted to the scene in updateInScene. // This function will execute on the rendering thread, so you cannot use network caches to fetch // data in this method if using multi-threaded rendering - virtual void doRenderUpdateAsynchronous(const EntityItemPointer& entity); + virtual void doRenderUpdateAsynchronous(const EntityItemPointer& entity) { } // Called by the `render` method after `needsRenderUpdate` virtual void doRender(RenderArgs* args) = 0; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 11c97f6716..064eacdb35 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1145,7 +1145,6 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce model->removeFromScene(scene, transaction); withWriteLock([&] { _model.reset(); }); } - emit requestRenderUpdate(); return; } @@ -1169,7 +1168,6 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce // Nothing else to do unless the model is loaded if (!model->isLoaded()) { - emit needsRenderUpdate(); return; } diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 8fcf2c090d..4028f105c8 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -88,31 +88,33 @@ void ShapeEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce } _color = vec4(toGlm(entity->getXColor()), entity->getLocalRenderAlpha()); + + _shape = entity->getShape(); + _position = entity->getPosition(); + _dimensions = entity->getDimensions(); + _orientation = entity->getOrientation(); + + if (_shape == entity::Sphere) { + _modelTransform.postScale(SPHERE_ENTITY_SCALE); + } + + _modelTransform.postScale(_dimensions); }); } void ShapeEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { - if (_procedural.isEnabled() && _procedural.isFading()) { - float isFading = Interpolate::calculateFadeRatio(_procedural.getFadeStartTime()) < 1.0f; - _procedural.setIsFading(isFading); - } - - _shape = entity->getShape(); - _position = entity->getPosition(); - _dimensions = entity->getDimensions(); - _orientation = entity->getOrientation(); - - if (_shape == entity::Sphere) { - _modelTransform.postScale(SPHERE_ENTITY_SCALE); - } - - _modelTransform.postScale(_dimensions); + withReadLock([&] { + if (_procedural.isEnabled() && _procedural.isFading()) { + float isFading = Interpolate::calculateFadeRatio(_procedural.getFadeStartTime()) < 1.0f; + _procedural.setIsFading(isFading); + } + }); } bool ShapeEntityRenderer::isTransparent() const { if (_procedural.isEnabled() && _procedural.isFading()) { return Interpolate::calculateFadeRatio(_procedural.getFadeStartTime()) < 1.0f; - } + } // return _entity->getLocalRenderAlpha() < 1.0f || Parent::isTransparent(); return Parent::isTransparent(); @@ -126,15 +128,16 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { gpu::Batch& batch = *args->_batch; - auto geometryShape = MAPPING[_shape]; - batch.setModelTransform(_modelTransform); // use a transform with scale, rotation, registration point and translation - + GeometryCache::Shape geometryShape; bool proceduralRender = false; - glm::vec4 outColor = _color; + glm::vec4 outColor; withReadLock([&] { + geometryShape = MAPPING[_shape]; + batch.setModelTransform(_modelTransform); // use a transform with scale, rotation, registration point and translation + outColor = _color; if (_procedural.isReady()) { _procedural.prepare(batch, _position, _dimensions, _orientation); - auto outColor = _procedural.getColor(_color); + outColor = _procedural.getColor(_color); outColor.a *= _procedural.isFading() ? Interpolate::calculateFadeRatio(_procedural.getFadeStartTime()) : 1.0f; proceduralRender = true; } @@ -149,13 +152,13 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { } } else { // FIXME, support instanced multi-shape rendering using multidraw indirect - _color.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; + outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; auto geometryCache = DependencyManager::get(); - auto pipeline = _color.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); + auto pipeline = outColor.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { - geometryCache->renderWireShapeInstance(args, batch, geometryShape, _color, pipeline); + geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline); } else { - geometryCache->renderSolidShapeInstance(args, batch, geometryShape, _color, pipeline); + geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline); } } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 0561fa5130..4688ef5d2b 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -118,31 +118,30 @@ void WebEntityRenderer::onTimeout() { } void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { - // This work must be done on the main thread - if (!hasWebSurface()) { - buildWebSurface(entity); - } + withWriteLock([&] { + // This work must be done on the main thread + if (!hasWebSurface()) { + buildWebSurface(entity); + } - if (_contextPosition != entity->getPosition()) { - // update globalPosition - _contextPosition = entity->getPosition(); - _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(_contextPosition)); - } + if (_contextPosition != entity->getPosition()) { + // update globalPosition + _contextPosition = entity->getPosition(); + _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(_contextPosition)); + } - if (_lastSourceUrl != entity->getSourceUrl()) { - _lastSourceUrl = entity->getSourceUrl(); - loadSourceURL(); - } + if (_lastSourceUrl != entity->getSourceUrl()) { + _lastSourceUrl = entity->getSourceUrl(); + loadSourceURL(); + } - _lastDPI = entity->getDPI(); + _lastDPI = entity->getDPI(); - glm::vec2 windowSize = getWindowSize(entity); - _webSurface->resize(QSize(windowSize.x, windowSize.y)); -} + glm::vec2 windowSize = getWindowSize(entity); + _webSurface->resize(QSize(windowSize.x, windowSize.y)); -void WebEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { - Parent::doRenderUpdateAsynchronousTyped(entity); - _modelTransform.postScale(entity->getDimensions()); + _modelTransform.postScale(entity->getDimensions()); + }); } void WebEntityRenderer::doRender(RenderArgs* args) { @@ -180,7 +179,9 @@ void WebEntityRenderer::doRender(RenderArgs* args) { static const glm::vec2 texMin(0.0f), texMax(1.0f), topLeft(-0.5f), bottomRight(0.5f); gpu::Batch& batch = *args->_batch; - batch.setModelTransform(_modelTransform); + withReadLock([&] { + batch.setModelTransform(_modelTransform); + }); batch.setResourceTexture(0, _texture); float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; batch._glColor4f(1.0f, 1.0f, 1.0f, fadeRatio); @@ -190,7 +191,7 @@ void WebEntityRenderer::doRender(RenderArgs* args) { } bool WebEntityRenderer::hasWebSurface() { - return resultWithReadLock([&] { return (bool)_webSurface; }); + return (bool)_webSurface; } bool WebEntityRenderer::buildWebSurface(const TypedEntityPointer& entity) { @@ -213,11 +214,8 @@ bool WebEntityRenderer::buildWebSurface(const TypedEntityPointer& entity) { }; { - QSharedPointer webSurface = QSharedPointer(new OffscreenQmlSurface(), deleter); - webSurface->create(); - withWriteLock([&] { - _webSurface = webSurface; - }); + _webSurface = QSharedPointer(new OffscreenQmlSurface(), deleter); + _webSurface->create(); } // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces @@ -322,33 +320,31 @@ glm::vec2 WebEntityRenderer::getWindowSize(const TypedEntityPointer& entity) con } void WebEntityRenderer::loadSourceURL() { - withWriteLock([&] { - const QUrl sourceUrl(_lastSourceUrl); - if (sourceUrl.scheme() == "http" || sourceUrl.scheme() == "https" || - _lastSourceUrl.toLower().endsWith(".htm") || _lastSourceUrl.toLower().endsWith(".html")) { - _contentType = htmlContent; - _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "qml/controls/")); + const QUrl sourceUrl(_lastSourceUrl); + if (sourceUrl.scheme() == "http" || sourceUrl.scheme() == "https" || + _lastSourceUrl.toLower().endsWith(".htm") || _lastSourceUrl.toLower().endsWith(".html")) { + _contentType = htmlContent; + _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "qml/controls/")); - // We special case YouTube URLs since we know they are videos that we should play with at least 30 FPS. - if (sourceUrl.host().endsWith("youtube.com", Qt::CaseInsensitive)) { - _webSurface->setMaxFps(YOUTUBE_MAX_FPS); - } else { - _webSurface->setMaxFps(DEFAULT_MAX_FPS); - } - - _webSurface->load("WebEntityView.qml", [this](QQmlContext* context, QObject* item) { - item->setProperty("url", _lastSourceUrl); - }); + // We special case YouTube URLs since we know they are videos that we should play with at least 30 FPS. + if (sourceUrl.host().endsWith("youtube.com", Qt::CaseInsensitive)) { + _webSurface->setMaxFps(YOUTUBE_MAX_FPS); } else { - _contentType = qmlContent; - _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath())); - _webSurface->load(_lastSourceUrl); - if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { - auto tabletScriptingInterface = DependencyManager::get(); - tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface.data()); - } + _webSurface->setMaxFps(DEFAULT_MAX_FPS); } - }); + + _webSurface->load("WebEntityView.qml", [this](QQmlContext* context, QObject* item) { + item->setProperty("url", _lastSourceUrl); + }); + } else { + _contentType = qmlContent; + _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath())); + _webSurface->load(_lastSourceUrl); + if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { + auto tabletScriptingInterface = DependencyManager::get(); + tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface.data()); + } + } } void WebEntityRenderer::handlePointerEvent(const TypedEntityPointer& entity, const PointerEvent& event) { diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index a67eb39670..4b7e7e25a1 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -29,7 +29,6 @@ protected: virtual bool needsRenderUpdate() const override; virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; - virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override; virtual void doRender(RenderArgs* args) override; virtual bool isTransparent() const override; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 837f485417..b6b85783eb 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -267,6 +267,11 @@ void Model::updateRenderItems() { }); } +void Model::setRenderItemsNeedUpdate() { + _renderItemsNeedUpdate = true; + emit requestRenderUpdate(); +} + void Model::initJointTransforms() { if (isLoaded()) { glm::mat4 modelOffset = glm::scale(_scale) * glm::translate(_offset); @@ -814,13 +819,11 @@ void Model::setTextures(const QVariantMap& textures) { _needsUpdateTextures = true; _needsFixupInScene = true; _renderGeometry->setTextures(textures); - emit requestRenderUpdate(); } else { // FIXME(Huffman): Disconnect previously connected lambdas so we don't set textures multiple // after the geometry has finished loading. connect(&_renderWatcher, &GeometryResourceWatcher::finished, this, [this, textures]() { _renderGeometry->setTextures(textures); - emit requestRenderUpdate(); }); } } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 95dc171ff5..a742b46d6a 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -103,7 +103,7 @@ public: bool isLayeredInFront() const { return _isLayeredInFront; } virtual void updateRenderItems(); - void setRenderItemsNeedUpdate() { _renderItemsNeedUpdate = true; emit requestRenderUpdate(); } + void setRenderItemsNeedUpdate(); bool getRenderItemsNeedUpdate() { return _renderItemsNeedUpdate; } AABox getRenderableMeshBound() const; const render::ItemIDs& fetchRenderItemIDs() const; From 8e8209513502e97c83e8716c0c156300c00ad23b Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 5 Oct 2017 12:11:31 -0700 Subject: [PATCH 697/722] Number sold and limited run --- .../resources/qml/hifi/commerce/purchases/PurchasedItem.qml | 4 ++++ interface/resources/qml/hifi/commerce/purchases/Purchases.qml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index a026a818c0..1e26806b30 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -36,6 +36,8 @@ Item { property string itemHref; property int displayedItemCount; property int itemEdition; + property int numberSold; + property int limitedRun; property string originalStatusText; property string originalStatusColor; @@ -222,6 +224,8 @@ Item { "PENDING..." } else if (root.purchaseStatus === "invalidated") { "INVALIDATED" + } else if (root.numberSold !== -1) { + ("Sales: " + root.numberSold + "/" + (root.limitedRun === -1 ? "INFTY" : root.limitedRun)) } else { "" } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 1f7f2e6e53..990fd348c6 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -430,6 +430,8 @@ Rectangle { purchaseStatus: status; purchaseStatusChanged: statusChanged; itemEdition: model.edition_number; + numberSold: model.number_sold; + limitedRun: model.limited_run; displayedItemCount: model.displayedItemCount; anchors.topMargin: 12; anchors.bottomMargin: 12; From 6b02cbb9112923e90fd92baa91304f1bbd3ebe93 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 5 Oct 2017 13:17:57 -0700 Subject: [PATCH 698/722] move locking inside laserpointer and raypick --- interface/src/raypick/LaserPointer.cpp | 56 +++++++++++++++++++ interface/src/raypick/LaserPointer.h | 20 +++---- interface/src/raypick/LaserPointerManager.cpp | 16 ------ interface/src/raypick/RayPick.cpp | 44 +++++++++++++++ interface/src/raypick/RayPick.h | 18 +++--- interface/src/raypick/RayPickManager.cpp | 10 ---- 6 files changed, 119 insertions(+), 45 deletions(-) diff --git a/interface/src/raypick/LaserPointer.cpp b/interface/src/raypick/LaserPointer.cpp index 55ddd01123..0e0f13cd6c 100644 --- a/interface/src/raypick/LaserPointer.cpp +++ b/interface/src/raypick/LaserPointer.cpp @@ -49,11 +49,13 @@ LaserPointer::~LaserPointer() { } void LaserPointer::enable() { + QWriteLocker lock(getLock()); DependencyManager::get()->enableRayPick(_rayPickUID); _renderingEnabled = true; } void LaserPointer::disable() { + QWriteLocker lock(getLock()); DependencyManager::get()->disableRayPick(_rayPickUID); _renderingEnabled = false; if (!_currentRenderState.empty()) { @@ -67,6 +69,7 @@ void LaserPointer::disable() { } void LaserPointer::setRenderState(const std::string& state) { + QWriteLocker lock(getLock()); if (!_currentRenderState.empty() && state != _currentRenderState) { if (_renderStates.find(_currentRenderState) != _renderStates.end()) { disableRenderState(_renderStates[_currentRenderState]); @@ -79,6 +82,7 @@ void LaserPointer::setRenderState(const std::string& state) { } void LaserPointer::editRenderState(const std::string& state, const QVariant& startProps, const QVariant& pathProps, const QVariant& endProps) { + QWriteLocker lock(getLock()); updateRenderStateOverlay(_renderStates[state].getStartID(), startProps); updateRenderStateOverlay(_renderStates[state].getPathID(), pathProps); updateRenderStateOverlay(_renderStates[state].getEndID(), endProps); @@ -92,6 +96,11 @@ void LaserPointer::updateRenderStateOverlay(const OverlayID& id, const QVariant& } } +const RayPickResult LaserPointer::getPrevRayPickResult() { + QReadLocker lock(getLock()); + return DependencyManager::get()->getPrevRayPickResult(_rayPickUID); +} + void LaserPointer::updateRenderState(const RenderState& renderState, const IntersectionType type, const float distance, const QUuid& objectID, const PickRay& pickRay, const bool defaultState) { if (!renderState.getStartID().isNull()) { QVariantMap startProps; @@ -183,6 +192,8 @@ void LaserPointer::disableRenderState(const RenderState& renderState) { } void LaserPointer::update() { + // This only needs to be a read lock because update won't change any of the properties that can be modified from scripts + QReadLocker lock(getLock()); RayPickResult prevRayPickResult = DependencyManager::get()->getPrevRayPickResult(_rayPickUID); if (_renderingEnabled && !_currentRenderState.empty() && _renderStates.find(_currentRenderState) != _renderStates.end() && (prevRayPickResult.type != IntersectionType::NONE || _laserLength > 0.0f || !_objectLockEnd.first.isNull())) { @@ -198,6 +209,51 @@ void LaserPointer::update() { } } +void LaserPointer::setPrecisionPicking(const bool precisionPicking) { + QWriteLocker lock(getLock()); + DependencyManager::get()->setPrecisionPicking(_rayPickUID, precisionPicking); +} + +void LaserPointer::setLaserLength(const float laserLength) { + QWriteLocker lock(getLock()); + _laserLength = laserLength; +} + +void LaserPointer::setLockEndUUID(QUuid objectID, const bool isOverlay) { + QWriteLocker lock(getLock()); + _objectLockEnd = std::pair(objectID, isOverlay); +} + +void LaserPointer::setIgnoreEntities(const QScriptValue& ignoreEntities) { + QWriteLocker lock(getLock()); + DependencyManager::get()->setIgnoreEntities(_rayPickUID, ignoreEntities); +} + +void LaserPointer::setIncludeEntities(const QScriptValue& includeEntities) { + QWriteLocker lock(getLock()); + DependencyManager::get()->setIncludeEntities(_rayPickUID, includeEntities); +} + +void LaserPointer::setIgnoreOverlays(const QScriptValue& ignoreOverlays) { + QWriteLocker lock(getLock()); + DependencyManager::get()->setIgnoreOverlays(_rayPickUID, ignoreOverlays); +} + +void LaserPointer::setIncludeOverlays(const QScriptValue& includeOverlays) { + QWriteLocker lock(getLock()); + DependencyManager::get()->setIncludeOverlays(_rayPickUID, includeOverlays); +} + +void LaserPointer::setIgnoreAvatars(const QScriptValue& ignoreAvatars) { + QWriteLocker lock(getLock()); + DependencyManager::get()->setIgnoreAvatars(_rayPickUID, ignoreAvatars); +} + +void LaserPointer::setIncludeAvatars(const QScriptValue& includeAvatars) { + QWriteLocker lock(getLock()); + DependencyManager::get()->setIncludeAvatars(_rayPickUID, includeAvatars); +} + RenderState::RenderState(const OverlayID& startID, const OverlayID& pathID, const OverlayID& endID) : _startID(startID), _pathID(pathID), _endID(endID) { diff --git a/interface/src/raypick/LaserPointer.h b/interface/src/raypick/LaserPointer.h index fa7d396ae8..01dfe01cfd 100644 --- a/interface/src/raypick/LaserPointer.h +++ b/interface/src/raypick/LaserPointer.h @@ -58,22 +58,22 @@ public: QUuid getRayUID() { return _rayPickUID; } void enable(); void disable(); - const RayPickResult getPrevRayPickResult() { return DependencyManager::get()->getPrevRayPickResult(_rayPickUID); } + const RayPickResult getPrevRayPickResult(); void setRenderState(const std::string& state); // You cannot use editRenderState to change the overlay type of any part of the laser pointer. You can only edit the properties of the existing overlays. void editRenderState(const std::string& state, const QVariant& startProps, const QVariant& pathProps, const QVariant& endProps); - void setPrecisionPicking(const bool precisionPicking) { DependencyManager::get()->setPrecisionPicking(_rayPickUID, precisionPicking); } - void setLaserLength(const float laserLength) { _laserLength = laserLength; } - void setIgnoreEntities(const QScriptValue& ignoreEntities) { DependencyManager::get()->setIgnoreEntities(_rayPickUID, ignoreEntities); } - void setIncludeEntities(const QScriptValue& includeEntities) { DependencyManager::get()->setIncludeEntities(_rayPickUID, includeEntities); } - void setIgnoreOverlays(const QScriptValue& ignoreOverlays) { DependencyManager::get()->setIgnoreOverlays(_rayPickUID, ignoreOverlays); } - void setIncludeOverlays(const QScriptValue& includeOverlays) { DependencyManager::get()->setIncludeOverlays(_rayPickUID, includeOverlays); } - void setIgnoreAvatars(const QScriptValue& ignoreAvatars) { DependencyManager::get()->setIgnoreAvatars(_rayPickUID, ignoreAvatars); } - void setIncludeAvatars(const QScriptValue& includeAvatars) { DependencyManager::get()->setIncludeAvatars(_rayPickUID, includeAvatars); } + void setPrecisionPicking(const bool precisionPicking); + void setLaserLength(const float laserLength); + void setLockEndUUID(QUuid objectID, const bool isOverlay); - void setLockEndUUID(QUuid objectID, const bool isOverlay) { _objectLockEnd = std::pair(objectID, isOverlay); } + void setIgnoreEntities(const QScriptValue& ignoreEntities); + void setIncludeEntities(const QScriptValue& includeEntities); + void setIgnoreOverlays(const QScriptValue& ignoreOverlays); + void setIncludeOverlays(const QScriptValue& includeOverlays); + void setIgnoreAvatars(const QScriptValue& ignoreAvatars); + void setIncludeAvatars(const QScriptValue& includeAvatars); QReadWriteLock* getLock() { return &_lock; } diff --git a/interface/src/raypick/LaserPointerManager.cpp b/interface/src/raypick/LaserPointerManager.cpp index 387f88724e..8615a96c3f 100644 --- a/interface/src/raypick/LaserPointerManager.cpp +++ b/interface/src/raypick/LaserPointerManager.cpp @@ -31,7 +31,6 @@ void LaserPointerManager::enableLaserPointer(const QUuid uid) { QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QWriteLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->enable(); } } @@ -40,7 +39,6 @@ void LaserPointerManager::disableLaserPointer(const QUuid uid) { QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QWriteLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->disable(); } } @@ -49,7 +47,6 @@ void LaserPointerManager::setRenderState(QUuid uid, const std::string& renderSta QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QWriteLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->setRenderState(renderState); } } @@ -58,7 +55,6 @@ void LaserPointerManager::editRenderState(QUuid uid, const std::string& state, c QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QWriteLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->editRenderState(state, startProps, pathProps, endProps); } } @@ -67,7 +63,6 @@ const RayPickResult LaserPointerManager::getPrevRayPickResult(const QUuid uid) { QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QReadLocker laserLock(laserPointer.value()->getLock()); return laserPointer.value()->getPrevRayPickResult(); } return RayPickResult(); @@ -76,9 +71,7 @@ const RayPickResult LaserPointerManager::getPrevRayPickResult(const QUuid uid) { void LaserPointerManager::update() { QReadLocker lock(&_containsLock); for (QUuid& uid : _laserPointers.keys()) { - // This only needs to be a read lock because update won't change any of the properties that can be modified from scripts auto laserPointer = _laserPointers.find(uid); - QReadLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->update(); } } @@ -87,7 +80,6 @@ void LaserPointerManager::setPrecisionPicking(QUuid uid, const bool precisionPic QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QWriteLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->setPrecisionPicking(precisionPicking); } } @@ -96,7 +88,6 @@ void LaserPointerManager::setLaserLength(QUuid uid, const float laserLength) { QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QWriteLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->setLaserLength(laserLength); } } @@ -105,7 +96,6 @@ void LaserPointerManager::setIgnoreEntities(QUuid uid, const QScriptValue& ignor QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QWriteLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->setIgnoreEntities(ignoreEntities); } } @@ -114,7 +104,6 @@ void LaserPointerManager::setIncludeEntities(QUuid uid, const QScriptValue& incl QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QWriteLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->setIncludeEntities(includeEntities); } } @@ -123,7 +112,6 @@ void LaserPointerManager::setIgnoreOverlays(QUuid uid, const QScriptValue& ignor QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QWriteLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->setIgnoreOverlays(ignoreOverlays); } } @@ -132,7 +120,6 @@ void LaserPointerManager::setIncludeOverlays(QUuid uid, const QScriptValue& incl QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QWriteLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->setIncludeOverlays(includeOverlays); } } @@ -141,7 +128,6 @@ void LaserPointerManager::setIgnoreAvatars(QUuid uid, const QScriptValue& ignore QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QWriteLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->setIgnoreAvatars(ignoreAvatars); } } @@ -150,7 +136,6 @@ void LaserPointerManager::setIncludeAvatars(QUuid uid, const QScriptValue& inclu QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QWriteLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->setIncludeAvatars(includeAvatars); } } @@ -159,7 +144,6 @@ void LaserPointerManager::setLockEndUUID(QUuid uid, QUuid objectID, const bool i QReadLocker lock(&_containsLock); auto laserPointer = _laserPointers.find(uid); if (laserPointer != _laserPointers.end()) { - QWriteLocker laserLock(laserPointer.value()->getLock()); laserPointer.value()->setLockEndUUID(objectID, isOverlay); } } diff --git a/interface/src/raypick/RayPick.cpp b/interface/src/raypick/RayPick.cpp index 70170a8f85..a5b1299210 100644 --- a/interface/src/raypick/RayPick.cpp +++ b/interface/src/raypick/RayPick.cpp @@ -16,3 +16,47 @@ RayPick::RayPick(const RayPickFilter& filter, const float maxDistance, const boo _enabled(enabled) { } +void RayPick::enable() { + QWriteLocker lock(getLock()); + _enabled = true; +} + +void RayPick::disable() { + QWriteLocker lock(getLock()); + _enabled = false; +} + +const RayPickResult& RayPick::getPrevRayPickResult() { + QReadLocker lock(getLock()); + return _prevResult; +} + +void RayPick::setIgnoreEntities(const QScriptValue& ignoreEntities) { + QWriteLocker lock(getLock()); + _ignoreEntities = qVectorEntityItemIDFromScriptValue(ignoreEntities); +} + +void RayPick::setIncludeEntities(const QScriptValue& includeEntities) { + QWriteLocker lock(getLock()); + _includeEntities = qVectorEntityItemIDFromScriptValue(includeEntities); +} + +void RayPick::setIgnoreOverlays(const QScriptValue& ignoreOverlays) { + QWriteLocker lock(getLock()); + _ignoreOverlays = qVectorOverlayIDFromScriptValue(ignoreOverlays); +} + +void RayPick::setIncludeOverlays(const QScriptValue& includeOverlays) { + QWriteLocker lock(getLock()); + _includeOverlays = qVectorOverlayIDFromScriptValue(includeOverlays); +} + +void RayPick::setIgnoreAvatars(const QScriptValue& ignoreAvatars) { + QWriteLocker lock(getLock()); + _ignoreAvatars = qVectorEntityItemIDFromScriptValue(ignoreAvatars); +} + +void RayPick::setIncludeAvatars(const QScriptValue& includeAvatars) { + QWriteLocker lock(getLock()); + _includeAvatars = qVectorEntityItemIDFromScriptValue(includeAvatars); +} \ No newline at end of file diff --git a/interface/src/raypick/RayPick.h b/interface/src/raypick/RayPick.h index 428e44d670..6dacc084b4 100644 --- a/interface/src/raypick/RayPick.h +++ b/interface/src/raypick/RayPick.h @@ -103,13 +103,13 @@ public: virtual const PickRay getPickRay(bool& valid) const = 0; - void enable() { _enabled = true; } - void disable() { _enabled = false; } + void enable(); + void disable(); const RayPickFilter& getFilter() { return _filter; } float getMaxDistance() { return _maxDistance; } bool isEnabled() { return _enabled; } - const RayPickResult& getPrevRayPickResult() { return _prevResult; } + const RayPickResult& getPrevRayPickResult(); void setPrecisionPicking(bool precisionPicking) { _filter.setFlag(RayPickFilter::PICK_COURSE, !precisionPicking); } @@ -121,12 +121,12 @@ public: const QVector& getIncludeOverlays() { return _includeOverlays; } const QVector& getIgnoreAvatars() { return _ignoreAvatars; } const QVector& getIncludeAvatars() { return _includeAvatars; } - void setIgnoreEntities(const QScriptValue& ignoreEntities) { _ignoreEntities = qVectorEntityItemIDFromScriptValue(ignoreEntities); } - void setIncludeEntities(const QScriptValue& includeEntities) { _includeEntities = qVectorEntityItemIDFromScriptValue(includeEntities); } - void setIgnoreOverlays(const QScriptValue& ignoreOverlays) { _ignoreOverlays = qVectorOverlayIDFromScriptValue(ignoreOverlays); } - void setIncludeOverlays(const QScriptValue& includeOverlays) { _includeOverlays = qVectorOverlayIDFromScriptValue(includeOverlays); } - void setIgnoreAvatars(const QScriptValue& ignoreAvatars) { _ignoreAvatars = qVectorEntityItemIDFromScriptValue(ignoreAvatars); } - void setIncludeAvatars(const QScriptValue& includeAvatars) { _includeAvatars = qVectorEntityItemIDFromScriptValue(includeAvatars); } + void setIgnoreEntities(const QScriptValue& ignoreEntities); + void setIncludeEntities(const QScriptValue& includeEntities); + void setIgnoreOverlays(const QScriptValue& ignoreOverlays); + void setIncludeOverlays(const QScriptValue& includeOverlays); + void setIgnoreAvatars(const QScriptValue& ignoreAvatars); + void setIncludeAvatars(const QScriptValue& includeAvatars); QReadWriteLock* getLock() { return &_lock; } diff --git a/interface/src/raypick/RayPickManager.cpp b/interface/src/raypick/RayPickManager.cpp index 65f82dcd5f..1728ecd01a 100644 --- a/interface/src/raypick/RayPickManager.cpp +++ b/interface/src/raypick/RayPickManager.cpp @@ -153,7 +153,6 @@ void RayPickManager::enableRayPick(const QUuid uid) { QReadLocker containsLock(&_containsLock); auto rayPick = _rayPicks.find(uid); if (rayPick != _rayPicks.end()) { - QWriteLocker rayPickLock(rayPick.value()->getLock()); rayPick.value()->enable(); } } @@ -162,7 +161,6 @@ void RayPickManager::disableRayPick(const QUuid uid) { QReadLocker containsLock(&_containsLock); auto rayPick = _rayPicks.find(uid); if (rayPick != _rayPicks.end()) { - QWriteLocker rayPickLock(rayPick.value()->getLock()); rayPick.value()->disable(); } } @@ -171,7 +169,6 @@ const RayPickResult RayPickManager::getPrevRayPickResult(const QUuid uid) { QReadLocker containsLock(&_containsLock); auto rayPick = _rayPicks.find(uid); if (rayPick != _rayPicks.end()) { - QReadLocker lock(rayPick.value()->getLock()); return rayPick.value()->getPrevRayPickResult(); } return RayPickResult(); @@ -181,7 +178,6 @@ void RayPickManager::setPrecisionPicking(QUuid uid, const bool precisionPicking) QReadLocker containsLock(&_containsLock); auto rayPick = _rayPicks.find(uid); if (rayPick != _rayPicks.end()) { - QWriteLocker lock(rayPick.value()->getLock()); rayPick.value()->setPrecisionPicking(precisionPicking); } } @@ -190,7 +186,6 @@ void RayPickManager::setIgnoreEntities(QUuid uid, const QScriptValue& ignoreEnti QReadLocker containsLock(&_containsLock); auto rayPick = _rayPicks.find(uid); if (rayPick != _rayPicks.end()) { - QWriteLocker lock(rayPick.value()->getLock()); rayPick.value()->setIgnoreEntities(ignoreEntities); } } @@ -199,7 +194,6 @@ void RayPickManager::setIncludeEntities(QUuid uid, const QScriptValue& includeEn QReadLocker containsLock(&_containsLock); auto rayPick = _rayPicks.find(uid); if (rayPick != _rayPicks.end()) { - QWriteLocker lock(rayPick.value()->getLock()); rayPick.value()->setIncludeEntities(includeEntities); } } @@ -208,7 +202,6 @@ void RayPickManager::setIgnoreOverlays(QUuid uid, const QScriptValue& ignoreOver QReadLocker containsLock(&_containsLock); auto rayPick = _rayPicks.find(uid); if (rayPick != _rayPicks.end()) { - QWriteLocker lock(rayPick.value()->getLock()); rayPick.value()->setIgnoreOverlays(ignoreOverlays); } } @@ -217,7 +210,6 @@ void RayPickManager::setIncludeOverlays(QUuid uid, const QScriptValue& includeOv QReadLocker containsLock(&_containsLock); auto rayPick = _rayPicks.find(uid); if (rayPick != _rayPicks.end()) { - QWriteLocker lock(rayPick.value()->getLock()); rayPick.value()->setIncludeOverlays(includeOverlays); } } @@ -226,7 +218,6 @@ void RayPickManager::setIgnoreAvatars(QUuid uid, const QScriptValue& ignoreAvata QReadLocker containsLock(&_containsLock); auto rayPick = _rayPicks.find(uid); if (rayPick != _rayPicks.end()) { - QWriteLocker lock(rayPick.value()->getLock()); rayPick.value()->setIgnoreAvatars(ignoreAvatars); } } @@ -235,7 +226,6 @@ void RayPickManager::setIncludeAvatars(QUuid uid, const QScriptValue& includeAva QReadLocker containsLock(&_containsLock); auto rayPick = _rayPicks.find(uid); if (rayPick != _rayPicks.end()) { - QWriteLocker lock(rayPick.value()->getLock()); rayPick.value()->setIncludeAvatars(includeAvatars); } } \ No newline at end of file From 9290a516851f7e70a437468cc313752baa65e504 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 5 Oct 2017 14:03:56 -0700 Subject: [PATCH 699/722] Fix confirmed text --- .../resources/qml/hifi/commerce/purchases/PurchasedItem.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index 1e26806b30..5eb5516519 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -52,7 +52,6 @@ Item { statusText.text = "CONFIRMED!"; statusText.color = hifi.colors.blueAccent; confirmedTimer.start(); - root.purchaseStatusChanged = false; } } @@ -62,6 +61,7 @@ Item { onTriggered: { statusText.text = root.originalStatusText; statusText.color = root.originalStatusColor; + root.purchaseStatusChanged = false; } } @@ -205,7 +205,7 @@ Item { Item { id: statusContainer; - visible: root.purchaseStatus === "pending" || root.purchaseStatus === "invalidated"; + visible: root.purchaseStatus === "pending" || root.purchaseStatus === "invalidated" || root.purchaseStatusChanged; anchors.left: itemName.left; anchors.top: certificateContainer.bottom; anchors.topMargin: 8; From 21d286b4220111c0ff07f4932acfa1bf9d1c1b4b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 6 Oct 2017 10:04:14 +1300 Subject: [PATCH 700/722] Fix undo not coping with cloning with both hands simultaneously --- scripts/shapes/modules/createPalette.js | 1 + scripts/shapes/modules/history.js | 24 ++++++++++----------- scripts/shapes/modules/selection.js | 28 +++++++++---------------- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/scripts/shapes/modules/createPalette.js b/scripts/shapes/modules/createPalette.js index 694705cb2f..0eea8379d6 100644 --- a/scripts/shapes/modules/createPalette.js +++ b/scripts/shapes/modules/createPalette.js @@ -405,6 +405,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { entityID = Entities.addEntity(properties); if (entityID !== Uuid.NULL) { History.prePush( + otherSide, { deleteEntities: [{ entityID: entityID }] }, { createEntities: [{ entityID: entityID, properties: properties }] } ); diff --git a/scripts/shapes/modules/history.js b/scripts/shapes/modules/history.js index 7065a3c52b..4b84a5699d 100644 --- a/scripts/shapes/modules/history.js +++ b/scripts/shapes/modules/history.js @@ -47,8 +47,8 @@ History = (function () { ], MAX_HISTORY_ITEMS = 1000, undoPosition = -1, // The next history item to undo; the next history item to redo = undoIndex + 1. - undoData = {}, - redoData = {}; + undoData = [{}, {}], + redoData = [{}, {}]; function doKick(entityID) { var properties, @@ -73,16 +73,16 @@ History = (function () { }, KICK_DELAY); } - function prePush(undo, redo) { - // Stores undo and redo data to include in the next history entry. - undoData = undo; - redoData = redo; + function prePush(side, undo, redo) { + // Stores undo and redo data to include in the next history entry generated for the side. + undoData[side] = undo; + redoData[side] = redo; } - function push(undo, redo) { + function push(side, undo, redo) { // Add a history entry. - undoData = Object.merge(undoData, undo); - redoData = Object.merge(redoData, redo); + undoData[side] = Object.merge(undoData[side], undo); + redoData[side] = Object.merge(redoData[side], redo); // Wipe any redo history after current undo position. if (undoPosition < history.length - 1) { @@ -95,11 +95,11 @@ History = (function () { undoPosition = history.length - 1; } - history.push({ undoData: undoData, redoData: redoData }); + history.push({ undoData: undoData[side], redoData: redoData[side] }); undoPosition++; - undoData = {}; - redoData = {}; + undoData[side] = {}; + redoData[side] = {}; } function updateEntityIDs(oldEntityID, newEntityID) { diff --git a/scripts/shapes/modules/selection.js b/scripts/shapes/modules/selection.js index afa0da5fd2..5720e0bee0 100644 --- a/scripts/shapes/modules/selection.js +++ b/scripts/shapes/modules/selection.js @@ -282,6 +282,7 @@ SelectionManager = function (side) { && (!Vec3.equal(startPosition, rootPosition) || !Quat.equal(startOrientation, rootOrientation))) { // Positions and orientations can be identical if change grabbing hands when finish scaling. History.push( + side, { setProperties: [ { entityID: rootEntityID, properties: { position: startPosition, rotation: startOrientation } } @@ -331,6 +332,7 @@ SelectionManager = function (side) { if (Vec3.distance(startPosition, rootPosition) >= MIN_HISTORY_MOVE_DISTANCE || Quat.rotationBetween(startOrientation, rootOrientation) >= MIN_HISTORY_ROTATE_ANGLE) { History.push( + side, { setProperties: [ { entityID: rootEntityID, properties: { position: startPosition, rotation: startOrientation } } @@ -426,10 +428,7 @@ SelectionManager = function (side) { } // Add history entry. - History.push( - { setProperties: undoData }, - { setProperties: redoData } - ); + History.push(side, { setProperties: undoData }, { setProperties: redoData }); // Update grab start data for its undo. startPosition = rootPosition; @@ -445,6 +444,7 @@ SelectionManager = function (side) { if (Vec3.distance(startPosition, rootPosition) >= MIN_HISTORY_MOVE_DISTANCE || Quat.rotationBetween(startOrientation, rootOrientation) >= MIN_HISTORY_ROTATE_ANGLE) { History.push( + side, { setProperties: [ { entityID: rootEntityID, properties: { position: startPosition, rotation: startOrientation } } @@ -542,10 +542,7 @@ SelectionManager = function (side) { } // Add history entry. - History.push( - { setProperties: undoData }, - { setProperties: redoData } - ); + History.push(side, { setProperties: undoData }, { setProperties: redoData }); // Update grab start data for its undo. startPosition = rootPosition; @@ -593,10 +590,7 @@ SelectionManager = function (side) { rootEntityID = selection[0].id; // Add history entry. - History.prePush( - { deleteEntities: undoData }, - { createEntities: redoData } - ); + History.prePush(side, { deleteEntities: undoData }, { createEntities: redoData }); } function applyColor(color, isApplyToAll) { @@ -621,7 +615,7 @@ SelectionManager = function (side) { } } if (undoData.length > 0) { - History.push({ setProperties: undoData }, { setProperties: redoData }); + History.push(side, { setProperties: undoData }, { setProperties: redoData }); } } else { properties = Entities.getEntityProperties(intersectedEntityID, ["type", "color"]); @@ -630,6 +624,7 @@ SelectionManager = function (side) { color: color }); History.push( + side, { setProperties: [{ entityID: intersectedEntityID, properties: { color: properties.color } }] }, { setProperties: [{ entityID: intersectedEntityID, properties: { color: color } }] } ); @@ -740,10 +735,7 @@ SelectionManager = function (side) { }); // Add history entry. - History.push( - { setProperties: undoData }, - { setProperties: redoData } - ); + History.push(side, { setProperties: undoData }, { setProperties: redoData }); // Kick off physics if necessary. if (physicsProperties.dynamic) { @@ -759,7 +751,7 @@ SelectionManager = function (side) { function deleteEntities() { if (rootEntityID) { - History.push({ createEntities: selectionProperties }, { deleteEntities: [{ entityID: rootEntityID }] }); + History.push(side, { createEntities: selectionProperties }, { deleteEntities: [{ entityID: rootEntityID }] }); Entities.deleteEntity(rootEntityID); // Children are automatically deleted. clear(); } From 0b600a74c3b6990e4caf90bb9c9adbfccc582163 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 5 Oct 2017 14:34:53 -0700 Subject: [PATCH 701/722] Fix passphrase input focus problem; fix getting inventory and balance after logging in on Checkout --- interface/resources/qml/hifi/commerce/checkout/Checkout.qml | 4 ++++ .../qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml | 2 +- .../resources/qml/hifi/commerce/wallet/PassphraseModal.qml | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 32f324aea9..09c2f6fa76 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -892,6 +892,10 @@ Rectangle { } else { root.activeView = "checkoutSuccess"; } + root.balanceReceived = false; + root.purchasesReceived = false; + commerce.inventory(); + commerce.balance(); } // diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index 420b51ba15..cc316a70e9 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -39,7 +39,7 @@ Item { sendToParent({method: "needsLogIn"}); } else if (walletStatus === 3) { commerce.getSecurityImage(); - } else { + } else if (walletStatus > 3) { console.log("ERROR in EmulatedMarketplaceHeader.qml: Unknown wallet status: " + walletStatus); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml index 5bd88ba790..8d5d9f97de 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml @@ -197,6 +197,8 @@ Item { height: 50; echoMode: TextInput.Password; placeholderText: "passphrase"; + activeFocusOnPress: true; + activeFocusOnTab: true; onFocusChanged: { root.keyboardRaised = focus; @@ -206,8 +208,8 @@ Item { anchors.fill: parent; onClicked: { - parent.focus = true; root.keyboardRaised = true; + mouse.accepted = false; } } From 1673c0835349d64b6657517140144493a45c445a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 6 Oct 2017 10:42:23 +1300 Subject: [PATCH 702/722] Update undo for grouping --- scripts/shapes/modules/groups.js | 10 ++-------- scripts/shapes/modules/history.js | 8 ++++++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/scripts/shapes/modules/groups.js b/scripts/shapes/modules/groups.js index 223992d646..3153a622ee 100644 --- a/scripts/shapes/modules/groups.js +++ b/scripts/shapes/modules/groups.js @@ -143,10 +143,7 @@ Groups = function () { selections.splice(1, selections.length - 1); // Add history entry. - History.push( - { setProperties: undoData }, - { setProperties: redoData } - ); + History.push(null, { setProperties: undoData }, { setProperties: redoData }); } function ungroup() { @@ -248,10 +245,7 @@ Groups = function () { } // Add history entry. - History.push( - { setProperties: undoData }, - { setProperties: redoData } - ); + History.push(null, { setProperties: undoData }, { setProperties: redoData }); } function clear() { diff --git a/scripts/shapes/modules/history.js b/scripts/shapes/modules/history.js index 4b84a5699d..3820feaf81 100644 --- a/scripts/shapes/modules/history.js +++ b/scripts/shapes/modules/history.js @@ -47,8 +47,9 @@ History = (function () { ], MAX_HISTORY_ITEMS = 1000, undoPosition = -1, // The next history item to undo; the next history item to redo = undoIndex + 1. - undoData = [{}, {}], - redoData = [{}, {}]; + undoData = [{}, {}, {}], // Left side, right side, no side. + redoData = [{}, {}, {}], + NO_SIDE = 2; function doKick(entityID) { var properties, @@ -81,6 +82,9 @@ History = (function () { function push(side, undo, redo) { // Add a history entry. + if (side === null) { + side = NO_SIDE; + } undoData[side] = Object.merge(undoData[side], undo); redoData[side] = Object.merge(redoData[side], redo); From ba50fcc5097835c443f9e753e9888d1ab3aa6069 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Thu, 5 Oct 2017 15:07:49 -0700 Subject: [PATCH 703/722] fix mouse dissapearing in desktop mode --- scripts/system/controllers/controllerModules/mouseHMD.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/mouseHMD.js b/scripts/system/controllers/controllerModules/mouseHMD.js index 9ccf4912a1..ed0bc03223 100644 --- a/scripts/system/controllers/controllerModules/mouseHMD.js +++ b/scripts/system/controllers/controllerModules/mouseHMD.js @@ -115,7 +115,12 @@ this.run = function(controllerData, deltaTime) { var now = Date.now(); if (this.mouseActivity.expired(now) || this.triggersPressed(controllerData, now)) { - Reticle.visible = false; + if (!HMD.active) { + Reticle.visible = true; + } else { + Reticle.visible = false; + } + return ControllerDispatcherUtils.makeRunningValues(false, [], []); } this.adjustReticleDepth(controllerData); From 3139f5ef2a2b29c4b6e4328b59bf7679256bec54 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Thu, 5 Oct 2017 15:12:58 -0700 Subject: [PATCH 704/722] improve mouseHMD exit logic --- scripts/system/controllers/controllerModules/mouseHMD.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/controllerModules/mouseHMD.js b/scripts/system/controllers/controllerModules/mouseHMD.js index ed0bc03223..1d8aeee1f9 100644 --- a/scripts/system/controllers/controllerModules/mouseHMD.js +++ b/scripts/system/controllers/controllerModules/mouseHMD.js @@ -114,8 +114,9 @@ this.run = function(controllerData, deltaTime) { var now = Date.now(); - if (this.mouseActivity.expired(now) || this.triggersPressed(controllerData, now)) { - if (!HMD.active) { + var hmdActive = HMD.active; + if (this.mouseActivity.expired(now) || this.triggersPressed(controllerData, now) || !hmdActive) { + if (!hmdActive) { Reticle.visible = true; } else { Reticle.visible = false; From 5587a2fdf842f0ab29fe42b78ad02f6fd27df7c5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 6 Oct 2017 11:13:53 +1300 Subject: [PATCH 705/722] Fix creating an entity while a tool is active --- scripts/shapes/shapes.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/shapes/shapes.js b/scripts/shapes/shapes.js index 465ce6258f..2fdf56900b 100644 --- a/scripts/shapes/shapes.js +++ b/scripts/shapes/shapes.js @@ -979,7 +979,9 @@ && (!wasTriggerClicked || isAutoGrab) && isTriggerClicked) { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); - if (otherEditor.isEditing(rootEntityID)) { + if (isAutoGrab) { + setState(EDITOR_GRABBING); + } else if (otherEditor.isEditing(rootEntityID)) { if (toolSelected !== TOOL_SCALE) { setState(EDITOR_DIRECT_SCALING); } @@ -1013,10 +1015,10 @@ selection.deleteEntities(); setState(EDITOR_SEARCHING); } else { - setState(EDITOR_GRABBING); + log(side, "ERROR: Editor: Unexpected condition A in EDITOR_SEARCHING!"); } } else { - log(side, "ERROR: Editor: Unexpected condition in EDITOR_SEARCHING!"); + log(side, "ERROR: Editor: Unexpected condition B in EDITOR_SEARCHING!"); } break; case EDITOR_HIGHLIGHTING: From 5c72a411638b85f8c72aa50992d004e9f640a7bc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 6 Oct 2017 11:14:07 +1300 Subject: [PATCH 706/722] Delete some redundant code --- scripts/shapes/shapes.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/shapes/shapes.js b/scripts/shapes/shapes.js index 2fdf56900b..4686a48470 100644 --- a/scripts/shapes/shapes.js +++ b/scripts/shapes/shapes.js @@ -1043,10 +1043,6 @@ wasScaleTool = toolSelected === TOOL_SCALE; doUpdateState = true; } - if (toolSelected === TOOL_COLOR && intersection.entityID !== intersectedEntityID) { - intersectedEntityID = intersection.entityID; - doUpdateState = true; - } if ((toolSelected === TOOL_COLOR || toolSelected === TOOL_PHYSICS) && intersection.entityID !== intersectedEntityID) { intersectedEntityID = intersection.entityID; From 525e0d8f61418a567e6f15d0f602f67e8be78279 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 5 Oct 2017 16:51:13 -0700 Subject: [PATCH 707/722] Make SettingsScriptingInterface accessible to tablet --- interface/src/ui/overlays/Web3DOverlay.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 811e169faf..526890b9c1 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -40,6 +40,7 @@ #include "scripting/HMDScriptingInterface.h" #include "scripting/AssetMappingsScriptingInterface.h" #include "scripting/MenuScriptingInterface.h" +#include "scripting/SettingsScriptingInterface.h" #include #include #include "FileDialogHelper.h" @@ -243,6 +244,7 @@ void Web3DOverlay::setupQmlSurface() { _webSurface->getSurfaceContext()->setContextProperty("InputConfiguration", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("SoundCache", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); _webSurface->getSurfaceContext()->setContextProperty("pathToFonts", "../../"); From 105457b388b2c065733aa21a52b9fe62e440c69a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 6 Oct 2017 13:27:08 +1300 Subject: [PATCH 708/722] Don't unbusubscribe from possibly shared channel --- scripts/shapes/shapes.js | 4 ++-- scripts/system/edit.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/shapes/shapes.js b/scripts/shapes/shapes.js index 4686a48470..290424f55e 100644 --- a/scripts/shapes/shapes.js +++ b/scripts/shapes/shapes.js @@ -1988,8 +1988,8 @@ Entities.canRezChanged.disconnect(onCanRezChanged); Entities.canRezTmpChanged.disconnect(onCanRezChanged); Messages.messageReceived.disconnect(onMessageReceived); - // Messages.unsubscribe(DOMAIN_CHANGED_MESSAGE); Do NOT unsubscribe because edit.js also subscribes and - // Messages.subscribe works client-wide. + // Messages.unsubscribe(DOMAIN_CHANGED_MESSAGE); Do not unsubscribe because edit.js also subscribes and + // Messages.subscribe works script engine-wide which would mess things up if they're both run in the same engine. MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged); MyAvatar.skeletonChanged.disconnect(onSkeletonChanged); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 9f15abaa1f..d6d4de2a4b 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1234,7 +1234,8 @@ Script.scriptEnding.connect(function () { Messages.messageReceived.disconnect(handleMessagesReceived); Messages.unsubscribe("entityToolUpdates"); - Messages.unsubscribe("Toolbar-DomainChanged"); + // Messages.unsubscribe("Toolbar-DomainChanged"); // Do not unsubscribe because the shapes.js app also subscribes and + // Messages.subscribe works script engine-wide which would mess things up if they're both run in the same engine. createButton = null; }); From 0c33f46ab1f48eb518c05d6c794356736056607d Mon Sep 17 00:00:00 2001 From: Flame Soulis Date: Thu, 5 Oct 2017 21:42:16 -0400 Subject: [PATCH 709/722] Update SetupQt.cmake This change allows -DQT_CMAKE_PREFIX_PATH to function again. Previously, set(QT_CMAKE_PREFIX_PATH "$ENV{QT_CMAKE_PREFIX_PATH}") always ran, which overrode QT_CMAKE_PREFIX_PATH every time on cmake. This proposed change now only uses the environment variable if DQT_CMAKE_PREFIX_PATH is not specified. --- cmake/macros/SetupQt.cmake | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmake/macros/SetupQt.cmake b/cmake/macros/SetupQt.cmake index ece8607b9b..bbdce06f37 100644 --- a/cmake/macros/SetupQt.cmake +++ b/cmake/macros/SetupQt.cmake @@ -44,7 +44,11 @@ endfunction() # Sets the QT_CMAKE_PREFIX_PATH and QT_DIR variables # Also enables CMAKE_AUTOMOC and CMAKE_AUTORCC macro(setup_qt) - set(QT_CMAKE_PREFIX_PATH "$ENV{QT_CMAKE_PREFIX_PATH}") + # if QT_CMAKE_PREFIX_PATH was not specified before hand, + # try to use the environment variable + if (NOT QT_CMAKE_PREFIX_PATH) + set(QT_CMAKE_PREFIX_PATH "$ENV{QT_CMAKE_PREFIX_PATH}") + endif() if (("QT_CMAKE_PREFIX_PATH" STREQUAL "") OR (NOT EXISTS "${QT_CMAKE_PREFIX_PATH}")) calculate_default_qt_dir(QT_DIR) set(QT_CMAKE_PREFIX_PATH "${QT_DIR}/lib/cmake") @@ -81,4 +85,4 @@ macro(setup_qt) add_paths_to_fixup_libs("${QT_DIR}/bin") endif () -endmacro() \ No newline at end of file +endmacro() From 24974fdae3f3797b625c3d19a425ab1280e73057 Mon Sep 17 00:00:00 2001 From: Nex-Pro <7314019+Nex-Pro@users.noreply.github.com> Date: Fri, 6 Oct 2017 18:42:02 +0100 Subject: [PATCH 710/722] Fixed typo in qWarning message. --- interface/src/assets/ATPAssetMigrator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/assets/ATPAssetMigrator.cpp b/interface/src/assets/ATPAssetMigrator.cpp index 8de40865b7..45ac80b054 100644 --- a/interface/src/assets/ATPAssetMigrator.cpp +++ b/interface/src/assets/ATPAssetMigrator.cpp @@ -78,7 +78,7 @@ void ATPAssetMigrator::loadEntityServerFile() { request->send(); } else { ++_errorCount; - qWarning() << "Count not create request for asset at" << migrationURL.toString(); + qWarning() << "Could not create request for asset at" << migrationURL.toString(); } }; From f4dedf05bb45df2fc744cd763ab2e4dc7f064259 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 6 Oct 2017 11:02:26 -0700 Subject: [PATCH 711/722] move variables into block that needs them --- libraries/entities/src/EntityTree.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index ef1d27640c..e19d7a3a7f 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1941,9 +1941,9 @@ bool EntityTree::readFromMap(QVariantMap& map) { entityItemID = EntityItemID(QUuid::createUuid()); } - auto nodeList = DependencyManager::get(); - const QUuid myNodeID = nodeList->getSessionUUID(); if (properties.getClientOnly()) { + auto nodeList = DependencyManager::get(); + const QUuid myNodeID = nodeList->getSessionUUID(); properties.setOwningAvatarID(myNodeID); } From 9064114ce5336676963f3fdeda690ce8a683f3c7 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 6 Oct 2017 11:03:48 -0700 Subject: [PATCH 712/722] fix mistaken logic-flip in recent PR --- libraries/entities/src/ModelEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 14813a68fe..9c3ce47886 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -497,7 +497,7 @@ bool ModelEntityItem::hasModel() const { }); } bool ModelEntityItem::hasCompoundShapeURL() const { - return _compoundShapeURL.get().isEmpty(); + return !_compoundShapeURL.get().isEmpty(); } QString ModelEntityItem::getModelURL() const { From 3fbaf250a4bd8030f1441e36ddcab5408700350e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 7 Oct 2017 11:50:31 +1300 Subject: [PATCH 713/722] Increase delay in kicking off physics --- scripts/shapes/modules/history.js | 2 +- scripts/shapes/modules/selection.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/shapes/modules/history.js b/scripts/shapes/modules/history.js index 3820feaf81..9f2c45046a 100644 --- a/scripts/shapes/modules/history.js +++ b/scripts/shapes/modules/history.js @@ -66,7 +66,7 @@ History = (function () { function kickPhysics(entityID) { // Gives entities a small kick to start off physics, if necessary. - var KICK_DELAY = 500; // ms + var KICK_DELAY = 750; // ms // Give physics a chance to catch up. Avoids some erratic behavior. Script.setTimeout(function () { diff --git a/scripts/shapes/modules/selection.js b/scripts/shapes/modules/selection.js index 5720e0bee0..955cde6bda 100644 --- a/scripts/shapes/modules/selection.js +++ b/scripts/shapes/modules/selection.js @@ -234,7 +234,7 @@ SelectionManager = function (side) { function kickPhysics(entityID) { // Gives entities a small kick to start off physics, if necessary. - var KICK_DELAY = 500; // ms + var KICK_DELAY = 750; // ms // Give physics a chance to catch up. Avoids some erratic behavior. Script.setTimeout(function () { From 596ce8e9c12c8a75db2a1d7092cf3f9c38ab3969 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 7 Oct 2017 21:46:56 +1300 Subject: [PATCH 714/722] Add script that tests setting your listening position and orientation --- scripts/developer/utilities/tools/testEars.js | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 scripts/developer/utilities/tools/testEars.js diff --git a/scripts/developer/utilities/tools/testEars.js b/scripts/developer/utilities/tools/testEars.js new file mode 100644 index 0000000000..600358eeb1 --- /dev/null +++ b/scripts/developer/utilities/tools/testEars.js @@ -0,0 +1,101 @@ +// +// testEars.js +// +// Positions and orients your listening position at a virtual head created in front of you. +// +// Created by David Rowe on 7 Oct 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html. +// + +(function () { + + "use strict"; + + var overlays = [ + { + // Head + type: "sphere", + properties: { + dimensions: { x: 0.2, y: 0.3, z: 0.25 }, + position: { x: 0, y: 0, z: -2 }, + rotation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }), + color: { red: 128, green: 128, blue: 128 }, + alpha: 1.0, + solid: true + } + }, + { + // Left ear + type: "sphere", + properties: { + dimensions: { x: 0.04, y: 0.10, z: 0.08 }, + localPosition: { x: -0.1, y: 0, z: 0.05 }, + localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: 0 }), + color: { red: 255, green: 0, blue: 0 }, // Red for "port". + alpha: 1.0, + solid: true + } + }, + { + // Right ear + type: "sphere", + properties: { + dimensions: { x: 0.04, y: 0.10, z: 0.08 }, + localPosition: { x: 0.1, y: 0, z: 0.05 }, + localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: 0 }), + color: { red: 0, green: 255, blue: 0 }, // Green for "starboard". + alpha: 1.0, + solid: true + } + }, + { + // Nose + type: "sphere", + properties: { + dimensions: { x: 0.04, y: 0.04, z: 0.04 }, + localPosition: { x: 0, y: 0, z: -0.125 }, + localRotation: Quat.fromVec3Degrees({ x: 0, y: -0.08, z: 0 }), + color: { red: 160, green: 160, blue: 160 }, + alpha: 1.0, + solid: true + } + } + ], + originalListenerMode; + + function setUp() { + var i, length; + + originalListenerMode = MyAvatar.audioListenerMode; + + for (i = 0, length = overlays.length; i < length; i++) { + if (i === 0) { + overlays[i].properties.position = Vec3.sum(MyAvatar.getHeadPosition(), + Vec3.multiplyQbyV(MyAvatar.orientation, overlays[i].properties.position)); + overlays[i].properties.rotation = Quat.multiply(MyAvatar.orientation, overlays[i].properties.rotation); + } else { + overlays[i].properties.parentID = overlays[0].id; + } + overlays[i].id = Overlays.addOverlay(overlays[i].type, overlays[i].properties); + } + + MyAvatar.audioListenerMode = MyAvatar.audioListenerModeCustom; + MyAvatar.customListenPosition = overlays[0].properties.position; + MyAvatar.customListenOrientation = overlays[0].properties.orientation; + } + + function tearDown() { + var i, length; + for (i = 0, length = overlays.length; i < length; i++) { + Overlays.deleteOverlay(overlays[i].id); + } + + MyAvatar.audioListenerMode = originalListenerMode; + } + + Script.setTimeout(setUp, 2000); // Delay so that overlays display if script runs at Interface start. + Script.scriptEnding.connect(tearDown); +}()); From 28a8b180605c205d913442414f8423d3a4b76d64 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 Oct 2017 16:54:05 -0700 Subject: [PATCH 715/722] Revert "fix importing of avatar entities" --- interface/src/avatar/MyAvatar.cpp | 6 ------ interface/src/avatar/MyAvatar.h | 2 -- .../entities/src/EntityEditPacketSender.cpp | 2 +- libraries/entities/src/EntityTree.cpp | 18 +++--------------- libraries/shared/src/SpatiallyNestable.cpp | 18 ++++-------------- .../libraries/controllerDispatcherUtils.js | 2 +- 6 files changed, 9 insertions(+), 39 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 5d82405aee..10e2202553 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3238,9 +3238,3 @@ void MyAvatar::setModelScale(float scale) { emit sensorToWorldScaleChanged(sensorToWorldScale); } } - -SpatialParentTree* MyAvatar::getParentTree() const { - auto entityTreeRenderer = qApp->getEntities(); - EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; - return entityTree.get(); -} diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 952315a85f..9620d61a49 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -544,8 +544,6 @@ public: float getUserHeight() const; float getUserEyeHeight() const; - virtual SpatialParentTree* getParentTree() const override; - public slots: void increaseSize(); void decreaseSize(); diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index f93b6a49ec..ee0fcf8218 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -45,7 +45,7 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type, } EntityItemPointer entity = entityTree->findEntityByEntityItemID(entityItemID); if (!entity) { - qCDebug(entities) << "EntityEditPacketSender::queueEditAvatarEntityMessage can't find entity: " << entityItemID; + qCDebug(entities) << "EntityEditPacketSender::queueEditEntityMessage can't find entity."; return; } diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index e19d7a3a7f..bf37a08386 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1842,13 +1842,7 @@ bool EntityTree::sendEntitiesOperation(const OctreeElementPointer& element, void QHash::iterator iter = args->map->find(oldID); if (iter == args->map->end()) { - EntityItemID newID; - if (oldID == AVATAR_SELF_ID) { - auto nodeList = DependencyManager::get(); - newID = EntityItemID(nodeList->getSessionUUID()); - } else { - newID = QUuid::createUuid(); - } + EntityItemID newID = QUuid::createUuid(); args->map->insert(oldID, newID); return newID; } @@ -1865,8 +1859,8 @@ bool EntityTree::sendEntitiesOperation(const OctreeElementPointer& element, void properties.setPosition(properties.getPosition() + args->root); } else { EntityItemPointer parentEntity = args->ourTree->findEntityByEntityItemID(oldParentID); - if (parentEntity || oldParentID == AVATAR_SELF_ID) { // map the parent - properties.setParentID(getMapped(oldParentID)); + if (parentEntity) { // map the parent + properties.setParentID(getMapped(parentEntity->getID())); // But do not add root offset in this case. } else { // Should not happen, but let's try to be helpful... item->globalizeProperties(properties, "Cannot find %3 parent of %2 %1", args->root); @@ -1941,12 +1935,6 @@ bool EntityTree::readFromMap(QVariantMap& map) { entityItemID = EntityItemID(QUuid::createUuid()); } - if (properties.getClientOnly()) { - auto nodeList = DependencyManager::get(); - const QUuid myNodeID = nodeList->getSessionUUID(); - properties.setOwningAvatarID(myNodeID); - } - EntityItemPointer entity = addEntity(entityItemID, properties); if (!entity) { qCDebug(entities) << "adding Entity failed:" << entityItemID << properties.getType(); diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 8c43632456..8ea38f5f13 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -102,11 +102,8 @@ SpatiallyNestablePointer SpatiallyNestable::getParentPointer(bool& success) cons if (parent && parent->getID() == parentID) { // parent pointer is up-to-date if (!_parentKnowsMe) { - SpatialParentTree* parentTree = parent->getParentTree(); - if (!parentTree || parentTree == getParentTree()) { - parent->beParentOfChild(getThisPointer()); - _parentKnowsMe = true; - } + parent->beParentOfChild(getThisPointer()); + _parentKnowsMe = true; } success = true; return parent; @@ -132,15 +129,8 @@ SpatiallyNestablePointer SpatiallyNestable::getParentPointer(bool& success) cons parent = _parent.lock(); if (parent) { - - // it's possible for an entity with a parent of AVATAR_SELF_ID can be imported into a side-tree - // such as the clipboard's. if this is the case, we don't want the parent to consider this a - // child. - SpatialParentTree* parentTree = parent->getParentTree(); - if (!parentTree || parentTree == getParentTree()) { - parent->beParentOfChild(getThisPointer()); - _parentKnowsMe = true; - } + parent->beParentOfChild(getThisPointer()); + _parentKnowsMe = true; } success = (parent || parentID.isNull()); diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index d6d80541ee..e9e25b058b 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -222,7 +222,7 @@ getControllerJointIndex = function (hand) { return controllerJointIndex; } - return -1; + return MyAvatar.getJointIndex("Head"); }; propsArePhysical = function (props) { From 7aeeaeb4e7efecca8bd2c96cd1bb51b304213ec6 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sat, 7 Oct 2017 19:41:43 -0700 Subject: [PATCH 716/722] Fix rendering transform for text3d overlays --- interface/src/ui/overlays/Text3DOverlay.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/ui/overlays/Text3DOverlay.cpp b/interface/src/ui/overlays/Text3DOverlay.cpp index 57e3c32060..2e55a9a471 100644 --- a/interface/src/ui/overlays/Text3DOverlay.cpp +++ b/interface/src/ui/overlays/Text3DOverlay.cpp @@ -88,6 +88,7 @@ void Text3DOverlay::update(float deltatime) { applyTransformTo(transform); setTransform(transform); } + Parent::update(deltatime); } void Text3DOverlay::render(RenderArgs* args) { From 765f3fc38175a6ab53d0c1c50b0821da08f72dda Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 9 Oct 2017 10:38:40 +1300 Subject: [PATCH 717/722] Move Shapes app files from scripts directory to marketplace directory --- .../marketplace}/shapes/assets/audio/clone.wav | Bin .../marketplace}/shapes/assets/audio/create.wav | Bin .../marketplace}/shapes/assets/audio/delete.wav | Bin .../marketplace}/shapes/assets/audio/drop.wav | Bin .../marketplace}/shapes/assets/audio/equip.wav | Bin .../marketplace}/shapes/assets/audio/error.wav | Bin .../marketplace}/shapes/assets/audio/select.wav | Bin .../marketplace}/shapes/assets/blue-header-bar.fbx | Bin .../marketplace}/shapes/assets/create/circle.fbx | Bin .../marketplace}/shapes/assets/create/cone.fbx | Bin .../shapes/assets/create/create-heading.svg | 0 .../marketplace}/shapes/assets/create/cube.fbx | Bin .../marketplace}/shapes/assets/create/cylinder.fbx | Bin .../shapes/assets/create/dodecahedron.fbx | Bin .../marketplace}/shapes/assets/create/hexagon.fbx | Bin .../shapes/assets/create/icosahedron.fbx | Bin .../marketplace}/shapes/assets/create/octagon.fbx | Bin .../shapes/assets/create/octahedron.fbx | Bin .../marketplace}/shapes/assets/create/prism.fbx | Bin .../marketplace}/shapes/assets/create/sphere.fbx | Bin .../shapes/assets/create/tetrahedron.fbx | Bin .../marketplace}/shapes/assets/gray-header.fbx | Bin .../marketplace}/shapes/assets/green-header-bar.fbx | Bin .../marketplace}/shapes/assets/green-header.fbx | Bin .../marketplace}/shapes/assets/horizontal-rule.svg | 0 .../marketplace}/shapes/assets/shapes-a.svg | 0 .../marketplace}/shapes/assets/shapes-d.svg | 0 .../marketplace}/shapes/assets/shapes-i.svg | 0 .../shapes/assets/tools/back-heading.svg | 0 .../marketplace}/shapes/assets/tools/back-icon.svg | 0 .../marketplace}/shapes/assets/tools/clone-icon.svg | 0 .../shapes/assets/tools/clone-label.svg | 0 .../shapes/assets/tools/clone-tool-heading.svg | 0 .../marketplace}/shapes/assets/tools/color-icon.svg | 0 .../shapes/assets/tools/color-label.svg | 0 .../shapes/assets/tools/color-tool-heading.svg | 0 .../assets/tools/color/color-circle-black.png | Bin .../shapes/assets/tools/color/color-circle.png | Bin .../shapes/assets/tools/color/pick-color-label.svg | 0 .../shapes/assets/tools/color/slider-alpha.png | Bin .../shapes/assets/tools/color/slider-white.png | Bin .../shapes/assets/tools/color/swatches-label.svg | 0 .../shapes/assets/tools/common/actions-label.svg | 0 .../shapes/assets/tools/common/down-arrow.svg | 0 .../shapes/assets/tools/common/finish-label.svg | 0 .../shapes/assets/tools/common/info-icon.svg | 0 .../shapes/assets/tools/common/up-arrow.svg | 0 .../shapes/assets/tools/delete-icon.svg | 0 .../shapes/assets/tools/delete-label.svg | 0 .../shapes/assets/tools/delete-tool-heading.svg | 0 .../shapes/assets/tools/delete/info-text.svg | 0 .../marketplace}/shapes/assets/tools/group-icon.svg | 0 .../shapes/assets/tools/group-label.svg | 0 .../shapes/assets/tools/group-tool-heading.svg | 0 .../shapes/assets/tools/group/clear-label.svg | 0 .../shapes/assets/tools/group/group-label.svg | 0 .../assets/tools/group/selection-box-label.svg | 0 .../shapes/assets/tools/group/ungroup-label.svg | 0 .../shapes/assets/tools/physics-icon.svg | 0 .../shapes/assets/tools/physics-label.svg | 0 .../shapes/assets/tools/physics-tool-heading.svg | 0 .../tools/physics/buttons/collisions-label.svg | 0 .../tools/physics/buttons/grabbable-label.svg | 0 .../assets/tools/physics/buttons/gravity-label.svg | 0 .../assets/tools/physics/buttons/off-label.svg | 0 .../assets/tools/physics/buttons/on-label.svg | 0 .../shapes/assets/tools/physics/presets-label.svg | 0 .../assets/tools/physics/presets/balloon-label.svg | 0 .../assets/tools/physics/presets/cotton-label.svg | 0 .../assets/tools/physics/presets/custom-label.svg | 0 .../assets/tools/physics/presets/default-label.svg | 0 .../assets/tools/physics/presets/ice-label.svg | 0 .../assets/tools/physics/presets/lead-label.svg | 0 .../assets/tools/physics/presets/rubber-label.svg | 0 .../tools/physics/presets/tumbleweed-label.svg | 0 .../assets/tools/physics/presets/wood-label.svg | 0 .../assets/tools/physics/presets/zero-g-label.svg | 0 .../assets/tools/physics/properties-label.svg | 0 .../assets/tools/physics/sliders/bounce-label.svg | 0 .../assets/tools/physics/sliders/density-label.svg | 0 .../assets/tools/physics/sliders/friction-label.svg | 0 .../assets/tools/physics/sliders/gravity-label.svg | 0 .../marketplace}/shapes/assets/tools/redo-icon.svg | 0 .../marketplace}/shapes/assets/tools/redo-label.svg | 0 .../shapes/assets/tools/stretch-icon.svg | 0 .../shapes/assets/tools/stretch-label.svg | 0 .../shapes/assets/tools/stretch-tool-heading.svg | 0 .../shapes/assets/tools/stretch/info-text.svg | 0 .../marketplace}/shapes/assets/tools/tool-icon.fbx | Bin .../marketplace}/shapes/assets/tools/tool-label.svg | 0 .../shapes/assets/tools/tools-heading.svg | 0 .../marketplace}/shapes/assets/tools/undo-icon.svg | 0 .../marketplace}/shapes/assets/tools/undo-label.svg | 0 .../marketplace}/shapes/modules/createPalette.js | 0 .../marketplace}/shapes/modules/feedback.js | 0 .../marketplace}/shapes/modules/groups.js | 0 .../marketplace}/shapes/modules/hand.js | 0 .../marketplace}/shapes/modules/handles.js | 0 .../marketplace}/shapes/modules/highlights.js | 0 .../marketplace}/shapes/modules/history.js | 0 .../marketplace}/shapes/modules/laser.js | 0 .../marketplace}/shapes/modules/selection.js | 0 .../marketplace}/shapes/modules/toolIcon.js | 0 .../marketplace}/shapes/modules/toolsMenu.js | 0 .../marketplace}/shapes/modules/uit.js | 0 .../marketplace}/shapes/shapes.js | 0 .../marketplace}/shapes/utilities/utilities.js | 0 107 files changed, 0 insertions(+), 0 deletions(-) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/audio/clone.wav (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/audio/create.wav (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/audio/delete.wav (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/audio/drop.wav (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/audio/equip.wav (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/audio/error.wav (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/audio/select.wav (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/blue-header-bar.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/create/circle.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/create/cone.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/create/create-heading.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/create/cube.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/create/cylinder.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/create/dodecahedron.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/create/hexagon.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/create/icosahedron.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/create/octagon.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/create/octahedron.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/create/prism.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/create/sphere.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/create/tetrahedron.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/gray-header.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/green-header-bar.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/green-header.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/horizontal-rule.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/shapes-a.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/shapes-d.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/shapes-i.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/back-heading.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/back-icon.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/clone-icon.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/clone-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/clone-tool-heading.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/color-icon.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/color-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/color-tool-heading.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/color/color-circle-black.png (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/color/color-circle.png (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/color/pick-color-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/color/slider-alpha.png (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/color/slider-white.png (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/color/swatches-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/common/actions-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/common/down-arrow.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/common/finish-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/common/info-icon.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/common/up-arrow.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/delete-icon.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/delete-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/delete-tool-heading.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/delete/info-text.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/group-icon.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/group-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/group-tool-heading.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/group/clear-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/group/group-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/group/selection-box-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/group/ungroup-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics-icon.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics-tool-heading.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/buttons/collisions-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/buttons/grabbable-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/buttons/gravity-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/buttons/off-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/buttons/on-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/presets-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/presets/balloon-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/presets/cotton-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/presets/custom-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/presets/default-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/presets/ice-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/presets/lead-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/presets/rubber-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/presets/tumbleweed-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/presets/wood-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/presets/zero-g-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/properties-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/sliders/bounce-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/sliders/density-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/sliders/friction-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/physics/sliders/gravity-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/redo-icon.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/redo-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/stretch-icon.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/stretch-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/stretch-tool-heading.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/stretch/info-text.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/tool-icon.fbx (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/tool-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/tools-heading.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/undo-icon.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/assets/tools/undo-label.svg (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/modules/createPalette.js (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/modules/feedback.js (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/modules/groups.js (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/modules/hand.js (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/modules/handles.js (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/modules/highlights.js (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/modules/history.js (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/modules/laser.js (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/modules/selection.js (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/modules/toolIcon.js (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/modules/toolsMenu.js (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/modules/uit.js (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/shapes.js (100%) rename {scripts => unpublishedScripts/marketplace}/shapes/utilities/utilities.js (100%) diff --git a/scripts/shapes/assets/audio/clone.wav b/unpublishedScripts/marketplace/shapes/assets/audio/clone.wav similarity index 100% rename from scripts/shapes/assets/audio/clone.wav rename to unpublishedScripts/marketplace/shapes/assets/audio/clone.wav diff --git a/scripts/shapes/assets/audio/create.wav b/unpublishedScripts/marketplace/shapes/assets/audio/create.wav similarity index 100% rename from scripts/shapes/assets/audio/create.wav rename to unpublishedScripts/marketplace/shapes/assets/audio/create.wav diff --git a/scripts/shapes/assets/audio/delete.wav b/unpublishedScripts/marketplace/shapes/assets/audio/delete.wav similarity index 100% rename from scripts/shapes/assets/audio/delete.wav rename to unpublishedScripts/marketplace/shapes/assets/audio/delete.wav diff --git a/scripts/shapes/assets/audio/drop.wav b/unpublishedScripts/marketplace/shapes/assets/audio/drop.wav similarity index 100% rename from scripts/shapes/assets/audio/drop.wav rename to unpublishedScripts/marketplace/shapes/assets/audio/drop.wav diff --git a/scripts/shapes/assets/audio/equip.wav b/unpublishedScripts/marketplace/shapes/assets/audio/equip.wav similarity index 100% rename from scripts/shapes/assets/audio/equip.wav rename to unpublishedScripts/marketplace/shapes/assets/audio/equip.wav diff --git a/scripts/shapes/assets/audio/error.wav b/unpublishedScripts/marketplace/shapes/assets/audio/error.wav similarity index 100% rename from scripts/shapes/assets/audio/error.wav rename to unpublishedScripts/marketplace/shapes/assets/audio/error.wav diff --git a/scripts/shapes/assets/audio/select.wav b/unpublishedScripts/marketplace/shapes/assets/audio/select.wav similarity index 100% rename from scripts/shapes/assets/audio/select.wav rename to unpublishedScripts/marketplace/shapes/assets/audio/select.wav diff --git a/scripts/shapes/assets/blue-header-bar.fbx b/unpublishedScripts/marketplace/shapes/assets/blue-header-bar.fbx similarity index 100% rename from scripts/shapes/assets/blue-header-bar.fbx rename to unpublishedScripts/marketplace/shapes/assets/blue-header-bar.fbx diff --git a/scripts/shapes/assets/create/circle.fbx b/unpublishedScripts/marketplace/shapes/assets/create/circle.fbx similarity index 100% rename from scripts/shapes/assets/create/circle.fbx rename to unpublishedScripts/marketplace/shapes/assets/create/circle.fbx diff --git a/scripts/shapes/assets/create/cone.fbx b/unpublishedScripts/marketplace/shapes/assets/create/cone.fbx similarity index 100% rename from scripts/shapes/assets/create/cone.fbx rename to unpublishedScripts/marketplace/shapes/assets/create/cone.fbx diff --git a/scripts/shapes/assets/create/create-heading.svg b/unpublishedScripts/marketplace/shapes/assets/create/create-heading.svg similarity index 100% rename from scripts/shapes/assets/create/create-heading.svg rename to unpublishedScripts/marketplace/shapes/assets/create/create-heading.svg diff --git a/scripts/shapes/assets/create/cube.fbx b/unpublishedScripts/marketplace/shapes/assets/create/cube.fbx similarity index 100% rename from scripts/shapes/assets/create/cube.fbx rename to unpublishedScripts/marketplace/shapes/assets/create/cube.fbx diff --git a/scripts/shapes/assets/create/cylinder.fbx b/unpublishedScripts/marketplace/shapes/assets/create/cylinder.fbx similarity index 100% rename from scripts/shapes/assets/create/cylinder.fbx rename to unpublishedScripts/marketplace/shapes/assets/create/cylinder.fbx diff --git a/scripts/shapes/assets/create/dodecahedron.fbx b/unpublishedScripts/marketplace/shapes/assets/create/dodecahedron.fbx similarity index 100% rename from scripts/shapes/assets/create/dodecahedron.fbx rename to unpublishedScripts/marketplace/shapes/assets/create/dodecahedron.fbx diff --git a/scripts/shapes/assets/create/hexagon.fbx b/unpublishedScripts/marketplace/shapes/assets/create/hexagon.fbx similarity index 100% rename from scripts/shapes/assets/create/hexagon.fbx rename to unpublishedScripts/marketplace/shapes/assets/create/hexagon.fbx diff --git a/scripts/shapes/assets/create/icosahedron.fbx b/unpublishedScripts/marketplace/shapes/assets/create/icosahedron.fbx similarity index 100% rename from scripts/shapes/assets/create/icosahedron.fbx rename to unpublishedScripts/marketplace/shapes/assets/create/icosahedron.fbx diff --git a/scripts/shapes/assets/create/octagon.fbx b/unpublishedScripts/marketplace/shapes/assets/create/octagon.fbx similarity index 100% rename from scripts/shapes/assets/create/octagon.fbx rename to unpublishedScripts/marketplace/shapes/assets/create/octagon.fbx diff --git a/scripts/shapes/assets/create/octahedron.fbx b/unpublishedScripts/marketplace/shapes/assets/create/octahedron.fbx similarity index 100% rename from scripts/shapes/assets/create/octahedron.fbx rename to unpublishedScripts/marketplace/shapes/assets/create/octahedron.fbx diff --git a/scripts/shapes/assets/create/prism.fbx b/unpublishedScripts/marketplace/shapes/assets/create/prism.fbx similarity index 100% rename from scripts/shapes/assets/create/prism.fbx rename to unpublishedScripts/marketplace/shapes/assets/create/prism.fbx diff --git a/scripts/shapes/assets/create/sphere.fbx b/unpublishedScripts/marketplace/shapes/assets/create/sphere.fbx similarity index 100% rename from scripts/shapes/assets/create/sphere.fbx rename to unpublishedScripts/marketplace/shapes/assets/create/sphere.fbx diff --git a/scripts/shapes/assets/create/tetrahedron.fbx b/unpublishedScripts/marketplace/shapes/assets/create/tetrahedron.fbx similarity index 100% rename from scripts/shapes/assets/create/tetrahedron.fbx rename to unpublishedScripts/marketplace/shapes/assets/create/tetrahedron.fbx diff --git a/scripts/shapes/assets/gray-header.fbx b/unpublishedScripts/marketplace/shapes/assets/gray-header.fbx similarity index 100% rename from scripts/shapes/assets/gray-header.fbx rename to unpublishedScripts/marketplace/shapes/assets/gray-header.fbx diff --git a/scripts/shapes/assets/green-header-bar.fbx b/unpublishedScripts/marketplace/shapes/assets/green-header-bar.fbx similarity index 100% rename from scripts/shapes/assets/green-header-bar.fbx rename to unpublishedScripts/marketplace/shapes/assets/green-header-bar.fbx diff --git a/scripts/shapes/assets/green-header.fbx b/unpublishedScripts/marketplace/shapes/assets/green-header.fbx similarity index 100% rename from scripts/shapes/assets/green-header.fbx rename to unpublishedScripts/marketplace/shapes/assets/green-header.fbx diff --git a/scripts/shapes/assets/horizontal-rule.svg b/unpublishedScripts/marketplace/shapes/assets/horizontal-rule.svg similarity index 100% rename from scripts/shapes/assets/horizontal-rule.svg rename to unpublishedScripts/marketplace/shapes/assets/horizontal-rule.svg diff --git a/scripts/shapes/assets/shapes-a.svg b/unpublishedScripts/marketplace/shapes/assets/shapes-a.svg similarity index 100% rename from scripts/shapes/assets/shapes-a.svg rename to unpublishedScripts/marketplace/shapes/assets/shapes-a.svg diff --git a/scripts/shapes/assets/shapes-d.svg b/unpublishedScripts/marketplace/shapes/assets/shapes-d.svg similarity index 100% rename from scripts/shapes/assets/shapes-d.svg rename to unpublishedScripts/marketplace/shapes/assets/shapes-d.svg diff --git a/scripts/shapes/assets/shapes-i.svg b/unpublishedScripts/marketplace/shapes/assets/shapes-i.svg similarity index 100% rename from scripts/shapes/assets/shapes-i.svg rename to unpublishedScripts/marketplace/shapes/assets/shapes-i.svg diff --git a/scripts/shapes/assets/tools/back-heading.svg b/unpublishedScripts/marketplace/shapes/assets/tools/back-heading.svg similarity index 100% rename from scripts/shapes/assets/tools/back-heading.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/back-heading.svg diff --git a/scripts/shapes/assets/tools/back-icon.svg b/unpublishedScripts/marketplace/shapes/assets/tools/back-icon.svg similarity index 100% rename from scripts/shapes/assets/tools/back-icon.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/back-icon.svg diff --git a/scripts/shapes/assets/tools/clone-icon.svg b/unpublishedScripts/marketplace/shapes/assets/tools/clone-icon.svg similarity index 100% rename from scripts/shapes/assets/tools/clone-icon.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/clone-icon.svg diff --git a/scripts/shapes/assets/tools/clone-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/clone-label.svg similarity index 100% rename from scripts/shapes/assets/tools/clone-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/clone-label.svg diff --git a/scripts/shapes/assets/tools/clone-tool-heading.svg b/unpublishedScripts/marketplace/shapes/assets/tools/clone-tool-heading.svg similarity index 100% rename from scripts/shapes/assets/tools/clone-tool-heading.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/clone-tool-heading.svg diff --git a/scripts/shapes/assets/tools/color-icon.svg b/unpublishedScripts/marketplace/shapes/assets/tools/color-icon.svg similarity index 100% rename from scripts/shapes/assets/tools/color-icon.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/color-icon.svg diff --git a/scripts/shapes/assets/tools/color-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/color-label.svg similarity index 100% rename from scripts/shapes/assets/tools/color-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/color-label.svg diff --git a/scripts/shapes/assets/tools/color-tool-heading.svg b/unpublishedScripts/marketplace/shapes/assets/tools/color-tool-heading.svg similarity index 100% rename from scripts/shapes/assets/tools/color-tool-heading.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/color-tool-heading.svg diff --git a/scripts/shapes/assets/tools/color/color-circle-black.png b/unpublishedScripts/marketplace/shapes/assets/tools/color/color-circle-black.png similarity index 100% rename from scripts/shapes/assets/tools/color/color-circle-black.png rename to unpublishedScripts/marketplace/shapes/assets/tools/color/color-circle-black.png diff --git a/scripts/shapes/assets/tools/color/color-circle.png b/unpublishedScripts/marketplace/shapes/assets/tools/color/color-circle.png similarity index 100% rename from scripts/shapes/assets/tools/color/color-circle.png rename to unpublishedScripts/marketplace/shapes/assets/tools/color/color-circle.png diff --git a/scripts/shapes/assets/tools/color/pick-color-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/color/pick-color-label.svg similarity index 100% rename from scripts/shapes/assets/tools/color/pick-color-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/color/pick-color-label.svg diff --git a/scripts/shapes/assets/tools/color/slider-alpha.png b/unpublishedScripts/marketplace/shapes/assets/tools/color/slider-alpha.png similarity index 100% rename from scripts/shapes/assets/tools/color/slider-alpha.png rename to unpublishedScripts/marketplace/shapes/assets/tools/color/slider-alpha.png diff --git a/scripts/shapes/assets/tools/color/slider-white.png b/unpublishedScripts/marketplace/shapes/assets/tools/color/slider-white.png similarity index 100% rename from scripts/shapes/assets/tools/color/slider-white.png rename to unpublishedScripts/marketplace/shapes/assets/tools/color/slider-white.png diff --git a/scripts/shapes/assets/tools/color/swatches-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/color/swatches-label.svg similarity index 100% rename from scripts/shapes/assets/tools/color/swatches-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/color/swatches-label.svg diff --git a/scripts/shapes/assets/tools/common/actions-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/common/actions-label.svg similarity index 100% rename from scripts/shapes/assets/tools/common/actions-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/common/actions-label.svg diff --git a/scripts/shapes/assets/tools/common/down-arrow.svg b/unpublishedScripts/marketplace/shapes/assets/tools/common/down-arrow.svg similarity index 100% rename from scripts/shapes/assets/tools/common/down-arrow.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/common/down-arrow.svg diff --git a/scripts/shapes/assets/tools/common/finish-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/common/finish-label.svg similarity index 100% rename from scripts/shapes/assets/tools/common/finish-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/common/finish-label.svg diff --git a/scripts/shapes/assets/tools/common/info-icon.svg b/unpublishedScripts/marketplace/shapes/assets/tools/common/info-icon.svg similarity index 100% rename from scripts/shapes/assets/tools/common/info-icon.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/common/info-icon.svg diff --git a/scripts/shapes/assets/tools/common/up-arrow.svg b/unpublishedScripts/marketplace/shapes/assets/tools/common/up-arrow.svg similarity index 100% rename from scripts/shapes/assets/tools/common/up-arrow.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/common/up-arrow.svg diff --git a/scripts/shapes/assets/tools/delete-icon.svg b/unpublishedScripts/marketplace/shapes/assets/tools/delete-icon.svg similarity index 100% rename from scripts/shapes/assets/tools/delete-icon.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/delete-icon.svg diff --git a/scripts/shapes/assets/tools/delete-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/delete-label.svg similarity index 100% rename from scripts/shapes/assets/tools/delete-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/delete-label.svg diff --git a/scripts/shapes/assets/tools/delete-tool-heading.svg b/unpublishedScripts/marketplace/shapes/assets/tools/delete-tool-heading.svg similarity index 100% rename from scripts/shapes/assets/tools/delete-tool-heading.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/delete-tool-heading.svg diff --git a/scripts/shapes/assets/tools/delete/info-text.svg b/unpublishedScripts/marketplace/shapes/assets/tools/delete/info-text.svg similarity index 100% rename from scripts/shapes/assets/tools/delete/info-text.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/delete/info-text.svg diff --git a/scripts/shapes/assets/tools/group-icon.svg b/unpublishedScripts/marketplace/shapes/assets/tools/group-icon.svg similarity index 100% rename from scripts/shapes/assets/tools/group-icon.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/group-icon.svg diff --git a/scripts/shapes/assets/tools/group-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/group-label.svg similarity index 100% rename from scripts/shapes/assets/tools/group-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/group-label.svg diff --git a/scripts/shapes/assets/tools/group-tool-heading.svg b/unpublishedScripts/marketplace/shapes/assets/tools/group-tool-heading.svg similarity index 100% rename from scripts/shapes/assets/tools/group-tool-heading.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/group-tool-heading.svg diff --git a/scripts/shapes/assets/tools/group/clear-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/group/clear-label.svg similarity index 100% rename from scripts/shapes/assets/tools/group/clear-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/group/clear-label.svg diff --git a/scripts/shapes/assets/tools/group/group-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/group/group-label.svg similarity index 100% rename from scripts/shapes/assets/tools/group/group-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/group/group-label.svg diff --git a/scripts/shapes/assets/tools/group/selection-box-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/group/selection-box-label.svg similarity index 100% rename from scripts/shapes/assets/tools/group/selection-box-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/group/selection-box-label.svg diff --git a/scripts/shapes/assets/tools/group/ungroup-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/group/ungroup-label.svg similarity index 100% rename from scripts/shapes/assets/tools/group/ungroup-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/group/ungroup-label.svg diff --git a/scripts/shapes/assets/tools/physics-icon.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics-icon.svg similarity index 100% rename from scripts/shapes/assets/tools/physics-icon.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics-icon.svg diff --git a/scripts/shapes/assets/tools/physics-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics-label.svg diff --git a/scripts/shapes/assets/tools/physics-tool-heading.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics-tool-heading.svg similarity index 100% rename from scripts/shapes/assets/tools/physics-tool-heading.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics-tool-heading.svg diff --git a/scripts/shapes/assets/tools/physics/buttons/collisions-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/buttons/collisions-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/buttons/collisions-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/buttons/collisions-label.svg diff --git a/scripts/shapes/assets/tools/physics/buttons/grabbable-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/buttons/grabbable-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/buttons/grabbable-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/buttons/grabbable-label.svg diff --git a/scripts/shapes/assets/tools/physics/buttons/gravity-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/buttons/gravity-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/buttons/gravity-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/buttons/gravity-label.svg diff --git a/scripts/shapes/assets/tools/physics/buttons/off-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/buttons/off-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/buttons/off-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/buttons/off-label.svg diff --git a/scripts/shapes/assets/tools/physics/buttons/on-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/buttons/on-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/buttons/on-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/buttons/on-label.svg diff --git a/scripts/shapes/assets/tools/physics/presets-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/presets-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/presets-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/presets-label.svg diff --git a/scripts/shapes/assets/tools/physics/presets/balloon-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/balloon-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/presets/balloon-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/balloon-label.svg diff --git a/scripts/shapes/assets/tools/physics/presets/cotton-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/cotton-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/presets/cotton-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/cotton-label.svg diff --git a/scripts/shapes/assets/tools/physics/presets/custom-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/custom-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/presets/custom-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/custom-label.svg diff --git a/scripts/shapes/assets/tools/physics/presets/default-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/default-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/presets/default-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/default-label.svg diff --git a/scripts/shapes/assets/tools/physics/presets/ice-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/ice-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/presets/ice-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/ice-label.svg diff --git a/scripts/shapes/assets/tools/physics/presets/lead-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/lead-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/presets/lead-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/lead-label.svg diff --git a/scripts/shapes/assets/tools/physics/presets/rubber-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/rubber-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/presets/rubber-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/rubber-label.svg diff --git a/scripts/shapes/assets/tools/physics/presets/tumbleweed-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/tumbleweed-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/presets/tumbleweed-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/tumbleweed-label.svg diff --git a/scripts/shapes/assets/tools/physics/presets/wood-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/wood-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/presets/wood-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/wood-label.svg diff --git a/scripts/shapes/assets/tools/physics/presets/zero-g-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/zero-g-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/presets/zero-g-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/presets/zero-g-label.svg diff --git a/scripts/shapes/assets/tools/physics/properties-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/properties-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/properties-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/properties-label.svg diff --git a/scripts/shapes/assets/tools/physics/sliders/bounce-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/sliders/bounce-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/sliders/bounce-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/sliders/bounce-label.svg diff --git a/scripts/shapes/assets/tools/physics/sliders/density-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/sliders/density-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/sliders/density-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/sliders/density-label.svg diff --git a/scripts/shapes/assets/tools/physics/sliders/friction-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/sliders/friction-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/sliders/friction-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/sliders/friction-label.svg diff --git a/scripts/shapes/assets/tools/physics/sliders/gravity-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/physics/sliders/gravity-label.svg similarity index 100% rename from scripts/shapes/assets/tools/physics/sliders/gravity-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/physics/sliders/gravity-label.svg diff --git a/scripts/shapes/assets/tools/redo-icon.svg b/unpublishedScripts/marketplace/shapes/assets/tools/redo-icon.svg similarity index 100% rename from scripts/shapes/assets/tools/redo-icon.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/redo-icon.svg diff --git a/scripts/shapes/assets/tools/redo-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/redo-label.svg similarity index 100% rename from scripts/shapes/assets/tools/redo-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/redo-label.svg diff --git a/scripts/shapes/assets/tools/stretch-icon.svg b/unpublishedScripts/marketplace/shapes/assets/tools/stretch-icon.svg similarity index 100% rename from scripts/shapes/assets/tools/stretch-icon.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/stretch-icon.svg diff --git a/scripts/shapes/assets/tools/stretch-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/stretch-label.svg similarity index 100% rename from scripts/shapes/assets/tools/stretch-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/stretch-label.svg diff --git a/scripts/shapes/assets/tools/stretch-tool-heading.svg b/unpublishedScripts/marketplace/shapes/assets/tools/stretch-tool-heading.svg similarity index 100% rename from scripts/shapes/assets/tools/stretch-tool-heading.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/stretch-tool-heading.svg diff --git a/scripts/shapes/assets/tools/stretch/info-text.svg b/unpublishedScripts/marketplace/shapes/assets/tools/stretch/info-text.svg similarity index 100% rename from scripts/shapes/assets/tools/stretch/info-text.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/stretch/info-text.svg diff --git a/scripts/shapes/assets/tools/tool-icon.fbx b/unpublishedScripts/marketplace/shapes/assets/tools/tool-icon.fbx similarity index 100% rename from scripts/shapes/assets/tools/tool-icon.fbx rename to unpublishedScripts/marketplace/shapes/assets/tools/tool-icon.fbx diff --git a/scripts/shapes/assets/tools/tool-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/tool-label.svg similarity index 100% rename from scripts/shapes/assets/tools/tool-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/tool-label.svg diff --git a/scripts/shapes/assets/tools/tools-heading.svg b/unpublishedScripts/marketplace/shapes/assets/tools/tools-heading.svg similarity index 100% rename from scripts/shapes/assets/tools/tools-heading.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/tools-heading.svg diff --git a/scripts/shapes/assets/tools/undo-icon.svg b/unpublishedScripts/marketplace/shapes/assets/tools/undo-icon.svg similarity index 100% rename from scripts/shapes/assets/tools/undo-icon.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/undo-icon.svg diff --git a/scripts/shapes/assets/tools/undo-label.svg b/unpublishedScripts/marketplace/shapes/assets/tools/undo-label.svg similarity index 100% rename from scripts/shapes/assets/tools/undo-label.svg rename to unpublishedScripts/marketplace/shapes/assets/tools/undo-label.svg diff --git a/scripts/shapes/modules/createPalette.js b/unpublishedScripts/marketplace/shapes/modules/createPalette.js similarity index 100% rename from scripts/shapes/modules/createPalette.js rename to unpublishedScripts/marketplace/shapes/modules/createPalette.js diff --git a/scripts/shapes/modules/feedback.js b/unpublishedScripts/marketplace/shapes/modules/feedback.js similarity index 100% rename from scripts/shapes/modules/feedback.js rename to unpublishedScripts/marketplace/shapes/modules/feedback.js diff --git a/scripts/shapes/modules/groups.js b/unpublishedScripts/marketplace/shapes/modules/groups.js similarity index 100% rename from scripts/shapes/modules/groups.js rename to unpublishedScripts/marketplace/shapes/modules/groups.js diff --git a/scripts/shapes/modules/hand.js b/unpublishedScripts/marketplace/shapes/modules/hand.js similarity index 100% rename from scripts/shapes/modules/hand.js rename to unpublishedScripts/marketplace/shapes/modules/hand.js diff --git a/scripts/shapes/modules/handles.js b/unpublishedScripts/marketplace/shapes/modules/handles.js similarity index 100% rename from scripts/shapes/modules/handles.js rename to unpublishedScripts/marketplace/shapes/modules/handles.js diff --git a/scripts/shapes/modules/highlights.js b/unpublishedScripts/marketplace/shapes/modules/highlights.js similarity index 100% rename from scripts/shapes/modules/highlights.js rename to unpublishedScripts/marketplace/shapes/modules/highlights.js diff --git a/scripts/shapes/modules/history.js b/unpublishedScripts/marketplace/shapes/modules/history.js similarity index 100% rename from scripts/shapes/modules/history.js rename to unpublishedScripts/marketplace/shapes/modules/history.js diff --git a/scripts/shapes/modules/laser.js b/unpublishedScripts/marketplace/shapes/modules/laser.js similarity index 100% rename from scripts/shapes/modules/laser.js rename to unpublishedScripts/marketplace/shapes/modules/laser.js diff --git a/scripts/shapes/modules/selection.js b/unpublishedScripts/marketplace/shapes/modules/selection.js similarity index 100% rename from scripts/shapes/modules/selection.js rename to unpublishedScripts/marketplace/shapes/modules/selection.js diff --git a/scripts/shapes/modules/toolIcon.js b/unpublishedScripts/marketplace/shapes/modules/toolIcon.js similarity index 100% rename from scripts/shapes/modules/toolIcon.js rename to unpublishedScripts/marketplace/shapes/modules/toolIcon.js diff --git a/scripts/shapes/modules/toolsMenu.js b/unpublishedScripts/marketplace/shapes/modules/toolsMenu.js similarity index 100% rename from scripts/shapes/modules/toolsMenu.js rename to unpublishedScripts/marketplace/shapes/modules/toolsMenu.js diff --git a/scripts/shapes/modules/uit.js b/unpublishedScripts/marketplace/shapes/modules/uit.js similarity index 100% rename from scripts/shapes/modules/uit.js rename to unpublishedScripts/marketplace/shapes/modules/uit.js diff --git a/scripts/shapes/shapes.js b/unpublishedScripts/marketplace/shapes/shapes.js similarity index 100% rename from scripts/shapes/shapes.js rename to unpublishedScripts/marketplace/shapes/shapes.js diff --git a/scripts/shapes/utilities/utilities.js b/unpublishedScripts/marketplace/shapes/utilities/utilities.js similarity index 100% rename from scripts/shapes/utilities/utilities.js rename to unpublishedScripts/marketplace/shapes/utilities/utilities.js From 0f4e7fb53236b7e9cbaf5536e92b93dfdb0b4508 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 3 Oct 2017 18:03:25 -0700 Subject: [PATCH 718/722] workaround for bad physics polling on login --- interface/src/Application.cpp | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 0fc8c46cdc..f1e1f918e2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5639,10 +5639,10 @@ bool Application::nearbyEntitiesAreReadyForPhysics() { return false; } + AABox avatarBox(getMyAvatar()->getPosition() - glm::vec3(0.5f * PHYSICS_READY_RANGE), glm::vec3(PHYSICS_READY_RANGE)); QVector entities; entityTree->withReadLock([&] { - AABox box(getMyAvatar()->getPosition() - glm::vec3(PHYSICS_READY_RANGE), glm::vec3(2 * PHYSICS_READY_RANGE)); - entityTree->findEntities(box, entities); + entityTree->findEntities(avatarBox, entities); }); // For reasons I haven't found, we don't necessarily have the full scene when we receive a stats packet. Apply @@ -5662,11 +5662,18 @@ bool Application::nearbyEntitiesAreReadyForPhysics() { bool result = true; foreach (EntityItemPointer entity, entities) { if (entity->shouldBePhysical() && !entity->isReadyToComputeShape()) { - static QString repeatedMessage = - LogHandler::getInstance().addRepeatedMessageRegex("Physics disabled until entity loads: .*"); - qCDebug(interfaceapp) << "Physics disabled until entity loads: " << entity->getID() << entity->getName(); - // don't break here because we want all the relevant entities to start their downloads - result = false; + // BUG: the findEntities() query above is sometimes returning objects that don't actually overlap + // TODO: investigate and fix findQueries() but in the meantime... + // WORKAROUND: test the overlap of each entity to verify it matters + bool success = false; + AACube entityCube = entity->getQueryAACube(success); + if (success && avatarBox.touches(entityCube)) { + static QString repeatedMessage = + LogHandler::getInstance().addRepeatedMessageRegex("Physics disabled until entity loads: .*"); + qCDebug(interfaceapp) << "Physics disabled until entity loads: " << entity->getID() << entity->getName(); + // don't break here because we want all the relevant entities to start their downloads + result = false; + } } } return result; From 0bcecdbe668466b09e209c40c3ca70b331f89292 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 5 Oct 2017 11:17:40 -0700 Subject: [PATCH 719/722] be picky when finding nearby entities at login --- interface/src/Application.cpp | 49 +++++++++++++------- libraries/entities/src/EntityTree.cpp | 6 +++ libraries/entities/src/EntityTree.h | 5 ++ libraries/entities/src/EntityTreeElement.cpp | 8 ++++ libraries/entities/src/EntityTreeElement.h | 6 +++ 5 files changed, 58 insertions(+), 16 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f1e1f918e2..835a2fed56 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5639,14 +5639,38 @@ bool Application::nearbyEntitiesAreReadyForPhysics() { return false; } - AABox avatarBox(getMyAvatar()->getPosition() - glm::vec3(0.5f * PHYSICS_READY_RANGE), glm::vec3(PHYSICS_READY_RANGE)); + // We don't want to use EntityTree::findEntities(AABox, ...) method because that scan will snarf parented entities + // whose bounding boxes cannot be computed (it is too loose for our purposes here). Instead we manufacture + // custom filters and use the general-purpose EntityTree::findEntities(filter, ...) QVector entities; entityTree->withReadLock([&] { - entityTree->findEntities(avatarBox, entities); + AABox avatarBox(getMyAvatar()->getPosition() - glm::vec3(PHYSICS_READY_RANGE), glm::vec3(2 * PHYSICS_READY_RANGE)); + + // create two functions that use avatarBox (entityScan and elementScan), the second calls the first + std::function entityScan = [=](EntityItemPointer& entity) { + if (entity->shouldBePhysical()) { + bool success = false; + AABox entityBox = entity->getAABox(success); + // important: bail for entities that cannot supply a valid AABox + return success && avatarBox.touches(entityBox); + } + return false; + }; + std::function elementScan = [&](const OctreeElementPointer& element, void* unused) { + if (element->getAACube().touches(avatarBox)) { + EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); + entityTreeElement->getEntities(entityScan, entities); + return true; + } + return false; + }; + + // Pass the second function to the general-purpose EntityTree::findEntities() + // which will traverse the tree, apply the two filter functions (to element, then to entities) + // as it traverses. The end result will be a list of entities that match. + entityTree->findEntities(elementScan, entities); }); - // For reasons I haven't found, we don't necessarily have the full scene when we receive a stats packet. Apply - // a heuristic to try to decide when we actually know about all of the nearby entities. uint32_t nearbyCount = entities.size(); if (nearbyCount == _nearbyEntitiesCountAtLastPhysicsCheck) { _nearbyEntitiesStabilityCount++; @@ -5662,18 +5686,11 @@ bool Application::nearbyEntitiesAreReadyForPhysics() { bool result = true; foreach (EntityItemPointer entity, entities) { if (entity->shouldBePhysical() && !entity->isReadyToComputeShape()) { - // BUG: the findEntities() query above is sometimes returning objects that don't actually overlap - // TODO: investigate and fix findQueries() but in the meantime... - // WORKAROUND: test the overlap of each entity to verify it matters - bool success = false; - AACube entityCube = entity->getQueryAACube(success); - if (success && avatarBox.touches(entityCube)) { - static QString repeatedMessage = - LogHandler::getInstance().addRepeatedMessageRegex("Physics disabled until entity loads: .*"); - qCDebug(interfaceapp) << "Physics disabled until entity loads: " << entity->getID() << entity->getName(); - // don't break here because we want all the relevant entities to start their downloads - result = false; - } + static QString repeatedMessage = + LogHandler::getInstance().addRepeatedMessageRegex("Physics disabled until entity loads: .*"); + qCDebug(interfaceapp) << "Physics disabled until entity loads: " << entity->getID() << entity->getName(); + // don't break here because we want all the relevant entities to start their downloads + result = false; } } return result; diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index bf37a08386..fa386ae090 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -864,6 +864,12 @@ void EntityTree::findEntities(const ViewFrustum& frustum, QVector& foundEntities) { + recurseTreeWithOperation(elementFilter, nullptr); +} + EntityItemPointer EntityTree::findEntityByID(const QUuid& id) { EntityItemID entityID(id); return findEntityByEntityItemID(entityID); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index d0448f438a..53e36bc7c7 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -165,6 +165,11 @@ public: /// \param foundEntities[out] vector of EntityItemPointer void findEntities(const ViewFrustum& frustum, QVector& foundEntities); + /// finds all entities that match scanOperator + /// \parameter scanOperator function that scans entities that match criteria + /// \parameter foundEntities[out] vector of EntityItemPointer + void findEntities(RecurseOctreeOperation& scanOperator, QVector& foundEntities); + void addNewlyCreatedHook(NewlyCreatedEntityHook* hook); void removeNewlyCreatedHook(NewlyCreatedEntityHook* hook); diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 0c33855a61..2696377028 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -869,6 +869,14 @@ void EntityTreeElement::getEntities(const ViewFrustum& frustum, QVector& foundEntities) { + forEachEntity([&](EntityItemPointer entity) { + if (filter(entity)) { + foundEntities.push_back(entity); + } + }); +} + EntityItemPointer EntityTreeElement::getEntityWithEntityItemID(const EntityItemID& id) const { EntityItemPointer foundEntity = NULL; withReadLock([&] { diff --git a/libraries/entities/src/EntityTreeElement.h b/libraries/entities/src/EntityTreeElement.h index c7fb80c330..cafae9941a 100644 --- a/libraries/entities/src/EntityTreeElement.h +++ b/libraries/entities/src/EntityTreeElement.h @@ -27,6 +27,7 @@ class EntityTreeElement; using EntityItems = QVector; using EntityTreeElementWeakPointer = std::weak_ptr; using EntityTreeElementPointer = std::shared_ptr; +using EntityItemFilter = std::function; class EntityTreeUpdateArgs { public: @@ -199,6 +200,11 @@ public: /// \param entities[out] vector of non-const EntityItemPointer void getEntities(const ViewFrustum& frustum, QVector& foundEntities); + /// finds all entities that match filter + /// \param filter function that adds matching entities to foundEntities + /// \param entities[out] vector of non-const EntityItemPointer + void getEntities(EntityItemFilter& filter, QVector& foundEntities); + EntityItemPointer getEntityWithID(uint32_t id) const; EntityItemPointer getEntityWithEntityItemID(const EntityItemID& id) const; void getEntitiesInside(const AACube& box, QVector& foundEntities); From bbde1bcd632767466478fdc001350f7257f5cd80 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 9 Oct 2017 10:42:42 -0700 Subject: [PATCH 720/722] move stuff out of writelock when possible --- interface/src/Application.cpp | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 835a2fed56..527e3607f5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5643,20 +5643,18 @@ bool Application::nearbyEntitiesAreReadyForPhysics() { // whose bounding boxes cannot be computed (it is too loose for our purposes here). Instead we manufacture // custom filters and use the general-purpose EntityTree::findEntities(filter, ...) QVector entities; - entityTree->withReadLock([&] { - AABox avatarBox(getMyAvatar()->getPosition() - glm::vec3(PHYSICS_READY_RANGE), glm::vec3(2 * PHYSICS_READY_RANGE)); - - // create two functions that use avatarBox (entityScan and elementScan), the second calls the first - std::function entityScan = [=](EntityItemPointer& entity) { - if (entity->shouldBePhysical()) { - bool success = false; - AABox entityBox = entity->getAABox(success); - // important: bail for entities that cannot supply a valid AABox - return success && avatarBox.touches(entityBox); - } - return false; - }; - std::function elementScan = [&](const OctreeElementPointer& element, void* unused) { + AABox avatarBox(getMyAvatar()->getPosition() - glm::vec3(PHYSICS_READY_RANGE), glm::vec3(2 * PHYSICS_READY_RANGE)); + // create two functions that use avatarBox (entityScan and elementScan), the second calls the first + std::function entityScan = [=](EntityItemPointer& entity) { + if (entity->shouldBePhysical()) { + bool success = false; + AABox entityBox = entity->getAABox(success); + // important: bail for entities that cannot supply a valid AABox + return success && avatarBox.touches(entityBox); + } + return false; + }; + std::function elementScan = [&](const OctreeElementPointer& element, void* unused) { if (element->getAACube().touches(avatarBox)) { EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); entityTreeElement->getEntities(entityScan, entities); @@ -5665,6 +5663,7 @@ bool Application::nearbyEntitiesAreReadyForPhysics() { return false; }; + entityTree->withReadLock([&] { // Pass the second function to the general-purpose EntityTree::findEntities() // which will traverse the tree, apply the two filter functions (to element, then to entities) // as it traverses. The end result will be a list of entities that match. From cd97ab0fdfd4f23ee5d90d37c979590a377c2dc7 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Mon, 9 Oct 2017 11:39:57 -0700 Subject: [PATCH 721/722] fixed looping sample sound bug --- .../qml/hifi/audio/PlaySampleSound.qml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/hifi/audio/PlaySampleSound.qml b/interface/resources/qml/hifi/audio/PlaySampleSound.qml index 99f3648ec3..fdf579420d 100644 --- a/interface/resources/qml/hifi/audio/PlaySampleSound.qml +++ b/interface/resources/qml/hifi/audio/PlaySampleSound.qml @@ -29,12 +29,22 @@ RowLayout { function playSound() { // FIXME: MyAvatar is not properly exposed to QML; MyAvatar.qmlPosition is a stopgap // FIXME: Audio.playSystemSound should not require position - sample = Audio.playSystemSound(sound, MyAvatar.qmlPosition); - isPlaying = true; - sample.finished.connect(function() { isPlaying = false; sample = null; }); + if (sample === null && !isPlaying) { + sample = Audio.playSystemSound(sound, MyAvatar.qmlPosition); + isPlaying = true; + sample.finished.connect(reset); + } } function stopSound() { - sample && sample.stop(); + if (sample && isPlaying) { + sample.stop(); + } + } + + function reset() { + sample.finished.disconnect(reset); + isPlaying = false; + sample = null; } Component.onCompleted: createSampleSound(); From ee02fa9a91ffb14aa5f04a9f34013f885e9d437f Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 9 Oct 2017 13:36:38 -0700 Subject: [PATCH 722/722] model asks for renderUpdate if animating (cherry picked from commit 873ae9b9d650244fd1bb303492759c92912b3ca0) --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 064eacdb35..9120cd1788 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1240,6 +1240,7 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce mapJoints(entity, model->getJointNames()); } animate(entity); + emit requestRenderUpdate(); } }

  • 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000000..344907f039 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #ffffff + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..5d6a4c1b99 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + TestApp + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..033324ac58 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000000..77c3dd498c --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,91 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.3.3' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +task extractQt5jars(type: Copy) { + from fileTree(QT5_ROOT + "/jar") + into("${project.rootDir}/libraries/jar") + include("*.jar") +} + +task extractQt5so(type: Copy) { + from fileTree(QT5_ROOT + "/lib") + into("${project.rootDir}/libraries/jni/armeabi-v7a/") + include("libQt5AndroidExtras.so") + include("libQt5Concurrent.so") + include("libQt5Core.so") + include("libQt5Gamepad.so") + include("libQt5Gui.so") + include("libQt5Location.so") + include("libQt5Multimedia.so") + include("libQt5MultimediaQuick_p.so") + include("libQt5Network.so") + include("libQt5NetworkAuth.so") + include("libQt5OpenGL.so") + include("libQt5Positioning.so") + include("libQt5Qml.so") + include("libQt5Quick.so") + include("libQt5QuickControls2.so") + include("libQt5QuickParticles.so") + include("libQt5QuickTemplates2.so") + include("libQt5QuickWidgets.so") + include("libQt5Script.so") + include("libQt5ScriptTools.so") + include("libQt5Sensors.so") + include("libQt5Svg.so") + include("libQt5WebChannel.so") + include("libQt5WebSockets.so") + include("libQt5WebView.so") + include("libQt5Widgets.so") + include("libQt5Xml.so") + include("libQt5XmlPatterns.so") +} + +task extractAudioSo(type: Copy) { + from zipTree(GVR_ROOT + "/libraries/sdk-audio-1.80.0.aar") + into "${project.rootDir}/libraries/" + include "jni/armeabi-v7a/libgvr_audio.so" +} + +task extractGvrSo(type: Copy) { + from zipTree(GVR_ROOT + "/libraries/sdk-base-1.80.0.aar") + into "${project.rootDir}/libraries/" + include "jni/armeabi-v7a/libgvr.so" +} + +task extractNdk { } +extractNdk.dependsOn extractAudioSo +extractNdk.dependsOn extractGvrSo + +task extractQt5 { } +extractQt5.dependsOn extractQt5so +extractQt5.dependsOn extractQt5jars + +task extractBinaries { } +extractBinaries.dependsOn extractQt5 +extractBinaries.dependsOn extractNdk + +task deleteBinaries(type: Delete) { + delete "${project.rootDir}/libraries/jni" +} + +//clean.dependsOn(deleteBinaries) diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000000..aac7c9b461 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000000..e7b4def49c --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/cmake/android/AndroidManifest.xml.in b/cmake/android/AndroidManifest.xml.in deleted file mode 100755 index aa834f3384..0000000000 --- a/cmake/android/AndroidManifest.xml.in +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ${ANDROID_EXTRA_ACTIVITY_XML} - - - - - - ${ANDROID_EXTRA_APPLICATION_XML} - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/cmake/android/QtCreateAPK.cmake b/cmake/android/QtCreateAPK.cmake deleted file mode 100644 index 30ee2f57bd..0000000000 --- a/cmake/android/QtCreateAPK.cmake +++ /dev/null @@ -1,159 +0,0 @@ -# -# QtCreateAPK.cmake -# -# Created by Stephen Birarda on 11/18/14. -# 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 -# - -# -# OPTIONS -# These options will modify how QtCreateAPK behaves. May be useful if somebody wants to fork. -# For High Fidelity purposes these should not need to be changed. -# -set(ANDROID_THIS_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}) # Directory this CMake file is in - -if (POLICY CMP0026) - cmake_policy(SET CMP0026 OLD) -endif () - -macro(qt_create_apk) - if(ANDROID_APK_FULLSCREEN) - set(ANDROID_APK_THEME "android:theme=\"@android:style/Theme.NoTitleBar.Fullscreen\"") - else() - set(ANDROID_APK_THEME "") - endif() - - if (UPPER_CMAKE_BUILD_TYPE MATCHES RELEASE) - set(ANDROID_APK_DEBUGGABLE "false") - set(ANDROID_APK_RELEASE_LOCAL ${ANDROID_APK_RELEASE}) - else () - set(ANDROID_APK_DEBUGGABLE "true") - set(ANDROID_APK_RELEASE_LOCAL "0") - endif () - - # Create "AndroidManifest.xml" - configure_file("${ANDROID_THIS_DIRECTORY}/AndroidManifest.xml.in" "${ANDROID_APK_BUILD_DIR}/AndroidManifest.xml") - - # create "strings.xml" - configure_file("${ANDROID_THIS_DIRECTORY}/strings.xml.in" "${ANDROID_APK_BUILD_DIR}/res/values/strings.xml") - - # find androiddeployqt - find_program(ANDROID_DEPLOY_QT androiddeployqt HINTS "${QT_DIR}/bin") - - # set the path to our app shared library - set(EXECUTABLE_DESTINATION_PATH "${ANDROID_APK_OUTPUT_DIR}/libs/${ANDROID_ABI}/lib${TARGET_NAME}.so") - - # add our dependencies to the deployment file - get_property(_DEPENDENCIES TARGET ${TARGET_NAME} PROPERTY INTERFACE_LINK_LIBRARIES) - - foreach(_IGNORE_COPY IN LISTS IGNORE_COPY_LIBS) - list(REMOVE_ITEM _DEPENDENCIES ${_IGNORE_COPY}) - endforeach() - - foreach(_DEP IN LISTS _DEPENDENCIES) - if (NOT TARGET ${_DEP}) - list(APPEND _DEPS_LIST ${_DEP}) - else () - if(NOT _DEP MATCHES "Qt5::.*") - get_property(_DEP_LOCATION TARGET ${_DEP} PROPERTY "LOCATION_${CMAKE_BUILD_TYPE}") - - # recurisvely add libraries which are dependencies of this target - get_property(_DEP_DEPENDENCIES TARGET ${_DEP} PROPERTY INTERFACE_LINK_LIBRARIES) - - foreach(_SUB_DEP IN LISTS _DEP_DEPENDENCIES) - if (NOT TARGET ${_SUB_DEP} AND NOT _SUB_DEP MATCHES "Qt5::.*") - list(APPEND _DEPS_LIST ${_SUB_DEP}) - endif() - endforeach() - - list(APPEND _DEPS_LIST ${_DEP_LOCATION}) - endif() - endif () - endforeach() - - list(REMOVE_DUPLICATES _DEPS_LIST) - - # just copy static libs to apk libs folder - don't add to deps list - foreach(_LOCATED_DEP IN LISTS _DEPS_LIST) - if (_LOCATED_DEP MATCHES "\\.a$") - add_custom_command( - TARGET ${TARGET_NAME} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy ${_LOCATED_DEP} "${ANDROID_APK_OUTPUT_DIR}/libs/${ANDROID_ABI}" - ) - list(REMOVE_ITEM _DEPS_LIST ${_LOCATED_DEP}) - endif () - endforeach() - - string(REPLACE ";" "," _DEPS "${_DEPS_LIST}") - - configure_file("${ANDROID_THIS_DIRECTORY}/deployment-file.json.in" "${TARGET_NAME}-deployment.json") - - # copy the res folder from the target to the apk build dir - add_custom_target( - ${TARGET_NAME}-copy-res - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/res" "${ANDROID_APK_BUILD_DIR}/res" - ) - - # copy the assets folder from the target to the apk build dir - add_custom_target( - ${TARGET_NAME}-copy-assets - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/assets" "${ANDROID_APK_BUILD_DIR}/assets" - ) - - # copy the java folder from src to the apk build dir - add_custom_target( - ${TARGET_NAME}-copy-java - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/src/java" "${ANDROID_APK_BUILD_DIR}/src" - ) - - # copy the libs folder from src to the apk build dir - add_custom_target( - ${TARGET_NAME}-copy-libs - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/libs" "${ANDROID_APK_BUILD_DIR}/libs" - ) - - # handle setup for ndk-gdb - add_custom_target(${TARGET_NAME}-gdb DEPENDS ${TARGET_NAME}) - - if (ANDROID_APK_DEBUGGABLE) - get_property(TARGET_LOCATION TARGET ${TARGET_NAME} PROPERTY LOCATION) - - set(GDB_SOLIB_PATH ${ANDROID_APK_BUILD_DIR}/obj/local/${ANDROID_NDK_ABI_NAME}/) - - # generate essential Android Makefiles - file(WRITE ${ANDROID_APK_BUILD_DIR}/jni/Android.mk "APP_ABI := ${ANDROID_NDK_ABI_NAME}\n") - file(WRITE ${ANDROID_APK_BUILD_DIR}/jni/Application.mk "APP_ABI := ${ANDROID_NDK_ABI_NAME}\n") - - # create gdb.setup - get_directory_property(PROJECT_INCLUDES DIRECTORY ${PROJECT_SOURCE_DIR} INCLUDE_DIRECTORIES) - string(REGEX REPLACE ";" " " PROJECT_INCLUDES "${PROJECT_INCLUDES}") - file(WRITE ${ANDROID_APK_BUILD_DIR}/libs/${ANDROID_NDK_ABI_NAME}/gdb.setup "set solib-search-path ${GDB_SOLIB_PATH}\n") - file(APPEND ${ANDROID_APK_BUILD_DIR}/libs/${ANDROID_NDK_ABI_NAME}/gdb.setup "directory ${PROJECT_INCLUDES}\n") - - # copy lib to obj - add_custom_command(TARGET ${TARGET_NAME}-gdb PRE_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${GDB_SOLIB_PATH}) - add_custom_command(TARGET ${TARGET_NAME}-gdb PRE_BUILD COMMAND cp ${TARGET_LOCATION} ${GDB_SOLIB_PATH}) - - # strip symbols - add_custom_command(TARGET ${TARGET_NAME}-gdb PRE_BUILD COMMAND ${CMAKE_STRIP} ${TARGET_LOCATION}) - endif () - - # use androiddeployqt to create the apk - add_custom_target(${TARGET_NAME}-apk - COMMAND ${ANDROID_DEPLOY_QT} --input "${TARGET_NAME}-deployment.json" --output "${ANDROID_APK_OUTPUT_DIR}" --android-platform android-${ANDROID_API_LEVEL} ${ANDROID_DEPLOY_QT_INSTALL} --verbose --deployment bundled "\\$(ARGS)" - DEPENDS ${TARGET_NAME} ${TARGET_NAME}-copy-res ${TARGET_NAME}-copy-assets ${TARGET_NAME}-copy-java ${TARGET_NAME}-copy-libs ${TARGET_NAME}-gdb - ) - - # rename the APK if the caller asked us to - if (ANDROID_APK_CUSTOM_NAME) - add_custom_command( - TARGET ${TARGET_NAME}-apk - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E rename "${ANDROID_APK_OUTPUT_DIR}/bin/QtApp-debug.apk" "${ANDROID_APK_OUTPUT_DIR}/bin/${ANDROID_APK_CUSTOM_NAME}" - ) - endif () -endmacro() \ No newline at end of file diff --git a/cmake/android/android.toolchain.cmake b/cmake/android/android.toolchain.cmake deleted file mode 100755 index 806cef6b18..0000000000 --- a/cmake/android/android.toolchain.cmake +++ /dev/null @@ -1,1725 +0,0 @@ -# Copyright (c) 2010-2011, Ethan Rublee -# Copyright (c) 2011-2014, Andrey Kamaev -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# 3. Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived from this -# software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -# ------------------------------------------------------------------------------ -# Android CMake toolchain file, for use with the Android NDK r5-r10d -# Requires cmake 2.6.3 or newer (2.8.9 or newer is recommended). -# See home page: https://github.com/taka-no-me/android-cmake -# -# Usage Linux: -# $ export ANDROID_NDK=/absolute/path/to/the/android-ndk -# $ mkdir build && cd build -# $ cmake -DCMAKE_TOOLCHAIN_FILE=path/to/the/android.toolchain.cmake .. -# $ make -j8 -# -# Usage Windows: -# You need native port of make to build your project. -# Android NDK r7 (and newer) already has make.exe on board. -# For older NDK you have to install it separately. -# For example, this one: http://gnuwin32.sourceforge.net/packages/make.htm -# -# $ SET ANDROID_NDK=C:\absolute\path\to\the\android-ndk -# $ mkdir build && cd build -# $ cmake.exe -G"MinGW Makefiles" -# -DCMAKE_TOOLCHAIN_FILE=path\to\the\android.toolchain.cmake -# -DCMAKE_MAKE_PROGRAM="%ANDROID_NDK%\prebuilt\windows\bin\make.exe" .. -# $ cmake.exe --build . -# -# -# Options (can be set as cmake parameters: -D=): -# ANDROID_NDK=/opt/android-ndk - path to the NDK root. -# Can be set as environment variable. Can be set only at first cmake run. -# -# ANDROID_ABI=armeabi-v7a - specifies the target Application Binary -# Interface (ABI). This option nearly matches to the APP_ABI variable -# used by ndk-build tool from Android NDK. -# -# Possible targets are: -# "armeabi" - ARMv5TE based CPU with software floating point operations -# "armeabi-v7a" - ARMv7 based devices with hardware FPU instructions -# this ABI target is used by default -# "armeabi-v7a with NEON" - same as armeabi-v7a, but -# sets NEON as floating-point unit -# "armeabi-v7a with VFPV3" - same as armeabi-v7a, but -# sets VFPV3 as floating-point unit (has 32 registers instead of 16) -# "armeabi-v6 with VFP" - tuned for ARMv6 processors having VFP -# "x86" - IA-32 instruction set -# "mips" - MIPS32 instruction set -# -# 64-bit ABIs for NDK r10 and newer: -# "arm64-v8a" - ARMv8 AArch64 instruction set -# "x86_64" - Intel64 instruction set (r1) -# "mips64" - MIPS64 instruction set (r6) -# -# ANDROID_NATIVE_API_LEVEL=android-8 - level of Android API compile for. -# Option is read-only when standalone toolchain is used. -# Note: building for "android-L" requires explicit configuration. -# -# ANDROID_TOOLCHAIN_NAME=arm-linux-androideabi-4.9 - the name of compiler -# toolchain to be used. The list of possible values depends on the NDK -# version. For NDK r10c the possible values are: -# -# * aarch64-linux-android-4.9 -# * aarch64-linux-android-clang3.4 -# * aarch64-linux-android-clang3.5 -# * arm-linux-androideabi-4.6 -# * arm-linux-androideabi-4.8 -# * arm-linux-androideabi-4.9 (default) -# * arm-linux-androideabi-clang3.4 -# * arm-linux-androideabi-clang3.5 -# * mips64el-linux-android-4.9 -# * mips64el-linux-android-clang3.4 -# * mips64el-linux-android-clang3.5 -# * mipsel-linux-android-4.6 -# * mipsel-linux-android-4.8 -# * mipsel-linux-android-4.9 -# * mipsel-linux-android-clang3.4 -# * mipsel-linux-android-clang3.5 -# * x86-4.6 -# * x86-4.8 -# * x86-4.9 -# * x86-clang3.4 -# * x86-clang3.5 -# * x86_64-4.9 -# * x86_64-clang3.4 -# * x86_64-clang3.5 -# -# ANDROID_FORCE_ARM_BUILD=OFF - set ON to generate 32-bit ARM instructions -# instead of Thumb. Is not available for "armeabi-v6 with VFP" -# (is forced to be ON) ABI. -# -# ANDROID_NO_UNDEFINED=ON - set ON to show all undefined symbols as linker -# errors even if they are not used. -# -# ANDROID_SO_UNDEFINED=OFF - set ON to allow undefined symbols in shared -# libraries. Automatically turned for NDK r5x and r6x due to GLESv2 -# problems. -# -# ANDROID_STL=gnustl_static - specify the runtime to use. -# -# Possible values are: -# none -> Do not configure the runtime. -# system -> Use the default minimal system C++ runtime library. -# Implies -fno-rtti -fno-exceptions. -# Is not available for standalone toolchain. -# system_re -> Use the default minimal system C++ runtime library. -# Implies -frtti -fexceptions. -# Is not available for standalone toolchain. -# gabi++_static -> Use the GAbi++ runtime as a static library. -# Implies -frtti -fno-exceptions. -# Available for NDK r7 and newer. -# Is not available for standalone toolchain. -# gabi++_shared -> Use the GAbi++ runtime as a shared library. -# Implies -frtti -fno-exceptions. -# Available for NDK r7 and newer. -# Is not available for standalone toolchain. -# stlport_static -> Use the STLport runtime as a static library. -# Implies -fno-rtti -fno-exceptions for NDK before r7. -# Implies -frtti -fno-exceptions for NDK r7 and newer. -# Is not available for standalone toolchain. -# stlport_shared -> Use the STLport runtime as a shared library. -# Implies -fno-rtti -fno-exceptions for NDK before r7. -# Implies -frtti -fno-exceptions for NDK r7 and newer. -# Is not available for standalone toolchain. -# gnustl_static -> Use the GNU STL as a static library. -# Implies -frtti -fexceptions. -# gnustl_shared -> Use the GNU STL as a shared library. -# Implies -frtti -fno-exceptions. -# Available for NDK r7b and newer. -# Silently degrades to gnustl_static if not available. -# c++_static -> Use the LLVM libc++ runtime as a static library. -# c++_shared -> Use the LLVM libc++ runtime as a shared library. -# -# ANDROID_STL_FORCE_FEATURES=ON - turn rtti and exceptions support based on -# chosen runtime. If disabled, then the user is responsible for settings -# these options. -# -# What?: -# android-cmake toolchain searches for NDK/toolchain in the following order: -# ANDROID_NDK - cmake parameter -# ANDROID_NDK - environment variable -# ANDROID_STANDALONE_TOOLCHAIN - cmake parameter -# ANDROID_STANDALONE_TOOLCHAIN - environment variable -# ANDROID_NDK - default locations -# ANDROID_STANDALONE_TOOLCHAIN - default locations -# -# Make sure to do the following in your scripts: -# SET( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${my_cxx_flags}" ) -# SET( CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${my_cxx_flags}" ) -# The flags will be prepopulated with critical flags, so don't loose them. -# Also be aware that toolchain also sets configuration-specific compiler -# flags and linker flags. -# -# ANDROID and BUILD_ANDROID will be set to true, you may test any of these -# variables to make necessary Android-specific configuration changes. -# -# Also ARMEABI or ARMEABI_V7A or X86 or MIPS or ARM64_V8A or X86_64 or MIPS64 -# will be set true, mutually exclusive. NEON option will be set true -# if VFP is set to NEON. -# -# ------------------------------------------------------------------------------ - -cmake_minimum_required( VERSION 2.6.3 ) - -if( DEFINED CMAKE_CROSSCOMPILING ) - # subsequent toolchain loading is not really needed - return() -endif() - -if( CMAKE_TOOLCHAIN_FILE ) - # touch toolchain variable to suppress "unused variable" warning -endif() - -# inherit settings in recursive loads -get_property( _CMAKE_IN_TRY_COMPILE GLOBAL PROPERTY IN_TRY_COMPILE ) -if( _CMAKE_IN_TRY_COMPILE ) - include( "${CMAKE_CURRENT_SOURCE_DIR}/../android.toolchain.config.cmake" OPTIONAL ) -endif() - -# this one is important -if( CMAKE_VERSION VERSION_GREATER "3.0.99" ) - set( CMAKE_SYSTEM_NAME Android ) -else() - set( CMAKE_SYSTEM_NAME Linux ) -endif() - -# this one not so much -set( CMAKE_SYSTEM_VERSION 1 ) - -# rpath makes low sense for Android -set( CMAKE_SHARED_LIBRARY_RUNTIME_C_FLAG "" ) -set( CMAKE_SKIP_RPATH TRUE CACHE BOOL "If set, runtime paths are not added when using shared libraries." ) - -# NDK search paths -set( ANDROID_SUPPORTED_NDK_VERSIONS ${ANDROID_EXTRA_NDK_VERSIONS} -r10d -r10c -r10b -r10 -r9d -r9c -r9b -r9 -r8e -r8d -r8c -r8b -r8 -r7c -r7b -r7 -r6b -r6 -r5c -r5b -r5 "" ) -if( NOT DEFINED ANDROID_NDK_SEARCH_PATHS ) - if( CMAKE_HOST_WIN32 ) - file( TO_CMAKE_PATH "$ENV{PROGRAMFILES}" ANDROID_NDK_SEARCH_PATHS ) - set( ANDROID_NDK_SEARCH_PATHS "${ANDROID_NDK_SEARCH_PATHS}" "$ENV{SystemDrive}/NVPACK" ) - else() - file( TO_CMAKE_PATH "$ENV{HOME}" ANDROID_NDK_SEARCH_PATHS ) - set( ANDROID_NDK_SEARCH_PATHS /opt "${ANDROID_NDK_SEARCH_PATHS}/NVPACK" ) - endif() -endif() -if( NOT DEFINED ANDROID_STANDALONE_TOOLCHAIN_SEARCH_PATH ) - set( ANDROID_STANDALONE_TOOLCHAIN_SEARCH_PATH /opt/android-toolchain ) -endif() - -# known ABIs -set( ANDROID_SUPPORTED_ABIS_arm "armeabi-v7a;armeabi;armeabi-v7a with NEON;armeabi-v7a with VFPV3;armeabi-v6 with VFP" ) -set( ANDROID_SUPPORTED_ABIS_arm64 "arm64-v8a" ) -set( ANDROID_SUPPORTED_ABIS_x86 "x86" ) -set( ANDROID_SUPPORTED_ABIS_x86_64 "x86_64" ) -set( ANDROID_SUPPORTED_ABIS_mips "mips" ) -set( ANDROID_SUPPORTED_ABIS_mips64 "mips64" ) - -# API level defaults -set( ANDROID_DEFAULT_NDK_API_LEVEL 8 ) -set( ANDROID_DEFAULT_NDK_API_LEVEL_arm64 21 ) -set( ANDROID_DEFAULT_NDK_API_LEVEL_x86 9 ) -set( ANDROID_DEFAULT_NDK_API_LEVEL_x86_64 21 ) -set( ANDROID_DEFAULT_NDK_API_LEVEL_mips 9 ) -set( ANDROID_DEFAULT_NDK_API_LEVEL_mips64 21 ) - - -macro( __LIST_FILTER listvar regex ) - if( ${listvar} ) - foreach( __val ${${listvar}} ) - if( __val MATCHES "${regex}" ) - list( REMOVE_ITEM ${listvar} "${__val}" ) - endif() - endforeach() - endif() -endmacro() - -macro( __INIT_VARIABLE var_name ) - set( __test_path 0 ) - foreach( __var ${ARGN} ) - if( __var STREQUAL "PATH" ) - set( __test_path 1 ) - break() - endif() - endforeach() - - if( __test_path AND NOT EXISTS "${${var_name}}" ) - unset( ${var_name} CACHE ) - endif() - - if( " ${${var_name}}" STREQUAL " " ) - set( __values 0 ) - foreach( __var ${ARGN} ) - if( __var STREQUAL "VALUES" ) - set( __values 1 ) - elseif( NOT __var STREQUAL "PATH" ) - if( __var MATCHES "^ENV_.*$" ) - string( REPLACE "ENV_" "" __var "${__var}" ) - set( __value "$ENV{${__var}}" ) - elseif( DEFINED ${__var} ) - set( __value "${${__var}}" ) - elseif( __values ) - set( __value "${__var}" ) - else() - set( __value "" ) - endif() - - if( NOT " ${__value}" STREQUAL " " AND (NOT __test_path OR EXISTS "${__value}") ) - set( ${var_name} "${__value}" ) - break() - endif() - endif() - endforeach() - unset( __value ) - unset( __values ) - endif() - - if( __test_path ) - file( TO_CMAKE_PATH "${${var_name}}" ${var_name} ) - endif() - unset( __test_path ) -endmacro() - -macro( __DETECT_NATIVE_API_LEVEL _var _path ) - set( __ndkApiLevelRegex "^[\t ]*#define[\t ]+__ANDROID_API__[\t ]+([0-9]+)[\t ]*.*$" ) - file( STRINGS ${_path} __apiFileContent REGEX "${__ndkApiLevelRegex}" ) - if( NOT __apiFileContent ) - message( SEND_ERROR "Could not get Android native API level. Probably you have specified invalid level value, or your copy of NDK/toolchain is broken." ) - endif() - string( REGEX REPLACE "${__ndkApiLevelRegex}" "\\1" ${_var} "${__apiFileContent}" ) - unset( __apiFileContent ) - unset( __ndkApiLevelRegex ) -endmacro() - -macro( __DETECT_TOOLCHAIN_MACHINE_NAME _var _root ) - if( EXISTS "${_root}" ) - file( GLOB __gccExePath RELATIVE "${_root}/bin/" "${_root}/bin/*-gcc${TOOL_OS_SUFFIX}" ) - __LIST_FILTER( __gccExePath "^[.].*" ) - list( LENGTH __gccExePath __gccExePathsCount ) - if( NOT __gccExePathsCount EQUAL 1 AND NOT _CMAKE_IN_TRY_COMPILE ) - message( WARNING "Could not determine machine name for compiler from ${_root}" ) - set( ${_var} "" ) - else() - get_filename_component( __gccExeName "${__gccExePath}" NAME_WE ) - string( REPLACE "-gcc" "" ${_var} "${__gccExeName}" ) - endif() - unset( __gccExePath ) - unset( __gccExePathsCount ) - unset( __gccExeName ) - else() - set( ${_var} "" ) - endif() -endmacro() - - -# fight against cygwin -set( ANDROID_FORBID_SYGWIN TRUE CACHE BOOL "Prevent cmake from working under cygwin and using cygwin tools") -mark_as_advanced( ANDROID_FORBID_SYGWIN ) -if( ANDROID_FORBID_SYGWIN ) - if( CYGWIN ) - message( FATAL_ERROR "Android NDK and android-cmake toolchain are not welcome Cygwin. It is unlikely that this cmake toolchain will work under cygwin. But if you want to try then you can set cmake variable ANDROID_FORBID_SYGWIN to FALSE and rerun cmake." ) - endif() - - if( CMAKE_HOST_WIN32 ) - # remove cygwin from PATH - set( __new_path "$ENV{PATH}") - __LIST_FILTER( __new_path "cygwin" ) - set(ENV{PATH} "${__new_path}") - unset(__new_path) - endif() -endif() - - -# detect current host platform -if( NOT DEFINED ANDROID_NDK_HOST_X64 AND (CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "amd64|x86_64|AMD64" OR CMAKE_HOST_APPLE) ) - set( ANDROID_NDK_HOST_X64 1 CACHE BOOL "Try to use 64-bit compiler toolchain" ) - mark_as_advanced( ANDROID_NDK_HOST_X64 ) -endif() - -set( TOOL_OS_SUFFIX "" ) -if( CMAKE_HOST_APPLE ) - set( ANDROID_NDK_HOST_SYSTEM_NAME "darwin-x86_64" ) - set( ANDROID_NDK_HOST_SYSTEM_NAME2 "darwin-x86" ) -elseif( CMAKE_HOST_WIN32 ) - set( ANDROID_NDK_HOST_SYSTEM_NAME "windows-x86_64" ) - set( ANDROID_NDK_HOST_SYSTEM_NAME2 "windows" ) - set( TOOL_OS_SUFFIX ".exe" ) -elseif( CMAKE_HOST_UNIX ) - set( ANDROID_NDK_HOST_SYSTEM_NAME "linux-x86_64" ) - set( ANDROID_NDK_HOST_SYSTEM_NAME2 "linux-x86" ) -else() - message( FATAL_ERROR "Cross-compilation on your platform is not supported by this cmake toolchain" ) -endif() - -if( NOT ANDROID_NDK_HOST_X64 ) - set( ANDROID_NDK_HOST_SYSTEM_NAME ${ANDROID_NDK_HOST_SYSTEM_NAME2} ) -endif() - -# see if we have path to Android NDK -if( NOT ANDROID_NDK AND NOT ANDROID_STANDALONE_TOOLCHAIN ) - __INIT_VARIABLE( ANDROID_NDK PATH ENV_ANDROID_NDK ) -endif() -if( NOT ANDROID_NDK ) - # see if we have path to Android standalone toolchain - __INIT_VARIABLE( ANDROID_STANDALONE_TOOLCHAIN PATH ENV_ANDROID_STANDALONE_TOOLCHAIN ) - - if( NOT ANDROID_STANDALONE_TOOLCHAIN ) - #try to find Android NDK in one of the the default locations - set( __ndkSearchPaths ) - foreach( __ndkSearchPath ${ANDROID_NDK_SEARCH_PATHS} ) - foreach( suffix ${ANDROID_SUPPORTED_NDK_VERSIONS} ) - list( APPEND __ndkSearchPaths "${__ndkSearchPath}/android-ndk${suffix}" ) - endforeach() - endforeach() - __INIT_VARIABLE( ANDROID_NDK PATH VALUES ${__ndkSearchPaths} ) - unset( __ndkSearchPaths ) - - if( ANDROID_NDK ) - message( STATUS "Using default path for Android NDK: ${ANDROID_NDK}" ) - message( STATUS " If you prefer to use a different location, please define a cmake or environment variable: ANDROID_NDK" ) - else() - #try to find Android standalone toolchain in one of the the default locations - __INIT_VARIABLE( ANDROID_STANDALONE_TOOLCHAIN PATH ANDROID_STANDALONE_TOOLCHAIN_SEARCH_PATH ) - - if( ANDROID_STANDALONE_TOOLCHAIN ) - message( STATUS "Using default path for standalone toolchain ${ANDROID_STANDALONE_TOOLCHAIN}" ) - message( STATUS " If you prefer to use a different location, please define the variable: ANDROID_STANDALONE_TOOLCHAIN" ) - endif( ANDROID_STANDALONE_TOOLCHAIN ) - endif( ANDROID_NDK ) - endif( NOT ANDROID_STANDALONE_TOOLCHAIN ) -endif( NOT ANDROID_NDK ) - -# remember found paths -if( ANDROID_NDK ) - get_filename_component( ANDROID_NDK "${ANDROID_NDK}" ABSOLUTE ) - set( ANDROID_NDK "${ANDROID_NDK}" CACHE INTERNAL "Path of the Android NDK" FORCE ) - set( BUILD_WITH_ANDROID_NDK True ) - if( EXISTS "${ANDROID_NDK}/RELEASE.TXT" ) - file( STRINGS "${ANDROID_NDK}/RELEASE.TXT" ANDROID_NDK_RELEASE_FULL LIMIT_COUNT 1 REGEX "r[0-9]+[a-z]?" ) - string( REGEX MATCH "r([0-9]+)([a-z]?)" ANDROID_NDK_RELEASE "${ANDROID_NDK_RELEASE_FULL}" ) - else() - set( ANDROID_NDK_RELEASE "r1x" ) - set( ANDROID_NDK_RELEASE_FULL "unreleased" ) - endif() - string( REGEX REPLACE "r([0-9]+)([a-z]?)" "\\1*1000" ANDROID_NDK_RELEASE_NUM "${ANDROID_NDK_RELEASE}" ) - string( FIND " abcdefghijklmnopqastuvwxyz" "${CMAKE_MATCH_2}" __ndkReleaseLetterNum ) - math( EXPR ANDROID_NDK_RELEASE_NUM "${ANDROID_NDK_RELEASE_NUM}+${__ndkReleaseLetterNum}" ) -elseif( ANDROID_STANDALONE_TOOLCHAIN ) - get_filename_component( ANDROID_STANDALONE_TOOLCHAIN "${ANDROID_STANDALONE_TOOLCHAIN}" ABSOLUTE ) - # try to detect change - if( CMAKE_AR ) - string( LENGTH "${ANDROID_STANDALONE_TOOLCHAIN}" __length ) - string( SUBSTRING "${CMAKE_AR}" 0 ${__length} __androidStandaloneToolchainPreviousPath ) - if( NOT __androidStandaloneToolchainPreviousPath STREQUAL ANDROID_STANDALONE_TOOLCHAIN ) - message( FATAL_ERROR "It is not possible to change path to the Android standalone toolchain on subsequent run." ) - endif() - unset( __androidStandaloneToolchainPreviousPath ) - unset( __length ) - endif() - set( ANDROID_STANDALONE_TOOLCHAIN "${ANDROID_STANDALONE_TOOLCHAIN}" CACHE INTERNAL "Path of the Android standalone toolchain" FORCE ) - set( BUILD_WITH_STANDALONE_TOOLCHAIN True ) -else() - list(GET ANDROID_NDK_SEARCH_PATHS 0 ANDROID_NDK_SEARCH_PATH) - message( FATAL_ERROR "Could not find neither Android NDK nor Android standalone toolchain. - You should either set an environment variable: - export ANDROID_NDK=~/my-android-ndk - or - export ANDROID_STANDALONE_TOOLCHAIN=~/my-android-toolchain - or put the toolchain or NDK in the default path: - sudo ln -s ~/my-android-ndk ${ANDROID_NDK_SEARCH_PATH}/android-ndk - sudo ln -s ~/my-android-toolchain ${ANDROID_STANDALONE_TOOLCHAIN_SEARCH_PATH}" ) -endif() - -# android NDK layout -if( BUILD_WITH_ANDROID_NDK ) - if( NOT DEFINED ANDROID_NDK_LAYOUT ) - # try to automatically detect the layout - if( EXISTS "${ANDROID_NDK}/RELEASE.TXT") - set( ANDROID_NDK_LAYOUT "RELEASE" ) - elseif( EXISTS "${ANDROID_NDK}/../../linux-x86/toolchain/" ) - set( ANDROID_NDK_LAYOUT "LINARO" ) - elseif( EXISTS "${ANDROID_NDK}/../../gcc/" ) - set( ANDROID_NDK_LAYOUT "ANDROID" ) - endif() - endif() - set( ANDROID_NDK_LAYOUT "${ANDROID_NDK_LAYOUT}" CACHE STRING "The inner layout of NDK" ) - mark_as_advanced( ANDROID_NDK_LAYOUT ) - if( ANDROID_NDK_LAYOUT STREQUAL "LINARO" ) - set( ANDROID_NDK_HOST_SYSTEM_NAME ${ANDROID_NDK_HOST_SYSTEM_NAME2} ) # only 32-bit at the moment - set( ANDROID_NDK_TOOLCHAINS_PATH "${ANDROID_NDK}/../../${ANDROID_NDK_HOST_SYSTEM_NAME}/toolchain" ) - set( ANDROID_NDK_TOOLCHAINS_SUBPATH "" ) - set( ANDROID_NDK_TOOLCHAINS_SUBPATH2 "" ) - elseif( ANDROID_NDK_LAYOUT STREQUAL "ANDROID" ) - set( ANDROID_NDK_HOST_SYSTEM_NAME ${ANDROID_NDK_HOST_SYSTEM_NAME2} ) # only 32-bit at the moment - set( ANDROID_NDK_TOOLCHAINS_PATH "${ANDROID_NDK}/../../gcc/${ANDROID_NDK_HOST_SYSTEM_NAME}/arm" ) - set( ANDROID_NDK_TOOLCHAINS_SUBPATH "" ) - set( ANDROID_NDK_TOOLCHAINS_SUBPATH2 "" ) - else() # ANDROID_NDK_LAYOUT STREQUAL "RELEASE" - set( ANDROID_NDK_TOOLCHAINS_PATH "${ANDROID_NDK}/toolchains" ) - set( ANDROID_NDK_TOOLCHAINS_SUBPATH "/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}" ) - set( ANDROID_NDK_TOOLCHAINS_SUBPATH2 "/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME2}" ) - endif() - get_filename_component( ANDROID_NDK_TOOLCHAINS_PATH "${ANDROID_NDK_TOOLCHAINS_PATH}" ABSOLUTE ) - - # try to detect change of NDK - if( CMAKE_AR ) - string( LENGTH "${ANDROID_NDK_TOOLCHAINS_PATH}" __length ) - string( SUBSTRING "${CMAKE_AR}" 0 ${__length} __androidNdkPreviousPath ) - if( NOT __androidNdkPreviousPath STREQUAL ANDROID_NDK_TOOLCHAINS_PATH ) - message( FATAL_ERROR "It is not possible to change the path to the NDK on subsequent CMake run. You must remove all generated files from your build folder first. - " ) - endif() - unset( __androidNdkPreviousPath ) - unset( __length ) - endif() -endif() - - -# get all the details about standalone toolchain -if( BUILD_WITH_STANDALONE_TOOLCHAIN ) - __DETECT_NATIVE_API_LEVEL( ANDROID_SUPPORTED_NATIVE_API_LEVELS "${ANDROID_STANDALONE_TOOLCHAIN}/sysroot/usr/include/android/api-level.h" ) - set( ANDROID_STANDALONE_TOOLCHAIN_API_LEVEL ${ANDROID_SUPPORTED_NATIVE_API_LEVELS} ) - set( __availableToolchains "standalone" ) - __DETECT_TOOLCHAIN_MACHINE_NAME( __availableToolchainMachines "${ANDROID_STANDALONE_TOOLCHAIN}" ) - if( NOT __availableToolchainMachines ) - message( FATAL_ERROR "Could not determine machine name of your toolchain. Probably your Android standalone toolchain is broken." ) - endif() - if( __availableToolchainMachines MATCHES x86_64 ) - set( __availableToolchainArchs "x86_64" ) - elseif( __availableToolchainMachines MATCHES i686 ) - set( __availableToolchainArchs "x86" ) - elseif( __availableToolchainMachines MATCHES aarch64 ) - set( __availableToolchainArchs "arm64" ) - elseif( __availableToolchainMachines MATCHES arm ) - set( __availableToolchainArchs "arm" ) - elseif( __availableToolchainMachines MATCHES mips64el ) - set( __availableToolchainArchs "mips64" ) - elseif( __availableToolchainMachines MATCHES mipsel ) - set( __availableToolchainArchs "mips" ) - endif() - execute_process( COMMAND "${ANDROID_STANDALONE_TOOLCHAIN}/bin/${__availableToolchainMachines}-gcc${TOOL_OS_SUFFIX}" -dumpversion - OUTPUT_VARIABLE __availableToolchainCompilerVersions OUTPUT_STRIP_TRAILING_WHITESPACE ) - string( REGEX MATCH "[0-9]+[.][0-9]+([.][0-9]+)?" __availableToolchainCompilerVersions "${__availableToolchainCompilerVersions}" ) - if( EXISTS "${ANDROID_STANDALONE_TOOLCHAIN}/bin/clang${TOOL_OS_SUFFIX}" ) - list( APPEND __availableToolchains "standalone-clang" ) - list( APPEND __availableToolchainMachines ${__availableToolchainMachines} ) - list( APPEND __availableToolchainArchs ${__availableToolchainArchs} ) - list( APPEND __availableToolchainCompilerVersions ${__availableToolchainCompilerVersions} ) - endif() -endif() - -macro( __GLOB_NDK_TOOLCHAINS __availableToolchainsVar __availableToolchainsLst __toolchain_subpath ) - foreach( __toolchain ${${__availableToolchainsLst}} ) - if( "${__toolchain}" MATCHES "-clang3[.][0-9]$" AND NOT EXISTS "${ANDROID_NDK_TOOLCHAINS_PATH}/${__toolchain}${__toolchain_subpath}" ) - SET( __toolchainVersionRegex "^TOOLCHAIN_VERSION[\t ]+:=[\t ]+(.*)$" ) - FILE( STRINGS "${ANDROID_NDK_TOOLCHAINS_PATH}/${__toolchain}/setup.mk" __toolchainVersionStr REGEX "${__toolchainVersionRegex}" ) - if( __toolchainVersionStr ) - string( REGEX REPLACE "${__toolchainVersionRegex}" "\\1" __toolchainVersionStr "${__toolchainVersionStr}" ) - string( REGEX REPLACE "-clang3[.][0-9]$" "-${__toolchainVersionStr}" __gcc_toolchain "${__toolchain}" ) - else() - string( REGEX REPLACE "-clang3[.][0-9]$" "-4.6" __gcc_toolchain "${__toolchain}" ) - endif() - unset( __toolchainVersionStr ) - unset( __toolchainVersionRegex ) - else() - set( __gcc_toolchain "${__toolchain}" ) - endif() - __DETECT_TOOLCHAIN_MACHINE_NAME( __machine "${ANDROID_NDK_TOOLCHAINS_PATH}/${__gcc_toolchain}${__toolchain_subpath}" ) - if( __machine ) - string( REGEX MATCH "[0-9]+[.][0-9]+([.][0-9x]+)?$" __version "${__gcc_toolchain}" ) - if( __machine MATCHES x86_64 ) - set( __arch "x86_64" ) - elseif( __machine MATCHES i686 ) - set( __arch "x86" ) - elseif( __machine MATCHES aarch64 ) - set( __arch "arm64" ) - elseif( __machine MATCHES arm ) - set( __arch "arm" ) - elseif( __machine MATCHES mips64el ) - set( __arch "mips64" ) - elseif( __machine MATCHES mipsel ) - set( __arch "mips" ) - else() - set( __arch "" ) - endif() - #message("machine: !${__machine}!\narch: !${__arch}!\nversion: !${__version}!\ntoolchain: !${__toolchain}!\n") - if (__arch) - list( APPEND __availableToolchainMachines "${__machine}" ) - list( APPEND __availableToolchainArchs "${__arch}" ) - list( APPEND __availableToolchainCompilerVersions "${__version}" ) - list( APPEND ${__availableToolchainsVar} "${__toolchain}" ) - endif() - endif() - unset( __gcc_toolchain ) - endforeach() -endmacro() - -# get all the details about NDK -if( BUILD_WITH_ANDROID_NDK ) - file( GLOB ANDROID_SUPPORTED_NATIVE_API_LEVELS RELATIVE "${ANDROID_NDK}/platforms" "${ANDROID_NDK}/platforms/android-*" ) - string( REPLACE "android-" "" ANDROID_SUPPORTED_NATIVE_API_LEVELS "${ANDROID_SUPPORTED_NATIVE_API_LEVELS}" ) - set( __availableToolchains "" ) - set( __availableToolchainMachines "" ) - set( __availableToolchainArchs "" ) - set( __availableToolchainCompilerVersions "" ) - if( ANDROID_TOOLCHAIN_NAME AND EXISTS "${ANDROID_NDK_TOOLCHAINS_PATH}/${ANDROID_TOOLCHAIN_NAME}/" ) - # do not go through all toolchains if we know the name - set( __availableToolchainsLst "${ANDROID_TOOLCHAIN_NAME}" ) - __GLOB_NDK_TOOLCHAINS( __availableToolchains __availableToolchainsLst "${ANDROID_NDK_TOOLCHAINS_SUBPATH}" ) - if( NOT __availableToolchains AND NOT ANDROID_NDK_TOOLCHAINS_SUBPATH STREQUAL ANDROID_NDK_TOOLCHAINS_SUBPATH2 ) - __GLOB_NDK_TOOLCHAINS( __availableToolchains __availableToolchainsLst "${ANDROID_NDK_TOOLCHAINS_SUBPATH2}" ) - if( __availableToolchains ) - set( ANDROID_NDK_TOOLCHAINS_SUBPATH ${ANDROID_NDK_TOOLCHAINS_SUBPATH2} ) - endif() - endif() - endif() - if( NOT __availableToolchains ) - file( GLOB __availableToolchainsLst RELATIVE "${ANDROID_NDK_TOOLCHAINS_PATH}" "${ANDROID_NDK_TOOLCHAINS_PATH}/*" ) - if( __availableToolchainsLst ) - list(SORT __availableToolchainsLst) # we need clang to go after gcc - endif() - __LIST_FILTER( __availableToolchainsLst "^[.]" ) - __LIST_FILTER( __availableToolchainsLst "llvm" ) - __LIST_FILTER( __availableToolchainsLst "renderscript" ) - __GLOB_NDK_TOOLCHAINS( __availableToolchains __availableToolchainsLst "${ANDROID_NDK_TOOLCHAINS_SUBPATH}" ) - if( NOT __availableToolchains AND NOT ANDROID_NDK_TOOLCHAINS_SUBPATH STREQUAL ANDROID_NDK_TOOLCHAINS_SUBPATH2 ) - __GLOB_NDK_TOOLCHAINS( __availableToolchains __availableToolchainsLst "${ANDROID_NDK_TOOLCHAINS_SUBPATH2}" ) - if( __availableToolchains ) - set( ANDROID_NDK_TOOLCHAINS_SUBPATH ${ANDROID_NDK_TOOLCHAINS_SUBPATH2} ) - endif() - endif() - endif() - if( NOT __availableToolchains ) - message( FATAL_ERROR "Could not find any working toolchain in the NDK. Probably your Android NDK is broken." ) - endif() -endif() - -# build list of available ABIs -set( ANDROID_SUPPORTED_ABIS "" ) -set( __uniqToolchainArchNames ${__availableToolchainArchs} ) -list( REMOVE_DUPLICATES __uniqToolchainArchNames ) -list( SORT __uniqToolchainArchNames ) -foreach( __arch ${__uniqToolchainArchNames} ) - list( APPEND ANDROID_SUPPORTED_ABIS ${ANDROID_SUPPORTED_ABIS_${__arch}} ) -endforeach() -unset( __uniqToolchainArchNames ) -if( NOT ANDROID_SUPPORTED_ABIS ) - message( FATAL_ERROR "No one of known Android ABIs is supported by this cmake toolchain." ) -endif() - -# choose target ABI -__INIT_VARIABLE( ANDROID_ABI VALUES ${ANDROID_SUPPORTED_ABIS} ) -# verify that target ABI is supported -list( FIND ANDROID_SUPPORTED_ABIS "${ANDROID_ABI}" __androidAbiIdx ) -if( __androidAbiIdx EQUAL -1 ) - string( REPLACE ";" "\", \"" PRINTABLE_ANDROID_SUPPORTED_ABIS "${ANDROID_SUPPORTED_ABIS}" ) - message( FATAL_ERROR "Specified ANDROID_ABI = \"${ANDROID_ABI}\" is not supported by this cmake toolchain or your NDK/toolchain. - Supported values are: \"${PRINTABLE_ANDROID_SUPPORTED_ABIS}\" - " ) -endif() -unset( __androidAbiIdx ) - -# set target ABI options -if( ANDROID_ABI STREQUAL "x86" ) - set( X86 true ) - set( ANDROID_NDK_ABI_NAME "x86" ) - set( ANDROID_ARCH_NAME "x86" ) - set( ANDROID_LLVM_TRIPLE "i686-none-linux-android" ) - set( CMAKE_SYSTEM_PROCESSOR "i686" ) -elseif( ANDROID_ABI STREQUAL "x86_64" ) - set( X86 true ) - set( X86_64 true ) - set( ANDROID_NDK_ABI_NAME "x86_64" ) - set( ANDROID_ARCH_NAME "x86_64" ) - set( CMAKE_SYSTEM_PROCESSOR "x86_64" ) - set( ANDROID_LLVM_TRIPLE "x86_64-none-linux-android" ) -elseif( ANDROID_ABI STREQUAL "mips64" ) - set( MIPS64 true ) - set( ANDROID_NDK_ABI_NAME "mips64" ) - set( ANDROID_ARCH_NAME "mips64" ) - set( ANDROID_LLVM_TRIPLE "mips64el-none-linux-android" ) - set( CMAKE_SYSTEM_PROCESSOR "mips64" ) -elseif( ANDROID_ABI STREQUAL "mips" ) - set( MIPS true ) - set( ANDROID_NDK_ABI_NAME "mips" ) - set( ANDROID_ARCH_NAME "mips" ) - set( ANDROID_LLVM_TRIPLE "mipsel-none-linux-android" ) - set( CMAKE_SYSTEM_PROCESSOR "mips" ) -elseif( ANDROID_ABI STREQUAL "arm64-v8a" ) - set( ARM64_V8A true ) - set( ANDROID_NDK_ABI_NAME "arm64-v8a" ) - set( ANDROID_ARCH_NAME "arm64" ) - set( ANDROID_LLVM_TRIPLE "aarch64-none-linux-android" ) - set( CMAKE_SYSTEM_PROCESSOR "aarch64" ) - set( VFPV3 true ) - set( NEON true ) -elseif( ANDROID_ABI STREQUAL "armeabi" ) - set( ARMEABI true ) - set( ANDROID_NDK_ABI_NAME "armeabi" ) - set( ANDROID_ARCH_NAME "arm" ) - set( ANDROID_LLVM_TRIPLE "armv5te-none-linux-androideabi" ) - set( CMAKE_SYSTEM_PROCESSOR "armv5te" ) -elseif( ANDROID_ABI STREQUAL "armeabi-v6 with VFP" ) - set( ARMEABI_V6 true ) - set( ANDROID_NDK_ABI_NAME "armeabi" ) - set( ANDROID_ARCH_NAME "arm" ) - set( ANDROID_LLVM_TRIPLE "armv5te-none-linux-androideabi" ) - set( CMAKE_SYSTEM_PROCESSOR "armv6" ) - # need always fallback to older platform - set( ARMEABI true ) -elseif( ANDROID_ABI STREQUAL "armeabi-v7a") - set( ARMEABI_V7A true ) - set( ANDROID_NDK_ABI_NAME "armeabi-v7a" ) - set( ANDROID_ARCH_NAME "arm" ) - set( ANDROID_LLVM_TRIPLE "armv7-none-linux-androideabi" ) - set( CMAKE_SYSTEM_PROCESSOR "armv7-a" ) -elseif( ANDROID_ABI STREQUAL "armeabi-v7a with VFPV3" ) - set( ARMEABI_V7A true ) - set( ANDROID_NDK_ABI_NAME "armeabi-v7a" ) - set( ANDROID_ARCH_NAME "arm" ) - set( ANDROID_LLVM_TRIPLE "armv7-none-linux-androideabi" ) - set( CMAKE_SYSTEM_PROCESSOR "armv7-a" ) - set( VFPV3 true ) -elseif( ANDROID_ABI STREQUAL "armeabi-v7a with NEON" ) - set( ARMEABI_V7A true ) - set( ANDROID_NDK_ABI_NAME "armeabi-v7a" ) - set( ANDROID_ARCH_NAME "arm" ) - set( ANDROID_LLVM_TRIPLE "armv7-none-linux-androideabi" ) - set( CMAKE_SYSTEM_PROCESSOR "armv7-a" ) - set( VFPV3 true ) - set( NEON true ) -else() - message( SEND_ERROR "Unknown ANDROID_ABI=\"${ANDROID_ABI}\" is specified." ) -endif() - -if( CMAKE_BINARY_DIR AND EXISTS "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeSystem.cmake" ) - # really dirty hack - # it is not possible to change CMAKE_SYSTEM_PROCESSOR after the first run... - file( APPEND "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeSystem.cmake" "SET(CMAKE_SYSTEM_PROCESSOR \"${CMAKE_SYSTEM_PROCESSOR}\")\n" ) -endif() - -if( ANDROID_ARCH_NAME STREQUAL "arm" AND NOT ARMEABI_V6 ) - __INIT_VARIABLE( ANDROID_FORCE_ARM_BUILD VALUES OFF ) - set( ANDROID_FORCE_ARM_BUILD ${ANDROID_FORCE_ARM_BUILD} CACHE BOOL "Use 32-bit ARM instructions instead of Thumb-1" FORCE ) - mark_as_advanced( ANDROID_FORCE_ARM_BUILD ) -else() - unset( ANDROID_FORCE_ARM_BUILD CACHE ) -endif() - -# choose toolchain -if( ANDROID_TOOLCHAIN_NAME ) - list( FIND __availableToolchains "${ANDROID_TOOLCHAIN_NAME}" __toolchainIdx ) - if( __toolchainIdx EQUAL -1 ) - list( SORT __availableToolchains ) - string( REPLACE ";" "\n * " toolchains_list "${__availableToolchains}" ) - set( toolchains_list " * ${toolchains_list}") - message( FATAL_ERROR "Specified toolchain \"${ANDROID_TOOLCHAIN_NAME}\" is missing in your NDK or broken. Please verify that your NDK is working or select another compiler toolchain. -To configure the toolchain set CMake variable ANDROID_TOOLCHAIN_NAME to one of the following values:\n${toolchains_list}\n" ) - endif() - list( GET __availableToolchainArchs ${__toolchainIdx} __toolchainArch ) - if( NOT __toolchainArch STREQUAL ANDROID_ARCH_NAME ) - message( SEND_ERROR "Selected toolchain \"${ANDROID_TOOLCHAIN_NAME}\" is not able to compile binaries for the \"${ANDROID_ARCH_NAME}\" platform." ) - endif() -else() - set( __toolchainIdx -1 ) - set( __applicableToolchains "" ) - set( __toolchainMaxVersion "0.0.0" ) - list( LENGTH __availableToolchains __availableToolchainsCount ) - math( EXPR __availableToolchainsCount "${__availableToolchainsCount}-1" ) - foreach( __idx RANGE ${__availableToolchainsCount} ) - list( GET __availableToolchainArchs ${__idx} __toolchainArch ) - if( __toolchainArch STREQUAL ANDROID_ARCH_NAME ) - list( GET __availableToolchainCompilerVersions ${__idx} __toolchainVersion ) - string( REPLACE "x" "99" __toolchainVersion "${__toolchainVersion}") - if( __toolchainVersion VERSION_GREATER __toolchainMaxVersion ) - set( __toolchainMaxVersion "${__toolchainVersion}" ) - set( __toolchainIdx ${__idx} ) - endif() - endif() - endforeach() - unset( __availableToolchainsCount ) - unset( __toolchainMaxVersion ) - unset( __toolchainVersion ) -endif() -unset( __toolchainArch ) -if( __toolchainIdx EQUAL -1 ) - message( FATAL_ERROR "No one of available compiler toolchains is able to compile for ${ANDROID_ARCH_NAME} platform." ) -endif() -list( GET __availableToolchains ${__toolchainIdx} ANDROID_TOOLCHAIN_NAME ) -list( GET __availableToolchainMachines ${__toolchainIdx} ANDROID_TOOLCHAIN_MACHINE_NAME ) -list( GET __availableToolchainCompilerVersions ${__toolchainIdx} ANDROID_COMPILER_VERSION ) - -unset( __toolchainIdx ) -unset( __availableToolchains ) -unset( __availableToolchainMachines ) -unset( __availableToolchainArchs ) -unset( __availableToolchainCompilerVersions ) - -# choose native API level -__INIT_VARIABLE( ANDROID_NATIVE_API_LEVEL ENV_ANDROID_NATIVE_API_LEVEL ANDROID_API_LEVEL ENV_ANDROID_API_LEVEL ANDROID_STANDALONE_TOOLCHAIN_API_LEVEL ANDROID_DEFAULT_NDK_API_LEVEL_${ANDROID_ARCH_NAME} ANDROID_DEFAULT_NDK_API_LEVEL ) -string( REPLACE "android-" "" ANDROID_NATIVE_API_LEVEL "${ANDROID_NATIVE_API_LEVEL}" ) -string( STRIP "${ANDROID_NATIVE_API_LEVEL}" ANDROID_NATIVE_API_LEVEL ) -# adjust API level -set( __real_api_level ${ANDROID_DEFAULT_NDK_API_LEVEL_${ANDROID_ARCH_NAME}} ) -foreach( __level ${ANDROID_SUPPORTED_NATIVE_API_LEVELS} ) - if( (__level LESS ANDROID_NATIVE_API_LEVEL OR __level STREQUAL ANDROID_NATIVE_API_LEVEL) AND NOT __level LESS __real_api_level ) - set( __real_api_level ${__level} ) - endif() -endforeach() -if( __real_api_level AND NOT ANDROID_NATIVE_API_LEVEL STREQUAL __real_api_level ) - message( STATUS "Adjusting Android API level 'android-${ANDROID_NATIVE_API_LEVEL}' to 'android-${__real_api_level}'") - set( ANDROID_NATIVE_API_LEVEL ${__real_api_level} ) -endif() -unset(__real_api_level) -# validate -list( FIND ANDROID_SUPPORTED_NATIVE_API_LEVELS "${ANDROID_NATIVE_API_LEVEL}" __levelIdx ) -if( __levelIdx EQUAL -1 ) - message( SEND_ERROR "Specified Android native API level 'android-${ANDROID_NATIVE_API_LEVEL}' is not supported by your NDK/toolchain." ) -else() - if( BUILD_WITH_ANDROID_NDK ) - __DETECT_NATIVE_API_LEVEL( __realApiLevel "${ANDROID_NDK}/platforms/android-${ANDROID_NATIVE_API_LEVEL}/arch-${ANDROID_ARCH_NAME}/usr/include/android/api-level.h" ) - if( NOT __realApiLevel EQUAL ANDROID_NATIVE_API_LEVEL AND NOT __realApiLevel GREATER 9000 ) - message( SEND_ERROR "Specified Android API level (${ANDROID_NATIVE_API_LEVEL}) does not match to the level found (${__realApiLevel}). Probably your copy of NDK is broken." ) - endif() - unset( __realApiLevel ) - endif() - set( ANDROID_NATIVE_API_LEVEL "${ANDROID_NATIVE_API_LEVEL}" CACHE STRING "Android API level for native code" FORCE ) - set( CMAKE_ANDROID_API ${ANDROID_NATIVE_API_LEVEL} ) - if( CMAKE_VERSION VERSION_GREATER "2.8" ) - list( SORT ANDROID_SUPPORTED_NATIVE_API_LEVELS ) - set_property( CACHE ANDROID_NATIVE_API_LEVEL PROPERTY STRINGS ${ANDROID_SUPPORTED_NATIVE_API_LEVELS} ) - endif() -endif() -unset( __levelIdx ) - - -# remember target ABI -set( ANDROID_ABI "${ANDROID_ABI}" CACHE STRING "The target ABI for Android. If arm, then armeabi-v7a is recommended for hardware floating point." FORCE ) -if( CMAKE_VERSION VERSION_GREATER "2.8" ) - list( SORT ANDROID_SUPPORTED_ABIS_${ANDROID_ARCH_NAME} ) - set_property( CACHE ANDROID_ABI PROPERTY STRINGS ${ANDROID_SUPPORTED_ABIS_${ANDROID_ARCH_NAME}} ) -endif() - - -# runtime choice (STL, rtti, exceptions) -if( NOT ANDROID_STL ) - set( ANDROID_STL gnustl_static ) -endif() -set( ANDROID_STL "${ANDROID_STL}" CACHE STRING "C++ runtime" ) -set( ANDROID_STL_FORCE_FEATURES ON CACHE BOOL "automatically configure rtti and exceptions support based on C++ runtime" ) -mark_as_advanced( ANDROID_STL ANDROID_STL_FORCE_FEATURES ) - -if( BUILD_WITH_ANDROID_NDK ) - if( NOT "${ANDROID_STL}" MATCHES "^(none|system|system_re|gabi\\+\\+_static|gabi\\+\\+_shared|stlport_static|stlport_shared|gnustl_static|gnustl_shared|c\\+\\+_static|c\\+\\+_shared)$") - message( FATAL_ERROR "ANDROID_STL is set to invalid value \"${ANDROID_STL}\". -The possible values are: - none -> Do not configure the runtime. - system -> Use the default minimal system C++ runtime library. - system_re -> Same as system but with rtti and exceptions. - gabi++_static -> Use the GAbi++ runtime as a static library. - gabi++_shared -> Use the GAbi++ runtime as a shared library. - stlport_static -> Use the STLport runtime as a static library. - stlport_shared -> Use the STLport runtime as a shared library. - gnustl_static -> (default) Use the GNU STL as a static library. - gnustl_shared -> Use the GNU STL as a shared library. - c++_static -> Use the LLVM libc++ runtime as a static library. - c++_shared -> Use the LLVM libc++ runtime as a shared library. -" ) - endif() -elseif( BUILD_WITH_STANDALONE_TOOLCHAIN ) - if( NOT "${ANDROID_STL}" MATCHES "^(none|gnustl_static|gnustl_shared)$") - message( FATAL_ERROR "ANDROID_STL is set to invalid value \"${ANDROID_STL}\". -The possible values are: - none -> Do not configure the runtime. - gnustl_static -> (default) Use the GNU STL as a static library. - gnustl_shared -> Use the GNU STL as a shared library. -" ) - endif() -endif() - -unset( ANDROID_RTTI ) -unset( ANDROID_EXCEPTIONS ) -unset( ANDROID_STL_INCLUDE_DIRS ) -unset( __libstl ) -unset( __libsupcxx ) - -if( NOT _CMAKE_IN_TRY_COMPILE AND ANDROID_NDK_RELEASE STREQUAL "r7b" AND ARMEABI_V7A AND NOT VFPV3 AND ANDROID_STL MATCHES "gnustl" ) - message( WARNING "The GNU STL armeabi-v7a binaries from NDK r7b can crash non-NEON devices. The files provided with NDK r7b were not configured properly, resulting in crashes on Tegra2-based devices and others when trying to use certain floating-point functions (e.g., cosf, sinf, expf). -You are strongly recommended to switch to another NDK release. -" ) -endif() - -if( NOT _CMAKE_IN_TRY_COMPILE AND X86 AND ANDROID_STL MATCHES "gnustl" AND ANDROID_NDK_RELEASE STREQUAL "r6" ) - message( WARNING "The x86 system header file from NDK r6 has incorrect definition for ptrdiff_t. You are recommended to upgrade to a newer NDK release or manually patch the header: -See https://android.googlesource.com/platform/development.git f907f4f9d4e56ccc8093df6fee54454b8bcab6c2 - diff --git a/ndk/platforms/android-9/arch-x86/include/machine/_types.h b/ndk/platforms/android-9/arch-x86/include/machine/_types.h - index 5e28c64..65892a1 100644 - --- a/ndk/platforms/android-9/arch-x86/include/machine/_types.h - +++ b/ndk/platforms/android-9/arch-x86/include/machine/_types.h - @@ -51,7 +51,11 @@ typedef long int ssize_t; - #endif - #ifndef _PTRDIFF_T - #define _PTRDIFF_T - -typedef long ptrdiff_t; - +# ifdef __ANDROID__ - + typedef int ptrdiff_t; - +# else - + typedef long ptrdiff_t; - +# endif - #endif -" ) -endif() - - -# setup paths and STL for standalone toolchain -if( BUILD_WITH_STANDALONE_TOOLCHAIN ) - set( ANDROID_TOOLCHAIN_ROOT "${ANDROID_STANDALONE_TOOLCHAIN}" ) - set( ANDROID_CLANG_TOOLCHAIN_ROOT "${ANDROID_STANDALONE_TOOLCHAIN}" ) - set( ANDROID_SYSROOT "${ANDROID_STANDALONE_TOOLCHAIN}/sysroot" ) - - if( NOT ANDROID_STL STREQUAL "none" ) - set( ANDROID_STL_INCLUDE_DIRS "${ANDROID_STANDALONE_TOOLCHAIN}/include/c++/${ANDROID_COMPILER_VERSION}" ) - if( NOT EXISTS "${ANDROID_STL_INCLUDE_DIRS}" ) - # old location ( pre r8c ) - set( ANDROID_STL_INCLUDE_DIRS "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/include/c++/${ANDROID_COMPILER_VERSION}" ) - endif() - if( ARMEABI_V7A AND EXISTS "${ANDROID_STL_INCLUDE_DIRS}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/${CMAKE_SYSTEM_PROCESSOR}/bits" ) - list( APPEND ANDROID_STL_INCLUDE_DIRS "${ANDROID_STL_INCLUDE_DIRS}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/${CMAKE_SYSTEM_PROCESSOR}" ) - elseif( ARMEABI AND NOT ANDROID_FORCE_ARM_BUILD AND EXISTS "${ANDROID_STL_INCLUDE_DIRS}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/thumb/bits" ) - list( APPEND ANDROID_STL_INCLUDE_DIRS "${ANDROID_STL_INCLUDE_DIRS}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/thumb" ) - else() - list( APPEND ANDROID_STL_INCLUDE_DIRS "${ANDROID_STL_INCLUDE_DIRS}/${ANDROID_TOOLCHAIN_MACHINE_NAME}" ) - endif() - # always search static GNU STL to get the location of libsupc++.a - if( ARMEABI_V7A AND NOT ANDROID_FORCE_ARM_BUILD AND EXISTS "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/${CMAKE_SYSTEM_PROCESSOR}/thumb/libstdc++.a" ) - set( __libstl "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/${CMAKE_SYSTEM_PROCESSOR}/thumb" ) - elseif( ARMEABI_V7A AND EXISTS "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/${CMAKE_SYSTEM_PROCESSOR}/libstdc++.a" ) - set( __libstl "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/${CMAKE_SYSTEM_PROCESSOR}" ) - elseif( ARMEABI AND NOT ANDROID_FORCE_ARM_BUILD AND EXISTS "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/thumb/libstdc++.a" ) - set( __libstl "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/thumb" ) - elseif( EXISTS "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/libstdc++.a" ) - set( __libstl "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib" ) - endif() - if( __libstl ) - set( __libsupcxx "${__libstl}/libsupc++.a" ) - set( __libstl "${__libstl}/libstdc++.a" ) - endif() - if( NOT EXISTS "${__libsupcxx}" ) - message( FATAL_ERROR "The required libstdsupc++.a is missing in your standalone toolchain. - Usually it happens because of bug in make-standalone-toolchain.sh script from NDK r7, r7b and r7c. - You need to either upgrade to newer NDK or manually copy - $ANDROID_NDK/sources/cxx-stl/gnu-libstdc++/libs/${ANDROID_NDK_ABI_NAME}/libsupc++.a - to - ${__libsupcxx} - " ) - endif() - if( ANDROID_STL STREQUAL "gnustl_shared" ) - if( ARMEABI_V7A AND EXISTS "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/${CMAKE_SYSTEM_PROCESSOR}/libgnustl_shared.so" ) - set( __libstl "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/${CMAKE_SYSTEM_PROCESSOR}/libgnustl_shared.so" ) - elseif( ARMEABI AND NOT ANDROID_FORCE_ARM_BUILD AND EXISTS "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/thumb/libgnustl_shared.so" ) - set( __libstl "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/thumb/libgnustl_shared.so" ) - elseif( EXISTS "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/libgnustl_shared.so" ) - set( __libstl "${ANDROID_STANDALONE_TOOLCHAIN}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/libgnustl_shared.so" ) - endif() - endif() - endif() -endif() - -# clang -if( "${ANDROID_TOOLCHAIN_NAME}" STREQUAL "standalone-clang" ) - set( ANDROID_COMPILER_IS_CLANG 1 ) - execute_process( COMMAND "${ANDROID_CLANG_TOOLCHAIN_ROOT}/bin/clang${TOOL_OS_SUFFIX}" --version OUTPUT_VARIABLE ANDROID_CLANG_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE ) - string( REGEX MATCH "[0-9]+[.][0-9]+" ANDROID_CLANG_VERSION "${ANDROID_CLANG_VERSION}") -elseif( "${ANDROID_TOOLCHAIN_NAME}" MATCHES "-clang3[.][0-9]?$" ) - string( REGEX MATCH "3[.][0-9]$" ANDROID_CLANG_VERSION "${ANDROID_TOOLCHAIN_NAME}") - string( REGEX REPLACE "-clang${ANDROID_CLANG_VERSION}$" "-${ANDROID_COMPILER_VERSION}" ANDROID_GCC_TOOLCHAIN_NAME "${ANDROID_TOOLCHAIN_NAME}" ) - if( NOT EXISTS "${ANDROID_NDK_TOOLCHAINS_PATH}/llvm-${ANDROID_CLANG_VERSION}${ANDROID_NDK_TOOLCHAINS_SUBPATH}/bin/clang${TOOL_OS_SUFFIX}" ) - message( FATAL_ERROR "Could not find the Clang compiler driver" ) - endif() - set( ANDROID_COMPILER_IS_CLANG 1 ) - set( ANDROID_CLANG_TOOLCHAIN_ROOT "${ANDROID_NDK_TOOLCHAINS_PATH}/llvm-${ANDROID_CLANG_VERSION}${ANDROID_NDK_TOOLCHAINS_SUBPATH}" ) -else() - set( ANDROID_GCC_TOOLCHAIN_NAME "${ANDROID_TOOLCHAIN_NAME}" ) - unset( ANDROID_COMPILER_IS_CLANG CACHE ) -endif() - -string( REPLACE "." "" _clang_name "clang${ANDROID_CLANG_VERSION}" ) -if( NOT EXISTS "${ANDROID_CLANG_TOOLCHAIN_ROOT}/bin/${_clang_name}${TOOL_OS_SUFFIX}" ) - set( _clang_name "clang" ) -endif() - - -# setup paths and STL for NDK -if( BUILD_WITH_ANDROID_NDK ) - set( ANDROID_TOOLCHAIN_ROOT "${ANDROID_NDK_TOOLCHAINS_PATH}/${ANDROID_GCC_TOOLCHAIN_NAME}${ANDROID_NDK_TOOLCHAINS_SUBPATH}" ) - set( ANDROID_SYSROOT "${ANDROID_NDK}/platforms/android-${ANDROID_NATIVE_API_LEVEL}/arch-${ANDROID_ARCH_NAME}" ) - - if( ANDROID_STL STREQUAL "none" ) - # do nothing - elseif( ANDROID_STL STREQUAL "system" ) - set( ANDROID_RTTI OFF ) - set( ANDROID_EXCEPTIONS OFF ) - set( ANDROID_STL_INCLUDE_DIRS "${ANDROID_NDK}/sources/cxx-stl/system/include" ) - elseif( ANDROID_STL STREQUAL "system_re" ) - set( ANDROID_RTTI ON ) - set( ANDROID_EXCEPTIONS ON ) - set( ANDROID_STL_INCLUDE_DIRS "${ANDROID_NDK}/sources/cxx-stl/system/include" ) - elseif( ANDROID_STL MATCHES "gabi" ) - if( ANDROID_NDK_RELEASE_NUM LESS 7000 ) # before r7 - message( FATAL_ERROR "gabi++ is not available in your NDK. You have to upgrade to NDK r7 or newer to use gabi++.") - endif() - set( ANDROID_RTTI ON ) - set( ANDROID_EXCEPTIONS OFF ) - set( ANDROID_STL_INCLUDE_DIRS "${ANDROID_NDK}/sources/cxx-stl/gabi++/include" ) - set( __libstl "${ANDROID_NDK}/sources/cxx-stl/gabi++/libs/${ANDROID_NDK_ABI_NAME}/libgabi++_static.a" ) - elseif( ANDROID_STL MATCHES "stlport" ) - if( NOT ANDROID_NDK_RELEASE_NUM LESS 8004 ) # before r8d - set( ANDROID_EXCEPTIONS ON ) - else() - set( ANDROID_EXCEPTIONS OFF ) - endif() - if( ANDROID_NDK_RELEASE_NUM LESS 7000 ) # before r7 - set( ANDROID_RTTI OFF ) - else() - set( ANDROID_RTTI ON ) - endif() - set( ANDROID_STL_INCLUDE_DIRS "${ANDROID_NDK}/sources/cxx-stl/stlport/stlport" ) - set( __libstl "${ANDROID_NDK}/sources/cxx-stl/stlport/libs/${ANDROID_NDK_ABI_NAME}/libstlport_static.a" ) - elseif( ANDROID_STL MATCHES "gnustl" ) - set( ANDROID_EXCEPTIONS ON ) - set( ANDROID_RTTI ON ) - if( EXISTS "${ANDROID_NDK}/sources/cxx-stl/gnu-libstdc++/${ANDROID_COMPILER_VERSION}" ) - if( ARMEABI_V7A AND ANDROID_COMPILER_VERSION VERSION_EQUAL "4.7" AND ANDROID_NDK_RELEASE STREQUAL "r8d" ) - # gnustl binary for 4.7 compiler is buggy :( - # TODO: look for right fix - set( __libstl "${ANDROID_NDK}/sources/cxx-stl/gnu-libstdc++/4.6" ) - else() - set( __libstl "${ANDROID_NDK}/sources/cxx-stl/gnu-libstdc++/${ANDROID_COMPILER_VERSION}" ) - endif() - else() - set( __libstl "${ANDROID_NDK}/sources/cxx-stl/gnu-libstdc++" ) - endif() - set( ANDROID_STL_INCLUDE_DIRS "${__libstl}/include" "${__libstl}/libs/${ANDROID_NDK_ABI_NAME}/include" "${__libstl}/include/backward" ) - if( EXISTS "${__libstl}/libs/${ANDROID_NDK_ABI_NAME}/libgnustl_static.a" ) - set( __libstl "${__libstl}/libs/${ANDROID_NDK_ABI_NAME}/libgnustl_static.a" ) - else() - set( __libstl "${__libstl}/libs/${ANDROID_NDK_ABI_NAME}/libstdc++.a" ) - endif() - elseif( ANDROID_STL MATCHES "c\\+\\+_shared" OR ANDROID_STL MATCHES "c\\+\\+_static" ) - set( ANDROID_EXCEPTIONS ON ) - set( ANDROID_RTTI ON ) - set( ANDROID_CXX_ROOT "${ANDROID_NDK}/sources/cxx-stl/" ) - set( ANDROID_LLVM_ROOT "${ANDROID_CXX_ROOT}/llvm-libc++" ) - - if( X86 ) - set( ANDROID_ABI_INCLUDE_DIRS "${ANDROID_CXX_ROOT}/gabi++/include" ) - else() - set( ANDROID_ABI_INCLUDE_DIRS "${ANDROID_CXX_ROOT}/llvm-libc++abi/include" ) - endif() - - set( ANDROID_STL_INCLUDE_DIRS "${ANDROID_LLVM_ROOT}/libcxx/include" "${ANDROID_ABI_INCLUDE_DIRS}" ) - - # android support sfiles - include_directories ( SYSTEM ${ANDROID_NDK}/sources/android/support/include ) - - if(ANDROID_STL MATCHES "c\\+\\+_shared") - set ( LLVM_LIBRARY_NAME "libc++_shared.so") - else() - set ( LLVM_LIBRARY_NAME "libc++_static.a" ) - endif () - - if( EXISTS "${ANDROID_LLVM_ROOT}/libs/${ANDROID_NDK_ABI_NAME}/${LLVM_LIBRARY_NAME}" ) - set( __libstl "${ANDROID_LLVM_ROOT}/libs/${ANDROID_NDK_ABI_NAME}/${LLVM_LIBRARY_NAME}" ) - else() - message( FATAL_ERROR "Could not find libc++ library" ) - endif() - else() - message( FATAL_ERROR "Unknown runtime: ${ANDROID_STL}" ) - endif() - # find libsupc++.a - rtti & exceptions - if( ANDROID_STL STREQUAL "system_re" OR ANDROID_STL MATCHES "gnustl" ) - set( __libsupcxx "${ANDROID_NDK}/sources/cxx-stl/gnu-libstdc++/${ANDROID_COMPILER_VERSION}/libs/${ANDROID_NDK_ABI_NAME}/libsupc++.a" ) # r8b or newer - if( NOT EXISTS "${__libsupcxx}" ) - set( __libsupcxx "${ANDROID_NDK}/sources/cxx-stl/gnu-libstdc++/libs/${ANDROID_NDK_ABI_NAME}/libsupc++.a" ) # r7-r8 - endif() - if( NOT EXISTS "${__libsupcxx}" ) # before r7 - if( ARMEABI_V7A ) - if( ANDROID_FORCE_ARM_BUILD ) - set( __libsupcxx "${ANDROID_TOOLCHAIN_ROOT}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/${CMAKE_SYSTEM_PROCESSOR}/libsupc++.a" ) - else() - set( __libsupcxx "${ANDROID_TOOLCHAIN_ROOT}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/${CMAKE_SYSTEM_PROCESSOR}/thumb/libsupc++.a" ) - endif() - elseif( ARMEABI AND NOT ANDROID_FORCE_ARM_BUILD ) - set( __libsupcxx "${ANDROID_TOOLCHAIN_ROOT}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/thumb/libsupc++.a" ) - else() - set( __libsupcxx "${ANDROID_TOOLCHAIN_ROOT}/${ANDROID_TOOLCHAIN_MACHINE_NAME}/lib/libsupc++.a" ) - endif() - endif() - if( NOT EXISTS "${__libsupcxx}") - message( ERROR "Could not find libsupc++.a for a chosen platform. Either your NDK is not supported or is broken.") - endif() - endif() -endif() - - -# case of shared STL linkage -if( ANDROID_STL MATCHES "shared" AND DEFINED __libstl ) - string( REPLACE "_static.a" "_shared.so" __libstl "${__libstl}" ) - # TODO: check if .so file exists before the renaming -endif() - - -# ccache support -__INIT_VARIABLE( _ndk_ccache NDK_CCACHE ENV_NDK_CCACHE ) -if( _ndk_ccache ) - if( DEFINED NDK_CCACHE AND NOT EXISTS NDK_CCACHE ) - unset( NDK_CCACHE CACHE ) - endif() - find_program( NDK_CCACHE "${_ndk_ccache}" DOC "The path to ccache binary") -else() - unset( NDK_CCACHE CACHE ) -endif() -unset( _ndk_ccache ) - - -# setup the cross-compiler -if( NOT CMAKE_C_COMPILER ) - if( NDK_CCACHE AND NOT ANDROID_SYSROOT MATCHES "[ ;\"]" ) - set( CMAKE_C_COMPILER "${NDK_CCACHE}" CACHE PATH "ccache as C compiler" ) - set( CMAKE_CXX_COMPILER "${NDK_CCACHE}" CACHE PATH "ccache as C++ compiler" ) - if( ANDROID_COMPILER_IS_CLANG ) - set( CMAKE_C_COMPILER_ARG1 "${ANDROID_CLANG_TOOLCHAIN_ROOT}/bin/${_clang_name}${TOOL_OS_SUFFIX}" CACHE PATH "C compiler") - set( CMAKE_CXX_COMPILER_ARG1 "${ANDROID_CLANG_TOOLCHAIN_ROOT}/bin/${_clang_name}++${TOOL_OS_SUFFIX}" CACHE PATH "C++ compiler") - else() - set( CMAKE_C_COMPILER_ARG1 "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-gcc${TOOL_OS_SUFFIX}" CACHE PATH "C compiler") - set( CMAKE_CXX_COMPILER_ARG1 "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-g++${TOOL_OS_SUFFIX}" CACHE PATH "C++ compiler") - endif() - else() - if( ANDROID_COMPILER_IS_CLANG ) - set( CMAKE_C_COMPILER "${ANDROID_CLANG_TOOLCHAIN_ROOT}/bin/${_clang_name}${TOOL_OS_SUFFIX}" CACHE PATH "C compiler") - set( CMAKE_CXX_COMPILER "${ANDROID_CLANG_TOOLCHAIN_ROOT}/bin/${_clang_name}++${TOOL_OS_SUFFIX}" CACHE PATH "C++ compiler") - else() - set( CMAKE_C_COMPILER "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-gcc${TOOL_OS_SUFFIX}" CACHE PATH "C compiler" ) - set( CMAKE_CXX_COMPILER "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-g++${TOOL_OS_SUFFIX}" CACHE PATH "C++ compiler" ) - endif() - endif() - set( CMAKE_ASM_COMPILER "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-gcc${TOOL_OS_SUFFIX}" CACHE PATH "assembler" ) - set( CMAKE_STRIP "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-strip${TOOL_OS_SUFFIX}" CACHE PATH "strip" ) - if( EXISTS "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-gcc-ar${TOOL_OS_SUFFIX}" ) - # Use gcc-ar if we have it for better LTO support. - set( CMAKE_AR "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-gcc-ar${TOOL_OS_SUFFIX}" CACHE PATH "archive" ) - else() - set( CMAKE_AR "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-ar${TOOL_OS_SUFFIX}" CACHE PATH "archive" ) - endif() - set( CMAKE_LINKER "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-ld${TOOL_OS_SUFFIX}" CACHE PATH "linker" ) - set( CMAKE_NM "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-nm${TOOL_OS_SUFFIX}" CACHE PATH "nm" ) - set( CMAKE_OBJCOPY "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-objcopy${TOOL_OS_SUFFIX}" CACHE PATH "objcopy" ) - set( CMAKE_OBJDUMP "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-objdump${TOOL_OS_SUFFIX}" CACHE PATH "objdump" ) - set( CMAKE_RANLIB "${ANDROID_TOOLCHAIN_ROOT}/bin/${ANDROID_TOOLCHAIN_MACHINE_NAME}-ranlib${TOOL_OS_SUFFIX}" CACHE PATH "ranlib" ) -endif() - -set( _CMAKE_TOOLCHAIN_PREFIX "${ANDROID_TOOLCHAIN_MACHINE_NAME}-" ) -if( CMAKE_VERSION VERSION_LESS 2.8.5 ) - set( CMAKE_ASM_COMPILER_ARG1 "-c" ) -endif() -if( APPLE ) - find_program( CMAKE_INSTALL_NAME_TOOL NAMES install_name_tool ) - if( NOT CMAKE_INSTALL_NAME_TOOL ) - message( FATAL_ERROR "Could not find install_name_tool, please check your installation." ) - endif() - mark_as_advanced( CMAKE_INSTALL_NAME_TOOL ) -endif() - -# Force set compilers because standard identification works badly for us -include( CMakeForceCompiler ) -CMAKE_FORCE_C_COMPILER( "${CMAKE_C_COMPILER}" GNU ) -if( ANDROID_COMPILER_IS_CLANG ) - set( CMAKE_C_COMPILER_ID Clang ) -endif() -set( CMAKE_C_PLATFORM_ID Linux ) -if( X86_64 OR MIPS64 OR ARM64_V8A ) - set( CMAKE_C_SIZEOF_DATA_PTR 8 ) -else() - set( CMAKE_C_SIZEOF_DATA_PTR 4 ) -endif() -set( CMAKE_C_HAS_ISYSROOT 1 ) -set( CMAKE_C_COMPILER_ABI ELF ) -CMAKE_FORCE_CXX_COMPILER( "${CMAKE_CXX_COMPILER}" GNU ) -if( ANDROID_COMPILER_IS_CLANG ) - set( CMAKE_CXX_COMPILER_ID Clang) -endif() -set( CMAKE_CXX_PLATFORM_ID Linux ) -set( CMAKE_CXX_SIZEOF_DATA_PTR ${CMAKE_C_SIZEOF_DATA_PTR} ) -set( CMAKE_CXX_HAS_ISYSROOT 1 ) -set( CMAKE_CXX_COMPILER_ABI ELF ) -set( CMAKE_CXX_SOURCE_FILE_EXTENSIONS cc cp cxx cpp CPP c++ C ) -# force ASM compiler (required for CMake < 2.8.5) -set( CMAKE_ASM_COMPILER_ID_RUN TRUE ) -set( CMAKE_ASM_COMPILER_ID GNU ) -set( CMAKE_ASM_COMPILER_WORKS TRUE ) -set( CMAKE_ASM_COMPILER_FORCED TRUE ) -set( CMAKE_COMPILER_IS_GNUASM 1) -set( CMAKE_ASM_SOURCE_FILE_EXTENSIONS s S asm ) - -foreach( lang C CXX ASM ) - if( ANDROID_COMPILER_IS_CLANG ) - set( CMAKE_${lang}_COMPILER_VERSION ${ANDROID_CLANG_VERSION} ) - else() - set( CMAKE_${lang}_COMPILER_VERSION ${ANDROID_COMPILER_VERSION} ) - endif() -endforeach() - -# flags and definitions -remove_definitions( -DANDROID ) -add_definitions( -DANDROID ) - -if( ANDROID_SYSROOT MATCHES "[ ;\"]" ) - if( CMAKE_HOST_WIN32 ) - # try to convert path to 8.3 form - file( WRITE "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/cvt83.cmd" "@echo %~s1" ) - execute_process( COMMAND "$ENV{ComSpec}" /c "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/cvt83.cmd" "${ANDROID_SYSROOT}" - OUTPUT_VARIABLE __path OUTPUT_STRIP_TRAILING_WHITESPACE - RESULT_VARIABLE __result ERROR_QUIET ) - if( __result EQUAL 0 ) - file( TO_CMAKE_PATH "${__path}" ANDROID_SYSROOT ) - set( ANDROID_CXX_FLAGS "--sysroot=${ANDROID_SYSROOT}" ) - else() - set( ANDROID_CXX_FLAGS "--sysroot=\"${ANDROID_SYSROOT}\"" ) - endif() - else() - set( ANDROID_CXX_FLAGS "'--sysroot=${ANDROID_SYSROOT}'" ) - endif() - if( NOT _CMAKE_IN_TRY_COMPILE ) - # quotes can break try_compile and compiler identification - message(WARNING "Path to your Android NDK (or toolchain) has non-alphanumeric symbols.\nThe build might be broken.\n") - endif() -else() - set( ANDROID_CXX_FLAGS "--sysroot=${ANDROID_SYSROOT}" ) -endif() - -# NDK flags -if (ARM64_V8A ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -funwind-tables" ) - set( ANDROID_CXX_FLAGS_RELEASE "-fomit-frame-pointer -fstrict-aliasing" ) - set( ANDROID_CXX_FLAGS_DEBUG "-fno-omit-frame-pointer -fno-strict-aliasing" ) - if( NOT ANDROID_COMPILER_IS_CLANG ) - set( ANDROID_CXX_FLAGS_RELEASE "${ANDROID_CXX_FLAGS_RELEASE} -funswitch-loops -finline-limit=300" ) - endif() -elseif( ARMEABI OR ARMEABI_V7A) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -funwind-tables" ) - if( NOT ANDROID_FORCE_ARM_BUILD AND NOT ARMEABI_V6 ) - set( ANDROID_CXX_FLAGS_RELEASE "-mthumb -fomit-frame-pointer -fno-strict-aliasing" ) - set( ANDROID_CXX_FLAGS_DEBUG "-marm -fno-omit-frame-pointer -fno-strict-aliasing" ) - if( NOT ANDROID_COMPILER_IS_CLANG ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -finline-limit=64" ) - endif() - else() - # always compile ARMEABI_V6 in arm mode; otherwise there is no difference from ARMEABI - set( ANDROID_CXX_FLAGS_RELEASE "-marm -fomit-frame-pointer -fstrict-aliasing" ) - set( ANDROID_CXX_FLAGS_DEBUG "-marm -fno-omit-frame-pointer -fno-strict-aliasing" ) - if( NOT ANDROID_COMPILER_IS_CLANG ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -funswitch-loops -finline-limit=300" ) - endif() - endif() -elseif( X86 OR X86_64 ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -funwind-tables" ) - if( NOT ANDROID_COMPILER_IS_CLANG ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -funswitch-loops -finline-limit=300" ) - endif() - set( ANDROID_CXX_FLAGS_RELEASE "-fomit-frame-pointer -fstrict-aliasing" ) - set( ANDROID_CXX_FLAGS_DEBUG "-fno-omit-frame-pointer -fno-strict-aliasing" ) -elseif( MIPS OR MIPS64 ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -fno-strict-aliasing -finline-functions -funwind-tables -fmessage-length=0" ) - set( ANDROID_CXX_FLAGS_RELEASE "-fomit-frame-pointer" ) - set( ANDROID_CXX_FLAGS_DEBUG "-fno-omit-frame-pointer" ) - if( NOT ANDROID_COMPILER_IS_CLANG ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -fno-inline-functions-called-once -fgcse-after-reload -frerun-cse-after-loop -frename-registers" ) - set( ANDROID_CXX_FLAGS_RELEASE "${ANDROID_CXX_FLAGS_RELEASE} -funswitch-loops -finline-limit=300" ) - endif() -elseif() - set( ANDROID_CXX_FLAGS_RELEASE "" ) - set( ANDROID_CXX_FLAGS_DEBUG "" ) -endif() - -set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -fsigned-char" ) # good/necessary when porting desktop libraries - -if( NOT X86 AND NOT ANDROID_COMPILER_IS_CLANG ) - set( ANDROID_CXX_FLAGS "-Wno-psabi ${ANDROID_CXX_FLAGS}" ) -endif() - -if( NOT ANDROID_COMPILER_VERSION VERSION_LESS "4.6" ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -no-canonical-prefixes" ) # see https://android-review.googlesource.com/#/c/47564/ -endif() - -# ABI-specific flags -if( ARMEABI_V7A ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -march=armv7-a -mfloat-abi=softfp" ) - if( NEON ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -mfpu=neon" ) - elseif( VFPV3 ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -mfpu=vfpv3" ) - else() - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -mfpu=vfpv3-d16" ) - endif() -elseif( ARMEABI_V6 ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -march=armv6 -mfloat-abi=softfp -mfpu=vfp" ) # vfp == vfpv2 -elseif( ARMEABI ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -march=armv5te -mtune=xscale -msoft-float" ) -endif() - -if( ANDROID_STL MATCHES "gnustl" AND (EXISTS "${__libstl}" OR EXISTS "${__libsupcxx}") ) - set( CMAKE_CXX_CREATE_SHARED_LIBRARY " -o " ) - set( CMAKE_CXX_CREATE_SHARED_MODULE " -o " ) - set( CMAKE_CXX_LINK_EXECUTABLE " -o " ) -else() - set( CMAKE_CXX_CREATE_SHARED_LIBRARY " -o " ) - set( CMAKE_CXX_CREATE_SHARED_MODULE " -o " ) - set( CMAKE_CXX_LINK_EXECUTABLE " -o " ) -endif() - -# STL -if( EXISTS "${__libstl}" OR EXISTS "${__libsupcxx}" ) - if( EXISTS "${__libstl}" ) - set( CMAKE_CXX_CREATE_SHARED_LIBRARY "${CMAKE_CXX_CREATE_SHARED_LIBRARY} \"${__libstl}\"" ) - set( CMAKE_CXX_CREATE_SHARED_MODULE "${CMAKE_CXX_CREATE_SHARED_MODULE} \"${__libstl}\"" ) - set( CMAKE_CXX_LINK_EXECUTABLE "${CMAKE_CXX_LINK_EXECUTABLE} \"${__libstl}\"" ) - endif() - if( EXISTS "${__libsupcxx}" ) - set( CMAKE_CXX_CREATE_SHARED_LIBRARY "${CMAKE_CXX_CREATE_SHARED_LIBRARY} \"${__libsupcxx}\"" ) - set( CMAKE_CXX_CREATE_SHARED_MODULE "${CMAKE_CXX_CREATE_SHARED_MODULE} \"${__libsupcxx}\"" ) - set( CMAKE_CXX_LINK_EXECUTABLE "${CMAKE_CXX_LINK_EXECUTABLE} \"${__libsupcxx}\"" ) - # C objects: - set( CMAKE_C_CREATE_SHARED_LIBRARY " -o " ) - set( CMAKE_C_CREATE_SHARED_MODULE " -o " ) - set( CMAKE_C_LINK_EXECUTABLE " -o " ) - set( CMAKE_C_CREATE_SHARED_LIBRARY "${CMAKE_C_CREATE_SHARED_LIBRARY} \"${__libsupcxx}\"" ) - set( CMAKE_C_CREATE_SHARED_MODULE "${CMAKE_C_CREATE_SHARED_MODULE} \"${__libsupcxx}\"" ) - set( CMAKE_C_LINK_EXECUTABLE "${CMAKE_C_LINK_EXECUTABLE} \"${__libsupcxx}\"" ) - endif() - if( ANDROID_STL MATCHES "gnustl" ) - if( NOT EXISTS "${ANDROID_LIBM_PATH}" ) - set( ANDROID_LIBM_PATH -lm ) - endif() - set( CMAKE_CXX_CREATE_SHARED_LIBRARY "${CMAKE_CXX_CREATE_SHARED_LIBRARY} ${ANDROID_LIBM_PATH}" ) - set( CMAKE_CXX_CREATE_SHARED_MODULE "${CMAKE_CXX_CREATE_SHARED_MODULE} ${ANDROID_LIBM_PATH}" ) - set( CMAKE_CXX_LINK_EXECUTABLE "${CMAKE_CXX_LINK_EXECUTABLE} ${ANDROID_LIBM_PATH}" ) - endif() -endif() - -# variables controlling optional build flags -if( ANDROID_NDK_RELEASE_NUM LESS 7000 ) # before r7 - # libGLESv2.so in NDK's prior to r7 refers to missing external symbols. - # So this flag option is required for all projects using OpenGL from native. - __INIT_VARIABLE( ANDROID_SO_UNDEFINED VALUES ON ) -else() - __INIT_VARIABLE( ANDROID_SO_UNDEFINED VALUES OFF ) -endif() -__INIT_VARIABLE( ANDROID_NO_UNDEFINED VALUES ON ) -__INIT_VARIABLE( ANDROID_FUNCTION_LEVEL_LINKING VALUES ON ) -__INIT_VARIABLE( ANDROID_GOLD_LINKER VALUES ON ) -__INIT_VARIABLE( ANDROID_NOEXECSTACK VALUES ON ) -__INIT_VARIABLE( ANDROID_RELRO VALUES ON ) - -set( ANDROID_NO_UNDEFINED ${ANDROID_NO_UNDEFINED} CACHE BOOL "Show all undefined symbols as linker errors" ) -set( ANDROID_SO_UNDEFINED ${ANDROID_SO_UNDEFINED} CACHE BOOL "Allows or disallows undefined symbols in shared libraries" ) -set( ANDROID_FUNCTION_LEVEL_LINKING ${ANDROID_FUNCTION_LEVEL_LINKING} CACHE BOOL "Put each function in separate section and enable garbage collection of unused input sections at link time" ) -set( ANDROID_GOLD_LINKER ${ANDROID_GOLD_LINKER} CACHE BOOL "Enables gold linker" ) -set( ANDROID_NOEXECSTACK ${ANDROID_NOEXECSTACK} CACHE BOOL "Allows or disallows undefined symbols in shared libraries" ) -set( ANDROID_RELRO ${ANDROID_RELRO} CACHE BOOL "Enables RELRO - a memory corruption mitigation technique" ) -mark_as_advanced( ANDROID_NO_UNDEFINED ANDROID_SO_UNDEFINED ANDROID_FUNCTION_LEVEL_LINKING ANDROID_GOLD_LINKER ANDROID_NOEXECSTACK ANDROID_RELRO ) - -# linker flags -set( ANDROID_LINKER_FLAGS "" ) - -if( ARMEABI_V7A ) - # this is *required* to use the following linker flags that routes around - # a CPU bug in some Cortex-A8 implementations: - set( ANDROID_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} -Wl,--fix-cortex-a8" ) -endif() - -if( ANDROID_NO_UNDEFINED ) - if( MIPS ) - # there is some sysroot-related problem in mips linker... - if( NOT ANDROID_SYSROOT MATCHES "[ ;\"]" ) - set( ANDROID_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} -Wl,--no-undefined -Wl,-rpath-link,${ANDROID_SYSROOT}/usr/lib" ) - endif() - else() - set( ANDROID_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} -Wl,--no-undefined" ) - endif() -endif() - -if( ANDROID_SO_UNDEFINED ) - set( ANDROID_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} -Wl,-allow-shlib-undefined" ) -endif() - -if( ANDROID_FUNCTION_LEVEL_LINKING ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -fdata-sections -ffunction-sections" ) - set( ANDROID_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} -Wl,--gc-sections" ) -endif() - -if( ANDROID_COMPILER_VERSION VERSION_EQUAL "4.6" ) - if( ANDROID_GOLD_LINKER AND (CMAKE_HOST_UNIX OR ANDROID_NDK_RELEASE_NUM GREATER 8002) AND (ARMEABI OR ARMEABI_V7A OR X86) ) - set( ANDROID_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} -fuse-ld=gold" ) - elseif( ANDROID_NDK_RELEASE_NUM GREATER 8002 ) # after r8b - set( ANDROID_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} -fuse-ld=bfd" ) - elseif( ANDROID_NDK_RELEASE STREQUAL "r8b" AND ARMEABI AND NOT _CMAKE_IN_TRY_COMPILE ) - message( WARNING "The default bfd linker from arm GCC 4.6 toolchain can fail with 'unresolvable R_ARM_THM_CALL relocation' error message. See https://code.google.com/p/android/issues/detail?id=35342 - On Linux and OS X host platform you can workaround this problem using gold linker (default). - Rerun cmake with -DANDROID_GOLD_LINKER=ON option in case of problems. -" ) - endif() -endif() # version 4.6 - -if( ANDROID_NOEXECSTACK ) - if( ANDROID_COMPILER_IS_CLANG ) - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -Xclang -mnoexecstack" ) - else() - set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS} -Wa,--noexecstack" ) - endif() - set( ANDROID_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} -Wl,-z,noexecstack" ) -endif() - -if( ANDROID_RELRO ) - set( ANDROID_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} -Wl,-z,relro -Wl,-z,now" ) -endif() - -if( ANDROID_COMPILER_IS_CLANG ) - set( ANDROID_CXX_FLAGS "-target ${ANDROID_LLVM_TRIPLE} -Qunused-arguments ${ANDROID_CXX_FLAGS}" ) - if( BUILD_WITH_ANDROID_NDK ) - set( ANDROID_CXX_FLAGS "-gcc-toolchain ${ANDROID_TOOLCHAIN_ROOT} ${ANDROID_CXX_FLAGS}" ) - endif() -endif() - -# cache flags -set( CMAKE_CXX_FLAGS "" CACHE STRING "c++ flags" ) -set( CMAKE_C_FLAGS "" CACHE STRING "c flags" ) -set( CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "c++ Release flags" ) -set( CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "c Release flags" ) -set( CMAKE_CXX_FLAGS_DEBUG "-O0 -g -DDEBUG -D_DEBUG" CACHE STRING "c++ Debug flags" ) -set( CMAKE_C_FLAGS_DEBUG "-O0 -g -DDEBUG -D_DEBUG" CACHE STRING "c Debug flags" ) -set( CMAKE_SHARED_LINKER_FLAGS "" CACHE STRING "shared linker flags" ) -set( CMAKE_MODULE_LINKER_FLAGS "" CACHE STRING "module linker flags" ) -set( CMAKE_EXE_LINKER_FLAGS "-Wl,-z,nocopyreloc" CACHE STRING "executable linker flags" ) - -# put flags to cache (for debug purpose only) -set( ANDROID_CXX_FLAGS "${ANDROID_CXX_FLAGS}" CACHE INTERNAL "Android specific c/c++ flags" ) -set( ANDROID_CXX_FLAGS_RELEASE "${ANDROID_CXX_FLAGS_RELEASE}" CACHE INTERNAL "Android specific c/c++ Release flags" ) -set( ANDROID_CXX_FLAGS_DEBUG "${ANDROID_CXX_FLAGS_DEBUG}" CACHE INTERNAL "Android specific c/c++ Debug flags" ) -set( ANDROID_LINKER_FLAGS "${ANDROID_LINKER_FLAGS}" CACHE INTERNAL "Android specific c/c++ linker flags" ) - -# finish flags -set( CMAKE_CXX_FLAGS "${ANDROID_CXX_FLAGS} ${CMAKE_CXX_FLAGS}" ) -set( CMAKE_C_FLAGS "${ANDROID_CXX_FLAGS} ${CMAKE_C_FLAGS}" ) -set( CMAKE_CXX_FLAGS_RELEASE "${ANDROID_CXX_FLAGS_RELEASE} ${CMAKE_CXX_FLAGS_RELEASE}" ) -set( CMAKE_C_FLAGS_RELEASE "${ANDROID_CXX_FLAGS_RELEASE} ${CMAKE_C_FLAGS_RELEASE}" ) -set( CMAKE_CXX_FLAGS_DEBUG "${ANDROID_CXX_FLAGS_DEBUG} ${CMAKE_CXX_FLAGS_DEBUG}" ) -set( CMAKE_C_FLAGS_DEBUG "${ANDROID_CXX_FLAGS_DEBUG} ${CMAKE_C_FLAGS_DEBUG}" ) -set( CMAKE_SHARED_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} ${CMAKE_SHARED_LINKER_FLAGS}" ) -set( CMAKE_MODULE_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} ${CMAKE_MODULE_LINKER_FLAGS}" ) -set( CMAKE_EXE_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} ${CMAKE_EXE_LINKER_FLAGS}" ) - -if( MIPS AND BUILD_WITH_ANDROID_NDK AND ANDROID_NDK_RELEASE STREQUAL "r8" ) - set( CMAKE_SHARED_LINKER_FLAGS "-Wl,-T,${ANDROID_NDK_TOOLCHAINS_PATH}/${ANDROID_GCC_TOOLCHAIN_NAME}/mipself.xsc ${CMAKE_SHARED_LINKER_FLAGS}" ) - set( CMAKE_MODULE_LINKER_FLAGS "-Wl,-T,${ANDROID_NDK_TOOLCHAINS_PATH}/${ANDROID_GCC_TOOLCHAIN_NAME}/mipself.xsc ${CMAKE_MODULE_LINKER_FLAGS}" ) - set( CMAKE_EXE_LINKER_FLAGS "-Wl,-T,${ANDROID_NDK_TOOLCHAINS_PATH}/${ANDROID_GCC_TOOLCHAIN_NAME}/mipself.x ${CMAKE_EXE_LINKER_FLAGS}" ) -endif() - -# pie/pic -if( NOT (ANDROID_NATIVE_API_LEVEL LESS 16) AND (NOT DEFINED ANDROID_APP_PIE OR ANDROID_APP_PIE) AND (CMAKE_VERSION VERSION_GREATER 2.8.8) ) - set( CMAKE_POSITION_INDEPENDENT_CODE TRUE ) - set( CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fPIE -pie") -else() - set( CMAKE_POSITION_INDEPENDENT_CODE FALSE ) - set( CMAKE_CXX_FLAGS "-fpic ${CMAKE_CXX_FLAGS}" ) - set( CMAKE_C_FLAGS "-fpic ${CMAKE_C_FLAGS}" ) -endif() - -# configure rtti -if( DEFINED ANDROID_RTTI AND ANDROID_STL_FORCE_FEATURES ) - if( ANDROID_RTTI ) - set( CMAKE_CXX_FLAGS "-frtti ${CMAKE_CXX_FLAGS}" ) - else() - set( CMAKE_CXX_FLAGS "-fno-rtti ${CMAKE_CXX_FLAGS}" ) - endif() -endif() - -# configure exceptios -if( DEFINED ANDROID_EXCEPTIONS AND ANDROID_STL_FORCE_FEATURES ) - if( ANDROID_EXCEPTIONS ) - set( CMAKE_CXX_FLAGS "-fexceptions ${CMAKE_CXX_FLAGS}" ) - set( CMAKE_C_FLAGS "-fexceptions ${CMAKE_C_FLAGS}" ) - else() - set( CMAKE_CXX_FLAGS "-fno-exceptions ${CMAKE_CXX_FLAGS}" ) - set( CMAKE_C_FLAGS "-fno-exceptions ${CMAKE_C_FLAGS}" ) - endif() -endif() - -# global includes and link directories -include_directories( SYSTEM "${ANDROID_SYSROOT}/usr/include" ${ANDROID_STL_INCLUDE_DIRS} ) -get_filename_component(__android_install_path "${CMAKE_INSTALL_PREFIX}/libs/${ANDROID_NDK_ABI_NAME}" ABSOLUTE) # avoid CMP0015 policy warning -link_directories( "${__android_install_path}" ) - -# detect if need link crtbegin_so.o explicitly -if( NOT DEFINED ANDROID_EXPLICIT_CRT_LINK ) - set( __cmd "${CMAKE_CXX_CREATE_SHARED_LIBRARY}" ) - string( REPLACE "" "${CMAKE_CXX_COMPILER} ${CMAKE_CXX_COMPILER_ARG1}" __cmd "${__cmd}" ) - string( REPLACE "" "${CMAKE_C_COMPILER} ${CMAKE_C_COMPILER_ARG1}" __cmd "${__cmd}" ) - string( REPLACE "" "${CMAKE_CXX_FLAGS}" __cmd "${__cmd}" ) - string( REPLACE "" "" __cmd "${__cmd}" ) - string( REPLACE "" "${CMAKE_SHARED_LINKER_FLAGS}" __cmd "${__cmd}" ) - string( REPLACE "" "-shared" __cmd "${__cmd}" ) - string( REPLACE "" "" __cmd "${__cmd}" ) - string( REPLACE "" "" __cmd "${__cmd}" ) - string( REPLACE "" "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/toolchain_crtlink_test.so" __cmd "${__cmd}" ) - string( REPLACE "" "\"${ANDROID_SYSROOT}/usr/lib/crtbegin_so.o\"" __cmd "${__cmd}" ) - string( REPLACE "" "" __cmd "${__cmd}" ) - separate_arguments( __cmd ) - foreach( __var ANDROID_NDK ANDROID_NDK_TOOLCHAINS_PATH ANDROID_STANDALONE_TOOLCHAIN ) - if( ${__var} ) - set( __tmp "${${__var}}" ) - separate_arguments( __tmp ) - string( REPLACE "${__tmp}" "${${__var}}" __cmd "${__cmd}") - endif() - endforeach() - string( REPLACE "'" "" __cmd "${__cmd}" ) - string( REPLACE "\"" "" __cmd "${__cmd}" ) - execute_process( COMMAND ${__cmd} RESULT_VARIABLE __cmd_result OUTPUT_QUIET ERROR_QUIET ) - if( __cmd_result EQUAL 0 ) - set( ANDROID_EXPLICIT_CRT_LINK ON ) - else() - set( ANDROID_EXPLICIT_CRT_LINK OFF ) - endif() -endif() - -if( ANDROID_EXPLICIT_CRT_LINK ) - set( CMAKE_CXX_CREATE_SHARED_LIBRARY "${CMAKE_CXX_CREATE_SHARED_LIBRARY} \"${ANDROID_SYSROOT}/usr/lib/crtbegin_so.o\"" ) - set( CMAKE_CXX_CREATE_SHARED_MODULE "${CMAKE_CXX_CREATE_SHARED_MODULE} \"${ANDROID_SYSROOT}/usr/lib/crtbegin_so.o\"" ) -endif() - -# setup output directories -set( CMAKE_INSTALL_PREFIX "${ANDROID_TOOLCHAIN_ROOT}/user" CACHE STRING "path for installing" ) - -if( DEFINED LIBRARY_OUTPUT_PATH_ROOT - OR EXISTS "${CMAKE_SOURCE_DIR}/AndroidManifest.xml" - OR (EXISTS "${CMAKE_SOURCE_DIR}/../AndroidManifest.xml" AND EXISTS "${CMAKE_SOURCE_DIR}/../jni/") ) - set( LIBRARY_OUTPUT_PATH_ROOT ${CMAKE_SOURCE_DIR} CACHE PATH "Root for binaries output, set this to change where Android libs are installed to" ) - if( NOT _CMAKE_IN_TRY_COMPILE ) - if( EXISTS "${CMAKE_SOURCE_DIR}/jni/CMakeLists.txt" ) - set( EXECUTABLE_OUTPUT_PATH "${LIBRARY_OUTPUT_PATH_ROOT}/bin/${ANDROID_NDK_ABI_NAME}" CACHE PATH "Output directory for applications" ) - else() - set( EXECUTABLE_OUTPUT_PATH "${LIBRARY_OUTPUT_PATH_ROOT}/bin" CACHE PATH "Output directory for applications" ) - endif() - set( LIBRARY_OUTPUT_PATH "${LIBRARY_OUTPUT_PATH_ROOT}/libs/${ANDROID_NDK_ABI_NAME}" CACHE PATH "Output directory for Android libs" ) - endif() -endif() - -# copy shaed stl library to build directory -if( NOT _CMAKE_IN_TRY_COMPILE AND __libstl MATCHES "[.]so$" AND DEFINED LIBRARY_OUTPUT_PATH ) - get_filename_component( __libstlname "${__libstl}" NAME ) - execute_process( COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${__libstl}" "${LIBRARY_OUTPUT_PATH}/${__libstlname}" RESULT_VARIABLE __fileCopyProcess ) - if( NOT __fileCopyProcess EQUAL 0 OR NOT EXISTS "${LIBRARY_OUTPUT_PATH}/${__libstlname}") - message( SEND_ERROR "Failed copying of ${__libstl} to the ${LIBRARY_OUTPUT_PATH}/${__libstlname}" ) - endif() - unset( __fileCopyProcess ) - unset( __libstlname ) -endif() - - -# set these global flags for cmake client scripts to change behavior -set( ANDROID True ) -set( BUILD_ANDROID True ) - -# where is the target environment -set( CMAKE_FIND_ROOT_PATH "${ANDROID_TOOLCHAIN_ROOT}/bin" "${ANDROID_TOOLCHAIN_ROOT}/${ANDROID_TOOLCHAIN_MACHINE_NAME}" "${ANDROID_SYSROOT}" "${CMAKE_INSTALL_PREFIX}" "${CMAKE_INSTALL_PREFIX}/share" ) - -# only search for libraries and includes in the ndk toolchain -set( CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ONLY ) -set( CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY ) -set( CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY ) - - -# macro to find packages on the host OS -macro( find_host_package ) - set( CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER ) - set( CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER ) - set( CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER ) - if( CMAKE_HOST_WIN32 ) - SET( WIN32 1 ) - SET( UNIX ) - elseif( CMAKE_HOST_APPLE ) - SET( APPLE 1 ) - SET( UNIX ) - endif() - find_package( ${ARGN} ) - SET( WIN32 ) - SET( APPLE ) - SET( UNIX 1 ) - set( CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ONLY ) - set( CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY ) - set( CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY ) -endmacro() - - -# macro to find programs on the host OS -macro( find_host_program ) - set( CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER ) - set( CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER ) - set( CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER ) - if( CMAKE_HOST_WIN32 ) - SET( WIN32 1 ) - SET( UNIX ) - elseif( CMAKE_HOST_APPLE ) - SET( APPLE 1 ) - SET( UNIX ) - endif() - find_program( ${ARGN} ) - SET( WIN32 ) - SET( APPLE ) - SET( UNIX 1 ) - set( CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ONLY ) - set( CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY ) - set( CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY ) -endmacro() - - -# export toolchain settings for the try_compile() command -if( NOT _CMAKE_IN_TRY_COMPILE ) - set( __toolchain_config "") - foreach( __var NDK_CCACHE LIBRARY_OUTPUT_PATH_ROOT ANDROID_FORBID_SYGWIN - ANDROID_NDK_HOST_X64 - ANDROID_NDK - ANDROID_NDK_LAYOUT - ANDROID_STANDALONE_TOOLCHAIN - ANDROID_TOOLCHAIN_NAME - ANDROID_ABI - ANDROID_NATIVE_API_LEVEL - ANDROID_STL - ANDROID_STL_FORCE_FEATURES - ANDROID_FORCE_ARM_BUILD - ANDROID_NO_UNDEFINED - ANDROID_SO_UNDEFINED - ANDROID_FUNCTION_LEVEL_LINKING - ANDROID_GOLD_LINKER - ANDROID_NOEXECSTACK - ANDROID_RELRO - ANDROID_LIBM_PATH - ANDROID_EXPLICIT_CRT_LINK - ANDROID_APP_PIE - ) - if( DEFINED ${__var} ) - if( ${__var} MATCHES " ") - set( __toolchain_config "${__toolchain_config}set( ${__var} \"${${__var}}\" CACHE INTERNAL \"\" )\n" ) - else() - set( __toolchain_config "${__toolchain_config}set( ${__var} ${${__var}} CACHE INTERNAL \"\" )\n" ) - endif() - endif() - endforeach() - file( WRITE "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/android.toolchain.config.cmake" "${__toolchain_config}" ) - unset( __toolchain_config ) -endif() - - -# force cmake to produce / instead of \ in build commands for Ninja generator -if( CMAKE_GENERATOR MATCHES "Ninja" AND CMAKE_HOST_WIN32 ) - # it is a bad hack after all - # CMake generates Ninja makefiles with UNIX paths only if it thinks that we are going to build with MinGW - set( CMAKE_COMPILER_IS_MINGW TRUE ) # tell CMake that we are MinGW - set( CMAKE_CROSSCOMPILING TRUE ) # stop recursion - enable_language( C ) - enable_language( CXX ) - # unset( CMAKE_COMPILER_IS_MINGW ) # can't unset because CMake does not convert back-slashes in response files without it - unset( MINGW ) -endif() - - -# Variables controlling behavior or set by cmake toolchain: -# ANDROID_ABI : "armeabi-v7a" (default), "armeabi", "armeabi-v7a with NEON", "armeabi-v7a with VFPV3", "armeabi-v6 with VFP", "x86", "mips", "arm64-v8a", "x86_64", "mips64" -# ANDROID_NATIVE_API_LEVEL : 3,4,5,8,9,14,15,16,17,18,19,21 (depends on NDK version) -# ANDROID_STL : gnustl_static/gnustl_shared/stlport_static/stlport_shared/gabi++_static/gabi++_shared/system_re/system/none -# ANDROID_FORBID_SYGWIN : ON/OFF -# ANDROID_NO_UNDEFINED : ON/OFF -# ANDROID_SO_UNDEFINED : OFF/ON (default depends on NDK version) -# ANDROID_FUNCTION_LEVEL_LINKING : ON/OFF -# ANDROID_GOLD_LINKER : ON/OFF -# ANDROID_NOEXECSTACK : ON/OFF -# ANDROID_RELRO : ON/OFF -# ANDROID_FORCE_ARM_BUILD : ON/OFF -# ANDROID_STL_FORCE_FEATURES : ON/OFF -# ANDROID_LIBM_PATH : path to libm.so (set to something like $(TOP)/out/target/product//obj/lib/libm.so) to workaround unresolved `sincos` -# Can be set only at the first run: -# ANDROID_NDK : path to your NDK install -# NDK_CCACHE : path to your ccache executable -# ANDROID_TOOLCHAIN_NAME : the NDK name of compiler toolchain -# ANDROID_NDK_HOST_X64 : try to use x86_64 toolchain (default for x64 host systems) -# ANDROID_NDK_LAYOUT : the inner NDK structure (RELEASE, LINARO, ANDROID) -# LIBRARY_OUTPUT_PATH_ROOT : -# ANDROID_STANDALONE_TOOLCHAIN -# -# Primary read-only variables: -# ANDROID : always TRUE -# ARMEABI : TRUE for arm v6 and older devices -# ARMEABI_V6 : TRUE for arm v6 -# ARMEABI_V7A : TRUE for arm v7a -# ARM64_V8A : TRUE for arm64-v8a -# NEON : TRUE if NEON unit is enabled -# VFPV3 : TRUE if VFP version 3 is enabled -# X86 : TRUE if configured for x86 -# X86_64 : TRUE if configured for x86_64 -# MIPS : TRUE if configured for mips -# MIPS64 : TRUE if configured for mips64 -# BUILD_WITH_ANDROID_NDK : TRUE if NDK is used -# BUILD_WITH_STANDALONE_TOOLCHAIN : TRUE if standalone toolchain is used -# ANDROID_NDK_HOST_SYSTEM_NAME : "windows", "linux-x86" or "darwin-x86" depending on host platform -# ANDROID_NDK_ABI_NAME : "armeabi", "armeabi-v7a", "x86", "mips", "arm64-v8a", "x86_64", "mips64" depending on ANDROID_ABI -# ANDROID_NDK_RELEASE : from r5 to r10d; set only for NDK -# ANDROID_NDK_RELEASE_NUM : numeric ANDROID_NDK_RELEASE version (1000*major+minor) -# ANDROID_ARCH_NAME : "arm", "x86", "mips", "arm64", "x86_64", "mips64" depending on ANDROID_ABI -# ANDROID_SYSROOT : path to the compiler sysroot -# TOOL_OS_SUFFIX : "" or ".exe" depending on host platform -# ANDROID_COMPILER_IS_CLANG : TRUE if clang compiler is used -# -# Secondary (less stable) read-only variables: -# ANDROID_COMPILER_VERSION : GCC version used (not Clang version) -# ANDROID_CLANG_VERSION : version of clang compiler if clang is used -# ANDROID_CXX_FLAGS : C/C++ compiler flags required by Android platform -# ANDROID_SUPPORTED_ABIS : list of currently allowed values for ANDROID_ABI -# ANDROID_TOOLCHAIN_MACHINE_NAME : "arm-linux-androideabi", "arm-eabi" or "i686-android-linux" -# ANDROID_TOOLCHAIN_ROOT : path to the top level of toolchain (standalone or placed inside NDK) -# ANDROID_CLANG_TOOLCHAIN_ROOT : path to clang tools -# ANDROID_SUPPORTED_NATIVE_API_LEVELS : list of native API levels found inside NDK -# ANDROID_STL_INCLUDE_DIRS : stl include paths -# ANDROID_RTTI : if rtti is enabled by the runtime -# ANDROID_EXCEPTIONS : if exceptions are enabled by the runtime -# ANDROID_GCC_TOOLCHAIN_NAME : read-only, differs from ANDROID_TOOLCHAIN_NAME only if clang is used -# -# Defaults: -# ANDROID_DEFAULT_NDK_API_LEVEL -# ANDROID_DEFAULT_NDK_API_LEVEL_${ARCH} -# ANDROID_NDK_SEARCH_PATHS -# ANDROID_SUPPORTED_ABIS_${ARCH} -# ANDROID_SUPPORTED_NDK_VERSIONS diff --git a/cmake/android/deployment-file.json.in b/cmake/android/deployment-file.json.in deleted file mode 100644 index 81ed8a6ecc..0000000000 --- a/cmake/android/deployment-file.json.in +++ /dev/null @@ -1,13 +0,0 @@ -{ - "qt": "@QT_DIR@", - "sdk": "@ANDROID_SDK_ROOT@", - "ndk": "@ANDROID_NDK@", - "toolchain-prefix": "@ANDROID_TOOLCHAIN_MACHINE_NAME@", - "tool-prefix": "@ANDROID_TOOLCHAIN_MACHINE_NAME@", - "toolchain-version": "@ANDROID_COMPILER_VERSION@", - "ndk-host": "@ANDROID_NDK_HOST_SYSTEM_NAME@", - "target-architecture": "@ANDROID_ABI@", - "application-binary": "@EXECUTABLE_DESTINATION_PATH@", - "android-extra-libs": "@_DEPS@", - "android-package-source-directory": "@ANDROID_APK_BUILD_DIR@" -} diff --git a/cmake/android/strings.xml.in b/cmake/android/strings.xml.in deleted file mode 100644 index 6e6ce7b12e..0000000000 --- a/cmake/android/strings.xml.in +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - ${ANDROID_APP_DISPLAY_NAME} - - Can\'t find Ministro service.\nThe application can\'t start. - This application requires Ministro service. Would you like to install it? - Your application encountered a fatal error and cannot continue. - diff --git a/cmake/externals/glm/CMakeLists.txt b/cmake/externals/glm/CMakeLists.txt index 79a44fa48e..bc8089074f 100644 --- a/cmake/externals/glm/CMakeLists.txt +++ b/cmake/externals/glm/CMakeLists.txt @@ -6,7 +6,7 @@ ExternalProject_Add( URL https://hifi-public.s3.amazonaws.com/dependencies/glm-0.9.8.zip URL_MD5 579ac77a3110befa3244d68c0ceb7281 BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build - CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= ${EXTERNAL_ARGS} LOG_DOWNLOAD 1 LOG_CONFIGURE 1 LOG_BUILD 1 diff --git a/cmake/externals/tbb/CMakeLists.txt b/cmake/externals/tbb/CMakeLists.txt index 71d7b94597..9664fe7250 100644 --- a/cmake/externals/tbb/CMakeLists.txt +++ b/cmake/externals/tbb/CMakeLists.txt @@ -8,9 +8,6 @@ if (WIN32) elseif (APPLE) set(DOWNLOAD_URL http://s3.amazonaws.com/hifi-public/dependencies/tbb2017_20170604oss_mac_slim.tar.gz) set(DOWNLOAD_MD5 62bde626b396f8e1a85c6a8ded1d8105) -elseif (ANDROID) - set(DOWNLOAD_URL http://hifi-public.s3.amazonaws.com/dependencies/tbb2017_20170604oss_and_slim.tar.gz) - set(DOWNLOAD_MD5 04d50b64e1d81245a1be5f75f34d64c7) else () set(DOWNLOAD_URL http://hifi-public.s3.amazonaws.com/dependencies/tbb2017_20170604oss_lin_slim.tar.gz) set(DOWNLOAD_MD5 2a5c721f40fa3503ffc12c18dd00011c) @@ -107,3 +104,4 @@ endif () if (DEFINED ${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE) set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/include CACHE TYPE "List of tbb include directories") endif () + diff --git a/cmake/init.cmake b/cmake/init.cmake index 75fb3a4b52..9d7b0fd94c 100644 --- a/cmake/init.cmake +++ b/cmake/init.cmake @@ -34,10 +34,23 @@ file(GLOB HIFI_CUSTOM_MACROS "cmake/macros/*.cmake") foreach(CUSTOM_MACRO ${HIFI_CUSTOM_MACROS}) include(${CUSTOM_MACRO}) endforeach() +unset(HIFI_CUSTOM_MACROS) if (ANDROID) - file(GLOB ANDROID_CUSTOM_MACROS "cmake/android/*.cmake") - foreach(CUSTOM_MACRO ${ANDROID_CUSTOM_MACROS}) - include(${CUSTOM_MACRO}) - endforeach() + set(BUILD_SHARED_LIBS ON) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH) + + string(REGEX REPLACE "\\\\" "/" ANDROID_NDK ${ANDROID_NDK}) + string(REGEX REPLACE "\\\\" "/" CMAKE_TOOLCHAIN_FILE ${CMAKE_TOOLCHAIN_FILE}) + string(REGEX REPLACE "\\\\" "/" ANDROID_TOOLCHAIN ${ANDROID_TOOLCHAIN}) + string(REGEX REPLACE "\\\\" "/" CMAKE_MAKE_PROGRAM ${CMAKE_MAKE_PROGRAM}) + list(APPEND EXTERNAL_ARGS -DANDROID_ABI=${ANDROID_ABI}) + list(APPEND EXTERNAL_ARGS -DANDROID_NDK=${ANDROID_NDK}) + list(APPEND EXTERNAL_ARGS -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}) + list(APPEND EXTERNAL_ARGS -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}) + list(APPEND EXTERNAL_ARGS -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}) + list(APPEND EXTERNAL_ARGS -DHIFI_ANDROID=${HIFI_ANDROID}) + list(APPEND EXTERNAL_ARGS -DANDROID_PLATFORM=${ANDROID_PLATFORM}) + list(APPEND EXTERNAL_ARGS -DANDROID_TOOLCHAIN=${ANDROID_TOOLCHAIN}) + list(APPEND EXTERNAL_ARGS -DANDROID_STL=${ANDROID_STL}) endif () diff --git a/cmake/macros/AutoScribeShader.cmake b/cmake/macros/AutoScribeShader.cmake index c43ade45d2..c5b35b7e90 100755 --- a/cmake/macros/AutoScribeShader.cmake +++ b/cmake/macros/AutoScribeShader.cmake @@ -62,7 +62,9 @@ function(AUTOSCRIBE_SHADER SHADER_FILE) # since it's unrunnable by the cross-compiling build machine # so, we require the compiling user to point us at a compiled executable version for their native toolchain - find_program(NATIVE_SCRIBE scribe PATHS ${SCRIBE_PATH} ENV SCRIBE_PATH) + if (NOT NATIVE_SCRIBE) + find_program(NATIVE_SCRIBE scribe PATHS ${SCRIBE_PATH} ENV SCRIBE_PATH) + endif() if (NOT NATIVE_SCRIBE) message(FATAL_ERROR "The High Fidelity scribe tool is required for shader pre-processing. \ diff --git a/cmake/macros/SetPackagingParameters.cmake b/cmake/macros/SetPackagingParameters.cmake index 8458d53f68..8faa4e6d96 100644 --- a/cmake/macros/SetPackagingParameters.cmake +++ b/cmake/macros/SetPackagingParameters.cmake @@ -162,5 +162,6 @@ macro(SET_PACKAGING_PARAMETERS) # create a header file our targets can use to find out the application version file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/includes") configure_file("${HF_CMAKE_DIR}/templates/BuildInfo.h.in" "${CMAKE_BINARY_DIR}/includes/BuildInfo.h") + include_directories("${CMAKE_BINARY_DIR}/includes") endmacro(SET_PACKAGING_PARAMETERS) diff --git a/cmake/macros/SetupHifiLibrary.cmake b/cmake/macros/SetupHifiLibrary.cmake index d0fc58af0c..04687e2c84 100644 --- a/cmake/macros/SetupHifiLibrary.cmake +++ b/cmake/macros/SetupHifiLibrary.cmake @@ -12,7 +12,7 @@ macro(SETUP_HIFI_LIBRARY) project(${TARGET_NAME}) # grab the implementation and header files - file(GLOB_RECURSE LIB_SRCS "src/*.h" "src/*.cpp" "src/*.c") + file(GLOB_RECURSE LIB_SRCS "src/*.h" "src/*.cpp" "src/*.c" "src/*.qrc") list(APPEND ${TARGET_NAME}_SRCS ${LIB_SRCS}) # add compiler flags to AVX source files @@ -65,7 +65,7 @@ macro(SETUP_HIFI_LIBRARY) list(APPEND ${TARGET_NAME}_DEPENDENCY_QT_MODULES Core) # find these Qt modules and link them to our own target - find_package(Qt5 COMPONENTS ${${TARGET_NAME}_DEPENDENCY_QT_MODULES} REQUIRED) + find_package(Qt5 COMPONENTS ${${TARGET_NAME}_DEPENDENCY_QT_MODULES} REQUIRED CMAKE_FIND_ROOT_PATH_BOTH) foreach(QT_MODULE ${${TARGET_NAME}_DEPENDENCY_QT_MODULES}) target_link_libraries(${TARGET_NAME} Qt5::${QT_MODULE}) diff --git a/cmake/macros/SetupQt.cmake b/cmake/macros/SetupQt.cmake index b2a89f81e5..ece8607b9b 100644 --- a/cmake/macros/SetupQt.cmake +++ b/cmake/macros/SetupQt.cmake @@ -28,7 +28,7 @@ function(calculate_default_qt_dir _RESULT_NAME) set(QT_DEFAULT_ARCH "gcc_64") endif() - if (WIN32) + if (WIN32 OR (ANDROID AND ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows"))) set(QT_DEFAULT_ROOT "c:/Qt") else() set(QT_DEFAULT_ROOT "$ENV{HOME}/Qt") diff --git a/cmake/macros/TargetGlew.cmake b/cmake/macros/TargetGlew.cmake index 5f71f021ec..bc4d5cb033 100644 --- a/cmake/macros/TargetGlew.cmake +++ b/cmake/macros/TargetGlew.cmake @@ -6,9 +6,11 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # macro(TARGET_GLEW) - add_dependency_external_projects(glew) - find_package(GLEW REQUIRED) - add_definitions(-DGLEW_STATIC) - target_include_directories(${TARGET_NAME} PUBLIC ${GLEW_INCLUDE_DIRS}) - target_link_libraries(${TARGET_NAME} ${GLEW_LIBRARY}) + if (NOT ANDROID) + add_definitions(-DGLEW_STATIC) + add_dependency_external_projects(glew) + find_package(GLEW REQUIRED) + target_include_directories(${TARGET_NAME} PUBLIC ${GLEW_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${GLEW_LIBRARY}) + endif() endmacro() \ No newline at end of file diff --git a/cmake/macros/TargetOpenGL.cmake b/cmake/macros/TargetOpenGL.cmake index 73c92e651a..6ad92259bb 100644 --- a/cmake/macros/TargetOpenGL.cmake +++ b/cmake/macros/TargetOpenGL.cmake @@ -6,15 +6,13 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # macro(TARGET_OPENGL) - add_definitions(-DGLEW_STATIC) if (APPLE) # link in required OS X frameworks and include the right GL headers find_library(OpenGL OpenGL) target_link_libraries(${TARGET_NAME} ${OpenGL}) elseif(ANDROID) - target_link_libraries(${TARGET_NAME} "-lGLESv3" "-lEGL") + target_link_libraries(${TARGET_NAME} GLESv3 EGL) else() - target_nsight() find_package(OpenGL REQUIRED) if (${OPENGL_INCLUDE_DIR}) include_directories(SYSTEM "${OPENGL_INCLUDE_DIR}") @@ -22,4 +20,6 @@ macro(TARGET_OPENGL) target_link_libraries(${TARGET_NAME} "${OPENGL_LIBRARY}") target_include_directories(${TARGET_NAME} PUBLIC ${OPENGL_INCLUDE_DIR}) endif() + target_nsight() + target_glew() endmacro() diff --git a/cmake/macros/TargetOpenSSL.cmake b/cmake/macros/TargetOpenSSL.cmake new file mode 100644 index 0000000000..7ee0283a48 --- /dev/null +++ b/cmake/macros/TargetOpenSSL.cmake @@ -0,0 +1,32 @@ +# +# Copyright 2015 High Fidelity, Inc. +# Created by Bradley Austin Davis on 2015/10/10 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# +macro(TARGET_OPENSSL) + + if (ANDROID) + + # FIXME use a distributable binary + set(OPENSSL_INSTALL_DIR C:/Android/openssl) + set(OPENSSL_INCLUDE_DIR "${OPENSSL_INSTALL_DIR}/include" CACHE TYPE INTERNAL) + set(OPENSSL_LIBRARIES "${OPENSSL_INSTALL_DIR}/lib/libcrypto.a;${OPENSSL_INSTALL_DIR}/lib/libssl.a" CACHE TYPE INTERNAL) + + else() + + find_package(OpenSSL REQUIRED) + + if (APPLE AND ${OPENSSL_INCLUDE_DIR} STREQUAL "/usr/include") + # this is a user on OS X using system OpenSSL, which is going to throw warnings since they're deprecating for their common crypto + message(WARNING "The found version of OpenSSL is the OS X system version. This will produce deprecation warnings." + "\nWe recommend you install a newer version (at least 1.0.1h) in a different directory and set OPENSSL_ROOT_DIR in your env so Cmake can find it.") + endif() + + endif() + + include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}") + target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES}) + +endmacro() diff --git a/cmake/macros/TargetTBB.cmake b/cmake/macros/TargetTBB.cmake new file mode 100644 index 0000000000..e9c4639c3d --- /dev/null +++ b/cmake/macros/TargetTBB.cmake @@ -0,0 +1,24 @@ +# +# Copyright 2015 High Fidelity, Inc. +# Created by Bradley Austin Davis on 2015/10/10 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# +macro(TARGET_TBB) + +if (ANDROID) + set(TBB_INSTALL_DIR C:/tbb-2018/built) + set(TBB_LIBRARY ${HIFI_ANDROID_PRECOMPILED}/libtbb.so CACHE FILEPATH "TBB library location") + set(TBB_MALLOC_LIBRARY ${HIFI_ANDROID_PRECOMPILED}/libtbbmalloc.so CACHE FILEPATH "TBB malloc library location") + set(TBB_INCLUDE_DIRS ${TBB_INSTALL_DIR}/include CACHE TYPE "List of tbb include directories" CACHE FILEPATH "TBB includes location") + set(TBB_LIBRARIES ${TBB_LIBRARY} ${TBB_MALLOC_LIBRARY}) +else() + add_dependency_external_projects(tbb) + find_package(TBB REQUIRED) +endif() + +target_link_libraries(${TARGET_NAME} ${TBB_LIBRARIES}) +target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${TBB_INCLUDE_DIRS}) + +endmacro() diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 43e50c6d33..305a6475f6 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -171,8 +171,6 @@ else () add_executable(${TARGET_NAME} ${INTERFACE_SRCS} ${QM}) endif () -target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/includes") - if (WIN32) # These are external plugins, but we need to do the 'add dependency' here so that their # binary directories get added to the fixup path @@ -214,10 +212,6 @@ target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/libraries target_bullet() target_opengl() -if (NOT ANDROID) - target_glew() -endif () - # perform standard include and linking for found externals foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 4e7cc08919..621b1a3e72 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1503,6 +1503,11 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn int numFrameSamples = calculateNumberOfFrameSamples(_numInputCallbackBytes); _inputRingBuffer.resizeForFrameSize(numFrameSamples); +#if defined(Q_OS_ANDROID) + if (_audioInput) { + connect(_audioInput, SIGNAL(stateChanged(QAudio::State)), this, SLOT(audioInputStateChanged(QAudio::State))); + } +#endif _inputDevice = _audioInput->start(); if (_inputDevice) { @@ -1541,6 +1546,31 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn return supportedFormat; } +#if defined(Q_OS_ANDROID) +void AudioClient::audioInputStateChanged(QAudio::State state) { + switch (state) { + case QAudio::StoppedState: + if (!_audioInput) { + break; + } + // Stopped on purpose + if (_shouldRestartInputSetup) { + Lock lock(_deviceMutex); + _inputDevice = _audioInput->start(); + lock.unlock(); + if (_inputDevice) { + connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleMicAudioInput())); + } + } + break; + case QAudio::ActiveState: + break; + default: + break; + } +} +#endif + void AudioClient::outputNotify() { int recentUnfulfilled = _audioOutputIODevice.getRecentUnfulfilledReads(); if (recentUnfulfilled > 0) { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index ff0ea968a8..01a487455c 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -18,7 +18,7 @@ #include #include -#include +#include #include #include #include @@ -173,6 +173,9 @@ public slots: void sendDownstreamAudioStatsPacket() { _stats.publish(); } void handleMicAudioInput(); +#if defined(Q_OS_ANDROID) + void audioInputStateChanged(QAudio::State state); +#endif void handleDummyAudioInput(); void handleRecordedAudioInput(const QByteArray& audio); void reset(); @@ -403,6 +406,10 @@ private: RateCounter<> _silentInbound; RateCounter<> _audioInbound; +#if defined(Q_OS_ANDROID) + bool _shouldRestartInputSetup { true }; // Should we restart the input device because of an unintended stop? +#endif + QTimer* _checkDevicesTimer { nullptr }; QTimer* _checkPeakValuesTimer { nullptr }; }; diff --git a/libraries/audio/src/AudioGate.cpp b/libraries/audio/src/AudioGate.cpp index a4d731a447..5b2561da07 100644 --- a/libraries/audio/src/AudioGate.cpp +++ b/libraries/audio/src/AudioGate.cpp @@ -6,11 +6,12 @@ // Copyright 2017 High Fidelity, Inc. // +#include "AudioGate.h" + #include #include - +#include #include "AudioDynamics.h" -#include "AudioGate.h" // log2 domain headroom bits above 0dB (int32_t) static const int LOG2_HEADROOM_Q30 = 1; @@ -417,7 +418,7 @@ void GateMono::process(int16_t* input, int16_t* output, int numFrames) { _dc.process(x); // peak detect - int32_t peak = abs(x); + int32_t peak = std::abs(x); // convert to log2 domain peak = fixlog2(peak); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 1baf649e64..278ef25445 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -103,7 +103,7 @@ AvatarData::~AvatarData() { QUrl AvatarData::_defaultFullAvatarModelUrl = {}; // In C++, if this initialization were in the AvatarInfo, every file would have it's own copy, even for class vars. const QUrl& AvatarData::defaultFullAvatarModelUrl() { if (_defaultFullAvatarModelUrl.isEmpty()) { - _defaultFullAvatarModelUrl = QUrl::fromLocalFile(PathUtils::resourcesPath() + "meshes/defaultAvatar_full.fst"); + _defaultFullAvatarModelUrl = QUrl::fromLocalFile(PathUtils::resourcesPath() + "/meshes/defaultAvatar_full.fst"); } return _defaultFullAvatarModelUrl; } diff --git a/libraries/controllers/CMakeLists.txt b/libraries/controllers/CMakeLists.txt index 6b1ab72c60..9c6bbf4aae 100644 --- a/libraries/controllers/CMakeLists.txt +++ b/libraries/controllers/CMakeLists.txt @@ -8,7 +8,3 @@ link_hifi_libraries(shared) include_hifi_library_headers(networking) GroupSources("src/controllers") - -add_dependency_external_projects(glm) -find_package(GLM REQUIRED) -target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/includes") diff --git a/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp index 3e090dc7b3..da226d146b 100644 --- a/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp @@ -28,11 +28,14 @@ DisplayPluginList getDisplayPlugins() { #ifdef DEBUG new NullDisplayPlugin(), #endif + +#if !defined(Q_OS_ANDROID) // Stereo modes // SBS left/right new SideBySideStereoDisplayPlugin(), // Interleaved left/right new InterleavedStereoDisplayPlugin(), +#endif nullptr }; diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp index 1e0e7e6c1f..6e397efbe5 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp @@ -40,14 +40,6 @@ bool DebugHmdDisplayPlugin::beginFrameRender(uint32_t frameIndex) { return Parent::beginFrameRender(frameIndex); } -// DLL based display plugins MUST initialize GLEW inside the DLL code. -void DebugHmdDisplayPlugin::customizeContext() { - glewExperimental = true; - glewInit(); - glGetError(); // clear the potential error from glewExperimental - Parent::customizeContext(); -} - bool DebugHmdDisplayPlugin::internalActivate() { _ipd = 0.0327499993f * 2.0f; _eyeProjections[0][0] = vec4{ 0.759056330, 0.000000000, 0.000000000, 0.000000000 }; @@ -61,7 +53,7 @@ bool DebugHmdDisplayPlugin::internalActivate() { _eyeInverseProjections[0] = glm::inverse(_eyeProjections[0]); _eyeInverseProjections[1] = glm::inverse(_eyeProjections[1]); _eyeOffsets[0][3] = vec4{ -0.0327499993, 0.0, 0.0149999997, 1.0 }; - _eyeOffsets[0][3] = vec4{ 0.0327499993, 0.0, 0.0149999997, 1.0 }; + _eyeOffsets[1][3] = vec4{ 0.0327499993, 0.0, 0.0149999997, 1.0 }; _renderTargetSize = { 3024, 1680 }; _cullingProjection = _eyeProjections[0]; // This must come after the initialization, so that the values calculated diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h index 9bb82b1836..cd6fdd44b9 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h @@ -26,7 +26,6 @@ protected: void updatePresentPose() override; void hmdPresent() override {} bool isHmdMounted() const override { return true; } - void customizeContext() override; bool internalActivate() override; private: static const QString NAME; diff --git a/libraries/entities-renderer/src/paintStroke.slf b/libraries/entities-renderer/src/paintStroke.slf index bfbe6d7e5a..ccf2057e09 100644 --- a/libraries/entities-renderer/src/paintStroke.slf +++ b/libraries/entities-renderer/src/paintStroke.slf @@ -38,7 +38,7 @@ void main(void) { int frontCondition = 1 -int(gl_FrontFacing) * 2; vec3 color = varColor.rgb; packDeferredFragmentTranslucent( - interpolatedNormal * frontCondition, + float(frontCondition) * interpolatedNormal, texel.a * varColor.a, polyline.color * texel.rgb, vec3(0.01, 0.01, 0.01), diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index f91bc14fe4..c6b5bc953b 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -659,22 +659,22 @@ QVector EntityScriptingInterface::findEntitiesInFrustum(QVariantMap frust } QVector EntityScriptingInterface::findEntitiesByType(const QString entityType, const glm::vec3& center, float radius) const { - EntityTypes::EntityType type = EntityTypes::getEntityTypeFromName(entityType); + EntityTypes::EntityType type = EntityTypes::getEntityTypeFromName(entityType); - QVector result; - if (_entityTree) { - QVector entities; - _entityTree->withReadLock([&] { - _entityTree->findEntities(center, radius, entities); - }); + QVector result; + if (_entityTree) { + QVector entities; + _entityTree->withReadLock([&] { + _entityTree->findEntities(center, radius, entities); + }); - foreach(EntityItemPointer entity, entities) { - if (entity->getType() == type) { - result << entity->getEntityItemID(); - } - } - } - return result; + foreach(EntityItemPointer entity, entities) { + if (entity->getType() == type) { + result << entity->getEntityItemID(); + } + } + } + return result; } RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersection(const PickRay& ray, bool precisionPicking, diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 9b2b6360f3..7248c1f851 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -215,12 +215,12 @@ public slots: /// this function will not find any models in script engine contexts which don't have access to entities Q_INVOKABLE QVector findEntitiesInFrustum(QVariantMap frustum) const; - /// finds entities of the indicated type within a sphere given by the center point and radius - /// @param {QString} string representation of entity type - /// @param {vec3} center point - /// @param {float} radius to search - /// this function will not find any entities in script engine contexts which don't have access to entities - Q_INVOKABLE QVector findEntitiesByType(const QString entityType, const glm::vec3& center, float radius) const; + /// finds entities of the indicated type within a sphere given by the center point and radius + /// @param {QString} string representation of entity type + /// @param {vec3} center point + /// @param {float} radius to search + /// this function will not find any entities in script engine contexts which don't have access to entities + Q_INVOKABLE QVector findEntitiesByType(const QString entityType, const glm::vec3& center, float radius) const; /// If the scripting context has visible entities, this will determine a ray intersection, the results /// may be inaccurate if the engine is unable to access the visible entities, in which case result.accurate diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index cf5babdb1a..08acf9b058 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -590,7 +590,7 @@ bool EntityTree::findNearPointOperation(const OctreeElementPointer& element, voi bool findRayIntersectionOp(const OctreeElementPointer& element, void* extraData) { RayArgs* args = static_cast(extraData); bool keepSearching = true; - EntityTreeElementPointer entityTreeElementPointer = std::dynamic_pointer_cast(element); + EntityTreeElementPointer entityTreeElementPointer = std::static_pointer_cast(element); if (entityTreeElementPointer->findRayIntersection(args->origin, args->direction, keepSearching, args->element, args->distance, args->face, args->surfaceNormal, args->entityIdsToInclude, args->entityIdsToDiscard, args->visibleOnly, args->collidableOnly, args->intersectedObject, args->precisionPicking)) { diff --git a/libraries/entities/src/PolyLineEntityItem.h b/libraries/entities/src/PolyLineEntityItem.h index 4860e3d4a4..3e482fcd60 100644 --- a/libraries/entities/src/PolyLineEntityItem.h +++ b/libraries/entities/src/PolyLineEntityItem.h @@ -89,17 +89,17 @@ class PolyLineEntityItem : public EntityItem { BoxFace& face, glm::vec3& surfaceNormal, void** intersectedObject, bool precisionPicking) const override { return false; } - // disable these external interfaces as PolyLineEntities caculate their own dimensions based on the points they contain - virtual void setRegistrationPoint(const glm::vec3& value) override {}; - virtual void setScale(const glm::vec3& scale) override {}; - virtual void setScale(float value) override {}; + // disable these external interfaces as PolyLineEntities caculate their own dimensions based on the points they contain + virtual void setRegistrationPoint(const glm::vec3& value) override {}; + virtual void setScale(const glm::vec3& scale) override {}; + virtual void setScale(float value) override {}; virtual void debugDump() const override; static const float DEFAULT_LINE_WIDTH; static const int MAX_POINTS_PER_LINE; private: - void calculateScaleAndRegistrationPoint(); - + void calculateScaleAndRegistrationPoint(); + protected: rgbColor _color; float _lineWidth { DEFAULT_LINE_WIDTH }; diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index 9d74662e7c..586344ee81 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -223,7 +223,7 @@ void ShapeEntityItem::debugDump() const { qCDebug(entities) << " position:" << debugTreeVector(getPosition()); qCDebug(entities) << " dimensions:" << debugTreeVector(getDimensions()); qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); - qCDebug(entities) << "SHAPE EntityItem Ptr:" << this; + qCDebug(entities) << "SHAPE EntityItem Ptr:" << this; } void ShapeEntityItem::computeShapeInfo(ShapeInfo& info) { diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 3099782588..e0c2efd72e 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -28,6 +28,7 @@ #include "FBXReader.h" #include "ModelFormatLogging.h" +#include QHash COMMENT_SCALE_HINTS = {{"This file uses centimeters as units", 1.0f / 100.0f}, {"This file uses millimeters as units", 1.0f / 1000.0f}}; @@ -51,6 +52,10 @@ const QByteArray OBJTokenizer::getLineAsDatum() { return _device->readLine().trimmed(); } +float OBJTokenizer::getFloat() { + return std::stof((nextToken() != OBJTokenizer::DATUM_TOKEN) ? nullptr : getDatum().data()); +} + int OBJTokenizer::nextToken() { if (_pushedBackToken != NO_PUSHBACKED_TOKEN) { int token = _pushedBackToken; @@ -125,7 +130,7 @@ glm::vec3 OBJTokenizer::getVec3() { } bool OBJTokenizer::getVertex(glm::vec3& vertex, glm::vec3& vertexColor) { // Used for vertices which may also have a vertex color (RGB [0,1]) to follow. - // NOTE: Returns true if there is a vertex color. + // NOTE: Returns true if there is a vertex color. auto x = getFloat(); // N.B.: getFloat() has side-effect auto y = getFloat(); // And order of arguments is different on Windows/Linux. auto z = getFloat(); @@ -168,7 +173,7 @@ void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID) { } // OBJFace -// NOTE (trent, 7/20/17): The vertexColors vector being passed-in isn't necessary here, but I'm just +// NOTE (trent, 7/20/17): The vertexColors vector being passed-in isn't necessary here, but I'm just // pairing it with the vertices vector for consistency. bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors) { bool ok; @@ -544,9 +549,9 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, fbxMeshParts.append(FBXMeshPart()); FBXMeshPart& meshPartNew = fbxMeshParts.last(); - meshPartNew.quadIndices = QVector(meshPart.quadIndices); // Copy over quad indices [NOTE (trent/mittens, 4/3/17): Likely unnecessary since they go unused anyway]. + meshPartNew.quadIndices = QVector(meshPart.quadIndices); // Copy over quad indices [NOTE (trent/mittens, 4/3/17): Likely unnecessary since they go unused anyway]. meshPartNew.quadTrianglesIndices = QVector(meshPart.quadTrianglesIndices); // Copy over quad triangulated indices [NOTE (trent/mittens, 4/3/17): Likely unnecessary since they go unused anyway]. - meshPartNew.triangleIndices = QVector(meshPart.triangleIndices); // Copy over triangle indices. + meshPartNew.triangleIndices = QVector(meshPart.triangleIndices); // Copy over triangle indices. // Do some of the material logic (which previously lived below) now. // All the faces in the same group will have the same name and material. diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 9a32871590..fb250833cf 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -22,7 +22,7 @@ public: glm::vec3 getVec3(); bool getVertex(glm::vec3& vertex, glm::vec3& vertexColor); glm::vec2 getVec2(); - float getFloat() { return std::stof((nextToken() != OBJTokenizer::DATUM_TOKEN) ? nullptr : getDatum().data()); } + float getFloat(); private: QIODevice* _device; diff --git a/libraries/gl/CMakeLists.txt b/libraries/gl/CMakeLists.txt index fd3197410b..9fc7a0c10f 100644 --- a/libraries/gl/CMakeLists.txt +++ b/libraries/gl/CMakeLists.txt @@ -1,9 +1,5 @@ set(TARGET_NAME gl) setup_hifi_library(OpenGL Qml Quick) -link_hifi_libraries(shared networking) - +link_hifi_libraries(shared) target_opengl() -if (NOT ANDROID) - target_glew() -endif () diff --git a/libraries/gl/src/gl/Config.cpp b/libraries/gl/src/gl/Config.cpp new file mode 100644 index 0000000000..1f29fe21b1 --- /dev/null +++ b/libraries/gl/src/gl/Config.cpp @@ -0,0 +1,35 @@ +// +// GPUConfig.h +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 12/4/14. +// 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 "Config.h" + +#include + +#if defined(Q_OS_ANDROID) +PFNGLQUERYCOUNTEREXTPROC glQueryCounterEXT = NULL; +PFNGLGETQUERYOBJECTUI64VEXTPROC glGetQueryObjectui64vEXT = NULL; +PFNGLFRAMEBUFFERTEXTUREEXTPROC glFramebufferTextureEXT = NULL; +#endif + +void gl::initModuleGl() { + static std::once_flag once; + std::call_once(once, [] { +#if defined(Q_OS_ANDROID) + glQueryCounterEXT = (PFNGLQUERYCOUNTEREXTPROC)eglGetProcAddress("glQueryCounterEXT"); + glGetQueryObjectui64vEXT = (PFNGLGETQUERYOBJECTUI64VEXTPROC)eglGetProcAddress("glGetQueryObjectui64vEXT"); + glFramebufferTextureEXT = (PFNGLFRAMEBUFFERTEXTUREEXTPROC)eglGetProcAddress("glFramebufferTextureEXT"); +#else + glewExperimental = true; + glewInit(); +#endif + }); +} + diff --git a/libraries/gl/src/gl/Config.h b/libraries/gl/src/gl/Config.h index 9efae96f2a..ff282a1ca0 100644 --- a/libraries/gl/src/gl/Config.h +++ b/libraries/gl/src/gl/Config.h @@ -12,25 +12,67 @@ #ifndef hifi_gpu_GPUConfig_h #define hifi_gpu_GPUConfig_h -#define GL_GLEXT_PROTOTYPES 1 +#include +#if defined(QT_OPENGL_ES_3_1) +// Minimum GL ES version required is 3.1 +#define GL_MIN_VERSION_MAJOR 0x03 +#define GL_MIN_VERSION_MINOR 0x01 +#define GL_DEFAULT_VERSION_MAJOR GL_MIN_VERSION_MAJOR +#define GL_DEFAULT_VERSION_MINOR GL_MIN_VERSION_MINOR +#else +// Minimum desktop GL version required is 4.1 +#define GL_MIN_VERSION_MAJOR 0x04 +#define GL_MIN_VERSION_MINOR 0x01 +#define GL_DEFAULT_VERSION_MAJOR 0x04 +#define GL_DEFAULT_VERSION_MINOR 0x05 +#endif + +#define MINIMUM_GL_VERSION ((GL_MIN_VERSION_MAJOR << 8) | GL_MIN_VERSION_MINOR) + +#if defined(Q_OS_ANDROID) + +#include +#include + +#define GL_DEPTH_COMPONENT32_OES 0x81A7 +#define GL_TIME_ELAPSED_EXT 0x88BF +#define GL_TIMESTAMP_EXT 0x8E28 +#define GL_FRAMEBUFFER_SRGB_EXT 0x8DB9 +#define GL_TEXTURE_BORDER_COLOR_EXT 0x1004 +#define GL_CLAMP_TO_BORDER_EXT 0x812D +#define GL_TEXTURE_MAX_ANISOTROPY_EXT 0x84FE +#define GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT 0x84FF + + +// Add some additional extensions missing from GLES 3.1 +extern "C" { + typedef void (GL_APIENTRYP PFNGLQUERYCOUNTEREXTPROC) (GLuint id, GLenum target); + typedef void (GL_APIENTRYP PFNGLGETQUERYOBJECTUI64VEXTPROC) (GLuint id, GLenum pname, GLuint64 *params); + typedef void (GL_APIENTRYP PFNGLFRAMEBUFFERTEXTUREEXTPROC) (GLenum target, GLenum attachment, GLuint texture, GLint level); + extern PFNGLQUERYCOUNTEREXTPROC glQueryCounterEXT; + extern PFNGLGETQUERYOBJECTUI64VEXTPROC glGetQueryObjectui64vEXT; + extern PFNGLFRAMEBUFFERTEXTUREEXTPROC glFramebufferTextureEXT; +} + +#else // !defined(Q_OS_ANDROID) + +#define GL_GLEXT_PROTOTYPES 1 #include -#if defined(__APPLE__) - +#if defined(Q_OS_DARWIN) #include #include #include - -#endif - -#if defined(WIN32) - +#elif defined(Q_OS_WIN64) #include - -// Uncomment this define and recompile to be able to avoid code path preventing to be able to run nsight graphics debug -//#define HIFI_ENABLE_NSIGHT_DEBUG 1 - #endif +#endif // !defined(Q_OS_ANDROID) + +// Platform specific code to load the GL functions +namespace gl { + void initModuleGl(); +} + #endif // hifi_gpu_GPUConfig_h diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index 28982703dd..ed0594135a 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -28,6 +28,13 @@ const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { static QSurfaceFormat format; static std::once_flag once; std::call_once(once, [] { +#if defined(QT_OPENGL_ES_3_1) + format.setRenderableType(QSurfaceFormat::OpenGLES); + format.setRedBufferSize(8); + format.setGreenBufferSize(8); + format.setBlueBufferSize(8); + format.setAlphaBufferSize(8); +#endif // Qt Quick may need a depth and stencil buffer. Always make sure these are available. format.setDepthBufferSize(DEFAULT_GL_DEPTH_BUFFER_BITS); format.setStencilBufferSize(DEFAULT_GL_STENCIL_BUFFER_BITS); diff --git a/libraries/gl/src/gl/GLHelpers.h b/libraries/gl/src/gl/GLHelpers.h index 84229b97d2..80fc2c5f70 100644 --- a/libraries/gl/src/gl/GLHelpers.h +++ b/libraries/gl/src/gl/GLHelpers.h @@ -25,7 +25,12 @@ class QSurfaceFormat; class QGLFormat; template -void setGLFormatVersion(F& format, int major = 4, int minor = 5) { format.setVersion(major, minor); } +#if defined(QT_OPENGL_ES_3_1) +void setGLFormatVersion(F& format, int major = 3, int minor = 1) +#else +void setGLFormatVersion(F& format, int major = 4, int minor = 5) +#endif + { format.setVersion(major, minor); } size_t evalGLFormatSwapchainPixelSize(const QSurfaceFormat& format); diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index 3f1d629638..b974564705 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -59,7 +59,7 @@ bool OffscreenGLCanvas::create(QOpenGLContext* sharedContext) { bool OffscreenGLCanvas::makeCurrent() { bool result = _context->makeCurrent(_offscreenSurface); - std::call_once(_reportOnce, [this]{ + std::call_once(_reportOnce, []{ qCDebug(glLogging) << "GL Version: " << QString((const char*) glGetString(GL_VERSION)); qCDebug(glLogging) << "GL Shader Language Version: " << QString((const char*) glGetString(GL_SHADING_LANGUAGE_VERSION)); qCDebug(glLogging) << "GL Vendor: " << QString((const char*) glGetString(GL_VENDOR)); diff --git a/libraries/gl/src/gl/OpenGLVersionChecker.cpp b/libraries/gl/src/gl/OpenGLVersionChecker.cpp index f24a9bb932..771a8b9a75 100644 --- a/libraries/gl/src/gl/OpenGLVersionChecker.cpp +++ b/libraries/gl/src/gl/OpenGLVersionChecker.cpp @@ -21,9 +21,6 @@ #include "GLHelpers.h" -// Minimum gl version required is 4.1 -#define MINIMUM_GL_VERSION 0x0401 - OpenGLVersionChecker::OpenGLVersionChecker(int& argc, char** argv) : QApplication(argc, argv) { diff --git a/libraries/gpu-gl/CMakeLists.txt b/libraries/gpu-gl/CMakeLists.txt index 65130d6d07..dc744e73f2 100644 --- a/libraries/gpu-gl/CMakeLists.txt +++ b/libraries/gpu-gl/CMakeLists.txt @@ -5,10 +5,5 @@ if (UNIX) target_link_libraries(${TARGET_NAME} pthread) endif(UNIX) GroupSources("src") - target_opengl() -target_nsight() -if (NOT ANDROID) - target_glew() -endif () diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendShader.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendShader.cpp index b5a8dcb7a9..ff9ddaae63 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendShader.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendShader.cpp @@ -7,9 +7,9 @@ // #include "GL41Backend.h" #include "../gl/GLShader.h" -//#include using namespace gpu; +using namespace gpu::gl; using namespace gpu::gl41; // GLSL version @@ -84,7 +84,7 @@ int GL41Backend::makeResourceBufferSlots(GLuint glprogram, const Shader::Binding return ssboCount; } -void GL41Backend::makeProgramBindings(gl::ShaderObject& shaderObject) { +void GL41Backend::makeProgramBindings(ShaderObject& shaderObject) { if (!shaderObject.glprogram) { return; } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendShader.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendShader.cpp index 8a5e8d0064..c2490524ae 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendShader.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendShader.cpp @@ -10,6 +10,7 @@ //#include using namespace gpu; +using namespace gpu::gl; using namespace gpu::gl45; // GLSL version @@ -132,7 +133,7 @@ int GL45Backend::makeResourceBufferSlots(GLuint glprogram, const Shader::Binding return ssboCount;*/ } -void GL45Backend::makeProgramBindings(gl::ShaderObject& shaderObject) { +void GL45Backend::makeProgramBindings(ShaderObject& shaderObject) { if (!shaderObject.glprogram) { return; } diff --git a/libraries/gpu-gles/CMakeLists.txt b/libraries/gpu-gles/CMakeLists.txt new file mode 100644 index 0000000000..55ec53b184 --- /dev/null +++ b/libraries/gpu-gles/CMakeLists.txt @@ -0,0 +1,11 @@ +set(TARGET_NAME gpu-gles) +setup_hifi_library(OpenGL) +link_hifi_libraries(shared gl gpu) +GroupSources("src") + +target_opengl() +target_nsight() + +if (NOT ANDROID) + target_glew() +endif () diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp new file mode 100644 index 0000000000..1d66618703 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp @@ -0,0 +1,722 @@ +// +// GLBackend.cpp +// libraries/gpu-gl-android/src/gpu/gl +// +// Created by Cristian Duarte & Gabriel Calero on 9/21/2016. +// Copyright 2016 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 "GLBackend.h" + +#include +#include +#include +#include +#include + +#include "../gles/GLESBackend.h" + +#if defined(NSIGHT_FOUND) +#include "nvToolsExt.h" +#endif + +#include +#include +#include + +#include "GLTexture.h" +#include "GLShader.h" +using namespace gpu; +using namespace gpu::gl; + +static GLBackend* INSTANCE{ nullptr }; +static const char* GL_BACKEND_PROPERTY_NAME = "com.highfidelity.gl.backend"; + +BackendPointer GLBackend::createBackend() { + // FIXME provide a mechanism to override the backend for testing + // Where the gpuContext is initialized and where the TRUE Backend is created and assigned + auto version = QOpenGLContextWrapper::currentContextVersion(); + std::shared_ptr result; + + qDebug() << "Using OpenGL ES backend"; + result = std::make_shared(); + + result->initInput(); + result->initTransform(); + + INSTANCE = result.get(); + void* voidInstance = &(*result); + qApp->setProperty(GL_BACKEND_PROPERTY_NAME, QVariant::fromValue(voidInstance)); + + gl::GLTexture::initTextureTransferHelper(); + return result; +} + +GLBackend& getBackend() { + if (!INSTANCE) { + INSTANCE = static_cast(qApp->property(GL_BACKEND_PROPERTY_NAME).value()); + } + return *INSTANCE; +} + +bool GLBackend::makeProgram(Shader& shader, const Shader::BindingSet& slotBindings) { + return GLShader::makeProgram(getBackend(), shader, slotBindings); +} + +std::array commandNames = { + {QString("draw"),QString("drawIndexed"),QString("drawInstanced"),QString("drawIndexedInstanced"),QString("multiDrawIndirect"),QString("multiDrawIndexedIndirect"),QString("setInputFormat"),QString("setInputBuffer"),QString("setIndexBuffer"),QString("setIndirectBuffer"),QString("setModelTransform"),QString("setViewTransform"),QString("setProjectionTransform"),QString("setViewportTransform"),QString("setDepthRangeTransform"),QString("setPipeline"),QString("setStateBlendFactor"),QString("setStateScissorRect"),QString("setUniformBuffer"),QString("setResourceTexture"),QString("setFramebuffer"),QString("clearFramebuffer"),QString("blit"),QString("generateTextureMips"),QString("beginQuery"),QString("endQuery"),QString("getQuery"),QString("resetStages"),QString("runLambda"),QString("startNamedCall"),QString("stopNamedCall"),QString("glUniform1i"),QString("glUniform1f"),QString("glUniform2f"),QString("glUniform3f"),QString("glUniform4f"),QString("glUniform3fv"),QString("glUniform4fv"),QString("glUniform4iv"),QString("glUniformMatrix3fv"),QString("glUniformMatrix4fv"),QString("glColor4f"),QString("pushProfileRange"),QString("popProfileRange"),QString("NUM_COMMANDS")} +}; + +GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = +{ + (&::gpu::gl::GLBackend::do_draw), + (&::gpu::gl::GLBackend::do_drawIndexed), + (&::gpu::gl::GLBackend::do_drawInstanced), + (&::gpu::gl::GLBackend::do_drawIndexedInstanced), + (&::gpu::gl::GLBackend::do_multiDrawIndirect), + (&::gpu::gl::GLBackend::do_multiDrawIndexedIndirect), + + (&::gpu::gl::GLBackend::do_setInputFormat), + (&::gpu::gl::GLBackend::do_setInputBuffer), + (&::gpu::gl::GLBackend::do_setIndexBuffer), + (&::gpu::gl::GLBackend::do_setIndirectBuffer), + + (&::gpu::gl::GLBackend::do_setModelTransform), + (&::gpu::gl::GLBackend::do_setViewTransform), + (&::gpu::gl::GLBackend::do_setProjectionTransform), + (&::gpu::gl::GLBackend::do_setViewportTransform), + (&::gpu::gl::GLBackend::do_setDepthRangeTransform), + + (&::gpu::gl::GLBackend::do_setPipeline), + (&::gpu::gl::GLBackend::do_setStateBlendFactor), + (&::gpu::gl::GLBackend::do_setStateScissorRect), + + (&::gpu::gl::GLBackend::do_setUniformBuffer), + (&::gpu::gl::GLBackend::do_setResourceTexture), + + (&::gpu::gl::GLBackend::do_setFramebuffer), + (&::gpu::gl::GLBackend::do_clearFramebuffer), + (&::gpu::gl::GLBackend::do_blit), + (&::gpu::gl::GLBackend::do_generateTextureMips), + + (&::gpu::gl::GLBackend::do_beginQuery), + (&::gpu::gl::GLBackend::do_endQuery), + (&::gpu::gl::GLBackend::do_getQuery), + + (&::gpu::gl::GLBackend::do_resetStages), + + (&::gpu::gl::GLBackend::do_runLambda), + + (&::gpu::gl::GLBackend::do_startNamedCall), + (&::gpu::gl::GLBackend::do_stopNamedCall), + + (&::gpu::gl::GLBackend::do_glUniform1i), + (&::gpu::gl::GLBackend::do_glUniform1f), + (&::gpu::gl::GLBackend::do_glUniform2f), + (&::gpu::gl::GLBackend::do_glUniform3f), + (&::gpu::gl::GLBackend::do_glUniform4f), + (&::gpu::gl::GLBackend::do_glUniform3fv), + (&::gpu::gl::GLBackend::do_glUniform4fv), + (&::gpu::gl::GLBackend::do_glUniform4iv), + (&::gpu::gl::GLBackend::do_glUniformMatrix3fv), + (&::gpu::gl::GLBackend::do_glUniformMatrix4fv), + + (&::gpu::gl::GLBackend::do_glColor4f), + + (&::gpu::gl::GLBackend::do_pushProfileRange), + (&::gpu::gl::GLBackend::do_popProfileRange), +}; + +void GLBackend::init() { + static std::once_flag once; + std::call_once(once, [] { + QString vendor{ (const char*)glGetString(GL_VENDOR) }; + QString renderer{ (const char*)glGetString(GL_RENDERER) }; + qCDebug(gpugllogging) << "GL Version: " << QString((const char*) glGetString(GL_VERSION)); + qCDebug(gpugllogging) << "GL Shader Language Version: " << QString((const char*) glGetString(GL_SHADING_LANGUAGE_VERSION)); + qCDebug(gpugllogging) << "GL Vendor: " << vendor; + qCDebug(gpugllogging) << "GL Renderer: " << renderer; + GPUIdent* gpu = GPUIdent::getInstance(vendor, renderer); + // From here on, GPUIdent::getInstance()->getMumble() should efficiently give the same answers. + qCDebug(gpugllogging) << "GPU:"; + qCDebug(gpugllogging) << "\tcard:" << gpu->getName(); + qCDebug(gpugllogging) << "\tdriver:" << gpu->getDriver(); + qCDebug(gpugllogging) << "\tdedicated memory:" << gpu->getMemory() << "MB"; + + /*glewExperimental = true; + GLenum err = glewInit(); + glGetError(); // clear the potential error from glewExperimental + if (GLEW_OK != err) { + // glewInit failed, something is seriously wrong. + qCDebug(gpugllogging, "Error: %s\n", glewGetErrorString(err)); + } + qCDebug(gpugllogging, "Status: Using GLEW %s\n", glewGetString(GLEW_VERSION)); + */ + + }); +} + +GLBackend::GLBackend() { + _pipeline._cameraCorrectionBuffer._buffer->flush(); + glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &_uboAlignment); +} + + +GLBackend::~GLBackend() { + resetStages(); + + killInput(); + killTransform(); +} + +void GLBackend::renderPassTransfer(const Batch& batch) { + const size_t numCommands = batch.getCommands().size(); + const Batch::Commands::value_type* command = batch.getCommands().data(); + const Batch::CommandOffsets::value_type* offset = batch.getCommandOffsets().data(); + + _inRenderTransferPass = true; + { // Sync all the buffers + ANDROID_PROFILE(render, "syncGPUBuffer", 0xffaaffaa, 1) + + for (auto& cached : batch._buffers._items) { + if (cached._data) { + syncGPUObject(*cached._data); + } + } + } + + { // Sync all the buffers + ANDROID_PROFILE(render, "syncCPUTransform", 0xffaaaaff, 1) + _transform._cameras.clear(); + _transform._cameraOffsets.clear(); + + for (_commandIndex = 0; _commandIndex < numCommands; ++_commandIndex) { + switch (*command) { + case Batch::COMMAND_draw: + case Batch::COMMAND_drawIndexed: + case Batch::COMMAND_drawInstanced: + case Batch::COMMAND_drawIndexedInstanced: + case Batch::COMMAND_multiDrawIndirect: + case Batch::COMMAND_multiDrawIndexedIndirect: + _transform.preUpdate(_commandIndex, _stereo); + break; + + case Batch::COMMAND_setViewportTransform: + case Batch::COMMAND_setViewTransform: + case Batch::COMMAND_setProjectionTransform: { + ANDROID_PROFILE_COMMAND(render, (int)(*command), 0xffeeaaff, 1) + CommandCall call = _commandCalls[(*command)]; + (this->*(call))(batch, *offset); + break; + } + + default: + break; + } + command++; + offset++; + } + } + + { // Sync the transform buffers + //PROFILE_RANGE(render_gpu_gl, "transferTransformState"); + ANDROID_PROFILE(render, "transferTransformState", 0xff0000ff, 1) + transferTransformState(batch); + } + + _inRenderTransferPass = false; +} + +void GLBackend::renderPassDraw(const Batch& batch) { + _currentDraw = -1; + _transform._camerasItr = _transform._cameraOffsets.begin(); + const size_t numCommands = batch.getCommands().size(); + const Batch::Commands::value_type* command = batch.getCommands().data(); + const Batch::CommandOffsets::value_type* offset = batch.getCommandOffsets().data(); + for (_commandIndex = 0; _commandIndex < numCommands; ++_commandIndex) { + switch (*command) { + // Ignore these commands on this pass, taken care of in the transfer pass + // Note we allow COMMAND_setViewportTransform to occur in both passes + // as it both updates the transform object (and thus the uniforms in the + // UBO) as well as executes the actual viewport call + case Batch::COMMAND_setModelTransform: + case Batch::COMMAND_setViewTransform: + case Batch::COMMAND_setProjectionTransform: + break; + + case Batch::COMMAND_draw: + case Batch::COMMAND_drawIndexed: + case Batch::COMMAND_drawInstanced: + case Batch::COMMAND_drawIndexedInstanced: + case Batch::COMMAND_multiDrawIndirect: + case Batch::COMMAND_multiDrawIndexedIndirect: { + // updates for draw calls + ++_currentDraw; + updateInput(); + updateTransform(batch); + updatePipeline(); + {ANDROID_PROFILE_COMMAND(render, (int)(*command), 0xff0000ff, 1) + CommandCall call = _commandCalls[(*command)]; + (this->*(call))(batch, *offset); + } + break; + } + default: { + ANDROID_PROFILE_COMMAND(render, (int)(*command), 0xffff00ff, 1) + CommandCall call = _commandCalls[(*command)]; + (this->*(call))(batch, *offset); + break; + } + } + + command++; + offset++; + } +} + +void GLBackend::render(const Batch& batch) { + ANDROID_PROFILE(render, "GLBackendRender", 0xffff00ff, 1) + _transform._skybox = _stereo._skybox = batch.isSkyboxEnabled(); + // Allow the batch to override the rendering stereo settings + // for things like full framebuffer copy operations (deferred lighting passes) + bool savedStereo = _stereo._enable; + if (!batch.isStereoEnabled()) { + _stereo._enable = false; + } + + { + //PROFILE_RANGE(render_gpu_gl, "Transfer"); + ANDROID_PROFILE(render, "Transfer", 0xff0000ff, 1) + renderPassTransfer(batch); + } + + { + //PROFILE_RANGE(render_gpu_gl, _stereo._enable ? "Render Stereo" : "Render"); + ANDROID_PROFILE(render, "RenderPassDraw", 0xff00ddff, 1) + renderPassDraw(batch); + } + + // Restore the saved stereo state for the next batch + _stereo._enable = savedStereo; +} + + +void GLBackend::syncCache() { + syncTransformStateCache(); + syncPipelineStateCache(); + syncInputStateCache(); + syncOutputStateCache(); + + //glEnable(GL_LINE_SMOOTH); + qDebug() << "TODO: GLBackend.cpp:syncCache GL_LINE_SMOOTH"; +} + +void GLBackend::setupStereoSide(int side) { + ivec4 vp = _transform._viewport; + vp.z /= 2; + glViewport(vp.x + side * vp.z, vp.y, vp.z, vp.w); + +#ifdef GPU_STEREO_CAMERA_BUFFER +#ifdef GPU_STEREO_DRAWCALL_DOUBLED + //glVertexAttribI1i(14, side); + glVertexAttribI4i(14, side, 0, 0, 0); + +#endif +#else + _transform.bindCurrentCamera(side); +#endif +} + +void GLBackend::do_resetStages(const Batch& batch, size_t paramOffset) { + resetStages(); +} + +void GLBackend::do_runLambda(const Batch& batch, size_t paramOffset) { + std::function f = batch._lambdas.get(batch._params[paramOffset]._uint); + f(); +} + +void GLBackend::do_startNamedCall(const Batch& batch, size_t paramOffset) { + batch._currentNamedCall = batch._names.get(batch._params[paramOffset]._uint); + _currentDraw = -1; +} + +void GLBackend::do_stopNamedCall(const Batch& batch, size_t paramOffset) { + batch._currentNamedCall.clear(); +} + +void GLBackend::resetStages() { + resetInputStage(); + resetPipelineStage(); + resetTransformStage(); + resetUniformStage(); + resetResourceStage(); + resetOutputStage(); + resetQueryStage(); + + (void) CHECK_GL_ERROR(); +} + + +void GLBackend::do_pushProfileRange(const Batch& batch, size_t paramOffset) { + auto name = batch._profileRanges.get(batch._params[paramOffset]._uint); + profileRanges.push_back(name); +#if defined(NSIGHT_FOUND) + nvtxRangePush(name.c_str()); +#endif +} + +void GLBackend::do_popProfileRange(const Batch& batch, size_t paramOffset) { + profileRanges.pop_back(); +#if defined(NSIGHT_FOUND) + nvtxRangePop(); +#endif +} + +// TODO: As long as we have gl calls explicitely issued from interface +// code, we need to be able to record and batch these calls. THe long +// term strategy is to get rid of any GL calls in favor of the HIFI GPU API + +// As long as we don;t use several versions of shaders we can avoid this more complex code path +// #define GET_UNIFORM_LOCATION(shaderUniformLoc) _pipeline._programShader->getUniformLocation(shaderUniformLoc, isStereo()); +#define GET_UNIFORM_LOCATION(shaderUniformLoc) shaderUniformLoc + +void GLBackend::do_glUniform1i(const Batch& batch, size_t paramOffset) { + if (_pipeline._program == 0) { + // We should call updatePipeline() to bind the program but we are not doing that + // because these uniform setters are deprecated and we don;t want to create side effect + return; + } + updatePipeline(); + + glUniform1f( + GET_UNIFORM_LOCATION(batch._params[paramOffset + 1]._int), + batch._params[paramOffset + 0]._int); + (void)CHECK_GL_ERROR(); +} + +void GLBackend::do_glUniform1f(const Batch& batch, size_t paramOffset) { + if (_pipeline._program == 0) { + // We should call updatePipeline() to bind the program but we are not doing that + // because these uniform setters are deprecated and we don;t want to create side effect + return; + } + updatePipeline(); + + glUniform1f( + GET_UNIFORM_LOCATION(batch._params[paramOffset + 1]._int), + batch._params[paramOffset + 0]._float); + (void)CHECK_GL_ERROR(); +} + +void GLBackend::do_glUniform2f(const Batch& batch, size_t paramOffset) { + if (_pipeline._program == 0) { + // We should call updatePipeline() to bind the program but we are not doing that + // because these uniform setters are deprecated and we don;t want to create side effect + return; + } + updatePipeline(); + glUniform2f( + GET_UNIFORM_LOCATION(batch._params[paramOffset + 2]._int), + batch._params[paramOffset + 1]._float, + batch._params[paramOffset + 0]._float); + (void)CHECK_GL_ERROR(); +} + +void GLBackend::do_glUniform3f(const Batch& batch, size_t paramOffset) { + if (_pipeline._program == 0) { + // We should call updatePipeline() to bind the program but we are not doing that + // because these uniform setters are deprecated and we don;t want to create side effect + return; + } + updatePipeline(); + glUniform3f( + GET_UNIFORM_LOCATION(batch._params[paramOffset + 3]._int), + batch._params[paramOffset + 2]._float, + batch._params[paramOffset + 1]._float, + batch._params[paramOffset + 0]._float); + (void)CHECK_GL_ERROR(); +} + +void GLBackend::do_glUniform4f(const Batch& batch, size_t paramOffset) { + if (_pipeline._program == 0) { + // We should call updatePipeline() to bind the program but we are not doing that + // because these uniform setters are deprecated and we don;t want to create side effect + return; + } + updatePipeline(); + glUniform4f( + GET_UNIFORM_LOCATION(batch._params[paramOffset + 4]._int), + batch._params[paramOffset + 3]._float, + batch._params[paramOffset + 2]._float, + batch._params[paramOffset + 1]._float, + batch._params[paramOffset + 0]._float); + (void)CHECK_GL_ERROR(); +} + +void GLBackend::do_glUniform3fv(const Batch& batch, size_t paramOffset) { + if (_pipeline._program == 0) { + // We should call updatePipeline() to bind the program but we are not doing that + // because these uniform setters are deprecated and we don;t want to create side effect + return; + } + updatePipeline(); + glUniform3fv( + GET_UNIFORM_LOCATION(batch._params[paramOffset + 2]._int), + batch._params[paramOffset + 1]._uint, + (const GLfloat*)batch.readData(batch._params[paramOffset + 0]._uint)); + + (void)CHECK_GL_ERROR(); +} + +void GLBackend::do_glUniform4fv(const Batch& batch, size_t paramOffset) { + if (_pipeline._program == 0) { + // We should call updatePipeline() to bind the program but we are not doing that + // because these uniform setters are deprecated and we don;t want to create side effect + return; + } + updatePipeline(); + + GLint location = GET_UNIFORM_LOCATION(batch._params[paramOffset + 2]._int); + GLsizei count = batch._params[paramOffset + 1]._uint; + const GLfloat* value = (const GLfloat*)batch.readData(batch._params[paramOffset + 0]._uint); + glUniform4fv(location, count, value); + + (void)CHECK_GL_ERROR(); +} + +void GLBackend::do_glUniform4iv(const Batch& batch, size_t paramOffset) { + if (_pipeline._program == 0) { + // We should call updatePipeline() to bind the program but we are not doing that + // because these uniform setters are deprecated and we don;t want to create side effect + return; + } + updatePipeline(); + glUniform4iv( + GET_UNIFORM_LOCATION(batch._params[paramOffset + 2]._int), + batch._params[paramOffset + 1]._uint, + (const GLint*)batch.readData(batch._params[paramOffset + 0]._uint)); + + (void)CHECK_GL_ERROR(); +} + +void GLBackend::do_glUniformMatrix3fv(const Batch& batch, size_t paramOffset) { + if (_pipeline._program == 0) { + // We should call updatePipeline() to bind the program but we are not doing that + // because these uniform setters are deprecated and we don;t want to create side effect + return; + } + updatePipeline(); + + glUniformMatrix3fv( + GET_UNIFORM_LOCATION(batch._params[paramOffset + 3]._int), + batch._params[paramOffset + 2]._uint, + batch._params[paramOffset + 1]._uint, + (const GLfloat*)batch.readData(batch._params[paramOffset + 0]._uint)); + (void)CHECK_GL_ERROR(); +} + +void GLBackend::do_glUniformMatrix4fv(const Batch& batch, size_t paramOffset) { + if (_pipeline._program == 0) { + // We should call updatePipeline() to bind the program but we are not doing that + // because these uniform setters are deprecated and we don;t want to create side effect + return; + } + updatePipeline(); + + glUniformMatrix4fv( + GET_UNIFORM_LOCATION(batch._params[paramOffset + 3]._int), + batch._params[paramOffset + 2]._uint, + batch._params[paramOffset + 1]._uint, + (const GLfloat*)batch.readData(batch._params[paramOffset + 0]._uint)); + (void)CHECK_GL_ERROR(); +} + +void GLBackend::do_glColor4f(const Batch& batch, size_t paramOffset) { + + glm::vec4 newColor( + batch._params[paramOffset + 3]._float, + batch._params[paramOffset + 2]._float, + batch._params[paramOffset + 1]._float, + batch._params[paramOffset + 0]._float); + + if (_input._colorAttribute != newColor) { + _input._colorAttribute = newColor; + glVertexAttrib4fv(gpu::Stream::COLOR, &_input._colorAttribute.r); + } + (void)CHECK_GL_ERROR(); +} + +void GLBackend::releaseBuffer(GLuint id, Size size) const { + Lock lock(_trashMutex); + _buffersTrash.push_back({ id, size }); +} + +void GLBackend::releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const { + Lock lock(_trashMutex); + _externalTexturesTrash.push_back({ id, recycler }); +} + +void GLBackend::releaseTexture(GLuint id, Size size) const { + Lock lock(_trashMutex); + _texturesTrash.push_back({ id, size }); +} + +void GLBackend::releaseFramebuffer(GLuint id) const { + Lock lock(_trashMutex); + _framebuffersTrash.push_back(id); +} + +void GLBackend::releaseShader(GLuint id) const { + Lock lock(_trashMutex); + _shadersTrash.push_back(id); +} + +void GLBackend::releaseProgram(GLuint id) const { + Lock lock(_trashMutex); + _programsTrash.push_back(id); +} + +void GLBackend::releaseQuery(GLuint id) const { + Lock lock(_trashMutex); + _queriesTrash.push_back(id); +} + +void GLBackend::queueLambda(const std::function lambda) const { + Lock lock(_trashMutex); + _lambdaQueue.push_back(lambda); +} + +void GLBackend::recycle() const { + { + std::list> lamdbasTrash; + { + Lock lock(_trashMutex); + std::swap(_lambdaQueue, lamdbasTrash); + } + for (auto lambda : lamdbasTrash) { + lambda(); + } + } + + { + std::vector ids; + std::list> buffersTrash; + { + Lock lock(_trashMutex); + std::swap(_buffersTrash, buffersTrash); + } + ids.reserve(buffersTrash.size()); + for (auto pair : buffersTrash) { + ids.push_back(pair.first); + } + if (!ids.empty()) { + glDeleteBuffers((GLsizei)ids.size(), ids.data()); + } + } + + { + std::vector ids; + std::list framebuffersTrash; + { + Lock lock(_trashMutex); + std::swap(_framebuffersTrash, framebuffersTrash); + } + ids.reserve(framebuffersTrash.size()); + for (auto id : framebuffersTrash) { + ids.push_back(id); + } + if (!ids.empty()) { + glDeleteFramebuffers((GLsizei)ids.size(), ids.data()); + } + } + + { + std::vector ids; + std::list> texturesTrash; + { + Lock lock(_trashMutex); + std::swap(_texturesTrash, texturesTrash); + } + ids.reserve(texturesTrash.size()); + for (auto pair : texturesTrash) { + ids.push_back(pair.first); + } + if (!ids.empty()) { + glDeleteTextures((GLsizei)ids.size(), ids.data()); + } + } + + { + std::list> externalTexturesTrash; + { + Lock lock(_trashMutex); + std::swap(_externalTexturesTrash, externalTexturesTrash); + } + if (!externalTexturesTrash.empty()) { + std::vector fences; + fences.resize(externalTexturesTrash.size()); + for (size_t i = 0; i < externalTexturesTrash.size(); ++i) { + fences[i] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + // External texture fences will be read in another thread/context, so we need a flush + glFlush(); + size_t index = 0; + for (auto pair : externalTexturesTrash) { + auto fence = fences[index++]; + pair.second(pair.first, fence); + } + } + } + + { + std::list programsTrash; + { + Lock lock(_trashMutex); + std::swap(_programsTrash, programsTrash); + } + for (auto id : programsTrash) { + glDeleteProgram(id); + } + } + + { + std::list shadersTrash; + { + Lock lock(_trashMutex); + std::swap(_shadersTrash, shadersTrash); + } + for (auto id : shadersTrash) { + glDeleteShader(id); + } + } + + { + std::vector ids; + std::list queriesTrash; + { + Lock lock(_trashMutex); + std::swap(_queriesTrash, queriesTrash); + } + ids.reserve(queriesTrash.size()); + for (auto id : queriesTrash) { + ids.push_back(id); + } + if (!ids.empty()) { + glDeleteQueries((GLsizei)ids.size(), ids.data()); + } + } + +#ifndef THREADED_TEXTURE_TRANSFER + gl::GLTexture::_textureTransferHelper->process(); +#endif +} + +void GLBackend::setCameraCorrection(const Mat4& correction) { + _transform._correction.correction = correction; + _transform._correction.correctionInverse = glm::inverse(correction); + _pipeline._cameraCorrectionBuffer._buffer->setSubData(0, _transform._correction); + _pipeline._cameraCorrectionBuffer._buffer->flush(); +} diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackend.h b/libraries/gpu-gles/src/gpu/gl/GLBackend.h new file mode 100644 index 0000000000..f8f307bc17 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLBackend.h @@ -0,0 +1,427 @@ +// +// Created by Cristian Duarte & Gabriel Calero on 09/21/2016 +// Copyright 2016 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_gpu_gles_Backend_h +#define hifi_gpu_gles_Backend_h + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include + +#include "GLShared.h" + + +// Different versions for the stereo drawcall +// Current preferred is "instanced" which draw the shape twice but instanced and rely on clipping plane to draw left/right side only +//#define GPU_STEREO_TECHNIQUE_DOUBLED_SMARTER +//#define GPU_STEREO_TECHNIQUE_INSTANCED + +#ifdef GPU_STEREO_TECHNIQUE_DOUBLED_SMARTER +#define GPU_STEREO_DRAWCALL_DOUBLED +#define GPU_STEREO_CAMERA_BUFFER +#endif + +#ifdef GPU_STEREO_TECHNIQUE_INSTANCED +#define GPU_STEREO_DRAWCALL_INSTANCED +#define GPU_STEREO_CAMERA_BUFFER +#endif + +//#define ANDROID_INTENSIVE_INSTRUMENTATION 1 + +#ifdef ANDROID_INTENSIVE_INSTRUMENTATION +#define ANDROID_PROFILE_COMMAND(category, commandIndex, argbColor, payload, ...) PROFILE_RANGE_EX(category, commandNames[commandIndex], argbColor, payload, ##__VA_ARGS__); +#define ANDROID_PROFILE(category, name, argbColor, payload, ...) PROFILE_RANGE_EX(category, name, argbColor, payload, ##__VA_ARGS__); +#else +#define ANDROID_PROFILE_COMMAND(category, commandIndex, argbColor, payload, ...) +#define ANDROID_PROFILE(category, name, argbColor, payload, ...) +#endif +namespace gpu { namespace gl { + + class GLBackend : public Backend, public std::enable_shared_from_this { + // Context Backend static interface required + friend class gpu::Context; + static void init(); + static BackendPointer createBackend(); + + protected: + explicit GLBackend(bool syncCache); + GLBackend(); + public: + static bool makeProgram(Shader& shader, const Shader::BindingSet& slotBindings = Shader::BindingSet()); + + ~GLBackend(); + + void setCameraCorrection(const Mat4& correction); + void render(const Batch& batch) final override; + + // This call synchronize the Full Backend cache with the current GLState + // THis is only intended to be used when mixing raw gl calls with the gpu api usage in order to sync + // the gpu::Backend state with the true gl state which has probably been messed up by these ugly naked gl calls + // Let's try to avoid to do that as much as possible! + void syncCache() final override; + + // This is the ugly "download the pixels to sysmem for taking a snapshot" + // Just avoid using it, it's ugly and will break performances + virtual void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, + const Vec4i& region, QImage& destImage) final override; + + + // this is the maximum numeber of available input buffers + size_t getNumInputBuffers() const { return _input._invalidBuffers.size(); } + + // this is the maximum per shader stage on the low end apple + // TODO make it platform dependant at init time + static const int MAX_NUM_UNIFORM_BUFFERS = 12; + size_t getMaxNumUniformBuffers() const { return MAX_NUM_UNIFORM_BUFFERS; } + + // this is the maximum per shader stage on the low end apple + // TODO make it platform dependant at init time + static const int MAX_NUM_RESOURCE_TEXTURES = 16; + size_t getMaxNumResourceTextures() const { return MAX_NUM_RESOURCE_TEXTURES; } + + // Draw Stage + virtual void do_draw(const Batch& batch, size_t paramOffset) = 0; + virtual void do_drawIndexed(const Batch& batch, size_t paramOffset) = 0; + virtual void do_drawInstanced(const Batch& batch, size_t paramOffset) = 0; + virtual void do_drawIndexedInstanced(const Batch& batch, size_t paramOffset) = 0; + virtual void do_multiDrawIndirect(const Batch& batch, size_t paramOffset) = 0; + virtual void do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOffset) = 0; + + // Input Stage + virtual void do_setInputFormat(const Batch& batch, size_t paramOffset) final; + virtual void do_setInputBuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_setIndexBuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_setIndirectBuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_generateTextureMips(const Batch& batch, size_t paramOffset) final; + + // Transform Stage + virtual void do_setModelTransform(const Batch& batch, size_t paramOffset) final; + virtual void do_setViewTransform(const Batch& batch, size_t paramOffset) final; + virtual void do_setProjectionTransform(const Batch& batch, size_t paramOffset) final; + virtual void do_setViewportTransform(const Batch& batch, size_t paramOffset) final; + virtual void do_setDepthRangeTransform(const Batch& batch, size_t paramOffset) final; + + // Uniform Stage + virtual void do_setUniformBuffer(const Batch& batch, size_t paramOffset) final; + + // Resource Stage + virtual void do_setResourceTexture(const Batch& batch, size_t paramOffset) final; + + // Pipeline Stage + virtual void do_setPipeline(const Batch& batch, size_t paramOffset) final; + + // Output stage + virtual void do_setFramebuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_clearFramebuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_blit(const Batch& batch, size_t paramOffset) = 0; + + // Query section + virtual void do_beginQuery(const Batch& batch, size_t paramOffset) final; + virtual void do_endQuery(const Batch& batch, size_t paramOffset) final; + virtual void do_getQuery(const Batch& batch, size_t paramOffset) final; + + // Reset stages + virtual void do_resetStages(const Batch& batch, size_t paramOffset) final; + + virtual void do_runLambda(const Batch& batch, size_t paramOffset) final; + + virtual void do_startNamedCall(const Batch& batch, size_t paramOffset) final; + virtual void do_stopNamedCall(const Batch& batch, size_t paramOffset) final; + + static const int MAX_NUM_ATTRIBUTES = Stream::NUM_INPUT_SLOTS; + // The drawcall Info attribute channel is reserved and is the upper bound for the number of availables Input buffers + static const int MAX_NUM_INPUT_BUFFERS = Stream::DRAW_CALL_INFO; + + virtual void do_pushProfileRange(const Batch& batch, size_t paramOffset) final; + virtual void do_popProfileRange(const Batch& batch, size_t paramOffset) final; + + // TODO: As long as we have gl calls explicitely issued from interface + // code, we need to be able to record and batch these calls. THe long + // term strategy is to get rid of any GL calls in favor of the HIFI GPU API + virtual void do_glUniform1i(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform1f(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform2f(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform3f(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform4f(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform3fv(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform4fv(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform4iv(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniformMatrix3fv(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniformMatrix4fv(const Batch& batch, size_t paramOffset) final; + + virtual void do_glColor4f(const Batch& batch, size_t paramOffset) final; + + // The State setters called by the GLState::Commands when a new state is assigned + virtual void do_setStateFillMode(int32 mode) final; + virtual void do_setStateCullMode(int32 mode) final; + virtual void do_setStateFrontFaceClockwise(bool isClockwise) final; + virtual void do_setStateDepthClampEnable(bool enable) final; + virtual void do_setStateScissorEnable(bool enable) final; + virtual void do_setStateMultisampleEnable(bool enable) final; + virtual void do_setStateAntialiasedLineEnable(bool enable) final; + virtual void do_setStateDepthBias(Vec2 bias) final; + virtual void do_setStateDepthTest(State::DepthTest test) final; + virtual void do_setStateStencil(State::StencilActivation activation, State::StencilTest frontTest, State::StencilTest backTest) final; + virtual void do_setStateAlphaToCoverageEnable(bool enable) final; + virtual void do_setStateSampleMask(uint32 mask) final; + virtual void do_setStateBlend(State::BlendFunction blendFunction) final; + virtual void do_setStateColorWriteMask(uint32 mask) final; + virtual void do_setStateBlendFactor(const Batch& batch, size_t paramOffset) final; + virtual void do_setStateScissorRect(const Batch& batch, size_t paramOffset) final; + + virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; + virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) = 0; + virtual GLuint getBufferID(const Buffer& buffer) = 0; + virtual GLuint getQueryID(const QueryPointer& query) = 0; + virtual bool isTextureReady(const TexturePointer& texture); + + virtual void releaseBuffer(GLuint id, Size size) const; + virtual void releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const; + virtual void releaseTexture(GLuint id, Size size) const; + virtual void releaseFramebuffer(GLuint id) const; + virtual void releaseShader(GLuint id) const; + virtual void releaseProgram(GLuint id) const; + virtual void releaseQuery(GLuint id) const; + virtual void queueLambda(const std::function lambda) const; + + bool isTextureManagementSparseEnabled() const override { return (_textureManagement._sparseCapable && Texture::getEnableSparseTextures()); } + + protected: + + void recycle() const override; + virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; + virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; + virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) = 0; + virtual GLQuery* syncGPUObject(const Query& query) = 0; + + static const size_t INVALID_OFFSET = (size_t)-1; + bool _inRenderTransferPass { false }; + int32_t _uboAlignment { 0 }; + int _currentDraw { -1 }; + + std::list profileRanges; + mutable Mutex _trashMutex; + mutable std::list> _buffersTrash; + mutable std::list> _texturesTrash; + mutable std::list> _externalTexturesTrash; + mutable std::list _framebuffersTrash; + mutable std::list _shadersTrash; + mutable std::list _programsTrash; + mutable std::list _queriesTrash; + mutable std::list> _lambdaQueue; + + void renderPassTransfer(const Batch& batch); + void renderPassDraw(const Batch& batch); + void setupStereoSide(int side); + + virtual void initInput() final; + virtual void killInput() final; + virtual void syncInputStateCache() final; + virtual void resetInputStage(); + virtual void updateInput(); + + struct InputStageState { + bool _invalidFormat { true }; + Stream::FormatPointer _format; + std::string _formatKey; + + typedef std::bitset ActivationCache; + ActivationCache _attributeActivation { 0 }; + + typedef std::bitset BuffersState; + + BuffersState _invalidBuffers{ 0 }; + BuffersState _attribBindingBuffers{ 0 }; + + Buffers _buffers; + Offsets _bufferOffsets; + Offsets _bufferStrides; + std::vector _bufferVBOs; + + glm::vec4 _colorAttribute{ 0.0f }; + + BufferPointer _indexBuffer; + Offset _indexBufferOffset { 0 }; + Type _indexBufferType { UINT32 }; + + BufferPointer _indirectBuffer; + Offset _indirectBufferOffset{ 0 }; + Offset _indirectBufferStride{ 0 }; + + GLuint _defaultVAO { 0 }; + + InputStageState() : + _invalidFormat(true), + _format(0), + _formatKey(), + _attributeActivation(0), + _buffers(_invalidBuffers.size(), BufferPointer(0)), + _bufferOffsets(_invalidBuffers.size(), 0), + _bufferStrides(_invalidBuffers.size(), 0), + _bufferVBOs(_invalidBuffers.size(), 0) {} + } _input; + + virtual void initTransform() = 0; + void killTransform(); + // Synchronize the state cache of this Backend with the actual real state of the GL Context + void syncTransformStateCache(); + void updateTransform(const Batch& batch); + void resetTransformStage(); + + // Allows for correction of the camera pose to account for changes + // between the time when a was recorded and the time(s) when it is + // executed + struct CameraCorrection { + Mat4 correction; + Mat4 correctionInverse; + }; + + struct TransformStageState { +#ifdef GPU_STEREO_CAMERA_BUFFER + struct Cameras { + TransformCamera _cams[2]; + + Cameras() {}; + Cameras(const TransformCamera& cam) { memcpy(_cams, &cam, sizeof(TransformCamera)); }; + Cameras(const TransformCamera& camL, const TransformCamera& camR) { memcpy(_cams, &camL, sizeof(TransformCamera)); memcpy(_cams + 1, &camR, sizeof(TransformCamera)); }; + }; + + using CameraBufferElement = Cameras; +#else + using CameraBufferElement = TransformCamera; +#endif + using TransformCameras = std::vector; + + TransformCamera _camera; + TransformCameras _cameras; + + mutable std::map _drawCallInfoOffsets; + + GLuint _objectBuffer { 0 }; + GLuint _cameraBuffer { 0 }; + GLuint _drawCallInfoBuffer { 0 }; + GLuint _objectBufferTexture { 0 }; + size_t _cameraUboSize { 0 }; + bool _viewIsCamera{ false }; + bool _skybox { false }; + Transform _view; + CameraCorrection _correction; + + Mat4 _projection; + Vec4i _viewport { 0, 0, 1, 1 }; + Vec2 _depthRange { 0.0f, 1.0f }; + bool _invalidView { false }; + bool _invalidProj { false }; + bool _invalidViewport { false }; + + bool _enabledDrawcallInfoBuffer{ false }; + + using Pair = std::pair; + using List = std::list; + List _cameraOffsets; + mutable List::const_iterator _camerasItr; + mutable size_t _currentCameraOffset{ INVALID_OFFSET }; + + void preUpdate(size_t commandIndex, const StereoState& stereo); + void update(size_t commandIndex, const StereoState& stereo) const; + void bindCurrentCamera(int stereoSide) const; + } _transform; + + virtual void transferTransformState(const Batch& batch) const = 0; + + struct UniformStageState { + std::array _buffers; + //Buffers _buffers { }; + } _uniform; + + void releaseUniformBuffer(uint32_t slot); + void resetUniformStage(); + + // update resource cache and do the gl unbind call with the current gpu::Texture cached at slot s + void releaseResourceTexture(uint32_t slot); + + void resetResourceStage(); + + struct ResourceStageState { + std::array _textures; + //Textures _textures { { MAX_NUM_RESOURCE_TEXTURES } }; + int findEmptyTextureSlot() const; + } _resource; + + size_t _commandIndex{ 0 }; + + // Standard update pipeline check that the current Program and current State or good to go for a + void updatePipeline(); + // Force to reset all the state fields indicated by the 'toBeReset" signature + void resetPipelineState(State::Signature toBeReset); + // Synchronize the state cache of this Backend with the actual real state of the GL Context + void syncPipelineStateCache(); + void resetPipelineStage(); + + struct PipelineStageState { + PipelinePointer _pipeline; + + GLuint _program { 0 }; + GLint _cameraCorrectionLocation { -1 }; + GLShader* _programShader { nullptr }; + bool _invalidProgram { false }; + + BufferView _cameraCorrectionBuffer { gpu::BufferView(std::make_shared(sizeof(CameraCorrection), nullptr )) }; + + State::Data _stateCache{ State::DEFAULT }; + State::Signature _stateSignatureCache { 0 }; + + GLState* _state { nullptr }; + bool _invalidState { false }; + + PipelineStageState() { + _cameraCorrectionBuffer.edit() = CameraCorrection(); + } + } _pipeline; + + // Synchronize the state cache of this Backend with the actual real state of the GL Context + void syncOutputStateCache(); + void resetOutputStage(); + + struct OutputStageState { + FramebufferPointer _framebuffer { nullptr }; + GLuint _drawFBO { 0 }; + } _output; + + void resetQueryStage(); + struct QueryStageState { + uint32_t _rangeQueryDepth { 0 }; + } _queryStage; + + void resetStages(); + + struct TextureManagementStageState { + bool _sparseCapable { false }; + } _textureManagement; + virtual void initTextureManagementStage() {} + + typedef void (GLBackend::*CommandCall)(const Batch&, size_t); + static CommandCall _commandCalls[Batch::NUM_COMMANDS]; + friend class GLState; + friend class GLTexture; + }; + + } } + +#endif diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackendInput.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackendInput.cpp new file mode 100644 index 0000000000..057682584d --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLBackendInput.cpp @@ -0,0 +1,338 @@ +// +// GLBackendInput.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 3/8/2015. +// Copyright 2014 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 "GLBackend.h" +#include "GLShared.h" +#include "GLInputFormat.h" + +using namespace gpu; +using namespace gpu::gl; + +void GLBackend::do_setInputFormat(const Batch& batch, size_t paramOffset) { + Stream::FormatPointer format = batch._streamFormats.get(batch._params[paramOffset]._uint); + if (format != _input._format) { + _input._format = format; + if (format) { + auto inputFormat = GLInputFormat::sync((*format)); + assert(inputFormat); + if (_input._formatKey != inputFormat->key) { + _input._formatKey = inputFormat->key; + _input._invalidFormat = true; + } + } else { + _input._formatKey.clear(); + _input._invalidFormat = true; + } + } +} + +void GLBackend::do_setInputBuffer(const Batch& batch, size_t paramOffset) { + Offset stride = batch._params[paramOffset + 0]._uint; + Offset offset = batch._params[paramOffset + 1]._uint; + BufferPointer buffer = batch._buffers.get(batch._params[paramOffset + 2]._uint); + uint32 channel = batch._params[paramOffset + 3]._uint; + + if (channel < getNumInputBuffers()) { + bool isModified = false; + if (_input._buffers[channel] != buffer) { + _input._buffers[channel] = buffer; + + GLuint vbo = 0; + if (buffer) { + vbo = getBufferID((*buffer)); + } + _input._bufferVBOs[channel] = vbo; + + isModified = true; + } + + if (_input._bufferOffsets[channel] != offset) { + _input._bufferOffsets[channel] = offset; + isModified = true; + } + + if (_input._bufferStrides[channel] != stride) { + _input._bufferStrides[channel] = stride; + isModified = true; + } + + if (isModified) { + _input._invalidBuffers.set(channel); + } + } +} + +void GLBackend::initInput() { + if(!_input._defaultVAO) { + glGenVertexArrays(1, &_input._defaultVAO); + } + qDebug() << "glBindVertexArray(" << _input._defaultVAO << ")"; + glBindVertexArray(_input._defaultVAO); + (void) CHECK_GL_ERROR(); +} + +void GLBackend::killInput() { + qDebug() << "glBindVertexArray(0)"; + glBindVertexArray(0); + if(_input._defaultVAO) { + glDeleteVertexArrays(1, &_input._defaultVAO); + } + (void) CHECK_GL_ERROR(); +} + +void GLBackend::syncInputStateCache() { + for (uint32_t i = 0; i < _input._attributeActivation.size(); i++) { + GLint active = 0; + glGetVertexAttribiv(i, GL_VERTEX_ATTRIB_ARRAY_ENABLED, &active); + _input._attributeActivation[i] = active; + } + //_input._defaultVAO + qDebug() << "glBindVertexArray("<<_input._defaultVAO<< ")"; + glBindVertexArray(_input._defaultVAO); +} + +void GLBackend::resetInputStage() { + // Reset index buffer + _input._indexBufferType = UINT32; + _input._indexBufferOffset = 0; + _input._indexBuffer.reset(); + //qDebug() << "GLBackend::resetInputStage glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);"; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + (void) CHECK_GL_ERROR(); + + // Reset vertex buffer and format + _input._format.reset(); + _input._formatKey.clear(); + _input._invalidFormat = false; + _input._attributeActivation.reset(); + + for (uint32_t i = 0; i < _input._buffers.size(); i++) { + _input._buffers[i].reset(); + _input._bufferOffsets[i] = 0; + _input._bufferStrides[i] = 0; + _input._bufferVBOs[i] = 0; + } + _input._invalidBuffers.reset(); + + // THe vertex array binding MUST be reset in the specific Backend versions as they use different techniques +} + +void GLBackend::do_setIndexBuffer(const Batch& batch, size_t paramOffset) { + _input._indexBufferType = (Type)batch._params[paramOffset + 2]._uint; + _input._indexBufferOffset = batch._params[paramOffset + 0]._uint; + + BufferPointer indexBuffer = batch._buffers.get(batch._params[paramOffset + 1]._uint); + if (indexBuffer != _input._indexBuffer) { + _input._indexBuffer = indexBuffer; + if (indexBuffer) { + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, getBufferID(*indexBuffer)); + } else { + // FIXME do we really need this? Is there ever a draw call where we care that the element buffer is null? + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + } + } + (void) CHECK_GL_ERROR(); +} + +void GLBackend::do_setIndirectBuffer(const Batch& batch, size_t paramOffset) { + _input._indirectBufferOffset = batch._params[paramOffset + 1]._uint; + _input._indirectBufferStride = batch._params[paramOffset + 2]._uint; + + BufferPointer buffer = batch._buffers.get(batch._params[paramOffset]._uint); + if (buffer != _input._indirectBuffer) { + _input._indirectBuffer = buffer; + if (buffer) { + glBindBuffer(GL_DRAW_INDIRECT_BUFFER, getBufferID(*buffer)); + } else { + // FIXME do we really need this? Is there ever a draw call where we care that the element buffer is null? + glBindBuffer(GL_DRAW_INDIRECT_BUFFER, 0); + } + } + + (void)CHECK_GL_ERROR(); +} + + +// Core 41 doesn't expose the features to really separate the vertex format from the vertex buffers binding +// Core 43 does :) +// FIXME crashing problem with glVertexBindingDivisor / glVertexAttribFormat +// Once resolved, break this up into the GL 4.1 and 4.5 backends +#if 1 || (GPU_INPUT_PROFILE == GPU_CORE_41) +#define NO_SUPPORT_VERTEX_ATTRIB_FORMAT +#else +#define SUPPORT_VERTEX_ATTRIB_FORMAT +#endif + +void GLBackend::updateInput() { +#if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) + if (_input._invalidFormat) { + + InputStageState::ActivationCache newActivation; + + // Assign the vertex format required + if (_input._format) { + for (auto& it : _input._format->getAttributes()) { + const Stream::Attribute& attrib = (it).second; + + GLuint slot = attrib._slot; + GLuint count = attrib._element.getLocationScalarCount(); + uint8_t locationCount = attrib._element.getLocationCount(); + GLenum type = _elementTypeToGL41Type[attrib._element.getType()]; + GLuint offset = attrib._offset;; + GLboolean isNormalized = attrib._element.isNormalized(); + + GLenum perLocationSize = attrib._element.getLocationSize(); + + for (size_t locNum = 0; locNum < locationCount; ++locNum) { + newActivation.set(slot + locNum); + glVertexAttribFormat(slot + locNum, count, type, isNormalized, offset + locNum * perLocationSize); + glVertexAttribBinding(slot + locNum, attrib._channel); + } + glVertexBindingDivisor(attrib._channel, attrib._frequency); + } + (void)CHECK_GL_ERROR(); + } + + // Manage Activation what was and what is expected now + for (size_t i = 0; i < newActivation.size(); i++) { + bool newState = newActivation[i]; + if (newState != _input._attributeActivation[i]) { + if (newState) { + glEnableVertexAttribArray(i); + } else { + glDisableVertexAttribArray(i); + } + _input._attributeActivation.flip(i); + } + } + (void)CHECK_GL_ERROR(); + + _input._invalidFormat = false; + _stats._ISNumFormatChanges++; + } + + if (_input._invalidBuffers.any()) { + int numBuffers = _input._buffers.size(); + auto buffer = _input._buffers.data(); + auto vbo = _input._bufferVBOs.data(); + auto offset = _input._bufferOffsets.data(); + auto stride = _input._bufferStrides.data(); + + for (int bufferNum = 0; bufferNum < numBuffers; bufferNum++) { + if (_input._invalidBuffers.test(bufferNum)) { + glBindVertexBuffer(bufferNum, (*vbo), (*offset), (*stride)); + } + buffer++; + vbo++; + offset++; + stride++; + } + _input._invalidBuffers.reset(); + (void)CHECK_GL_ERROR(); + } +#else + if (_input._invalidFormat || _input._invalidBuffers.any()) { + + if (_input._invalidFormat) { + InputStageState::ActivationCache newActivation; + + _stats._ISNumFormatChanges++; + + // Check expected activation + if (_input._format) { + for (auto& it : _input._format->getAttributes()) { + const Stream::Attribute& attrib = (it).second; + uint8_t locationCount = attrib._element.getLocationCount(); + for (int i = 0; i < locationCount; ++i) { + newActivation.set(attrib._slot + i); + } + } + } + + // Manage Activation what was and what is expected now + for (unsigned int i = 0; i < newActivation.size(); i++) { + bool newState = newActivation[i]; + if (newState != _input._attributeActivation[i]) { + + if (newState) { + glEnableVertexAttribArray(i); + } else { + glDisableVertexAttribArray(i); + } + (void)CHECK_GL_ERROR(); + + _input._attributeActivation.flip(i); + } + } + } + + // now we need to bind the buffers and assign the attrib pointers + if (_input._format) { + const Buffers& buffers = _input._buffers; + const Offsets& offsets = _input._bufferOffsets; + const Offsets& strides = _input._bufferStrides; + + const Stream::Format::AttributeMap& attributes = _input._format->getAttributes(); + auto& inputChannels = _input._format->getChannels(); + _stats._ISNumInputBufferChanges++; + + GLuint boundVBO = 0; + for (auto& channelIt : inputChannels) { + const Stream::Format::ChannelMap::value_type::second_type& channel = (channelIt).second; + if ((channelIt).first < buffers.size()) { + int bufferNum = (channelIt).first; + + if (_input._invalidBuffers.test(bufferNum) || _input._invalidFormat) { + // GLuint vbo = gpu::GL41Backend::getBufferID((*buffers[bufferNum])); + GLuint vbo = _input._bufferVBOs[bufferNum]; + if (boundVBO != vbo) { + //qDebug() << "GLBackend::updateInput glBindBuffer(GL_ARRAY_BUFFER, " << vbo <<")"; + glBindBuffer(GL_ARRAY_BUFFER, vbo); + (void)CHECK_GL_ERROR(); + boundVBO = vbo; + } + _input._invalidBuffers[bufferNum] = false; + + for (unsigned int i = 0; i < channel._slots.size(); i++) { + const Stream::Attribute& attrib = attributes.at(channel._slots[i]); + GLuint slot = attrib._slot; + GLuint count = attrib._element.getLocationScalarCount(); + uint8_t locationCount = attrib._element.getLocationCount(); + GLenum type = gl::ELEMENT_TYPE_TO_GL[attrib._element.getType()]; + // GLenum perLocationStride = strides[bufferNum]; + GLenum perLocationStride = attrib._element.getLocationSize(); + GLuint stride = (GLuint)strides[bufferNum]; + GLuint pointer = (GLuint)(attrib._offset + offsets[bufferNum]); + GLboolean isNormalized = attrib._element.isNormalized(); + + for (size_t locNum = 0; locNum < locationCount; ++locNum) { + glVertexAttribPointer(slot + (GLuint)locNum, count, type, isNormalized, stride, + reinterpret_cast(pointer + perLocationStride * (GLuint)locNum)); +#ifdef GPU_STEREO_DRAWCALL_INSTANCED + glVertexAttribDivisor(slot + (GLuint)locNum, attrib._frequency * (isStereo() ? 2 : 1)); +#else + glVertexAttribDivisor(slot + (GLuint)locNum, attrib._frequency); +#endif + } + + // TODO: Support properly the IAttrib version + + (void)CHECK_GL_ERROR(); + } + } + } + } + } + // everything format related should be in sync now + _input._invalidFormat = false; + } +#endif +} + diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackendOutput.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackendOutput.cpp new file mode 100644 index 0000000000..6fddb810ee --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLBackendOutput.cpp @@ -0,0 +1,169 @@ +// +// GLBackendTexture.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 1/19/2015. +// Copyright 2014 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 "GLBackend.h" +#include "GLShared.h" +#include "GLFramebuffer.h" + +#include + +using namespace gpu; +using namespace gpu::gl; + +void GLBackend::syncOutputStateCache() { + GLint currentFBO; + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, ¤tFBO); + + _output._drawFBO = currentFBO; + _output._framebuffer.reset(); +} + +void GLBackend::resetOutputStage() { + if (_output._framebuffer) { + _output._framebuffer.reset(); + _output._drawFBO = 0; + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + } + + glEnable(GL_FRAMEBUFFER_SRGB_EXT); +} + +void GLBackend::do_setFramebuffer(const Batch& batch, size_t paramOffset) { + auto framebuffer = batch._framebuffers.get(batch._params[paramOffset]._uint); + if (_output._framebuffer != framebuffer) { + auto newFBO = getFramebufferID(framebuffer); + if (_output._drawFBO != newFBO) { + _output._drawFBO = newFBO; + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, newFBO); + } + _output._framebuffer = framebuffer; + } +} + +void GLBackend::do_clearFramebuffer(const Batch& batch, size_t paramOffset) { + if (_stereo._enable && !_pipeline._stateCache.scissorEnable) { + //qWarning("Clear without scissor in stereo mode"); + } + + uint32 masks = batch._params[paramOffset + 7]._uint; + Vec4 color; + color.x = batch._params[paramOffset + 6]._float; + color.y = batch._params[paramOffset + 5]._float; + color.z = batch._params[paramOffset + 4]._float; + color.w = batch._params[paramOffset + 3]._float; + float depth = batch._params[paramOffset + 2]._float; + int stencil = batch._params[paramOffset + 1]._int; + int useScissor = batch._params[paramOffset + 0]._int; + + GLuint glmask = 0; + if (masks & Framebuffer::BUFFER_STENCIL) { + glClearStencil(stencil); + glmask |= GL_STENCIL_BUFFER_BIT; + // TODO: we will probably need to also check the write mask of stencil like we do + // for depth buffer, but as would say a famous Fez owner "We'll cross that bridge when we come to it" + } + + bool restoreDepthMask = false; + if (masks & Framebuffer::BUFFER_DEPTH) { + glClearDepthf(depth); + + glmask |= GL_DEPTH_BUFFER_BIT; + + bool cacheDepthMask = _pipeline._stateCache.depthTest.getWriteMask(); + if (!cacheDepthMask) { + restoreDepthMask = true; + glDepthMask(GL_TRUE); + } + } + + std::vector drawBuffers; + if (masks & Framebuffer::BUFFER_COLORS) { + if (_output._framebuffer) { + for (unsigned int i = 0; i < Framebuffer::MAX_NUM_RENDER_BUFFERS; i++) { + if (masks & (1 << i)) { + drawBuffers.push_back(GL_COLOR_ATTACHMENT0 + i); + } + } + + if (!drawBuffers.empty()) { + glDrawBuffers((GLsizei)drawBuffers.size(), drawBuffers.data()); + glClearColor(color.x, color.y, color.z, color.w); + glmask |= GL_COLOR_BUFFER_BIT; + + (void) CHECK_GL_ERROR(); + } + } else { + glClearColor(color.x, color.y, color.z, color.w); + glmask |= GL_COLOR_BUFFER_BIT; + } + + // Force the color mask cache to WRITE_ALL if not the case + do_setStateColorWriteMask(State::ColorMask::WRITE_ALL); + } + + // Apply scissor if needed and if not already on + bool doEnableScissor = (useScissor && (!_pipeline._stateCache.scissorEnable)); + if (doEnableScissor) { + glEnable(GL_SCISSOR_TEST); + } + + // Clear! + glClear(glmask); + + // Restore scissor if needed + if (doEnableScissor) { + glDisable(GL_SCISSOR_TEST); + } + + // Restore write mask meaning turn back off + if (restoreDepthMask) { + glDepthMask(GL_FALSE); + } + + // Restore the color draw buffers only if a frmaebuffer is bound + if (_output._framebuffer && !drawBuffers.empty()) { + auto glFramebuffer = syncGPUObject(*_output._framebuffer); + if (glFramebuffer) { + glDrawBuffers((GLsizei)glFramebuffer->_colorBuffers.size(), glFramebuffer->_colorBuffers.data()); + } + } + + (void) CHECK_GL_ERROR(); +} + +void GLBackend::downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) { + auto readFBO = getFramebufferID(srcFramebuffer); + if (srcFramebuffer && readFBO) { + if ((srcFramebuffer->getWidth() < (region.x + region.z)) || (srcFramebuffer->getHeight() < (region.y + region.w))) { + qCDebug(gpugllogging) << "GLBackend::downloadFramebuffer : srcFramebuffer is too small to provide the region queried"; + return; + } + } + + if ((destImage.width() < region.z) || (destImage.height() < region.w)) { + qCDebug(gpugllogging) << "GLBackend::downloadFramebuffer : destImage is too small to receive the region of the framebuffer"; + return; + } + + GLenum format = GL_RGBA; + //GLenum format = GL_BGRA; + qDebug() << "TODO: GLBackendOutput.cpp:do_clearFramebuffer GL_BGRA"; + + if (destImage.format() != QImage::Format_ARGB32) { + qCDebug(gpugllogging) << "GLBackend::downloadFramebuffer : destImage format must be FORMAT_ARGB32 to receive the region of the framebuffer"; + return; + } + + glBindFramebuffer(GL_READ_FRAMEBUFFER, getFramebufferID(srcFramebuffer)); + glReadPixels(region.x, region.y, region.z, region.w, format, GL_UNSIGNED_BYTE, destImage.bits()); + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + + (void) CHECK_GL_ERROR(); +} diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackendPipeline.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackendPipeline.cpp new file mode 100644 index 0000000000..c35966d440 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLBackendPipeline.cpp @@ -0,0 +1,250 @@ +// +// GLBackendPipeline.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 3/8/2015. +// Copyright 2014 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 "GLBackend.h" +#include "GLShared.h" +#include "GLPipeline.h" +#include "GLShader.h" +#include "GLState.h" +#include "GLBuffer.h" +#include "GLTexture.h" + +using namespace gpu; +using namespace gpu::gl; + +void GLBackend::do_setPipeline(const Batch& batch, size_t paramOffset) { + PipelinePointer pipeline = batch._pipelines.get(batch._params[paramOffset + 0]._uint); + + if (_pipeline._pipeline == pipeline) { + return; + } + + // A true new Pipeline + _stats._PSNumSetPipelines++; + + // null pipeline == reset + if (!pipeline) { + qDebug() << " null pipeline"; + _pipeline._pipeline.reset(); + + _pipeline._program = 0; + _pipeline._cameraCorrectionLocation = -1; + _pipeline._programShader = nullptr; + _pipeline._invalidProgram = true; + + _pipeline._state = nullptr; + _pipeline._invalidState = true; + } else { + auto pipelineObject = GLPipeline::sync(*this, *pipeline); + if (!pipelineObject) { + return; + } + + // check the program cache + // pick the program version + // check the program cache + // pick the program version +#ifdef GPU_STEREO_CAMERA_BUFFER + GLuint glprogram = pipelineObject->_program->getProgram((GLShader::Version) isStereo()); +#else + GLuint glprogram = pipelineObject->_program->getProgram(); +#endif + + if (_pipeline._program != glprogram) { + _pipeline._program = glprogram; + _pipeline._programShader = pipelineObject->_program; + _pipeline._invalidProgram = true; + _pipeline._cameraCorrectionLocation = pipelineObject->_cameraCorrection; + } + + // Now for the state + if (_pipeline._state != pipelineObject->_state) { + _pipeline._state = pipelineObject->_state; + _pipeline._invalidState = true; + } + + // Remember the new pipeline + _pipeline._pipeline = pipeline; + } + + // THis should be done on Pipeline::update... + if (_pipeline._invalidProgram) { + glUseProgram(_pipeline._program); + if (_pipeline._cameraCorrectionLocation != -1) { + auto cameraCorrectionBuffer = syncGPUObject(*_pipeline._cameraCorrectionBuffer._buffer); + glBindBufferRange(GL_UNIFORM_BUFFER, _pipeline._cameraCorrectionLocation, cameraCorrectionBuffer->_id, 0, sizeof(CameraCorrection)); + } + (void) CHECK_GL_ERROR(); + _pipeline._invalidProgram = false; + } +} + +void GLBackend::updatePipeline() { + if (_pipeline._invalidProgram) { + // doing it here is aproblem for calls to glUniform.... so will do it on assing... + glUseProgram(_pipeline._program); + (void) CHECK_GL_ERROR(); + _pipeline._invalidProgram = false; + } + + if (_pipeline._invalidState) { + if (_pipeline._state) { + // first reset to default what should be + // the fields which were not to default and are default now + resetPipelineState(_pipeline._state->_signature); + + // Update the signature cache with what's going to be touched + _pipeline._stateSignatureCache |= _pipeline._state->_signature; + + // And perform + for (auto command: _pipeline._state->_commands) { + command->run(this); + } + } else { + // No state ? anyway just reset everything + resetPipelineState(0); + } + _pipeline._invalidState = false; + } +} + +void GLBackend::resetPipelineStage() { + // First reset State to default + State::Signature resetSignature(0); + resetPipelineState(resetSignature); + _pipeline._state = nullptr; + _pipeline._invalidState = false; + + // Second the shader side + _pipeline._invalidProgram = false; + _pipeline._program = 0; + _pipeline._programShader = nullptr; + _pipeline._pipeline.reset(); + glUseProgram(0); +} + +void GLBackend::releaseUniformBuffer(uint32_t slot) { + auto& buf = _uniform._buffers[slot]; + if (buf) { + auto* object = Backend::getGPUObject(*buf); + if (object) { + glBindBufferBase(GL_UNIFORM_BUFFER, slot, 0); // RELEASE + (void) CHECK_GL_ERROR(); + } + buf.reset(); + } +} + +void GLBackend::resetUniformStage() { + for (uint32_t i = 0; i < _uniform._buffers.size(); i++) { + releaseUniformBuffer(i); + } +} + +void GLBackend::do_setUniformBuffer(const Batch& batch, size_t paramOffset) { + GLuint slot = batch._params[paramOffset + 3]._uint; + BufferPointer uniformBuffer = batch._buffers.get(batch._params[paramOffset + 2]._uint); + GLintptr rangeStart = batch._params[paramOffset + 1]._uint; + GLsizeiptr rangeSize = batch._params[paramOffset + 0]._uint; + + if (!uniformBuffer) { + releaseUniformBuffer(slot); + return; + } + + // check cache before thinking + if (_uniform._buffers[slot] == uniformBuffer) { + return; + } + + // Sync BufferObject + auto* object = syncGPUObject(*uniformBuffer); + if (object) { + glBindBufferRange(GL_UNIFORM_BUFFER, slot, object->_buffer, rangeStart, rangeSize); + + _uniform._buffers[slot] = uniformBuffer; + (void) CHECK_GL_ERROR(); + } else { + releaseUniformBuffer(slot); + return; + } +} + +void GLBackend::releaseResourceTexture(uint32_t slot) { + auto& tex = _resource._textures[slot]; + if (tex) { + auto* object = Backend::getGPUObject(*tex); + if (object) { + GLuint target = object->_target; + glActiveTexture(GL_TEXTURE0 + slot); + glBindTexture(target, 0); // RELEASE + (void) CHECK_GL_ERROR(); + } + tex.reset(); + } +} + +void GLBackend::resetResourceStage() { + for (uint32_t i = 0; i < _resource._textures.size(); i++) { + releaseResourceTexture(i); + } +} + +void GLBackend::do_setResourceTexture(const Batch& batch, size_t paramOffset) { + GLuint slot = batch._params[paramOffset + 1]._uint; + if (slot >= (GLuint) MAX_NUM_RESOURCE_TEXTURES) { + // "GLBackend::do_setResourceTexture: Trying to set a resource Texture at slot #" + slot + " which doesn't exist. MaxNumResourceTextures = " + getMaxNumResourceTextures()); + return; + } + + TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); + + if (!resourceTexture) { + releaseResourceTexture(slot); + return; + } + // check cache before thinking + if (_resource._textures[slot] == resourceTexture) { + return; + } + + // One more True texture bound + _stats._RSNumTextureBounded++; + + // Always make sure the GLObject is in sync + GLTexture* object = syncGPUObject(resourceTexture); + if (object) { + GLuint to = object->_texture; + GLuint target = object->_target; + glActiveTexture(GL_TEXTURE0 + slot); + glBindTexture(target, to); + + (void) CHECK_GL_ERROR(); + + _resource._textures[slot] = resourceTexture; + + _stats._RSAmountTextureMemoryBounded += object->size(); + + } else { + releaseResourceTexture(slot); + return; + } +} + +int GLBackend::ResourceStageState::findEmptyTextureSlot() const { + // start from the end of the slots, try to find an empty one that can be used + for (auto i = MAX_NUM_RESOURCE_TEXTURES - 1; i > 0; i--) { + if (!_textures[i]) { + return i; + } + } + return -1; +} + diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackendQuery.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackendQuery.cpp new file mode 100644 index 0000000000..530e01d8ff --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLBackendQuery.cpp @@ -0,0 +1,93 @@ +// +// GLBackendQuery.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 7/7/2015. +// 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 "GLBackend.h" +#include "GLQuery.h" +#include "GLShared.h" + +using namespace gpu; +using namespace gpu::gl; + +// Eventually, we want to test with TIME_ELAPSED instead of TIMESTAMP +#ifdef Q_OS_MAC +const uint32_t MAX_RANGE_QUERY_DEPTH = 1; +static bool timeElapsed = true; +#else +const uint32_t MAX_RANGE_QUERY_DEPTH = 10000; +static bool timeElapsed = false; +#endif + +void GLBackend::do_beginQuery(const Batch& batch, size_t paramOffset) { + auto query = batch._queries.get(batch._params[paramOffset]._uint); + GLQuery* glquery = syncGPUObject(*query); + if (glquery) { + //glGetInteger64v(GL_TIMESTAMP_EXT, (GLint64*)&glquery->_batchElapsedTime); + glquery->_batchElapsedTime = 1; + if (timeElapsed) { + glBeginQuery(GL_TIME_ELAPSED_EXT, glquery->_endqo); + } else { + if (glQueryCounterEXT != NULL) { + glQueryCounterEXT(glquery->_beginqo, GL_TIMESTAMP_EXT); + } + } + glquery->_rangeQueryDepth = _queryStage._rangeQueryDepth; + (void)CHECK_GL_ERROR(); + } +} + +void GLBackend::do_endQuery(const Batch& batch, size_t paramOffset) { + auto query = batch._queries.get(batch._params[paramOffset]._uint); + GLQuery* glquery = syncGPUObject(*query); + if (glquery) { + if (timeElapsed) { + glEndQuery(GL_TIME_ELAPSED_EXT); + } else { + if (glQueryCounterEXT != NULL) { + glQueryCounterEXT(glquery->_endqo, GL_TIMESTAMP_EXT); + } + } + + --_queryStage._rangeQueryDepth; + GLint64 now; + //glGetInteger64v(GL_TIMESTAMP_EXT, &now); + //glquery->_batchElapsedTime = now - glquery->_batchElapsedTime; + now = 1; + glquery->_batchElapsedTime = 1; + + PROFILE_RANGE_END(render_gpu_gl, glquery->_profileRangeId); + + (void)CHECK_GL_ERROR(); + } +} + +void GLBackend::do_getQuery(const Batch& batch, size_t paramOffset) { + auto query = batch._queries.get(batch._params[paramOffset]._uint); + if (glGetQueryObjectui64vEXT == NULL) + return; + GLQuery* glquery = syncGPUObject(*query); + if (glquery) { + glGetQueryObjectui64vEXT(glquery->_endqo, GL_QUERY_RESULT_AVAILABLE, &glquery->_result); + if (glquery->_result == GL_TRUE) { + if (timeElapsed) { + glGetQueryObjectui64vEXT(glquery->_endqo, GL_QUERY_RESULT, &glquery->_result); + } else { + GLuint64 start, end; + glGetQueryObjectui64vEXT(glquery->_beginqo, GL_QUERY_RESULT, &start); + glGetQueryObjectui64vEXT(glquery->_endqo, GL_QUERY_RESULT, &end); + glquery->_result = end - start; + } + query->triggerReturnHandler(glquery->_result, glquery->_batchElapsedTime); + } + (void)CHECK_GL_ERROR(); + } +} + +void GLBackend::resetQueryStage() { +} diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackendState.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackendState.cpp new file mode 100644 index 0000000000..27b8d23bf3 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLBackendState.cpp @@ -0,0 +1,334 @@ +// +// GLBackendState.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 3/22/2015. +// Copyright 2014 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 "GLBackend.h" +#include "GLState.h" + +using namespace gpu; +using namespace gpu::gl; + +void GLBackend::resetPipelineState(State::Signature nextSignature) { + auto currentNotSignature = ~_pipeline._stateSignatureCache; + auto nextNotSignature = ~nextSignature; + auto fieldsToBeReset = currentNotSignature ^ (currentNotSignature | nextNotSignature); + if (fieldsToBeReset.any()) { + for (auto i = 0; i < State::NUM_FIELDS; i++) { + if (fieldsToBeReset[i]) { + GLState::_resetStateCommands[i]->run(this); + _pipeline._stateSignatureCache.reset(i); + } + } + } +} + +void GLBackend::syncPipelineStateCache() { + State::Data state; + + //glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); + qDebug() << "TODO: GLBackendState.cpp:syncPipelineStateCache GL_TEXTURE_CUBE_MAP_SEAMLESS"; + + // Point size is always on + // FIXME CORE + //glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); + //glEnable(GL_PROGRAM_POINT_SIZE_EXT); + qDebug() << "TODO: GLBackendState.cpp:syncPipelineStateCache GL_PROGRAM_POINT_SIZE_EXT"; + + //glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); + qDebug() << "TODO: GLBackendState.cpp:syncPipelineStateCache GL_VERTEX_PROGRAM_POINT_SIZE"; + + // Default line width accross the board + glLineWidth(1.0f); + + getCurrentGLState(state); + State::Signature signature = State::evalSignature(state); + + _pipeline._stateCache = state; + _pipeline._stateSignatureCache = signature; +} + + +void GLBackend::do_setStateFillMode(int32 mode) { + if (_pipeline._stateCache.fillMode != mode) { + static GLenum GL_FILL_MODES[] = { /*GL_POINT, GL_LINE, GL_FILL*/ }; + qDebug() << "TODO: GLBackendState.cpp:do_setStateFillMode GL_POINT"; + qDebug() << "TODO: GLBackendState.cpp:do_setStateFillMode GL_LINE"; + qDebug() << "TODO: GLBackendState.cpp:do_setStateFillMode GL_FILL"; + //glPolygonMode(GL_FRONT_AND_BACK, GL_FILL_MODES[mode]); + qDebug() << "TODO: GLBackendState.cpp:do_setStateFillMode glPolygonMode"; + (void)CHECK_GL_ERROR(); + + _pipeline._stateCache.fillMode = State::FillMode(mode); + } +} + +void GLBackend::do_setStateCullMode(int32 mode) { + if (_pipeline._stateCache.cullMode != mode) { + static GLenum GL_CULL_MODES[] = { GL_FRONT_AND_BACK, GL_FRONT, GL_BACK }; + if (mode == State::CULL_NONE) { + glDisable(GL_CULL_FACE); + glCullFace(GL_FRONT_AND_BACK); + } else { + glEnable(GL_CULL_FACE); + glCullFace(GL_CULL_MODES[mode]); + } + (void)CHECK_GL_ERROR(); + + _pipeline._stateCache.cullMode = State::CullMode(mode); + } +} + +void GLBackend::do_setStateFrontFaceClockwise(bool isClockwise) { + if (_pipeline._stateCache.frontFaceClockwise != isClockwise) { + static GLenum GL_FRONT_FACES[] = { GL_CCW, GL_CW }; + glFrontFace(GL_FRONT_FACES[isClockwise]); + (void)CHECK_GL_ERROR(); + + _pipeline._stateCache.frontFaceClockwise = isClockwise; + } +} + +void GLBackend::do_setStateDepthClampEnable(bool enable) { + if (_pipeline._stateCache.depthClampEnable != enable) { + if (enable) { + qDebug() << "TODO: GLBackendState.cpp:do_setStateDepthClampEnable GL_DEPTH_CLAMP"; + //glEnable(GL_DEPTH_CLAMP); + } else { + //glDisable(GL_DEPTH_CLAMP); + qDebug() << "TODO: GLBackendState.cpp:do_setStateDepthClampEnable GL_DEPTH_CLAMP"; + } + (void)CHECK_GL_ERROR(); + + _pipeline._stateCache.depthClampEnable = enable; + } +} + +void GLBackend::do_setStateScissorEnable(bool enable) { + if (_pipeline._stateCache.scissorEnable != enable) { + if (enable) { + glEnable(GL_SCISSOR_TEST); + } else { + glDisable(GL_SCISSOR_TEST); + } + (void)CHECK_GL_ERROR(); + + _pipeline._stateCache.scissorEnable = enable; + } +} + +void GLBackend::do_setStateMultisampleEnable(bool enable) { + if (_pipeline._stateCache.multisampleEnable != enable) { + if (enable) { + //glEnable(GL_MULTISAMPLE); + qDebug() << "TODO: GLBackendState.cpp:do_setStateMultisampleEnable GL_MULTISAMPLE"; + } else { + //glDisable(GL_MULTISAMPLE); + qDebug() << "TODO: GLBackendState.cpp:do_setStateMultisampleEnable GL_MULTISAMPLE"; + } + (void)CHECK_GL_ERROR(); + + _pipeline._stateCache.multisampleEnable = enable; + } +} + +void GLBackend::do_setStateAntialiasedLineEnable(bool enable) { + if (_pipeline._stateCache.antialisedLineEnable != enable) { + if (enable) { + //glEnable(GL_LINE_SMOOTH); + qDebug() << "TODO: GLBackendState.cpp:do_setStateAntialiasedLineEnable GL_LINE_SMOOTH"; + } else { + //glDisable(GL_LINE_SMOOTH); + qDebug() << "TODO: GLBackendState.cpp:do_setStateAntialiasedLineEnable GL_LINE_SMOOTH"; + } + (void)CHECK_GL_ERROR(); + + _pipeline._stateCache.antialisedLineEnable = enable; + } +} + +void GLBackend::do_setStateDepthBias(Vec2 bias) { + if ((bias.x != _pipeline._stateCache.depthBias) || (bias.y != _pipeline._stateCache.depthBiasSlopeScale)) { + if ((bias.x != 0.0f) || (bias.y != 0.0f)) { + glEnable(GL_POLYGON_OFFSET_FILL); + //glEnable(GL_POLYGON_OFFSET_LINE); + qDebug() << "TODO: GLBackendState.cpp:do_setStateDepthBias GL_POLYGON_OFFSET_LINE"; + //glEnable(GL_POLYGON_OFFSET_POINT); + qDebug() << "TODO: GLBackendState.cpp:do_setStateDepthBias GL_POLYGON_OFFSET_POINT"; + glPolygonOffset(bias.x, bias.y); + } else { + glDisable(GL_POLYGON_OFFSET_FILL); + //glDisable(GL_POLYGON_OFFSET_LINE); + qDebug() << "TODO: GLBackendState.cpp:do_setStateDepthBias GL_POLYGON_OFFSET_LINE"; + //glDisable(GL_POLYGON_OFFSET_POINT); + qDebug() << "TODO: GLBackendState.cpp:do_setStateDepthBias GL_POLYGON_OFFSET_POINT"; + } + (void)CHECK_GL_ERROR(); + + _pipeline._stateCache.depthBias = bias.x; + _pipeline._stateCache.depthBiasSlopeScale = bias.y; + } +} + +void GLBackend::do_setStateDepthTest(State::DepthTest test) { + const auto& current = _pipeline._stateCache.depthTest; + if (current != test) { + if (test.isEnabled()) { + glEnable(GL_DEPTH_TEST); + } else { + glDisable(GL_DEPTH_TEST); + } + if (test.getWriteMask() != current.getWriteMask()) { + glDepthMask(test.getWriteMask()); + } + if (test.getFunction() != current.getFunction()) { + glDepthFunc(COMPARISON_TO_GL[test.getFunction()]); + } + if (CHECK_GL_ERROR()) { + qDebug() << "DepthTest" << (test.isEnabled() ? "Enabled" : "Disabled") + << "Mask=" << (test.getWriteMask() ? "Write" : "no Write") + << "Func=" << test.getFunction() + << "Raw=" << test.getRaw(); + } + _pipeline._stateCache.depthTest = test; + } +} + +void GLBackend::do_setStateStencil(State::StencilActivation activation, State::StencilTest testFront, State::StencilTest testBack) { + const auto& currentActivation = _pipeline._stateCache.stencilActivation; + const auto& currentTestFront = _pipeline._stateCache.stencilTestFront; + const auto& currentTestBack = _pipeline._stateCache.stencilTestBack; + if ((currentActivation != activation) + || (currentTestFront != testFront) + || (currentTestBack != testBack)) { + + if (activation.isEnabled()) { + glEnable(GL_STENCIL_TEST); + } else { + glDisable(GL_STENCIL_TEST); + } + + if (activation.getWriteMaskFront() != activation.getWriteMaskBack()) { + glStencilMaskSeparate(GL_FRONT, activation.getWriteMaskFront()); + glStencilMaskSeparate(GL_BACK, activation.getWriteMaskBack()); + } else { + glStencilMask(activation.getWriteMaskFront()); + } + + static GLenum STENCIL_OPS[State::NUM_STENCIL_OPS] = { + GL_KEEP, + GL_ZERO, + GL_REPLACE, + GL_INCR_WRAP, + GL_DECR_WRAP, + GL_INVERT, + GL_INCR, + GL_DECR }; + + if (testFront != testBack) { + glStencilOpSeparate(GL_FRONT, STENCIL_OPS[testFront.getFailOp()], STENCIL_OPS[testFront.getDepthFailOp()], STENCIL_OPS[testFront.getPassOp()]); + glStencilFuncSeparate(GL_FRONT, COMPARISON_TO_GL[testFront.getFunction()], testFront.getReference(), testFront.getReadMask()); + + glStencilOpSeparate(GL_BACK, STENCIL_OPS[testBack.getFailOp()], STENCIL_OPS[testBack.getDepthFailOp()], STENCIL_OPS[testBack.getPassOp()]); + glStencilFuncSeparate(GL_BACK, COMPARISON_TO_GL[testBack.getFunction()], testBack.getReference(), testBack.getReadMask()); + } else { + glStencilOp(STENCIL_OPS[testFront.getFailOp()], STENCIL_OPS[testFront.getDepthFailOp()], STENCIL_OPS[testFront.getPassOp()]); + glStencilFunc(COMPARISON_TO_GL[testFront.getFunction()], testFront.getReference(), testFront.getReadMask()); + } + + (void)CHECK_GL_ERROR(); + + _pipeline._stateCache.stencilActivation = activation; + _pipeline._stateCache.stencilTestFront = testFront; + _pipeline._stateCache.stencilTestBack = testBack; + } +} + +void GLBackend::do_setStateAlphaToCoverageEnable(bool enable) { + if (_pipeline._stateCache.alphaToCoverageEnable != enable) { + if (enable) { + glEnable(GL_SAMPLE_ALPHA_TO_COVERAGE); + } else { + glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE); + } + (void)CHECK_GL_ERROR(); + + _pipeline._stateCache.alphaToCoverageEnable = enable; + } +} + +void GLBackend::do_setStateSampleMask(uint32 mask) { + if (_pipeline._stateCache.sampleMask != mask) { + if (mask == 0xFFFFFFFF) { + glDisable(GL_SAMPLE_MASK); + } else { + glEnable(GL_SAMPLE_MASK); + glSampleMaski(0, mask); + } + (void)CHECK_GL_ERROR(); + _pipeline._stateCache.sampleMask = mask; + } +} + +void GLBackend::do_setStateBlend(State::BlendFunction function) { + if (_pipeline._stateCache.blendFunction != function) { + if (function.isEnabled()) { + glEnable(GL_BLEND); + + glBlendEquationSeparate(BLEND_OPS_TO_GL[function.getOperationColor()], BLEND_OPS_TO_GL[function.getOperationAlpha()]); + (void)CHECK_GL_ERROR(); + + + glBlendFuncSeparate(BLEND_ARGS_TO_GL[function.getSourceColor()], BLEND_ARGS_TO_GL[function.getDestinationColor()], + BLEND_ARGS_TO_GL[function.getSourceAlpha()], BLEND_ARGS_TO_GL[function.getDestinationAlpha()]); + } else { + glDisable(GL_BLEND); + } + (void)CHECK_GL_ERROR(); + + _pipeline._stateCache.blendFunction = function; + } +} + +void GLBackend::do_setStateColorWriteMask(uint32 mask) { + if (_pipeline._stateCache.colorWriteMask != mask) { + glColorMask(mask & State::ColorMask::WRITE_RED, + mask & State::ColorMask::WRITE_GREEN, + mask & State::ColorMask::WRITE_BLUE, + mask & State::ColorMask::WRITE_ALPHA); + (void)CHECK_GL_ERROR(); + + _pipeline._stateCache.colorWriteMask = mask; + } +} + + +void GLBackend::do_setStateBlendFactor(const Batch& batch, size_t paramOffset) { + Vec4 factor(batch._params[paramOffset + 0]._float, + batch._params[paramOffset + 1]._float, + batch._params[paramOffset + 2]._float, + batch._params[paramOffset + 3]._float); + + glBlendColor(factor.x, factor.y, factor.z, factor.w); + (void)CHECK_GL_ERROR(); +} + +void GLBackend::do_setStateScissorRect(const Batch& batch, size_t paramOffset) { + Vec4i rect; + memcpy(&rect, batch.readData(batch._params[paramOffset]._uint), sizeof(Vec4i)); + + if (_stereo._enable) { + rect.z /= 2; + if (_stereo._pass) { + rect.x += rect.z; + } + } + glScissor(rect.x, rect.y, rect.z, rect.w); + (void)CHECK_GL_ERROR(); +} + diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackendTexture.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackendTexture.cpp new file mode 100644 index 0000000000..4be7682a4f --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLBackendTexture.cpp @@ -0,0 +1,40 @@ +// +// +// GLBackendTexture.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 1/19/2015. +// Copyright 2014 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 "GLBackend.h" +#include "GLTexture.h" + +using namespace gpu; +using namespace gpu::gl; + +bool GLBackend::isTextureReady(const TexturePointer& texture) { + // DO not transfer the texture, this call is expected for rendering texture + GLTexture* object = syncGPUObject(texture, true); + qDebug() << "GLBackendTexture isTextureReady syncGPUObject"; + return object && object->isReady(); +} + + +void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { + TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); + if (!resourceTexture) { + return; + } + + // DO not transfer the texture, this call is expected for rendering texture + GLTexture* object = syncGPUObject(resourceTexture, false); + qDebug() << "GLBackendTexture do_generateTextureMips syncGPUObject"; + if (!object) { + return; + } + + object->generateMips(); +} diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackendTransform.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackendTransform.cpp new file mode 100644 index 0000000000..3068e24dac --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLBackendTransform.cpp @@ -0,0 +1,212 @@ +// +// GLBackendTransform.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 3/8/2015. +// Copyright 2014 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 "GLBackend.h" + +using namespace gpu; +using namespace gpu::gl; + +// Transform Stage +void GLBackend::do_setModelTransform(const Batch& batch, size_t paramOffset) { + qDebug() << "do_setModelTransform"; +} + +void GLBackend::do_setViewTransform(const Batch& batch, size_t paramOffset) { + _transform._view = batch._transforms.get(batch._params[paramOffset]._uint); + _transform._viewIsCamera = batch._params[paramOffset + 1]._uint != 0; + _transform._invalidView = true; +} + +void GLBackend::do_setProjectionTransform(const Batch& batch, size_t paramOffset) { + memcpy(&_transform._projection, batch.readData(batch._params[paramOffset]._uint), sizeof(Mat4)); + _transform._invalidProj = true; +} + +void GLBackend::do_setViewportTransform(const Batch& batch, size_t paramOffset) { + memcpy(&_transform._viewport, batch.readData(batch._params[paramOffset]._uint), sizeof(Vec4i)); + +#ifdef GPU_STEREO_DRAWCALL_INSTANCED + { + ivec4& vp = _transform._viewport; + glViewport(vp.x, vp.y, vp.z, vp.w); + + // Where we assign the GL viewport + if (_stereo._enable) { + vp.z /= 2; + if (_stereo._pass) { + vp.x += vp.z; + } + } + } +#else + if (!_inRenderTransferPass && !isStereo()) { + ivec4& vp = _transform._viewport; + glViewport(vp.x, vp.y, vp.z, vp.w); + } +#endif + + // The Viewport is tagged invalid because the CameraTransformUBO is not up to date and will need update on next drawcall + _transform._invalidViewport = true; +} + +void GLBackend::do_setDepthRangeTransform(const Batch& batch, size_t paramOffset) { + + Vec2 depthRange(batch._params[paramOffset + 1]._float, batch._params[paramOffset + 0]._float); + + if ((depthRange.x != _transform._depthRange.x) || (depthRange.y != _transform._depthRange.y)) { + _transform._depthRange = depthRange; + + glDepthRangef(depthRange.x, depthRange.y); + } +} + +void GLBackend::killTransform() { + glDeleteBuffers(1, &_transform._objectBuffer); + glDeleteBuffers(1, &_transform._cameraBuffer); + glDeleteBuffers(1, &_transform._drawCallInfoBuffer); + glDeleteTextures(1, &_transform._objectBufferTexture); +} + +void GLBackend::syncTransformStateCache() { + _transform._invalidViewport = true; + _transform._invalidProj = true; + _transform._invalidView = true; + + glGetIntegerv(GL_VIEWPORT, (GLint*) &_transform._viewport); + + glGetFloatv(GL_DEPTH_RANGE, (GLfloat*)&_transform._depthRange); + + Mat4 modelView; + auto modelViewInv = glm::inverse(modelView); + _transform._view.evalFromRawMatrix(modelViewInv); + + glDisableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); + _transform._enabledDrawcallInfoBuffer = false; +} + +void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const StereoState& stereo) { + // Check all the dirty flags and update the state accordingly + if (_invalidViewport) { + _camera._viewport = glm::vec4(_viewport); + } + + if (_invalidProj) { + _camera._projection = _projection; + } + + if (_invalidView) { + // Apply the correction + if (_viewIsCamera && _correction.correction != glm::mat4()) { + // FIXME should I switch to using the camera correction buffer in Transform.slf and leave this out? + Transform result; + _view.mult(result, _view, _correction.correction); + if (_skybox) { + result.setTranslation(vec3()); + } + _view = result; + } + // This is when the _view matrix gets assigned + _view.getInverseMatrix(_camera._view); + } + + if (_invalidView || _invalidProj || _invalidViewport) { + size_t offset = _cameraUboSize * _cameras.size(); + _cameraOffsets.push_back(TransformStageState::Pair(commandIndex, offset)); + + if (stereo._enable) { +#ifdef GPU_STEREO_CAMERA_BUFFER + _cameras.push_back(CameraBufferElement(_camera.getEyeCamera(0, stereo, _view), _camera.getEyeCamera(1, stereo, _view))); +#else + _cameras.push_back((_camera.getEyeCamera(0, stereo, _view))); + _cameras.push_back((_camera.getEyeCamera(1, stereo, _view))); +#endif + } else { +#ifdef GPU_STEREO_CAMERA_BUFFER + _cameras.push_back(CameraBufferElement(_camera.recomputeDerived(_view))); +#else + _cameras.push_back((_camera.recomputeDerived(_view))); +#endif + } + } + + // Flags are clean + _invalidView = _invalidProj = _invalidViewport = false; +} + +void GLBackend::TransformStageState::update(size_t commandIndex, const StereoState& stereo) const { + size_t offset = INVALID_OFFSET; + while ((_camerasItr != _cameraOffsets.end()) && (commandIndex >= (*_camerasItr).first)) { + offset = (*_camerasItr).second; + _currentCameraOffset = offset; + ++_camerasItr; + } + + if (offset != INVALID_OFFSET) { +#ifdef GPU_STEREO_CAMERA_BUFFER + bindCurrentCamera(0); +#else + if (!stereo._enable) { + bindCurrentCamera(0); + } +#endif + } + (void)CHECK_GL_ERROR(); +} + +void GLBackend::TransformStageState::bindCurrentCamera(int eye) const { + if (_currentCameraOffset != INVALID_OFFSET) { + //qDebug() << "GLBackend::TransformStageState::bindCurrentCamera"; + glBindBufferRange(GL_UNIFORM_BUFFER, TRANSFORM_CAMERA_SLOT, _cameraBuffer, _currentCameraOffset + eye * _cameraUboSize, sizeof(CameraBufferElement)); + } +} + +void GLBackend::updateTransform(const Batch& batch) { + _transform.update(_commandIndex, _stereo); + + auto& drawCallInfoBuffer = batch.getDrawCallInfoBuffer(); + if (batch._currentNamedCall.empty()) { + (void)CHECK_GL_ERROR(); + auto& drawCallInfo = drawCallInfoBuffer[_currentDraw]; + glDisableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is disabled + (void)CHECK_GL_ERROR(); + GLint current_vao, current_vbo, maxVertexAtribs; + glGetIntegerv(GL_VERTEX_ARRAY_BINDING, ¤t_vao); + glGetIntegerv(GL_ARRAY_BUFFER_BINDING, ¤t_vbo); + glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &maxVertexAtribs); + glVertexAttribI4i(gpu::Stream::DRAW_CALL_INFO, drawCallInfo.index, drawCallInfo.unused, 0, 0); + + //int values[] = {drawCallInfo.index, drawCallInfo.unused}; + //glVertexAttribIPointer(gpu::Stream::DRAW_CALL_INFO, 2, GL_INT, 0, (const GLvoid *) values); + + /* + //glDisableVertexAttribArray currentvao 1 current vbo 0 + GL_INVALID_OPERATION is generated + a non-zero vertex array object is bound, + zero is bound to the GL_ARRAY_BUFFER buffer object binding point and + the pointer argument is not NULL. TRUE + */ + //qDebug() << "GLBackend::updateTransform glVertexAttribIPointer done"; + (void)CHECK_GL_ERROR(); + + } else { + //qDebug() << "GLBackend::updateTransform else"; + glEnableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is enabled + glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); + glVertexAttribIPointer(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0, + _transform._drawCallInfoOffsets[batch._currentNamedCall]); + glVertexAttribDivisor(gpu::Stream::DRAW_CALL_INFO, 1); + } + + (void)CHECK_GL_ERROR(); +} + +void GLBackend::resetTransformStage() { + +} diff --git a/libraries/gpu-gles/src/gpu/gl/GLBuffer.cpp b/libraries/gpu-gles/src/gpu/gl/GLBuffer.cpp new file mode 100644 index 0000000000..4f7d0a8632 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLBuffer.cpp @@ -0,0 +1,35 @@ +// +// Created by Gabriel Calero & Cristian Duarte on 09/27/2016 +// Copyright 2013-2016 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 "GLBuffer.h" +#include "GLBackend.h" + +using namespace gpu; +using namespace gpu::gl; + +GLBuffer::~GLBuffer() { + Backend::bufferCount.decrement(); + Backend::bufferGPUMemSize.update(_size, 0); + + if (_id) { + auto backend = _backend.lock(); + if (backend) { + backend->releaseBuffer(_id, _size); + } + } +} + +GLBuffer::GLBuffer(const std::weak_ptr& backend, const Buffer& buffer, GLuint id) : + GLObject(backend, buffer, id), + _size((GLuint)buffer._renderSysmem.getSize()), + _stamp(buffer._renderSysmem.getStamp()) +{ + Backend::bufferCount.increment(); + Backend::bufferGPUMemSize.update(0, _size); +} + diff --git a/libraries/gpu-gles/src/gpu/gl/GLBuffer.h b/libraries/gpu-gles/src/gpu/gl/GLBuffer.h new file mode 100644 index 0000000000..182014e764 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLBuffer.h @@ -0,0 +1,66 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 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_gpu_gl_GLBuffer_h +#define hifi_gpu_gl_GLBuffer_h + +#include "GLShared.h" +#include "GLBackend.h" + +namespace gpu { namespace gl { + +class GLBuffer : public GLObject { +public: + template + static GLBufferType* sync(GLBackend& backend, const Buffer& buffer) { + if (buffer.getSysmem().getSize() != 0) { + if (buffer._getUpdateCount == 0) { + qWarning() << "Unsynced buffer"; + } + if (buffer._getUpdateCount < buffer._applyUpdateCount) { + qWarning() << "Unsynced buffer " << buffer._getUpdateCount << " " << buffer._applyUpdateCount; + } + } + GLBufferType* object = Backend::getGPUObject(buffer); + + // Has the storage size changed? + if (!object || object->_stamp != buffer._renderSysmem.getStamp()) { + object = new GLBufferType(backend.shared_from_this(), buffer, object); + } + + if (0 != (buffer._renderPages._flags & PageManager::DIRTY)) { + object->transfer(); + } + + return object; + } + + template + static GLuint getId(GLBackend& backend, const Buffer& buffer) { + GLBuffer* bo = sync(backend, buffer); + if (bo) { + return bo->_buffer; + } else { + return 0; + } + } + + const GLuint& _buffer { _id }; + const GLuint _size; + const Stamp _stamp; + + ~GLBuffer(); + + virtual void transfer() = 0; + +protected: + GLBuffer(const std::weak_ptr& backend, const Buffer& buffer, GLuint id); +}; + +} } + +#endif diff --git a/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.cpp b/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.cpp new file mode 100644 index 0000000000..150bb2be70 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.cpp @@ -0,0 +1,48 @@ +// +// Created by Gabriel Calero & Cristian Duarte on 09/27/2016 +// Copyright 2016 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 "GLFramebuffer.h" +#include "GLBackend.h" + +using namespace gpu; +using namespace gpu::gl; + +GLFramebuffer::~GLFramebuffer() { + if (_id) { + auto backend = _backend.lock(); + if (backend) { + backend->releaseFramebuffer(_id); + } + } +} + +bool GLFramebuffer::checkStatus(GLenum target) const { + bool result = false; + switch (_status) { + case GL_FRAMEBUFFER_COMPLETE: + // Success ! + result = true; + break; + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT."; + break; + case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT."; + break; +/* TODO: case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: + qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER."; + break; + case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: + qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER."; + break; */ + case GL_FRAMEBUFFER_UNSUPPORTED: + qCDebug(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_UNSUPPORTED."; + break; + } + return result; +} diff --git a/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.h b/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.h new file mode 100644 index 0000000000..a2fd0999f3 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.h @@ -0,0 +1,77 @@ +// +// Created by Gabriel Calero & Cristian Duarte on 09/27/2016 +// Copyright 2016 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_gpu_gl_GLFramebuffer_h +#define hifi_gpu_gl_GLFramebuffer_h + +#include "GLShared.h" +#include "GLBackend.h" + +namespace gpu { namespace gl { + +class GLFramebuffer : public GLObject { +public: + template + static GLFramebufferType* sync(GLBackend& backend, const Framebuffer& framebuffer) { + GLFramebufferType* object = Backend::getGPUObject(framebuffer); + + bool needsUpate { false }; + if (!object || + framebuffer.getDepthStamp() != object->_depthStamp || + framebuffer.getColorStamps() != object->_colorStamps) { + needsUpate = true; + } + + // If GPU object already created and in sync + if (!needsUpate) { + return object; + } else if (framebuffer.isEmpty()) { + // NO framebuffer definition yet so let's avoid thinking + return nullptr; + } + + // need to have a gpu object? + if (!object) { + // All is green, assign the gpuobject to the Framebuffer + object = new GLFramebufferType(backend.shared_from_this(), framebuffer); + Backend::setGPUObject(framebuffer, object); + (void)CHECK_GL_ERROR(); + } + + object->update(); + return object; + } + + template + static GLuint getId(GLBackend& backend, const Framebuffer& framebuffer) { + GLFramebufferType* fbo = sync(backend, framebuffer); + if (fbo) { + return fbo->_id; + } else { + return 0; + } + } + + const GLuint& _fbo { _id }; + std::vector _colorBuffers; + Stamp _depthStamp { 0 }; + std::vector _colorStamps; + +protected: + GLenum _status { GL_FRAMEBUFFER_COMPLETE }; + virtual void update() = 0; + bool checkStatus(GLenum target) const; + + GLFramebuffer(const std::weak_ptr& backend, const Framebuffer& framebuffer, GLuint id) : GLObject(backend, framebuffer, id) {} + ~GLFramebuffer(); + +}; + +} } + + +#endif diff --git a/libraries/gpu-gles/src/gpu/gl/GLInputFormat.cpp b/libraries/gpu-gles/src/gpu/gl/GLInputFormat.cpp new file mode 100644 index 0000000000..7f42350c3b --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLInputFormat.cpp @@ -0,0 +1,33 @@ +// +// Created by Sam Gateau on 2016/07/21 +// Copyright 2013-2016 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 "GLInputFormat.h" +#include "GLBackend.h" + +using namespace gpu; +using namespace gpu::gl; + + +GLInputFormat::GLInputFormat() { +} + +GLInputFormat:: ~GLInputFormat() { + +} + +GLInputFormat* GLInputFormat::sync(const Stream::Format& inputFormat) { + GLInputFormat* object = Backend::getGPUObject(inputFormat); + + if (!object) { + object = new GLInputFormat(); + object->key = inputFormat.getKey(); + Backend::setGPUObject(inputFormat, object); + } + + return object; +} diff --git a/libraries/gpu-gles/src/gpu/gl/GLInputFormat.h b/libraries/gpu-gles/src/gpu/gl/GLInputFormat.h new file mode 100644 index 0000000000..a14e3d4d91 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLInputFormat.h @@ -0,0 +1,29 @@ +// +// Created by Sam Gateau on 2016/07/21 +// Copyright 2013-2016 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_gpu_gl_GLInputFormat_h +#define hifi_gpu_gl_GLInputFormat_h + +#include "GLShared.h" + +namespace gpu { +namespace gl { + +class GLInputFormat : public GPUObject { + public: + static GLInputFormat* sync(const Stream::Format& inputFormat); + + GLInputFormat(); + ~GLInputFormat(); + + std::string key; +}; + +} +} + +#endif diff --git a/libraries/gpu-gles/src/gpu/gl/GLPipeline.cpp b/libraries/gpu-gles/src/gpu/gl/GLPipeline.cpp new file mode 100644 index 0000000000..09c09de353 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLPipeline.cpp @@ -0,0 +1,62 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 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 "GLPipeline.h" + +#include "GLShader.h" +#include "GLState.h" + +using namespace gpu; +using namespace gpu::gl; + +GLPipeline* GLPipeline::sync(GLBackend& backend, const Pipeline& pipeline) { + GLPipeline* object = Backend::getGPUObject(pipeline); + + // If GPU object already created then good + if (object) { + return object; + } + + // No object allocated yet, let's see if it's worth it... + ShaderPointer shader = pipeline.getProgram(); + + // If this pipeline's shader has already failed to compile, don't try again + if (shader->compilationHasFailed()) { + return nullptr; + } + + GLShader* programObject = GLShader::sync(backend, *shader); + if (programObject == nullptr) { + shader->setCompilationHasFailed(true); + return nullptr; + } + + StatePointer state = pipeline.getState(); + GLState* stateObject = GLState::sync(*state); + if (stateObject == nullptr) { + return nullptr; + } + + // Program and state are valid, we can create the pipeline object + if (!object) { + object = new GLPipeline(); + Backend::setGPUObject(pipeline, object); + } + + // Special case for view correction matrices, any pipeline that declares the correction buffer + // uniform will automatically have it provided without any client code necessary. + // Required for stable lighting in the HMD. + //CLIMAX_MERGE_START + //getbuffers() doesnt exist anymore.. use get uniformbuffers()? + object->_cameraCorrection = shader->getUniformBuffers().findLocation("cameraCorrectionBuffer"); + //CLIMAX_MERGE_END + object->_program = programObject; + object->_state = stateObject; + + return object; +} diff --git a/libraries/gpu-gles/src/gpu/gl/GLPipeline.h b/libraries/gpu-gles/src/gpu/gl/GLPipeline.h new file mode 100644 index 0000000000..a298f149d9 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLPipeline.h @@ -0,0 +1,29 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 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_gpu_gl_GLPipeline_h +#define hifi_gpu_gl_GLPipeline_h + +#include "GLShared.h" + +namespace gpu { namespace gl { + +class GLPipeline : public GPUObject { +public: + static GLPipeline* sync(GLBackend& backend, const Pipeline& pipeline); + + GLShader* _program { nullptr }; + GLState* _state { nullptr }; + // Bit of a hack, any pipeline can need the camera correction buffer at execution time, so + // we store whether a given pipeline has declared the uniform buffer for it. + int32 _cameraCorrection { -1 }; +}; + +} } + + +#endif diff --git a/libraries/gpu-gles/src/gpu/gl/GLQuery.h b/libraries/gpu-gles/src/gpu/gl/GLQuery.h new file mode 100644 index 0000000000..23b1f38621 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLQuery.h @@ -0,0 +1,67 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 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_gpu_gl_GLQuery_h +#define hifi_gpu_gl_GLQuery_h + +#include "GLShared.h" +#include "GLBackend.h" + +namespace gpu { namespace gl { + +class GLQuery : public GLObject { + using Parent = gpu::gl::GLObject; +public: + template + static GLQueryType* sync(GLBackend& backend, const Query& query) { + GLQueryType* object = Backend::getGPUObject(query); + + // need to have a gpu object? + if (!object) { + // All is green, assign the gpuobject to the Query + object = new GLQueryType(backend.shared_from_this(), query); + (void)CHECK_GL_ERROR(); + Backend::setGPUObject(query, object); + } + + return object; + } + + template + static GLuint getId(GLBackend& backend, const QueryPointer& query) { + if (!query) { + return 0; + } + + GLQuery* object = sync(backend, *query); + if (!object) { + return 0; + } + + return object->_endqo; + } + + const GLuint& _endqo = { _id }; + const GLuint _beginqo = { 0 }; + GLuint64 _result { (GLuint64)-1 }; + GLuint64 _batchElapsedTime { (GLuint64) 0 }; + uint64_t _profileRangeId { 0 }; + uint32_t _rangeQueryDepth { 0 }; + +protected: + GLQuery(const std::weak_ptr& backend, const Query& query, GLuint endId, GLuint beginId) : Parent(backend, query, endId), _beginqo(beginId) {} + ~GLQuery() { + if (_id) { + GLuint ids[2] = { _endqo, _beginqo }; + glDeleteQueries(2, ids); + } + } +}; + +} } + +#endif diff --git a/libraries/gpu-gles/src/gpu/gl/GLShader.cpp b/libraries/gpu-gles/src/gpu/gl/GLShader.cpp new file mode 100644 index 0000000000..b728010470 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLShader.cpp @@ -0,0 +1,224 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 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 "GLShader.h" +#include + +#include "GLBackend.h" + +using namespace gpu; +using namespace gpu::gl; + +GLShader::GLShader(const std::weak_ptr& backend) : _backend(backend) { +} + +GLShader::~GLShader() { + for (auto& so : _shaderObjects) { + auto backend = _backend.lock(); + if (backend) { + if (so.glshader != 0) { + backend->releaseShader(so.glshader); + } + if (so.glprogram != 0) { + backend->releaseProgram(so.glprogram); + } + } + } +} + +// GLSL version +static const std::string glslVersion { + "#version 310 es" +}; + +// Shader domain +static const size_t NUM_SHADER_DOMAINS = 3; + +// GL Shader type enums +// Must match the order of type specified in gpu::Shader::Type +static const std::array SHADER_DOMAINS { { + GL_VERTEX_SHADER, + GL_FRAGMENT_SHADER, + //GL_GEOMETRY_SHADER, +} }; + +// Domain specific defines +// Must match the order of type specified in gpu::Shader::Type +static const std::array DOMAIN_DEFINES { { + "#define GPU_VERTEX_SHADER", + "#define GPU_PIXEL_SHADER", + "#define GPU_GEOMETRY_SHADER", +} }; + +// Stereo specific defines +static const std::string stereoVersion { +#ifdef GPU_STEREO_DRAWCALL_INSTANCED + "#define GPU_TRANSFORM_IS_STEREO\n#define GPU_TRANSFORM_STEREO_CAMERA\n#define GPU_TRANSFORM_STEREO_CAMERA_INSTANCED\n#define GPU_TRANSFORM_STEREO_SPLIT_SCREEN" +#endif +#ifdef GPU_STEREO_DRAWCALL_DOUBLED +#ifdef GPU_STEREO_CAMERA_BUFFER + "#define GPU_TRANSFORM_IS_STEREO\n#define GPU_TRANSFORM_STEREO_CAMERA\n#define GPU_TRANSFORM_STEREO_CAMERA_ATTRIBUTED" +#else + "#define GPU_TRANSFORM_IS_STEREO" +#endif +#endif +}; + +// Versions specific of the shader +static const std::array VERSION_DEFINES { { + "", + stereoVersion +} }; + +GLShader* compileBackendShader(GLBackend& backend, const Shader& shader) { + // Any GLSLprogram ? normally yes... + const std::string& shaderSource = shader.getSource().getCode(); + GLenum shaderDomain = SHADER_DOMAINS[shader.getType()]; + GLShader::ShaderObjects shaderObjects; + + for (int version = 0; version < GLShader::NumVersions; version++) { + auto& shaderObject = shaderObjects[version]; + std::string shaderDefines = glslVersion + "\n" + DOMAIN_DEFINES[shader.getType()] + "\n" + VERSION_DEFINES[version] + + "\n" + "#extension GL_EXT_texture_buffer : enable" + + "\nprecision lowp float; // check precision 2" + + "\nprecision lowp samplerBuffer;" + + "\nprecision lowp sampler2DShadow;"; + // TODO Delete bool result = compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader, shaderObject.glprogram); + std::string error; + + +#ifdef SEPARATE_PROGRAM + bool result = ::gl::compileShader(shaderDomain, shaderSource.c_str(), shaderDefines.c_str(), shaderObject.glshader, shaderObject.glprogram, error); +#else + bool result = ::gl::compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader, error); +#endif + if (!result) { + qCWarning(gpugllogging) << "GLBackend::compileBackendProgram - Shader didn't compile:\n" << error.c_str(); + return nullptr; + } + } + + // So far so good, the shader is created successfully + GLShader* object = new GLShader(backend.shared_from_this()); + object->_shaderObjects = shaderObjects; + + return object; +} + +GLShader* compileBackendProgram(GLBackend& backend, const Shader& program) { + if (!program.isProgram()) { + return nullptr; + } + + GLShader::ShaderObjects programObjects; + + for (int version = 0; version < GLShader::NumVersions; version++) { + auto& programObject = programObjects[version]; + + // Let's go through every shaders and make sure they are ready to go + std::vector< GLuint > shaderGLObjects; + for (auto subShader : program.getShaders()) { + auto object = GLShader::sync(backend, *subShader); + if (object) { + shaderGLObjects.push_back(object->_shaderObjects[version].glshader); + } else { + qCDebug(gpugllogging) << "GLShader::compileBackendProgram - One of the shaders of the program is not compiled?"; + return nullptr; + } + } + + std::string error; + GLuint glprogram = ::gl::compileProgram(shaderGLObjects, error); + if (glprogram == 0) { + qCWarning(gpugllogging) << error.c_str(); + return nullptr; + } + + programObject.glprogram = glprogram; + + makeProgramBindings(programObject); + } + + // So far so good, the program versions have all been created successfully + GLShader* object = new GLShader(backend.shared_from_this()); + object->_shaderObjects = programObjects; + + return object; +} + +GLShader* GLShader::sync(GLBackend& backend, const Shader& shader) { + GLShader* object = Backend::getGPUObject(shader); + + // If GPU object already created then good + if (object) { + return object; + } + // need to have a gpu object? + if (shader.isProgram()) { + GLShader* tempObject = compileBackendProgram(backend, shader); + if (tempObject) { + object = tempObject; + Backend::setGPUObject(shader, object); + } + } else if (shader.isDomain()) { + GLShader* tempObject = compileBackendShader(backend, shader); + if (tempObject) { + object = tempObject; + Backend::setGPUObject(shader, object); + } + } + + glFinish(); + return object; +} + +bool GLShader::makeProgram(GLBackend& backend, Shader& shader, const Shader::BindingSet& slotBindings) { + + // First make sure the Shader has been compiled + GLShader* object = sync(backend, shader); + if (!object) { + return false; + } + + // Apply bindings to all program versions and generate list of slots from default version + for (int version = 0; version < GLShader::NumVersions; version++) { + auto& shaderObject = object->_shaderObjects[version]; + if (shaderObject.glprogram) { + Shader::SlotSet buffers; + makeUniformBlockSlots(shaderObject.glprogram, slotBindings, buffers); + + Shader::SlotSet uniforms; + Shader::SlotSet textures; + Shader::SlotSet samplers; + makeUniformSlots(shaderObject.glprogram, slotBindings, uniforms, textures, samplers); + + Shader::SlotSet resourceBuffers; + makeResourceBufferSlots(shaderObject.glprogram, slotBindings, resourceBuffers); + + Shader::SlotSet inputs; + makeInputSlots(shaderObject.glprogram, slotBindings, inputs); + + Shader::SlotSet outputs; + makeOutputSlots(shaderObject.glprogram, slotBindings, outputs); + + // Define the public slots only from the default version + if (version == 0) { + shader.defineSlots(uniforms, buffers, resourceBuffers, textures, samplers, inputs, outputs); + } // else + { + GLShader::UniformMapping mapping; + for (auto srcUniform : shader.getUniforms()) { + mapping[srcUniform._location] = uniforms.findLocation(srcUniform._name); + } + object->_uniformMappings.push_back(mapping); + } + } + } + + return true; +} + diff --git a/libraries/gpu-gles/src/gpu/gl/GLShader.h b/libraries/gpu-gles/src/gpu/gl/GLShader.h new file mode 100644 index 0000000000..e03b487a60 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLShader.h @@ -0,0 +1,59 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 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_gpu_gl_GLShader_h +#define hifi_gpu_gl_GLShader_h + +#include "GLShared.h" + +namespace gpu { namespace gl { + +class GLShader : public GPUObject { +public: + static GLShader* sync(GLBackend& backend, const Shader& shader); + static bool makeProgram(GLBackend& backend, Shader& shader, const Shader::BindingSet& slotBindings); + + enum Version { + Mono = 0, + Stereo, + + NumVersions + }; + + using ShaderObject = gpu::gl::ShaderObject; + using ShaderObjects = std::array< ShaderObject, NumVersions >; + + using UniformMapping = std::map; + using UniformMappingVersions = std::vector; + + GLShader(const std::weak_ptr& backend); + ~GLShader(); + + ShaderObjects _shaderObjects; + UniformMappingVersions _uniformMappings; + + GLuint getProgram(Version version = Mono) const { + return _shaderObjects[version].glprogram; + } + + GLint getUniformLocation(GLint srcLoc, Version version = Mono) const { + // This check protect against potential invalid src location for this shader, if unknown then return -1. + const auto& mapping = _uniformMappings[version]; + auto found = mapping.find(srcLoc); + if (found == mapping.end()) { + return -1; + } + return found->second; + } + + const std::weak_ptr _backend; +}; + +} } + + +#endif diff --git a/libraries/gpu-gles/src/gpu/gl/GLShared.cpp b/libraries/gpu-gles/src/gpu/gl/GLShared.cpp new file mode 100644 index 0000000000..5d340889a6 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLShared.cpp @@ -0,0 +1,879 @@ +// +// Created by Bradley Austin Davis on 2016/05/14 +// Copyright 2013-2016 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 "GLShared.h" + +#include + +#include + +#include +#include +#include + +Q_LOGGING_CATEGORY(gpugllogging, "hifi.gpu.gl") +Q_LOGGING_CATEGORY(trace_render_gpu_gl, "trace.render.gpu.gl") + +namespace gpu { namespace gl { + +bool checkGLError(const char* name) { + GLenum error = glGetError(); + if (!error) { + return false; + } else { + switch (error) { + case GL_INVALID_ENUM: + qCDebug(gpugllogging) << "GLBackend::" << name << ": An unacceptable value is specified for an enumerated argument.The offending command is ignored and has no other side effect than to set the error flag."; + break; + case GL_INVALID_VALUE: + qCDebug(gpugllogging) << "GLBackend" << name << ": A numeric argument is out of range.The offending command is ignored and has no other side effect than to set the error flag"; + break; + case GL_INVALID_OPERATION: + qCDebug(gpugllogging) << "GLBackend" << name << ": The specified operation is not allowed in the current state.The offending command is ignored and has no other side effect than to set the error flag.."; + break; + case GL_INVALID_FRAMEBUFFER_OPERATION: + qCDebug(gpugllogging) << "GLBackend" << name << ": The framebuffer object is not complete.The offending command is ignored and has no other side effect than to set the error flag."; + break; + case GL_OUT_OF_MEMORY: + qCDebug(gpugllogging) << "GLBackend" << name << ": There is not enough memory left to execute the command.The state of the GL is undefined, except for the state of the error flags, after this error is recorded."; + break; + default: + qCDebug(gpugllogging) << "GLBackend" << name << ": Unknown error: " << error; + break; + } + return true; + } +} + +bool checkGLErrorDebug(const char* name) { +#ifdef DEBUG + return checkGLError(name); +#else + Q_UNUSED(name); + return false; +#endif +} + +gpu::Size getFreeDedicatedMemory() { + Size result { 0 }; + static bool nvidiaMemorySupported { false }; + static bool atiMemorySupported { false }; + if (nvidiaMemorySupported) { + + GLint nvGpuMemory { 0 }; + qDebug() << "TODO: GLShared.cpp getFreeDedicatedMemory GL_GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX"; + //glGetIntegerv(GL_GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX, &nvGpuMemory); + if (GL_NO_ERROR == glGetError()) { + result = KB_TO_BYTES(nvGpuMemory); + } else { + nvidiaMemorySupported = false; + } + } else if (atiMemorySupported) { + GLint atiGpuMemory[4]; + qDebug() << "TODO: GLShared.cpp getFreeDedicatedMemory GL_TEXTURE_FREE_MEMORY_ATI"; + // not really total memory, but close enough if called early enough in the application lifecycle + //glGetIntegerv(GL_TEXTURE_FREE_MEMORY_ATI, atiGpuMemory); + if (GL_NO_ERROR == glGetError()) { + result = KB_TO_BYTES(atiGpuMemory[0]); + } else { + atiMemorySupported = false; + } + } + return result; +} + +gpu::Size getDedicatedMemory() { + static Size dedicatedMemory { 0 }; + static std::once_flag once; + std::call_once(once, [&] { + if (!dedicatedMemory) { + GLint atiGpuMemory[4]; + // not really total memory, but close enough if called early enough in the application lifecycle + //glGetIntegerv(GL_TEXTURE_FREE_MEMORY_ATI, atiGpuMemory); + qDebug() << "TODO: GLShared.cpp.cpp:initInput GL_TEXTURE_FREE_MEMORY_ATI"; + if (GL_NO_ERROR == glGetError()) { + dedicatedMemory = KB_TO_BYTES(atiGpuMemory[0]); + } + } + + if (!dedicatedMemory) { + GLint nvGpuMemory { 0 }; + qDebug() << "TODO: GLShared.cpp.cpp:initInput GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX"; + //glGetIntegerv(GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX, &nvGpuMemory); + if (GL_NO_ERROR == glGetError()) { + dedicatedMemory = KB_TO_BYTES(nvGpuMemory); + } + } + + if (!dedicatedMemory) { + auto gpuIdent = GPUIdent::getInstance(); + if (gpuIdent && gpuIdent->isValid()) { + dedicatedMemory = MB_TO_BYTES(gpuIdent->getMemory()); + } + } + }); + + return dedicatedMemory; +} + + + + +ComparisonFunction comparisonFuncFromGL(GLenum func) { + if (func == GL_NEVER) { + return NEVER; + } else if (func == GL_LESS) { + return LESS; + } else if (func == GL_EQUAL) { + return EQUAL; + } else if (func == GL_LEQUAL) { + return LESS_EQUAL; + } else if (func == GL_GREATER) { + return GREATER; + } else if (func == GL_NOTEQUAL) { + return NOT_EQUAL; + } else if (func == GL_GEQUAL) { + return GREATER_EQUAL; + } else if (func == GL_ALWAYS) { + return ALWAYS; + } + + return ALWAYS; +} + +State::StencilOp stencilOpFromGL(GLenum stencilOp) { + if (stencilOp == GL_KEEP) { + return State::STENCIL_OP_KEEP; + } else if (stencilOp == GL_ZERO) { + return State::STENCIL_OP_ZERO; + } else if (stencilOp == GL_REPLACE) { + return State::STENCIL_OP_REPLACE; + } else if (stencilOp == GL_INCR_WRAP) { + return State::STENCIL_OP_INCR_SAT; + } else if (stencilOp == GL_DECR_WRAP) { + return State::STENCIL_OP_DECR_SAT; + } else if (stencilOp == GL_INVERT) { + return State::STENCIL_OP_INVERT; + } else if (stencilOp == GL_INCR) { + return State::STENCIL_OP_INCR; + } else if (stencilOp == GL_DECR) { + return State::STENCIL_OP_DECR; + } + + return State::STENCIL_OP_KEEP; +} + +State::BlendOp blendOpFromGL(GLenum blendOp) { + if (blendOp == GL_FUNC_ADD) { + return State::BLEND_OP_ADD; + } else if (blendOp == GL_FUNC_SUBTRACT) { + return State::BLEND_OP_SUBTRACT; + } else if (blendOp == GL_FUNC_REVERSE_SUBTRACT) { + return State::BLEND_OP_REV_SUBTRACT; + } else if (blendOp == GL_MIN) { + return State::BLEND_OP_MIN; + } else if (blendOp == GL_MAX) { + return State::BLEND_OP_MAX; + } + + return State::BLEND_OP_ADD; +} + +State::BlendArg blendArgFromGL(GLenum blendArg) { + if (blendArg == GL_ZERO) { + return State::ZERO; + } else if (blendArg == GL_ONE) { + return State::ONE; + } else if (blendArg == GL_SRC_COLOR) { + return State::SRC_COLOR; + } else if (blendArg == GL_ONE_MINUS_SRC_COLOR) { + return State::INV_SRC_COLOR; + } else if (blendArg == GL_DST_COLOR) { + return State::DEST_COLOR; + } else if (blendArg == GL_ONE_MINUS_DST_COLOR) { + return State::INV_DEST_COLOR; + } else if (blendArg == GL_SRC_ALPHA) { + return State::SRC_ALPHA; + } else if (blendArg == GL_ONE_MINUS_SRC_ALPHA) { + return State::INV_SRC_ALPHA; + } else if (blendArg == GL_DST_ALPHA) { + return State::DEST_ALPHA; + } else if (blendArg == GL_ONE_MINUS_DST_ALPHA) { + return State::INV_DEST_ALPHA; + } else if (blendArg == GL_CONSTANT_COLOR) { + return State::FACTOR_COLOR; + } else if (blendArg == GL_ONE_MINUS_CONSTANT_COLOR) { + return State::INV_FACTOR_COLOR; + } else if (blendArg == GL_CONSTANT_ALPHA) { + return State::FACTOR_ALPHA; + } else if (blendArg == GL_ONE_MINUS_CONSTANT_ALPHA) { + return State::INV_FACTOR_ALPHA; + } + + return State::ONE; +} + +void getCurrentGLState(State::Data& state) { + { + GLint modes[2]; + //glGetIntegerv(GL_POLYGON_MODE, modes); + qDebug() << "TODO: GLShared.cpp:getCurrentGLState GL_POLYGON_MODE"; + qDebug() << "TODO: GLShared.cpp:getCurrentGLState GL_FILL"; + qDebug() << "TODO: GLShared.cpp:getCurrentGLState GL_LINE"; + + if (modes[0] == 0 /*GL_FILL*/) { + state.fillMode = State::FILL_FACE; + } else { + if (modes[0] == 0 /*GL_LINE*/) { + state.fillMode = State::FILL_LINE; + } else { + state.fillMode = State::FILL_POINT; + } + } + } + { + if (glIsEnabled(GL_CULL_FACE)) { + GLint mode; + glGetIntegerv(GL_CULL_FACE_MODE, &mode); + state.cullMode = (mode == GL_FRONT ? State::CULL_FRONT : State::CULL_BACK); + } else { + state.cullMode = State::CULL_NONE; + } + } + { + GLint winding; + glGetIntegerv(GL_FRONT_FACE, &winding); + state.frontFaceClockwise = (winding == GL_CW); + //state.depthClampEnable = glIsEnabled(GL_DEPTH_CLAMP); + qDebug() << "TODO: GLShared.cpp.cpp:getCurrentGLState GL_DEPTH_CLAMP"; + state.scissorEnable = glIsEnabled(GL_SCISSOR_TEST); + //state.multisampleEnable = glIsEnabled(GL_MULTISAMPLE); + qDebug() << "TODO: GLShared.cpp.cpp:getCurrentGLState GL_MULTISAMPLE"; + + //state.antialisedLineEnable = glIsEnabled(GL_LINE_SMOOTH); + qDebug() << "TODO: GLShared.cpp.cpp:getCurrentGLState GL_LINE_SMOOTH"; + + } + { + if (glIsEnabled(GL_POLYGON_OFFSET_FILL)) { + glGetFloatv(GL_POLYGON_OFFSET_FACTOR, &state.depthBiasSlopeScale); + glGetFloatv(GL_POLYGON_OFFSET_UNITS, &state.depthBias); + } + } + { + GLboolean isEnabled = glIsEnabled(GL_DEPTH_TEST); + GLboolean writeMask; + glGetBooleanv(GL_DEPTH_WRITEMASK, &writeMask); + GLint func; + glGetIntegerv(GL_DEPTH_FUNC, &func); + + state.depthTest = State::DepthTest(isEnabled, writeMask, comparisonFuncFromGL(func)); + } + { + GLboolean isEnabled = glIsEnabled(GL_STENCIL_TEST); + + GLint frontWriteMask; + GLint frontReadMask; + GLint frontRef; + GLint frontFail; + GLint frontDepthFail; + GLint frontPass; + GLint frontFunc; + glGetIntegerv(GL_STENCIL_WRITEMASK, &frontWriteMask); + glGetIntegerv(GL_STENCIL_VALUE_MASK, &frontReadMask); + glGetIntegerv(GL_STENCIL_REF, &frontRef); + glGetIntegerv(GL_STENCIL_FAIL, &frontFail); + glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &frontDepthFail); + glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &frontPass); + glGetIntegerv(GL_STENCIL_FUNC, &frontFunc); + + GLint backWriteMask; + GLint backReadMask; + GLint backRef; + GLint backFail; + GLint backDepthFail; + GLint backPass; + GLint backFunc; + glGetIntegerv(GL_STENCIL_BACK_WRITEMASK, &backWriteMask); + glGetIntegerv(GL_STENCIL_BACK_VALUE_MASK, &backReadMask); + glGetIntegerv(GL_STENCIL_BACK_REF, &backRef); + glGetIntegerv(GL_STENCIL_BACK_FAIL, &backFail); + glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_FAIL, &backDepthFail); + glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_PASS, &backPass); + glGetIntegerv(GL_STENCIL_BACK_FUNC, &backFunc); + + state.stencilActivation = State::StencilActivation(isEnabled, frontWriteMask, backWriteMask); + state.stencilTestFront = State::StencilTest(frontRef, frontReadMask, comparisonFuncFromGL(frontFunc), stencilOpFromGL(frontFail), stencilOpFromGL(frontDepthFail), stencilOpFromGL(frontPass)); + state.stencilTestBack = State::StencilTest(backRef, backReadMask, comparisonFuncFromGL(backFunc), stencilOpFromGL(backFail), stencilOpFromGL(backDepthFail), stencilOpFromGL(backPass)); + } + { + GLint mask = 0xFFFFFFFF; + if (glIsEnabled(GL_SAMPLE_MASK)) { + glGetIntegerv(GL_SAMPLE_MASK, &mask); + state.sampleMask = mask; + } + state.sampleMask = mask; + } + { + state.alphaToCoverageEnable = glIsEnabled(GL_SAMPLE_ALPHA_TO_COVERAGE); + } + { + GLboolean isEnabled = glIsEnabled(GL_BLEND); + GLint srcRGB; + GLint srcA; + GLint dstRGB; + GLint dstA; + glGetIntegerv(GL_BLEND_SRC_RGB, &srcRGB); + glGetIntegerv(GL_BLEND_SRC_ALPHA, &srcA); + glGetIntegerv(GL_BLEND_DST_RGB, &dstRGB); + glGetIntegerv(GL_BLEND_DST_ALPHA, &dstA); + + GLint opRGB; + GLint opA; + glGetIntegerv(GL_BLEND_EQUATION_RGB, &opRGB); + glGetIntegerv(GL_BLEND_EQUATION_ALPHA, &opA); + + state.blendFunction = State::BlendFunction(isEnabled, + blendArgFromGL(srcRGB), blendOpFromGL(opRGB), blendArgFromGL(dstRGB), + blendArgFromGL(srcA), blendOpFromGL(opA), blendArgFromGL(dstA)); + } + { + GLboolean mask[4]; + glGetBooleanv(GL_COLOR_WRITEMASK, mask); + state.colorWriteMask = (mask[0] ? State::WRITE_RED : 0) + | (mask[1] ? State::WRITE_GREEN : 0) + | (mask[2] ? State::WRITE_BLUE : 0) + | (mask[3] ? State::WRITE_ALPHA : 0); + } + + (void)CHECK_GL_ERROR(); +} + + +class ElementResource { +public: + gpu::Element _element; + uint16 _resource; + + ElementResource(Element&& elem, uint16 resource) : _element(elem), _resource(resource) {} +}; + +ElementResource getFormatFromGLUniform(GLenum gltype) { + switch (gltype) { + case GL_FLOAT: return ElementResource(Element(SCALAR, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_VEC2: return ElementResource(Element(VEC2, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_VEC3: return ElementResource(Element(VEC3, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_VEC4: return ElementResource(Element(VEC4, gpu::FLOAT, UNIFORM), Resource::BUFFER); + /* + case GL_DOUBLE: return ElementResource(Element(SCALAR, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_DOUBLE_VEC2: return ElementResource(Element(VEC2, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_DOUBLE_VEC3: return ElementResource(Element(VEC3, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_DOUBLE_VEC4: return ElementResource(Element(VEC4, gpu::FLOAT, UNIFORM), Resource::BUFFER); + */ + case GL_INT: return ElementResource(Element(SCALAR, gpu::INT32, UNIFORM), Resource::BUFFER); + case GL_INT_VEC2: return ElementResource(Element(VEC2, gpu::INT32, UNIFORM), Resource::BUFFER); + case GL_INT_VEC3: return ElementResource(Element(VEC3, gpu::INT32, UNIFORM), Resource::BUFFER); + case GL_INT_VEC4: return ElementResource(Element(VEC4, gpu::INT32, UNIFORM), Resource::BUFFER); + + case GL_UNSIGNED_INT: return ElementResource(Element(SCALAR, gpu::UINT32, UNIFORM), Resource::BUFFER); +#if defined(Q_OS_WIN) + case GL_UNSIGNED_INT_VEC2: return ElementResource(Element(VEC2, gpu::UINT32, UNIFORM), Resource::BUFFER); + case GL_UNSIGNED_INT_VEC3: return ElementResource(Element(VEC3, gpu::UINT32, UNIFORM), Resource::BUFFER); + case GL_UNSIGNED_INT_VEC4: return ElementResource(Element(VEC4, gpu::UINT32, UNIFORM), Resource::BUFFER); +#endif + + case GL_BOOL: return ElementResource(Element(SCALAR, gpu::BOOL, UNIFORM), Resource::BUFFER); + case GL_BOOL_VEC2: return ElementResource(Element(VEC2, gpu::BOOL, UNIFORM), Resource::BUFFER); + case GL_BOOL_VEC3: return ElementResource(Element(VEC3, gpu::BOOL, UNIFORM), Resource::BUFFER); + case GL_BOOL_VEC4: return ElementResource(Element(VEC4, gpu::BOOL, UNIFORM), Resource::BUFFER); + + + case GL_FLOAT_MAT2: return ElementResource(Element(gpu::MAT2, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_MAT3: return ElementResource(Element(MAT3, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_MAT4: return ElementResource(Element(MAT4, gpu::FLOAT, UNIFORM), Resource::BUFFER); + + /* {GL_FLOAT_MAT2x3 mat2x3}, + {GL_FLOAT_MAT2x4 mat2x4}, + {GL_FLOAT_MAT3x2 mat3x2}, + {GL_FLOAT_MAT3x4 mat3x4}, + {GL_FLOAT_MAT4x2 mat4x2}, + {GL_FLOAT_MAT4x3 mat4x3}, + {GL_DOUBLE_MAT2 dmat2}, + {GL_DOUBLE_MAT3 dmat3}, + {GL_DOUBLE_MAT4 dmat4}, + {GL_DOUBLE_MAT2x3 dmat2x3}, + {GL_DOUBLE_MAT2x4 dmat2x4}, + {GL_DOUBLE_MAT3x2 dmat3x2}, + {GL_DOUBLE_MAT3x4 dmat3x4}, + {GL_DOUBLE_MAT4x2 dmat4x2}, + {GL_DOUBLE_MAT4x3 dmat4x3}, + */ + + //qDebug() << "TODO: GLShared.cpp.cpp:ElementResource GL_SAMPLER_1D"; + //case GL_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_1D); + case GL_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_2D); + + case GL_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_3D); + case GL_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_CUBE); + +#if defined(Q_OS_WIN) + case GL_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); + case GL_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_1D_ARRAY); + case GL_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_2D_ARRAY); + case GL_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); +#endif + + case GL_SAMPLER_2D_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_2D); +#if defined(Q_OS_WIN) + case GL_SAMPLER_CUBE_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_CUBE); + + case GL_SAMPLER_2D_ARRAY_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_2D_ARRAY); +#endif + + // {GL_SAMPLER_1D_SHADOW sampler1DShadow}, + // {GL_SAMPLER_1D_ARRAY_SHADOW sampler1DArrayShadow}, + + // {GL_SAMPLER_BUFFER samplerBuffer}, + // {GL_SAMPLER_2D_RECT sampler2DRect}, + // {GL_SAMPLER_2D_RECT_SHADOW sampler2DRectShadow}, + +#if defined(Q_OS_WIN) + case GL_INT_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_1D); + case GL_INT_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_2D); + case GL_INT_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); + case GL_INT_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_3D); + case GL_INT_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_CUBE); + + case GL_INT_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_1D_ARRAY); + case GL_INT_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_2D_ARRAY); + case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); + + // {GL_INT_SAMPLER_BUFFER isamplerBuffer}, + // {GL_INT_SAMPLER_2D_RECT isampler2DRect}, + + case GL_UNSIGNED_INT_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_1D); + case GL_UNSIGNED_INT_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_2D); + case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); + case GL_UNSIGNED_INT_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_3D); + case GL_UNSIGNED_INT_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_CUBE); + + case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_1D_ARRAY); + case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_2D_ARRAY); + case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); +#endif + // {GL_UNSIGNED_INT_SAMPLER_BUFFER usamplerBuffer}, + // {GL_UNSIGNED_INT_SAMPLER_2D_RECT usampler2DRect}, + /* + {GL_IMAGE_1D image1D}, + {GL_IMAGE_2D image2D}, + {GL_IMAGE_3D image3D}, + {GL_IMAGE_2D_RECT image2DRect}, + {GL_IMAGE_CUBE imageCube}, + {GL_IMAGE_BUFFER imageBuffer}, + {GL_IMAGE_1D_ARRAY image1DArray}, + {GL_IMAGE_2D_ARRAY image2DArray}, + {GL_IMAGE_2D_MULTISAMPLE image2DMS}, + {GL_IMAGE_2D_MULTISAMPLE_ARRAY image2DMSArray}, + {GL_INT_IMAGE_1D iimage1D}, + {GL_INT_IMAGE_2D iimage2D}, + {GL_INT_IMAGE_3D iimage3D}, + {GL_INT_IMAGE_2D_RECT iimage2DRect}, + {GL_INT_IMAGE_CUBE iimageCube}, + {GL_INT_IMAGE_BUFFER iimageBuffer}, + {GL_INT_IMAGE_1D_ARRAY iimage1DArray}, + {GL_INT_IMAGE_2D_ARRAY iimage2DArray}, + {GL_INT_IMAGE_2D_MULTISAMPLE iimage2DMS}, + {GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY iimage2DMSArray}, + {GL_UNSIGNED_INT_IMAGE_1D uimage1D}, + {GL_UNSIGNED_INT_IMAGE_2D uimage2D}, + {GL_UNSIGNED_INT_IMAGE_3D uimage3D}, + {GL_UNSIGNED_INT_IMAGE_2D_RECT uimage2DRect}, + {GL_UNSIGNED_INT_IMAGE_CUBE uimageCube},+ [0] {_name="fInnerRadius" _location=0 _element={_semantic=15 '\xf' _dimension=0 '\0' _type=0 '\0' } } gpu::Shader::Slot + + {GL_UNSIGNED_INT_IMAGE_BUFFER uimageBuffer}, + {GL_UNSIGNED_INT_IMAGE_1D_ARRAY uimage1DArray}, + {GL_UNSIGNED_INT_IMAGE_2D_ARRAY uimage2DArray}, + {GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE uimage2DMS}, + {GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY uimage2DMSArray}, + {GL_UNSIGNED_INT_ATOMIC_COUNTER atomic_uint} + */ + default: + return ElementResource(Element(), Resource::BUFFER); + } + +}; + +int makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, + Shader::SlotSet& uniforms, Shader::SlotSet& textures, Shader::SlotSet& samplers) { + GLint uniformsCount = 0; + + glGetProgramiv(glprogram, GL_ACTIVE_UNIFORMS, &uniformsCount); + + for (int i = 0; i < uniformsCount; i++) { + const GLint NAME_LENGTH = 256; + GLchar name[NAME_LENGTH]; + GLint length = 0; + GLint size = 0; + GLenum type = 0; + glGetActiveUniform(glprogram, i, NAME_LENGTH, &length, &size, &type, name); + GLint location = glGetUniformLocation(glprogram, name); + const GLint INVALID_UNIFORM_LOCATION = -1; + + // Try to make sense of the gltype + auto elementResource = getFormatFromGLUniform(type); + + // The uniform as a standard var type + if (location != INVALID_UNIFORM_LOCATION) { + // Let's make sure the name doesn't contains an array element + std::string sname(name); + auto foundBracket = sname.find_first_of('['); + if (foundBracket != std::string::npos) { + // std::string arrayname = sname.substr(0, foundBracket); + + if (sname[foundBracket + 1] == '0') { + sname = sname.substr(0, foundBracket); + } else { + // skip this uniform since it's not the first element of an array + continue; + } + } + + if (elementResource._resource == Resource::BUFFER) { + uniforms.insert(Shader::Slot(sname, location, elementResource._element, elementResource._resource)); + } else { + // For texture/Sampler, the location is the actual binding value + GLint binding = -1; + glGetUniformiv(glprogram, location, &binding); + + auto requestedBinding = slotBindings.find(std::string(sname)); + if (requestedBinding != slotBindings.end()) { + if (binding != (*requestedBinding)._location) { + binding = (*requestedBinding)._location; + glProgramUniform1i(glprogram, location, binding); + } + } + + textures.insert(Shader::Slot(name, binding, elementResource._element, elementResource._resource)); + samplers.insert(Shader::Slot(name, binding, elementResource._element, elementResource._resource)); + } + } + } + + return uniformsCount; +} + +const GLint UNUSED_SLOT = -1; +bool isUnusedSlot(GLint binding) { + return (binding == UNUSED_SLOT); +} + +int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers) { + GLint buffersCount = 0; + + glGetProgramiv(glprogram, GL_ACTIVE_UNIFORM_BLOCKS, &buffersCount); + + // fast exit + if (buffersCount == 0) { + return 0; + } + + GLint maxNumUniformBufferSlots = 0; + glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &maxNumUniformBufferSlots); + std::vector uniformBufferSlotMap(maxNumUniformBufferSlots, -1); + + struct UniformBlockInfo { + using Vector = std::vector; + const GLuint index{ 0 }; + const std::string name; + GLint binding{ -1 }; + GLint size{ 0 }; + + static std::string getName(GLuint glprogram, GLuint i) { + static const GLint NAME_LENGTH = 256; + GLint length = 0; + GLchar nameBuffer[NAME_LENGTH]; + glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_NAME_LENGTH, &length); + glGetActiveUniformBlockName(glprogram, i, NAME_LENGTH, &length, nameBuffer); + return std::string(nameBuffer); + } + + UniformBlockInfo(GLuint glprogram, GLuint i) : index(i), name(getName(glprogram, i)) { + glGetActiveUniformBlockiv(glprogram, index, GL_UNIFORM_BLOCK_BINDING, &binding); + glGetActiveUniformBlockiv(glprogram, index, GL_UNIFORM_BLOCK_DATA_SIZE, &size); + } + }; + + UniformBlockInfo::Vector uniformBlocks; + uniformBlocks.reserve(buffersCount); + for (int i = 0; i < buffersCount; i++) { + uniformBlocks.push_back(UniformBlockInfo(glprogram, i)); + } + + for (auto& info : uniformBlocks) { + auto requestedBinding = slotBindings.find(info.name); + if (requestedBinding != slotBindings.end()) { + info.binding = (*requestedBinding)._location; + glUniformBlockBinding(glprogram, info.index, info.binding); + uniformBufferSlotMap[info.binding] = info.index; + } + } + + for (auto& info : uniformBlocks) { + if (slotBindings.count(info.name)) { + continue; + } + + // If the binding is 0, or the binding maps to an already used binding + if (info.binding == 0 || uniformBufferSlotMap[info.binding] != UNUSED_SLOT) { + // If no binding was assigned then just do it finding a free slot + auto slotIt = std::find_if(uniformBufferSlotMap.begin(), uniformBufferSlotMap.end(), isUnusedSlot); + if (slotIt != uniformBufferSlotMap.end()) { + info.binding = slotIt - uniformBufferSlotMap.begin(); + glUniformBlockBinding(glprogram, info.index, info.binding); + } else { + // This should neve happen, an active ubo cannot find an available slot among the max available?! + info.binding = -1; + } + } + + uniformBufferSlotMap[info.binding] = info.index; + } + + for (auto& info : uniformBlocks) { + static const Element element(SCALAR, gpu::UINT32, gpu::UNIFORM_BUFFER); + buffers.insert(Shader::Slot(info.name, info.binding, element, Resource::BUFFER, info.size)); + } + return buffersCount; +} +//CLIMAX_MERGE_START +//This has been copied over from gl45backendshader.cpp +int makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) { + GLint buffersCount = 0; + glGetProgramInterfaceiv(glprogram, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES, &buffersCount); + + // fast exit + if (buffersCount == 0) { + return 0; + } + + GLint maxNumResourceBufferSlots = 0; + glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &maxNumResourceBufferSlots); + std::vector resourceBufferSlotMap(maxNumResourceBufferSlots, -1); + + struct ResourceBlockInfo { + using Vector = std::vector; + const GLuint index{ 0 }; + const std::string name; + GLint binding{ -1 }; + GLint size{ 0 }; + + static std::string getName(GLuint glprogram, GLuint i) { + static const GLint NAME_LENGTH = 256; + GLint length = 0; + GLchar nameBuffer[NAME_LENGTH]; + glGetProgramResourceName(glprogram, GL_SHADER_STORAGE_BLOCK, i, NAME_LENGTH, &length, nameBuffer); + return std::string(nameBuffer); + } + + ResourceBlockInfo(GLuint glprogram, GLuint i) : index(i), name(getName(glprogram, i)) { + GLenum props[2] = { GL_BUFFER_BINDING, GL_BUFFER_DATA_SIZE}; + glGetProgramResourceiv(glprogram, GL_SHADER_STORAGE_BLOCK, i, 2, props, 2, nullptr, &binding); + } + }; + + ResourceBlockInfo::Vector resourceBlocks; + resourceBlocks.reserve(buffersCount); + for (int i = 0; i < buffersCount; i++) { + resourceBlocks.push_back(ResourceBlockInfo(glprogram, i)); + } + + for (auto& info : resourceBlocks) { + auto requestedBinding = slotBindings.find(info.name); + if (requestedBinding != slotBindings.end()) { + info.binding = (*requestedBinding)._location; + glUniformBlockBinding(glprogram, info.index, info.binding); + resourceBufferSlotMap[info.binding] = info.index; + } + } + + for (auto& info : resourceBlocks) { + if (slotBindings.count(info.name)) { + continue; + } + + // If the binding is -1, or the binding maps to an already used binding + if (info.binding == -1 || !isUnusedSlot(resourceBufferSlotMap[info.binding])) { + // If no binding was assigned then just do it finding a free slot + auto slotIt = std::find_if(resourceBufferSlotMap.begin(), resourceBufferSlotMap.end(), isUnusedSlot); + if (slotIt != resourceBufferSlotMap.end()) { + info.binding = slotIt - resourceBufferSlotMap.begin(); + glUniformBlockBinding(glprogram, info.index, info.binding); + } else { + // This should never happen, an active ssbo cannot find an available slot among the max available?! + info.binding = -1; + } + } + + resourceBufferSlotMap[info.binding] = info.index; + } + + for (auto& info : resourceBlocks) { + static const Element element(SCALAR, gpu::UINT32, gpu::RESOURCE_BUFFER); + resourceBuffers.insert(Shader::Slot(info.name, info.binding, element, Resource::BUFFER, info.size)); + } + return buffersCount; +} +//CLIMAX_MERGE_END + +int makeInputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs) { + GLint inputsCount = 0; + + glGetProgramiv(glprogram, GL_ACTIVE_ATTRIBUTES, &inputsCount); + + for (int i = 0; i < inputsCount; i++) { + const GLint NAME_LENGTH = 256; + GLchar name[NAME_LENGTH]; + GLint length = 0; + GLint size = 0; + GLenum type = 0; + glGetActiveAttrib(glprogram, i, NAME_LENGTH, &length, &size, &type, name); + + GLint binding = glGetAttribLocation(glprogram, name); + + auto elementResource = getFormatFromGLUniform(type); + inputs.insert(Shader::Slot(name, binding, elementResource._element, -1)); + } + + return inputsCount; +} + +int makeOutputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs) { + /* GLint outputsCount = 0; + + glGetProgramiv(glprogram, GL_ACTIVE_, &outputsCount); + + for (int i = 0; i < inputsCount; i++) { + const GLint NAME_LENGTH = 256; + GLchar name[NAME_LENGTH]; + GLint length = 0; + GLint size = 0; + GLenum type = 0; + glGetActiveAttrib(glprogram, i, NAME_LENGTH, &length, &size, &type, name); + + auto element = getFormatFromGLUniform(type); + outputs.insert(Shader::Slot(name, i, element)); + } + */ + return 0; //inputsCount; +} + +void makeProgramBindings(ShaderObject& shaderObject) { + if (!shaderObject.glprogram) { + return; + } + GLuint glprogram = shaderObject.glprogram; + GLint loc = -1; + + //Check for gpu specific attribute slotBindings + loc = glGetAttribLocation(glprogram, "inPosition"); + if (loc >= 0 && loc != gpu::Stream::POSITION) { + glBindAttribLocation(glprogram, gpu::Stream::POSITION, "inPosition"); + } + + loc = glGetAttribLocation(glprogram, "inNormal"); + if (loc >= 0 && loc != gpu::Stream::NORMAL) { + glBindAttribLocation(glprogram, gpu::Stream::NORMAL, "inNormal"); + } + + loc = glGetAttribLocation(glprogram, "inColor"); + if (loc >= 0 && loc != gpu::Stream::COLOR) { + glBindAttribLocation(glprogram, gpu::Stream::COLOR, "inColor"); + } + + loc = glGetAttribLocation(glprogram, "inTexCoord0"); + if (loc >= 0 && loc != gpu::Stream::TEXCOORD) { + glBindAttribLocation(glprogram, gpu::Stream::TEXCOORD, "inTexCoord0"); + } + + loc = glGetAttribLocation(glprogram, "inTangent"); + if (loc >= 0 && loc != gpu::Stream::TANGENT) { + glBindAttribLocation(glprogram, gpu::Stream::TANGENT, "inTangent"); + } + + loc = glGetAttribLocation(glprogram, "inTexCoord1"); + if (loc >= 0 && loc != gpu::Stream::TEXCOORD1) { + glBindAttribLocation(glprogram, gpu::Stream::TEXCOORD1, "inTexCoord1"); + } + + loc = glGetAttribLocation(glprogram, "inSkinClusterIndex"); + if (loc >= 0 && loc != gpu::Stream::SKIN_CLUSTER_INDEX) { + glBindAttribLocation(glprogram, gpu::Stream::SKIN_CLUSTER_INDEX, "inSkinClusterIndex"); + } + + loc = glGetAttribLocation(glprogram, "inSkinClusterWeight"); + if (loc >= 0 && loc != gpu::Stream::SKIN_CLUSTER_WEIGHT) { + glBindAttribLocation(glprogram, gpu::Stream::SKIN_CLUSTER_WEIGHT, "inSkinClusterWeight"); + } + + loc = glGetAttribLocation(glprogram, "_drawCallInfo"); + if (loc >= 0 && loc != gpu::Stream::DRAW_CALL_INFO) { + glBindAttribLocation(glprogram, gpu::Stream::DRAW_CALL_INFO, "_drawCallInfo"); + } + + // Link again to take into account the assigned attrib location + glLinkProgram(glprogram); + + GLint linked = 0; + glGetProgramiv(glprogram, GL_LINK_STATUS, &linked); + if (!linked) { + qCWarning(gpugllogging) << "GLShader::makeBindings - failed to link after assigning slotBindings?"; + } + + // now assign the ubo binding, then DON't relink! + + //Check for gpu specific uniform slotBindings + loc = glGetProgramResourceIndex(glprogram, GL_SHADER_STORAGE_BLOCK, "transformObjectBuffer"); + if (loc >= 0) { + // FIXME GLES + // glShaderStorageBlockBinding(glprogram, loc, TRANSFORM_OBJECT_SLOT); + shaderObject.transformObjectSlot = TRANSFORM_OBJECT_SLOT; + } + + loc = glGetUniformBlockIndex(glprogram, "transformCameraBuffer"); + if (loc >= 0) { + glUniformBlockBinding(glprogram, loc, TRANSFORM_CAMERA_SLOT); + shaderObject.transformCameraSlot = TRANSFORM_CAMERA_SLOT; + } + + (void)CHECK_GL_ERROR(); +} + +void serverWait() { + auto fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + assert(fence); + glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(fence); +} + +void clientWait() { + auto fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + assert(fence); + auto result = glClientWaitSync(fence, GL_SYNC_FLUSH_COMMANDS_BIT, 0); + while (GL_TIMEOUT_EXPIRED == result || GL_WAIT_FAILED == result) { + // Minimum sleep + QThread::usleep(1); + result = glClientWaitSync(fence, 0, 0); + } + glDeleteSync(fence); +} + +} } + + +using namespace gpu; + + diff --git a/libraries/gpu-gles/src/gpu/gl/GLShared.h b/libraries/gpu-gles/src/gpu/gl/GLShared.h new file mode 100644 index 0000000000..54209b106d --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLShared.h @@ -0,0 +1,167 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 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_gpu_GLShared_h +#define hifi_gpu_GLShared_h + +#include +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(gpugllogging) +Q_DECLARE_LOGGING_CATEGORY(trace_render_gpu_gl) + +namespace gpu { namespace gl { + + static const GLint TRANSFORM_OBJECT_SLOT { 14 }; // SSBO binding slot + +// Create a fence and inject a GPU wait on the fence +void serverWait(); + +// Create a fence and synchronously wait on the fence +void clientWait(); + +gpu::Size getDedicatedMemory(); +gpu::Size getFreeDedicatedMemory(); +ComparisonFunction comparisonFuncFromGL(GLenum func); +State::StencilOp stencilOpFromGL(GLenum stencilOp); +State::BlendOp blendOpFromGL(GLenum blendOp); +State::BlendArg blendArgFromGL(GLenum blendArg); +void getCurrentGLState(State::Data& state); + +struct ShaderObject { + GLuint glshader { 0 }; + GLuint glprogram { 0 }; + GLint transformCameraSlot { -1 }; + GLint transformObjectSlot { -1 }; +}; + +int makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, + Shader::SlotSet& uniforms, Shader::SlotSet& textures, Shader::SlotSet& samplers); +int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers); +int makeInputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs); +int makeOutputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs); +//CLIMAX_MERGE_START +//makeResourceBufferSlots has been added to glbacked as a virtual function and is being used in gl42 and gl45 overrides. +//Since these files dont exist in the andoid version create a stub here. +int makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& resourceBuffers); +//CLIMAX_MERGE_END +void makeProgramBindings(ShaderObject& shaderObject); + +enum GLSyncState { + // The object is currently undergoing no processing, although it's content + // may be out of date, or it's storage may be invalid relative to the + // owning GPU object + Idle, + // The object has been queued for transfer to the GPU + Pending, + // The object has been transferred to the GPU, but is awaiting + // any post transfer operations that may need to occur on the + // primary rendering thread + Transferred, +}; + +static const GLenum BLEND_OPS_TO_GL[State::NUM_BLEND_OPS] = { + GL_FUNC_ADD, + GL_FUNC_SUBTRACT, + GL_FUNC_REVERSE_SUBTRACT, + GL_MIN, + GL_MAX +}; + +static const GLenum BLEND_ARGS_TO_GL[State::NUM_BLEND_ARGS] = { + GL_ZERO, + GL_ONE, + GL_SRC_COLOR, + GL_ONE_MINUS_SRC_COLOR, + GL_SRC_ALPHA, + GL_ONE_MINUS_SRC_ALPHA, + GL_DST_ALPHA, + GL_ONE_MINUS_DST_ALPHA, + GL_DST_COLOR, + GL_ONE_MINUS_DST_COLOR, + GL_SRC_ALPHA_SATURATE, + GL_CONSTANT_COLOR, + GL_ONE_MINUS_CONSTANT_COLOR, + GL_CONSTANT_ALPHA, + GL_ONE_MINUS_CONSTANT_ALPHA, +}; + +static const GLenum COMPARISON_TO_GL[gpu::NUM_COMPARISON_FUNCS] = { + GL_NEVER, + GL_LESS, + GL_EQUAL, + GL_LEQUAL, + GL_GREATER, + GL_NOTEQUAL, + GL_GEQUAL, + GL_ALWAYS +}; + +static const GLenum PRIMITIVE_TO_GL[gpu::NUM_PRIMITIVES] = { + GL_POINTS, + GL_LINES, + GL_LINE_STRIP, + GL_TRIANGLES, + GL_TRIANGLE_STRIP, + GL_TRIANGLE_FAN, +}; + +static const GLenum ELEMENT_TYPE_TO_GL[gpu::NUM_TYPES] = { + GL_FLOAT, + GL_INT, + GL_UNSIGNED_INT, + GL_HALF_FLOAT, + GL_SHORT, + GL_UNSIGNED_SHORT, + GL_BYTE, + GL_UNSIGNED_BYTE, + // Normalized values + GL_INT, + GL_UNSIGNED_INT, + GL_SHORT, + GL_UNSIGNED_SHORT, + GL_BYTE, + GL_UNSIGNED_BYTE +}; + +bool checkGLError(const char* name = nullptr); +bool checkGLErrorDebug(const char* name = nullptr); + +class GLBackend; + +template +struct GLObject : public GPUObject { +public: + GLObject(const std::weak_ptr& backend, const GPUType& gpuObject, GLuint id) : _gpuObject(gpuObject), _id(id), _backend(backend) {} + + virtual ~GLObject() { } + + const GPUType& _gpuObject; + const GLuint _id; +protected: + const std::weak_ptr _backend; +}; + +class GlBuffer; +class GLFramebuffer; +class GLPipeline; +class GLQuery; +class GLState; +class GLShader; +class GLTexture; + +} } // namespace gpu::gl + +#define CHECK_GL_ERROR() gpu::gl::checkGLErrorDebug(__FUNCTION__) + +#endif + + + diff --git a/libraries/gpu-gles/src/gpu/gl/GLState.cpp b/libraries/gpu-gles/src/gpu/gl/GLState.cpp new file mode 100644 index 0000000000..b6d917b928 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLState.cpp @@ -0,0 +1,248 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 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 +// + +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#if __GNUC__ >= 5 && __GNUC_MINOR__ >= 1 +#pragma GCC diagnostic ignored "-Wsuggest-override" +#endif +#endif + + +#include "GLState.h" + +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#endif + + +#include "GLBackend.h" + +using namespace gpu; +using namespace gpu::gl; + +typedef GLState::Command Command; +typedef GLState::CommandPointer CommandPointer; +typedef GLState::Command1 Command1U; +typedef GLState::Command1 Command1I; +typedef GLState::Command1 Command1B; +typedef GLState::Command1 CommandDepthBias; +typedef GLState::Command1 CommandDepthTest; +typedef GLState::Command3 CommandStencil; +typedef GLState::Command1 CommandBlend; + +const GLState::Commands makeResetStateCommands(); + +// NOTE: This must stay in sync with the ordering of the State::Field enum +const GLState::Commands makeResetStateCommands() { + // Since State::DEFAULT is a static defined in another .cpp the initialisation order is random + // and we have a 50/50 chance that State::DEFAULT is not yet initialized. + // Since State::DEFAULT = State::Data() it is much easier to not use the actual State::DEFAULT + // but another State::Data object with a default initialization. + const State::Data DEFAULT = State::Data(); + + auto depthBiasCommand = std::make_shared(&GLBackend::do_setStateDepthBias, + Vec2(DEFAULT.depthBias, DEFAULT.depthBiasSlopeScale)); + auto stencilCommand = std::make_shared(&GLBackend::do_setStateStencil, DEFAULT.stencilActivation, + DEFAULT.stencilTestFront, DEFAULT.stencilTestBack); + + // The state commands to reset to default, + // WARNING depending on the order of the State::Field enum + return { + std::make_shared(&GLBackend::do_setStateFillMode, DEFAULT.fillMode), + std::make_shared(&GLBackend::do_setStateCullMode, DEFAULT.cullMode), + std::make_shared(&GLBackend::do_setStateFrontFaceClockwise, DEFAULT.frontFaceClockwise), + std::make_shared(&GLBackend::do_setStateDepthClampEnable, DEFAULT.depthClampEnable), + std::make_shared(&GLBackend::do_setStateScissorEnable, DEFAULT.scissorEnable), + std::make_shared(&GLBackend::do_setStateMultisampleEnable, DEFAULT.multisampleEnable), + std::make_shared(&GLBackend::do_setStateAntialiasedLineEnable, DEFAULT.antialisedLineEnable), + + // Depth bias has 2 fields in State but really one call in GLBackend + CommandPointer(depthBiasCommand), + CommandPointer(depthBiasCommand), + + std::make_shared(&GLBackend::do_setStateDepthTest, DEFAULT.depthTest), + + // Depth bias has 3 fields in State but really one call in GLBackend + CommandPointer(stencilCommand), + CommandPointer(stencilCommand), + CommandPointer(stencilCommand), + + std::make_shared(&GLBackend::do_setStateSampleMask, DEFAULT.sampleMask), + + std::make_shared(&GLBackend::do_setStateAlphaToCoverageEnable, DEFAULT.alphaToCoverageEnable), + + std::make_shared(&GLBackend::do_setStateBlend, DEFAULT.blendFunction), + + std::make_shared(&GLBackend::do_setStateColorWriteMask, DEFAULT.colorWriteMask) + }; +} + +const GLState::Commands GLState::_resetStateCommands = makeResetStateCommands(); + + +void generateFillMode(GLState::Commands& commands, State::FillMode fillMode) { + commands.push_back(std::make_shared(&GLBackend::do_setStateFillMode, int32(fillMode))); +} + +void generateCullMode(GLState::Commands& commands, State::CullMode cullMode) { + commands.push_back(std::make_shared(&GLBackend::do_setStateCullMode, int32(cullMode))); +} + +void generateFrontFaceClockwise(GLState::Commands& commands, bool isClockwise) { + commands.push_back(std::make_shared(&GLBackend::do_setStateFrontFaceClockwise, isClockwise)); +} + +void generateDepthClampEnable(GLState::Commands& commands, bool enable) { + commands.push_back(std::make_shared(&GLBackend::do_setStateDepthClampEnable, enable)); +} + +void generateScissorEnable(GLState::Commands& commands, bool enable) { + commands.push_back(std::make_shared(&GLBackend::do_setStateScissorEnable, enable)); +} + +void generateMultisampleEnable(GLState::Commands& commands, bool enable) { + commands.push_back(std::make_shared(&GLBackend::do_setStateMultisampleEnable, enable)); +} + +void generateAntialiasedLineEnable(GLState::Commands& commands, bool enable) { + commands.push_back(std::make_shared(&GLBackend::do_setStateAntialiasedLineEnable, enable)); +} + +void generateDepthBias(GLState::Commands& commands, const State& state) { + commands.push_back(std::make_shared(&GLBackend::do_setStateDepthBias, Vec2(state.getDepthBias(), state.getDepthBiasSlopeScale()))); +} + +void generateDepthTest(GLState::Commands& commands, const State::DepthTest& test) { + commands.push_back(std::make_shared(&GLBackend::do_setStateDepthTest, int32(test.getRaw()))); +} + +void generateStencil(GLState::Commands& commands, const State& state) { + commands.push_back(std::make_shared(&GLBackend::do_setStateStencil, state.getStencilActivation(), state.getStencilTestFront(), state.getStencilTestBack())); +} + +void generateAlphaToCoverageEnable(GLState::Commands& commands, bool enable) { + commands.push_back(std::make_shared(&GLBackend::do_setStateAlphaToCoverageEnable, enable)); +} + +void generateSampleMask(GLState::Commands& commands, uint32 mask) { + commands.push_back(std::make_shared(&GLBackend::do_setStateSampleMask, mask)); +} + +void generateBlend(GLState::Commands& commands, const State& state) { + commands.push_back(std::make_shared(&GLBackend::do_setStateBlend, state.getBlendFunction())); +} + +void generateColorWriteMask(GLState::Commands& commands, uint32 mask) { + commands.push_back(std::make_shared(&GLBackend::do_setStateColorWriteMask, mask)); +} + +GLState* GLState::sync(const State& state) { + GLState* object = Backend::getGPUObject(state); + + // If GPU object already created then good + if (object) { + return object; + } + + // Else allocate and create the GLState + if (!object) { + object = new GLState(); + Backend::setGPUObject(state, object); + } + + // here, we need to regenerate something so let's do it all + object->_commands.clear(); + object->_stamp = state.getStamp(); + object->_signature = state.getSignature(); + + bool depthBias = false; + bool stencilState = false; + + // go thorugh the list of state fields in the State and record the corresponding gl command + for (int i = 0; i < State::NUM_FIELDS; i++) { + if (state.getSignature()[i]) { + switch (i) { + case State::FILL_MODE: { + generateFillMode(object->_commands, state.getFillMode()); + break; + } + case State::CULL_MODE: { + generateCullMode(object->_commands, state.getCullMode()); + break; + } + case State::DEPTH_BIAS: + case State::DEPTH_BIAS_SLOPE_SCALE: { + depthBias = true; + break; + } + case State::FRONT_FACE_CLOCKWISE: { + generateFrontFaceClockwise(object->_commands, state.isFrontFaceClockwise()); + break; + } + case State::DEPTH_CLAMP_ENABLE: { + generateDepthClampEnable(object->_commands, state.isDepthClampEnable()); + break; + } + case State::SCISSOR_ENABLE: { + generateScissorEnable(object->_commands, state.isScissorEnable()); + break; + } + case State::MULTISAMPLE_ENABLE: { + generateMultisampleEnable(object->_commands, state.isMultisampleEnable()); + break; + } + case State::ANTIALISED_LINE_ENABLE: { + generateAntialiasedLineEnable(object->_commands, state.isAntialiasedLineEnable()); + break; + } + case State::DEPTH_TEST: { + generateDepthTest(object->_commands, state.getDepthTest()); + break; + } + + case State::STENCIL_ACTIVATION: + case State::STENCIL_TEST_FRONT: + case State::STENCIL_TEST_BACK: { + stencilState = true; + break; + } + + case State::SAMPLE_MASK: { + generateSampleMask(object->_commands, state.getSampleMask()); + break; + } + case State::ALPHA_TO_COVERAGE_ENABLE: { + generateAlphaToCoverageEnable(object->_commands, state.isAlphaToCoverageEnabled()); + break; + } + + case State::BLEND_FUNCTION: { + generateBlend(object->_commands, state); + break; + } + + case State::COLOR_WRITE_MASK: { + generateColorWriteMask(object->_commands, state.getColorWriteMask()); + break; + } + } + } + } + + if (depthBias) { + generateDepthBias(object->_commands, state); + } + + if (stencilState) { + generateStencil(object->_commands, state); + } + + return object; +} + diff --git a/libraries/gpu-gles/src/gpu/gl/GLState.h b/libraries/gpu-gles/src/gpu/gl/GLState.h new file mode 100644 index 0000000000..82635db893 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLState.h @@ -0,0 +1,73 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 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_gpu_gl_GLState_h +#define hifi_gpu_gl_GLState_h + +#include "GLShared.h" + +#include + +namespace gpu { namespace gl { + +class GLBackend; +class GLState : public GPUObject { +public: + static GLState* sync(const State& state); + + class Command { + public: + virtual void run(GLBackend* backend) = 0; + Command() {} + virtual ~Command() {}; + }; + + template class Command1 : public Command { + public: + typedef void (GLBackend::*GLFunction)(T); + void run(GLBackend* backend) { (backend->*(_func))(_param); } + Command1(GLFunction func, T param) : _func(func), _param(param) {}; + GLFunction _func; + T _param; + }; + template class Command2 : public Command { + public: + typedef void (GLBackend::*GLFunction)(T, U); + void run(GLBackend* backend) { (backend->*(_func))(_param0, _param1); } + Command2(GLFunction func, T param0, U param1) : _func(func), _param0(param0), _param1(param1) {}; + GLFunction _func; + T _param0; + U _param1; + }; + + template class Command3 : public Command { + public: + typedef void (GLBackend::*GLFunction)(T, U, V); + void run(GLBackend* backend) { (backend->*(_func))(_param0, _param1, _param2); } + Command3(GLFunction func, T param0, U param1, V param2) : _func(func), _param0(param0), _param1(param1), _param2(param2) {}; + GLFunction _func; + T _param0; + U _param1; + V _param2; + }; + + typedef std::shared_ptr< Command > CommandPointer; + typedef std::vector< CommandPointer > Commands; + + Commands _commands; + Stamp _stamp; + State::Signature _signature; + + // The state commands to reset to default, + static const Commands _resetStateCommands; + + friend class GLBackend; +}; + +} } + +#endif diff --git a/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.cpp b/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.cpp new file mode 100644 index 0000000000..6eec4b5292 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.cpp @@ -0,0 +1,648 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 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 "GLTexelFormat.h" + +using namespace gpu; +using namespace gpu::gl; + + +GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { +// qDebug() << "GLTexelFormat::evalGLTexelFormatInternal " << dstFormat.getDimension() << ", " << dstFormat.getSemantic() << ", " << dstFormat.getType(); + GLenum result = GL_RGBA8; + switch (dstFormat.getDimension()) { + case gpu::SCALAR: { + switch (dstFormat.getSemantic()) { + case gpu::RGB: + case gpu::RGBA: + case gpu::SRGB: + case gpu::SRGBA: + switch (dstFormat.getType()) { + case gpu::UINT32: + result = GL_R32UI; + break; + case gpu::INT32: + result = GL_R32I; + break; + case gpu::NUINT32: + result = GL_R8; + break; + case gpu::NINT32: + result = GL_R8_SNORM; + break; + case gpu::FLOAT: + result = GL_R32F; + break; + case gpu::UINT16: + result = GL_R16UI; + break; + case gpu::INT16: + result = GL_R16I; + break; + case gpu::HALF: + result = GL_R16F; + break; + case gpu::UINT8: + result = GL_R8UI; + break; + case gpu::INT8: + result = GL_R8I; + break; + case gpu::NUINT8: + if ((dstFormat.getSemantic() == gpu::SRGB || dstFormat.getSemantic() == gpu::SRGBA)) { + //result = GL_SLUMINANCE8; + qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormatInternal GL_SLUMINANCE8"; + } else { + result = GL_R8; + } + break; + case gpu::NINT8: + result = GL_R8_SNORM; + break; + default: + qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormatInternal " << dstFormat.getType(); + Q_UNREACHABLE(); + break; + } + break; + case gpu::R11G11B10: + // the type should be float + result = GL_R11F_G11F_B10F; + break; + + case gpu::DEPTH: + result = GL_DEPTH_COMPONENT16; + switch (dstFormat.getType()) { + case gpu::FLOAT: + result = GL_DEPTH_COMPONENT32F; + break; + case gpu::UINT16: + case gpu::INT16: + case gpu::NUINT16: + case gpu::NINT16: + case gpu::HALF: + result = GL_DEPTH_COMPONENT16; + break; + case gpu::UINT8: + case gpu::INT8: + case gpu::NUINT8: + case gpu::NINT8: + result = GL_DEPTH_COMPONENT24; + break; + default: + Q_UNREACHABLE(); + break; + } + break; + + case gpu::DEPTH_STENCIL: + result = GL_DEPTH24_STENCIL8; + break; + + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + break; + } + + case gpu::VEC2: { + switch (dstFormat.getSemantic()) { + case gpu::RGB: + case gpu::RGBA: + result = GL_RG8; + break; + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + + break; + } + + case gpu::VEC3: { + switch (dstFormat.getSemantic()) { + case gpu::RGB: + case gpu::RGBA: + result = GL_RGB8; + break; + case gpu::SRGB: + case gpu::SRGBA: + //result = GL_SRGB8; // standard 2.2 gamma correction color + result = GL_RGB8; // standard 2.2 gamma correction color + break; + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + + break; + } + + case gpu::VEC4: { + switch (dstFormat.getSemantic()) { + case gpu::RGB: + result = GL_RGB8; + break; + case gpu::RGBA: + switch (dstFormat.getType()) { + case gpu::UINT32: + result = GL_RGBA32UI; + break; + case gpu::INT32: + result = GL_RGBA32I; + break; + case gpu::FLOAT: + result = GL_RGBA32F; + break; + case gpu::UINT16: + result = GL_RGBA16UI; + break; + case gpu::INT16: + result = GL_RGBA16I; + break; + case gpu::HALF: + result = GL_RGBA16F; + break; + case gpu::UINT8: + result = GL_RGBA8UI; + break; + case gpu::INT8: + result = GL_RGBA8I; + break; + case gpu::NUINT8: + result = GL_RGBA8; + break; + case gpu::NINT8: + result = GL_RGBA8_SNORM; + break; + case gpu::NUINT32: + case gpu::NINT32: + case gpu::NUM_TYPES: // quiet compiler + Q_UNREACHABLE(); + } + break; + case gpu::SRGB: + //result = GL_SRGB8; + result = GL_RGB8; + qDebug() << "SRGBA Here 2"; + break; + case gpu::SRGBA: + result = GL_SRGB8_ALPHA8; // standard 2.2 gamma correction color + break; + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + break; + } + + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + + //qDebug() << "GLTexelFormat::evalGLTexelFormatInternal result " << result; + + return result; +} + +GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const Element& srcFormat) { +// qDebug() << "GLTexelFormat::evalGLTexelFormat dst.getDimension=" << dstFormat.getDimension() << " dst.getType=" << dstFormat.getType() << " dst.getSemantic=" << dstFormat.getSemantic(); +// qDebug() << "GLTexelFormat::evalGLTexelFormat src.getDimension=" << srcFormat.getDimension() << " src.getType=" << srcFormat.getType() << " src.getSemantic=" << srcFormat.getSemantic(); + + if (dstFormat != srcFormat) { + GLTexelFormat texel = { GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE }; + + switch (dstFormat.getDimension()) { + case gpu::SCALAR: { + texel.format = GL_RED; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; + + switch (dstFormat.getSemantic()) { + case gpu::RGB: + case gpu::RGBA: + texel.internalFormat = GL_R8; + break; + + //CLIMAX_MERGE_START + // case gpu::COMPRESSED_R: + // qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_COMPRESSED_RED_RGTC1"; + // //texel.internalFormat = GL_COMPRESSED_RED_RGTC1; + // break; + //CLIMAX_MERGE_END + + case gpu::DEPTH: + texel.internalFormat = GL_DEPTH_COMPONENT32_OES; + break; + case gpu::DEPTH_STENCIL: + texel.type = GL_UNSIGNED_INT_24_8; + texel.format = GL_DEPTH_STENCIL; + texel.internalFormat = GL_DEPTH24_STENCIL8; + break; + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + break; + } + + case gpu::VEC2: { + texel.format = GL_RG; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; + + switch (dstFormat.getSemantic()) { + case gpu::RGB: + case gpu::RGBA: + texel.internalFormat = GL_RG8; + break; + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + + break; + } + + case gpu::VEC3: { + texel.format = GL_RGB; + + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; + + switch (dstFormat.getSemantic()) { + case gpu::RGB: + case gpu::RGBA: + texel.internalFormat = GL_RGB8; + break; + //CLIMAX_MERGE_START + //not needed? + // case gpu::COMPRESSED_RGB: + // qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_COMPRESSED_RGB"; + // //texel.internalFormat = GL_COMPRESSED_RGB; + // break; + // case gpu::COMPRESSED_SRGB: + // qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_COMPRESSED_SRGB"; + // //texel.internalFormat = GL_COMPRESSED_SRGB; + // break; + //CLIMAX_MERGE_END + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + + break; + } + + case gpu::VEC4: { + texel.format = GL_RGBA; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; + + switch (srcFormat.getSemantic()) { + case gpu::BGRA: + case gpu::SBGRA: + qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_BGRA"; + //texel.format = GL_BGRA; + break; + case gpu::RGB: + case gpu::RGBA: + case gpu::SRGB: + case gpu::SRGBA: + default: + break; + }; + + switch (dstFormat.getSemantic()) { + case gpu::RGB: + texel.internalFormat = GL_RGB8; + break; + case gpu::RGBA: + texel.internalFormat = GL_RGBA8; + break; + case gpu::SRGB: + //texel.internalFormat = GL_SRGB8; + texel.internalFormat = GL_RGB8; + qDebug() << "SRGBA Here 3"; + break; + case gpu::SRGBA: + texel.internalFormat = GL_SRGB8_ALPHA8; + break; + + //CLIMAX_MERGE_START + // case gpu::COMPRESSED_RGBA: + // //texel.internalFormat = GL_COMPRESSED_RGBA; + // qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_COMPRESSED_RGBA"; + // break; + // case gpu::COMPRESSED_SRGBA: + // //texel.internalFormat = GL_COMPRESSED_SRGB_ALPHA; + // qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_COMPRESSED_SRGB_ALPHA"; + // break; + //CLIMAX_MERGE_END + // FIXME: WE will want to support this later + /* + case gpu::COMPRESSED_BC3_RGBA: + texel.internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; + break; + case gpu::COMPRESSED_BC3_SRGBA: + texel.internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT; + break; + + case gpu::COMPRESSED_BC7_RGBA: + texel.internalFormat = GL_COMPRESSED_RGBA_BPTC_UNORM_ARB; + break; + case gpu::COMPRESSED_BC7_SRGBA: + texel.internalFormat = GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM; + break; + */ + + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + break; + } + + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + return texel; + } else { + GLTexelFormat texel = { GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE }; + + switch (dstFormat.getDimension()) { + case gpu::SCALAR: { + texel.format = GL_RED; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; + + switch (dstFormat.getSemantic()) { + //CLIMAX_MERGE_START + // case gpu::COMPRESSED_R: { + // qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_COMPRESSED_RED_RGTC1"; + // //texel.internalFormat = GL_COMPRESSED_RED_RGTC1; + // break; + // } + case gpu::RGB: + case gpu::RGBA: + case gpu::SRGB: + case gpu::SRGBA: + texel.internalFormat = GL_RED; + switch (dstFormat.getType()) { + case gpu::UINT32: { + texel.internalFormat = GL_R32UI; + break; + } + case gpu::INT32: { + texel.internalFormat = GL_R32I; + break; + } + case gpu::NUINT32: { + texel.internalFormat = GL_R8; + break; + } + case gpu::NINT32: { + texel.internalFormat = GL_R8_SNORM; + break; + } + case gpu::FLOAT: { + texel.internalFormat = GL_R32F; + break; + } + case gpu::UINT16: { + texel.internalFormat = GL_R16UI; + break; + } + case gpu::INT16: { + texel.internalFormat = GL_R16I; + break; + } + case gpu::NUINT16: { + //texel.internalFormat = GL_R16; + qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_R16"; + break; + } + case gpu::NINT16: { + //texel.internalFormat = GL_R16_SNORM; + qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_R16_SNORM"; + break; + } + case gpu::HALF: { + texel.internalFormat = GL_R16F; + break; + } + case gpu::UINT8: { + texel.internalFormat = GL_R8UI; + break; + } + case gpu::INT8: { + texel.internalFormat = GL_R8I; + break; + } + case gpu::NUINT8: { + if ((dstFormat.getSemantic() == gpu::SRGB || dstFormat.getSemantic() == gpu::SRGBA)) { +// texel.internalFormat = GL_SLUMINANCE8; + qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_SLUMINANCE8"; + + } else { + texel.internalFormat = GL_R8; + } + break; + } + case gpu::NINT8: { + texel.internalFormat = GL_R8_SNORM; + break; + } + case gpu::NUM_TYPES: { // quiet compiler + Q_UNREACHABLE(); + } + + } + break; + + case gpu::R11G11B10: + texel.format = GL_RGB; + // the type should be float + texel.internalFormat = GL_R11F_G11F_B10F; + break; + + case gpu::DEPTH: + texel.format = GL_DEPTH_COMPONENT; // It's depth component to load it + texel.internalFormat = GL_DEPTH_COMPONENT32_OES; + switch (dstFormat.getType()) { + case gpu::UINT32: + case gpu::INT32: + case gpu::NUINT32: + case gpu::NINT32: { + texel.internalFormat = GL_DEPTH_COMPONENT32_OES; + break; + } + case gpu::FLOAT: { + texel.internalFormat = GL_DEPTH_COMPONENT32F; + break; + } + case gpu::UINT16: + case gpu::INT16: + case gpu::NUINT16: + case gpu::NINT16: + case gpu::HALF: { + texel.internalFormat = GL_DEPTH_COMPONENT16; + break; + } + case gpu::UINT8: + case gpu::INT8: + case gpu::NUINT8: + case gpu::NINT8: { + texel.internalFormat = GL_DEPTH_COMPONENT24; + break; + } + case gpu::NUM_TYPES: { // quiet compiler + Q_UNREACHABLE(); + } + } + break; + case gpu::DEPTH_STENCIL: + texel.type = GL_UNSIGNED_INT_24_8; + texel.format = GL_DEPTH_STENCIL; + texel.internalFormat = GL_DEPTH24_STENCIL8; + break; + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + + break; + } + + case gpu::VEC2: { + texel.format = GL_RG; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; + + switch (dstFormat.getSemantic()) { + case gpu::RGB: + case gpu::RGBA: + texel.internalFormat = GL_RG8; + break; + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + + break; + } + + case gpu::VEC3: { + texel.format = GL_RGB; + + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; + + switch (dstFormat.getSemantic()) { + case gpu::RGB: + case gpu::RGBA: + texel.internalFormat = GL_RGB8; + break; + case gpu::SRGB: + case gpu::SRGBA: + //texel.internalFormat = GL_SRGB8; // standard 2.2 gamma correction color + texel.internalFormat = GL_RGB8; // standard 2.2 gamma correction color + break; + //CLIMAX_MERGE_START + // case gpu::COMPRESSED_RGB: + // //texel.internalFormat = GL_COMPRESSED_RGB; + // qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_COMPRESSED_RGB"; + // break; + // case gpu::COMPRESSED_SRGB: + // //texel.internalFormat = GL_COMPRESSED_SRGB; + // qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_COMPRESSED_SRGB"; + // break; + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + break; + } + + case gpu::VEC4: { + texel.format = GL_RGBA; + texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; + + switch (dstFormat.getSemantic()) { + case gpu::RGB: + texel.internalFormat = GL_RGB8; + break; + case gpu::RGBA: + texel.internalFormat = GL_RGBA8; + switch (dstFormat.getType()) { + case gpu::UINT32: + texel.format = GL_RGBA_INTEGER; + texel.internalFormat = GL_RGBA32UI; + break; + case gpu::INT32: + texel.format = GL_RGBA_INTEGER; + texel.internalFormat = GL_RGBA32I; + break; + case gpu::FLOAT: + texel.internalFormat = GL_RGBA32F; + break; + case gpu::UINT16: + texel.format = GL_RGBA_INTEGER; + texel.internalFormat = GL_RGBA16UI; + break; + case gpu::INT16: + texel.format = GL_RGBA_INTEGER; + texel.internalFormat = GL_RGBA16I; + break; + case gpu::NUINT16: + texel.format = GL_RGBA; + //texel.internalFormat = GL_RGBA16; + qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_RGBA16"; + break; + case gpu::NINT16: + texel.format = GL_RGBA; + qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_RGBA16_SNORM"; + //texel.internalFormat = GL_RGBA16_SNORM; + break; + case gpu::HALF: + texel.format = GL_RGBA; + texel.internalFormat = GL_RGBA16F; + break; + case gpu::UINT8: + texel.format = GL_RGBA_INTEGER; + texel.internalFormat = GL_RGBA8UI; + break; + case gpu::INT8: + texel.format = GL_RGBA_INTEGER; + texel.internalFormat = GL_RGBA8I; + break; + case gpu::NUINT8: + texel.format = GL_RGBA; + texel.internalFormat = GL_RGBA8; + break; + case gpu::NINT8: + texel.format = GL_RGBA; + texel.internalFormat = GL_RGBA8_SNORM; + break; + case gpu::NUINT32: + case gpu::NINT32: + case gpu::NUM_TYPES: // quiet compiler + Q_UNREACHABLE(); + } + break; + case gpu::SRGB: + //texel.internalFormat = GL_SRGB8; + texel.internalFormat = GL_RGB8; // standard 2.2 gamma correction color + break; + case gpu::SRGBA: + texel.internalFormat = GL_SRGB8_ALPHA8; // standard 2.2 gamma correction color + break; + //CLIMAX_MERGE_START + // case gpu::COMPRESSED_RGBA: + // //texel.internalFormat = GL_COMPRESSED_RGBA; + // qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_COMPRESSED_RGBA"; + // break; + // case gpu::COMPRESSED_SRGBA: + // qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_COMPRESSED_SRGB_ALPHA"; + // //texel.internalFormat = GL_COMPRESSED_SRGB_ALPHA; + // break; + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + break; + } + default: + qCDebug(gpugllogging) << "Unknown combination of texel format"; + } + //qDebug() << "GLTexelFormat::evalGLTexelFormat Texel.type " << texel.type << " - texel.format=" << texel.format << " texel.internalFormat=" << texel.internalFormat; + return texel; + } +} diff --git a/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.h b/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.h new file mode 100644 index 0000000000..94ded3dc23 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.h @@ -0,0 +1,32 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 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_gpu_gl_GLTexelFormat_h +#define hifi_gpu_gl_GLTexelFormat_h + +#include "GLShared.h" + +namespace gpu { namespace gl { + +class GLTexelFormat { +public: + GLenum internalFormat; + GLenum format; + GLenum type; + + static GLTexelFormat evalGLTexelFormat(const Element& dstFormat) { + return evalGLTexelFormat(dstFormat, dstFormat); + } + static GLenum evalGLTexelFormatInternal(const Element& dstFormat); + + static GLTexelFormat evalGLTexelFormat(const Element& dstFormat, const Element& srcFormat); +}; + +} } + + +#endif diff --git a/libraries/gpu-gles/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gles/src/gpu/gl/GLTexture.cpp new file mode 100644 index 0000000000..5f5e3a9be1 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLTexture.cpp @@ -0,0 +1,323 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 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 "GLTexture.h" + +#include + +#include "GLTextureTransfer.h" +#include "GLBackend.h" + +using namespace gpu; +using namespace gpu::gl; + +std::shared_ptr GLTexture::_textureTransferHelper; + +// FIXME placeholder for texture memory over-use +#define DEFAULT_MAX_MEMORY_MB 256 +#define MIN_FREE_GPU_MEMORY_PERCENTAGE 0.25f +#define OVER_MEMORY_PRESSURE 2.0f + +const GLenum GLTexture::CUBE_FACE_LAYOUT[6] = { + GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, + GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, + GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z +}; + +const GLenum GLTexture::WRAP_MODES[Sampler::NUM_WRAP_MODES] = { + GL_REPEAT, // WRAP_REPEAT, + GL_MIRRORED_REPEAT, // WRAP_MIRROR, + GL_CLAMP_TO_EDGE, // WRAP_CLAMP, + GL_CLAMP_TO_BORDER_EXT, // WRAP_BORDER, + + //GL_MIRROR_CLAMP_TO_EDGE_EXT // WRAP_MIRROR_ONCE, +// qDebug() << "TODO: GLTexture.cpp:WRAP_MODES GL_MIRROR_CLAMP_TO_EDGE_EXT"; +}; + +const GLFilterMode GLTexture::FILTER_MODES[Sampler::NUM_FILTERS] = { + { GL_NEAREST, GL_NEAREST }, //FILTER_MIN_MAG_POINT, + { GL_NEAREST, GL_LINEAR }, //FILTER_MIN_POINT_MAG_LINEAR, + { GL_LINEAR, GL_NEAREST }, //FILTER_MIN_LINEAR_MAG_POINT, + { GL_LINEAR, GL_LINEAR }, //FILTER_MIN_MAG_LINEAR, + + { GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST }, //FILTER_MIN_MAG_MIP_POINT, + { GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST }, //FILTER_MIN_MAG_POINT_MIP_LINEAR, + { GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR }, //FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT, + { GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR }, //FILTER_MIN_POINT_MAG_MIP_LINEAR, + { GL_LINEAR_MIPMAP_NEAREST, GL_NEAREST }, //FILTER_MIN_LINEAR_MAG_MIP_POINT, + { GL_LINEAR_MIPMAP_LINEAR, GL_NEAREST }, //FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR, + { GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR }, //FILTER_MIN_MAG_LINEAR_MIP_POINT, + { GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR }, //FILTER_MIN_MAG_MIP_LINEAR, + { GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR } //FILTER_ANISOTROPIC, +}; + +GLenum GLTexture::getGLTextureType(const Texture& texture) { + switch (texture.getType()) { + case Texture::TEX_2D: + return GL_TEXTURE_2D; + break; + + case Texture::TEX_CUBE: + return GL_TEXTURE_CUBE_MAP; + break; + + default: + qFatal("Unsupported texture type"); + } + Q_UNREACHABLE(); + return GL_TEXTURE_2D; +} + + +const std::vector& GLTexture::getFaceTargets(GLenum target) { + static std::vector cubeFaceTargets { + GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, + GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, + GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z + }; + static std::vector faceTargets { + GL_TEXTURE_2D + }; + switch (target) { + case GL_TEXTURE_2D: + return faceTargets; + case GL_TEXTURE_CUBE_MAP: + return cubeFaceTargets; + default: + Q_UNREACHABLE(); + break; + } + Q_UNREACHABLE(); + return faceTargets; +} + +// Default texture memory = GPU total memory - 2GB +#define GPU_MEMORY_RESERVE_BYTES MB_TO_BYTES(2048) +// Minimum texture memory = 1GB +#define TEXTURE_MEMORY_MIN_BYTES MB_TO_BYTES(1024) + + +float GLTexture::getMemoryPressure() { + // Check for an explicit memory limit + auto availableTextureMemory = Texture::getAllowedGPUMemoryUsage(); + + + // If no memory limit has been set, use a percentage of the total dedicated memory + if (!availableTextureMemory) { +#if 0 + auto totalMemory = getDedicatedMemory(); + if ((GPU_MEMORY_RESERVE_BYTES + TEXTURE_MEMORY_MIN_BYTES) > totalMemory) { + availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; + } else { + availableTextureMemory = totalMemory - GPU_MEMORY_RESERVE_BYTES; + } +#else + // Hardcode texture limit for sparse textures at 1 GB for now + availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; +#endif + } + + // Return the consumed texture memory divided by the available texture memory. + //CLIMAX_MERGE_START + //auto consumedGpuMemory = Context::getTextureGPUMemoryUsage() - Context::getTextureGPUFramebufferMemoryUsage(); + //float memoryPressure = (float)consumedGpuMemory / (float)availableTextureMemory; + //static Context::Size lastConsumedGpuMemory = 0; + //if (memoryPressure > 1.0f && lastConsumedGpuMemory != consumedGpuMemory) { + // lastConsumedGpuMemory = consumedGpuMemory; + // qCDebug(gpugllogging) << "Exceeded max allowed texture memory: " << consumedGpuMemory << " / " << availableTextureMemory; + //} + //return memoryPressure; + return 0; + +} + + +// Create the texture and allocate storage +GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable) : + GLObject(backend, texture, id), + _external(false), + _source(texture.source()), + _storageStamp(texture.getStamp()), + _target(getGLTextureType(texture)), + _internalFormat(gl::GLTexelFormat::evalGLTexelFormatInternal(texture.getTexelFormat())), + _maxMip(texture.getMaxMip()), + _minMip(texture.getMinMip()), + _virtualSize(texture.evalTotalSize()), + _transferrable(transferrable) +{ + //qDebug() << "GLTexture::GLTexture building GLTexture with _internalFormat" << _internalFormat; + auto strongBackend = _backend.lock(); + strongBackend->recycle(); + //CLIMAX_MERGE_START + //Backend::incrementTextureGPUCount(); + //Backend::updateTextureGPUVirtualMemoryUsage(0, _virtualSize); + //CLIMAX_MERGE_END + Backend::setGPUObject(texture, this); +} + +GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id) : + GLObject(backend, texture, id), + _external(true), + _source(texture.source()), + _storageStamp(0), + _target(getGLTextureType(texture)), + _internalFormat(GL_RGBA8), + // FIXME force mips to 0? + _maxMip(texture.getMaxMip()), + _minMip(texture.getMinMip()), + _virtualSize(0), + _transferrable(false) +{ + Backend::setGPUObject(texture, this); + + // FIXME Is this necessary? + //withPreservedTexture([this] { + // syncSampler(); + // if (_gpuObject.isAutogenerateMips()) { + // generateMips(); + // } + //}); +} + +GLTexture::~GLTexture() { + auto backend = _backend.lock(); + if (backend) { + if (_external) { + auto recycler = _gpuObject.getExternalRecycler(); + if (recycler) { + backend->releaseExternalTexture(_id, recycler); + } else { + qWarning() << "No recycler available for texture " << _id << " possible leak"; + } + } else if (_id) { + // WARNING! Sparse textures do not use this code path. See GL45BackendTexture for + // the GL45Texture destructor for doing any required work tracking GPU stats + backend->releaseTexture(_id, _size); + } + + ////CLIMAX_MERGE_START + //if (!_external && !_transferrable) { + // Backend::updateTextureGPUFramebufferMemoryUsage(_size, 0); + //} + } + //Backend::updateTextureGPUVirtualMemoryUsage(_virtualSize, 0); + //CLIMAX_MERGE_END +} + +void GLTexture::createTexture() { + withPreservedTexture([&] { + allocateStorage(); + (void)CHECK_GL_ERROR(); + syncSampler(); + (void)CHECK_GL_ERROR(); + }); +} + +void GLTexture::withPreservedTexture(std::function f) const { + GLint boundTex = -1; + switch (_target) { + case GL_TEXTURE_2D: + glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); + break; + + case GL_TEXTURE_CUBE_MAP: + glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); + break; + + default: + qFatal("Unsupported texture type"); + } + (void)CHECK_GL_ERROR(); + + glBindTexture(_target, _texture); + f(); + glBindTexture(_target, boundTex); + (void)CHECK_GL_ERROR(); +} + +void GLTexture::setSize(GLuint size) const { + ////CLIMAX_MERGE_START + //if (!_external && !_transferrable) { + // Backend::updateTextureGPUFramebufferMemoryUsage(_size, 0); + //} + //Backend::updateTextureGPUMemoryUsage(_size, size); + const_cast(_size) = size; +} + +bool GLTexture::isInvalid() const { + return _storageStamp < _gpuObject.getStamp(); +} + +bool GLTexture::isOutdated() const { + return GLSyncState::Idle == _syncState && _contentStamp < _gpuObject.getDataStamp(); +} + +bool GLTexture::isReady() const { + // If we have an invalid texture, we're never ready + if (isInvalid()) { + return false; + } + + auto syncState = _syncState.load(); + if (isOutdated() || Idle != syncState) { + return false; + } + + return true; +} + + +// Do any post-transfer operations that might be required on the main context / rendering thread +void GLTexture::postTransfer() { + //CLIMAX_MERGE_START + + // setSyncState(GLSyncState::Idle); + // ++_transferCount; + + // // At this point the mip pixels have been loaded, we can notify the gpu texture to abandon it's memory + // switch (_gpuObject.getType()) { + // case Texture::TEX_2D: + // for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + // if (_gpuObject.isStoredMipFaceAvailable(i)) { + // _gpuObject.notifyMipFaceGPULoaded(i); + // } + // } + // break; + + // case Texture::TEX_CUBE: + // // transfer pixels from each faces + // for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) { + // for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + // if (_gpuObject.isStoredMipFaceAvailable(i, f)) { + // _gpuObject.notifyMipFaceGPULoaded(i, f); + // } + // } + // } + // break; + + // default: + // qCWarning(gpugllogging) << __FUNCTION__ << " case for Texture Type " << _gpuObject.getType() << " not supported"; + // break; + // } + //CLIMAX_MERGE_END +} + +void GLTexture::initTextureTransferHelper() { + _textureTransferHelper = std::make_shared(); +} + +void GLTexture::startTransfer() { + createTexture(); +} + +void GLTexture::finishTransfer() { + if (_gpuObject.isAutogenerateMips()) { + generateMips(); + } +} + diff --git a/libraries/gpu-gles/src/gpu/gl/GLTexture.h b/libraries/gpu-gles/src/gpu/gl/GLTexture.h new file mode 100644 index 0000000000..03353ae67d --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLTexture.h @@ -0,0 +1,233 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 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_gpu_gl_GLTexture_h +#define hifi_gpu_gl_GLTexture_h + +#include "GLShared.h" +#include "GLTextureTransfer.h" +#include "GLBackend.h" +#include "GLTexelFormat.h" + +namespace gpu { namespace gl { + +struct GLFilterMode { + GLint minFilter; + GLint magFilter; +}; + + +class GLTexture : public GLObject { +public: + static const uint16_t INVALID_MIP { (uint16_t)-1 }; + static const uint8_t INVALID_FACE { (uint8_t)-1 }; + + static void initTextureTransferHelper(); + static std::shared_ptr _textureTransferHelper; + + template + static GLTexture* sync(GLBackend& backend, const TexturePointer& texturePointer, bool needTransfer) { + const Texture& texture = *texturePointer; + + // Special case external textures + //CLIMAX_MERGE_START + //Z:/HiFi_Android/HiFi_GIT/libraries/gpu-gl-android/src/gpu/gl/../gles/../gl/GLTexture.h:37:32: error: no member named 'isExternal' in 'gpu::Texture::Usage' + // The only instance of this being used again. replace. + // if (texture.getUsage().isExternal()) { + // Texture::ExternalUpdates updates = texture.getUpdates(); + // if (!updates.empty()) { + // Texture::ExternalRecycler recycler = texture.getExternalRecycler(); + // Q_ASSERT(recycler); + // // Discard any superfluous updates + // while (updates.size() > 1) { + // const auto& update = updates.front(); + // // Superfluous updates will never have been read, but we want to ensure the previous + // // writes to them are complete before they're written again, so return them with the + // // same fences they arrived with. This can happen on any thread because no GL context + // // work is involved + // recycler(update.first, update.second); + // updates.pop_front(); + // } + + // // The last texture remaining is the one we'll use to create the GLTexture + // const auto& update = updates.front(); + // // Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence + // if (update.second) { + // GLsync fence = static_cast(update.second); + // glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); + // glDeleteSync(fence); + // } + + // // Create the new texture object (replaces any previous texture object) + // new GLTextureType(backend.shared_from_this(), texture, update.first); + // } + + + // Return the texture object (if any) associated with the texture, without extensive logic + // (external textures are + //return Backend::getGPUObject(texture); + //} + //CLIMAX_MERGE_END + if (!texture.isDefined()) { + // NO texture definition yet so let's avoid thinking + return nullptr; + } + + // If the object hasn't been created, or the object definition is out of date, drop and re-create + GLTexture* object = Backend::getGPUObject(texture); + + // Create the texture if need be (force re-creation if the storage stamp changes + // for easier use of immutable storage) + if (!object || object->isInvalid()) { + // This automatically any previous texture + object = new GLTextureType(backend.shared_from_this(), texture, needTransfer); + if (!object->_transferrable) { + object->createTexture(); + object->_contentStamp = texture.getDataStamp(); + object->updateSize(); + object->postTransfer(); + } + } + + // Object maybe doens't neet to be tranasferred after creation + if (!object->_transferrable) { + return object; + } + + // If we just did a transfer, return the object after doing post-transfer work + if (GLSyncState::Transferred == object->getSyncState()) { + object->postTransfer(); + } + + if (object->isOutdated()) { + // Object might be outdated, if so, start the transfer + // (outdated objects that are already in transfer will have reported 'true' for ready() + _textureTransferHelper->transferTexture(texturePointer); + return nullptr; + } + + if (!object->isReady()) { + return nullptr; + } + + ((GLTexture*)object)->updateMips(); + + return object; + } + + template + static GLuint getId(GLBackend& backend, const TexturePointer& texture, bool shouldSync) { + if (!texture) { + return 0; + } + GLTexture* object { nullptr }; + if (shouldSync) { + object = sync(backend, texture, shouldSync); + } else { + object = Backend::getGPUObject(*texture); + } + + if (!object) { + return 0; + } + + if (!shouldSync) { + return object->_id; + } + + // Don't return textures that are in transfer state + if ((object->getSyncState() != GLSyncState::Idle) || + // Don't return transferrable textures that have never completed transfer + (!object->_transferrable || 0 != object->_transferCount)) { + return 0; + } + + return object->_id; + } + + ~GLTexture(); + + // Is this texture generated outside the GPU library? + const bool _external; + const GLuint& _texture { _id }; + const std::string _source; + const Stamp _storageStamp; + const GLenum _target; + const GLenum _internalFormat; + const uint16 _maxMip; + uint16 _minMip; + const GLuint _virtualSize; // theoretical size as expected + Stamp _contentStamp { 0 }; + const bool _transferrable; + Size _transferCount { 0 }; + GLuint size() const { return _size; } + GLSyncState getSyncState() const { return _syncState; } + + // Is the storage out of date relative to the gpu texture? + bool isInvalid() const; + + // Is the content out of date relative to the gpu texture? + bool isOutdated() const; + + // Is the texture in a state where it can be rendered with no work? + bool isReady() const; + + // Execute any post-move operations that must occur only on the main thread + virtual void postTransfer(); + + uint16 usedMipLevels() const { return (_maxMip - _minMip) + 1; } + + static const size_t CUBE_NUM_FACES = 6; + static const GLenum CUBE_FACE_LAYOUT[6]; + static const GLFilterMode FILTER_MODES[Sampler::NUM_FILTERS]; + static const GLenum WRAP_MODES[Sampler::NUM_WRAP_MODES]; + + // Return a floating point value indicating how much of the allowed + // texture memory we are currently consuming. A value of 0 indicates + // no texture memory usage, while a value of 1 indicates all available / allowed memory + // is consumed. A value above 1 indicates that there is a problem. + static float getMemoryPressure(); +protected: + + static const std::vector& getFaceTargets(GLenum textureType); + + static GLenum getGLTextureType(const Texture& texture); + + + const GLuint _size { 0 }; // true size as reported by the gl api + std::atomic _syncState { GLSyncState::Idle }; + + GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable); + GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); + + void setSyncState(GLSyncState syncState) { _syncState = syncState; } + + void createTexture(); + + virtual void updateMips() {} + virtual void allocateStorage() const = 0; + virtual void updateSize() const = 0; + virtual void syncSampler() const = 0; + virtual void generateMips() const = 0; + virtual void withPreservedTexture(std::function f) const; + +protected: + void setSize(GLuint size) const; + + virtual void startTransfer(); + // Returns true if this is the last block required to complete transfer + virtual bool continueTransfer() { return false; } + virtual void finishTransfer(); + +private: + friend class GLTextureTransferHelper; + friend class GLBackend; +}; + +} } + +#endif diff --git a/libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.cpp b/libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.cpp new file mode 100644 index 0000000000..cec46cb90d --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.cpp @@ -0,0 +1,207 @@ +// +// Created by Bradley Austin Davis on 2016/04/03 +// Copyright 2013-2016 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 "GLTextureTransfer.h" + +#include +#include + +#include "GLShared.h" +#include "GLTexture.h" + +#ifdef HAVE_NSIGHT +#include "nvToolsExt.h" +std::unordered_map _map; +#endif + + +#ifdef TEXTURE_TRANSFER_PBOS +#define TEXTURE_TRANSFER_BLOCK_SIZE (64 * 1024) +#define TEXTURE_TRANSFER_PBO_COUNT 128 +#endif + +using namespace gpu; +using namespace gpu::gl; + +GLTextureTransferHelper::GLTextureTransferHelper() { +#ifdef THREADED_TEXTURE_TRANSFER + setObjectName("TextureTransferThread"); + _context.create(); + initialize(true, QThread::LowPriority); + // Clean shutdown on UNIX, otherwise _canvas is freed early + connect(qApp, &QCoreApplication::aboutToQuit, [&] { terminate(); }); +#else + initialize(false, QThread::LowPriority); +#endif +} + +GLTextureTransferHelper::~GLTextureTransferHelper() { +#ifdef THREADED_TEXTURE_TRANSFER + if (isStillRunning()) { + terminate(); + } +#else + terminate(); +#endif +} + +void GLTextureTransferHelper::transferTexture(const gpu::TexturePointer& texturePointer) { + GLTexture* object = Backend::getGPUObject(*texturePointer); + + //CLIMAX_MERGE_START + //Backend::incrementTextureGPUTransferCount(); + object->setSyncState(GLSyncState::Pending); + Lock lock(_mutex); + _pendingTextures.push_back(texturePointer); +} + +void GLTextureTransferHelper::setup() { +#ifdef THREADED_TEXTURE_TRANSFER + _context.makeCurrent(); + +#ifdef TEXTURE_TRANSFER_FORCE_DRAW + // FIXME don't use opengl 4.5 DSA functionality without verifying it's present + glCreateRenderbuffers(1, &_drawRenderbuffer); + glNamedRenderbufferStorage(_drawRenderbuffer, GL_RGBA8, 128, 128); + glCreateFramebuffers(1, &_drawFramebuffer); + glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _drawRenderbuffer); + glCreateFramebuffers(1, &_readFramebuffer); +#endif + +#ifdef TEXTURE_TRANSFER_PBOS + std::array pbos; + glCreateBuffers(TEXTURE_TRANSFER_PBO_COUNT, &pbos[0]); + for (uint32_t i = 0; i < TEXTURE_TRANSFER_PBO_COUNT; ++i) { + TextureTransferBlock newBlock; + newBlock._pbo = pbos[i]; + glNamedBufferStorage(newBlock._pbo, TEXTURE_TRANSFER_BLOCK_SIZE, 0, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); + newBlock._mapped = glMapNamedBufferRange(newBlock._pbo, 0, TEXTURE_TRANSFER_BLOCK_SIZE, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); + _readyQueue.push(newBlock); + } +#endif +#endif +} + +void GLTextureTransferHelper::shutdown() { +#ifdef THREADED_TEXTURE_TRANSFER + _context.makeCurrent(); +#endif + +#ifdef TEXTURE_TRANSFER_FORCE_DRAW + glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, 0); + glDeleteFramebuffers(1, &_drawFramebuffer); + _drawFramebuffer = 0; + glDeleteFramebuffers(1, &_readFramebuffer); + _readFramebuffer = 0; + + glNamedFramebufferTexture(_readFramebuffer, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0); + glDeleteRenderbuffers(1, &_drawRenderbuffer); + _drawRenderbuffer = 0; +#endif +} + +void GLTextureTransferHelper::queueExecution(VoidLambda lambda) { + Lock lock(_mutex); + _pendingCommands.push_back(lambda); +} + +#define MAX_TRANSFERS_PER_PASS 2 + +bool GLTextureTransferHelper::process() { + // Take any new textures or commands off the queue + VoidLambdaList pendingCommands; + TextureList newTransferTextures; + { + Lock lock(_mutex); + newTransferTextures.swap(_pendingTextures); + pendingCommands.swap(_pendingCommands); + } + + if (!pendingCommands.empty()) { + for (auto command : pendingCommands) { + command(); + } + glFlush(); + } + + if (!newTransferTextures.empty()) { + for (auto& texturePointer : newTransferTextures) { +#ifdef HAVE_NSIGHT + _map[texturePointer] = nvtxRangeStart("TextureTansfer"); +#endif + GLTexture* object = Backend::getGPUObject(*texturePointer); + object->startTransfer(); + _transferringTextures.push_back(texturePointer); + _textureIterator = _transferringTextures.begin(); + } + _transferringTextures.sort([](const gpu::TexturePointer& a, const gpu::TexturePointer& b)->bool { + return a->getSize() < b->getSize(); + }); + } + + // No transfers in progress, sleep + if (_transferringTextures.empty()) { +#ifdef THREADED_TEXTURE_TRANSFER + QThread::usleep(1); +#endif + return true; + } + + static auto lastReport = usecTimestampNow(); + auto now = usecTimestampNow(); + auto lastReportInterval = now - lastReport; + if (lastReportInterval > USECS_PER_SECOND * 4) { + lastReport = now; + qDebug() << "Texture list " << _transferringTextures.size(); + } + + size_t transferCount = 0; + for (_textureIterator = _transferringTextures.begin(); _textureIterator != _transferringTextures.end();) { + if (++transferCount > MAX_TRANSFERS_PER_PASS) { + break; + } + auto texture = *_textureIterator; + GLTexture* gltexture = Backend::getGPUObject(*texture); + if (gltexture->continueTransfer()) { + ++_textureIterator; + continue; + } + + gltexture->finishTransfer(); + +#ifdef TEXTURE_TRANSFER_FORCE_DRAW + // FIXME force a draw on the texture transfer thread before passing the texture to the main thread for use +#endif + +#ifdef THREADED_TEXTURE_TRANSFER + clientWait(); +#endif + gltexture->_contentStamp = gltexture->_gpuObject.getDataStamp(); + gltexture->updateSize(); + gltexture->setSyncState(gpu::gl::GLSyncState::Transferred); + //CLIMAX_MERGE_START + //Backend::decrementTextureGPUTransferCount(); +#ifdef HAVE_NSIGHT + // Mark the texture as transferred + nvtxRangeEnd(_map[texture]); + _map.erase(texture); +#endif + _textureIterator = _transferringTextures.erase(_textureIterator); + } + +#ifdef THREADED_TEXTURE_TRANSFER + if (!_transferringTextures.empty()) { + // Don't saturate the GPU + clientWait(); + } else { + // Don't saturate the CPU + QThread::msleep(1); + } +#endif + + return true; +} diff --git a/libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.h b/libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.h new file mode 100644 index 0000000000..a23c282fd4 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.h @@ -0,0 +1,78 @@ +// +// Created by Bradley Austin Davis on 2016/04/03 +// Copyright 2013-2016 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_gpu_gl_GLTextureTransfer_h +#define hifi_gpu_gl_GLTextureTransfer_h + +#include +#include + +#include + +#include + +#include "GLShared.h" + +#ifdef Q_OS_WIN +#define THREADED_TEXTURE_TRANSFER +#endif + +#ifdef THREADED_TEXTURE_TRANSFER +// FIXME when sparse textures are enabled, it's harder to force a draw on the transfer thread +// also, the current draw code is implicitly using OpenGL 4.5 functionality +//#define TEXTURE_TRANSFER_FORCE_DRAW +// FIXME PBO's increase the complexity and don't seem to work reliably +//#define TEXTURE_TRANSFER_PBOS +#endif + +namespace gpu { namespace gl { + +using TextureList = std::list; +using TextureListIterator = TextureList::iterator; + +class GLTextureTransferHelper : public GenericThread { +public: + using VoidLambda = std::function; + using VoidLambdaList = std::list; + using Pointer = std::shared_ptr; + GLTextureTransferHelper(); + ~GLTextureTransferHelper(); + void transferTexture(const gpu::TexturePointer& texturePointer); + void queueExecution(VoidLambda lambda); + + void setup() override; + void shutdown() override; + bool process() override; + +private: +#ifdef THREADED_TEXTURE_TRANSFER + ::gl::OffscreenContext _context; +#endif + +#ifdef TEXTURE_TRANSFER_FORCE_DRAW + // Framebuffers / renderbuffers for forcing access to the texture on the transfer thread + GLuint _drawRenderbuffer { 0 }; + GLuint _drawFramebuffer { 0 }; + GLuint _readFramebuffer { 0 }; +#endif + + // A mutex for protecting items access on the render and transfer threads + Mutex _mutex; + // Commands that have been submitted for execution on the texture transfer thread + VoidLambdaList _pendingCommands; + // Textures that have been submitted for transfer + TextureList _pendingTextures; + // Textures currently in the transfer process + // Only used on the transfer thread + TextureList _transferringTextures; + TextureListIterator _textureIterator; + +}; + +} } + +#endif \ No newline at end of file diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackend.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackend.cpp new file mode 100644 index 0000000000..8c843c1ce3 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackend.cpp @@ -0,0 +1,197 @@ +// +// Created by Gabriel Calero & Cristian Duarte on 9/27/2016. +// Copyright 2016 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 "GLESBackend.h" + +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(gpugleslogging, "hifi.gpu.gles") + +using namespace gpu; +using namespace gpu::gles; + +const std::string GLESBackend::GLES_VERSION { "GLES" }; + +void GLESBackend::do_draw(const Batch& batch, size_t paramOffset) { + Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; + uint32 numVertices = batch._params[paramOffset + 1]._uint; + uint32 startVertex = batch._params[paramOffset + 0]._uint; + + if (isStereo()) { +#ifdef GPU_STEREO_DRAWCALL_INSTANCED + glDrawArraysInstanced(mode, startVertex, numVertices, 2); +#else + + setupStereoSide(0); + glDrawArrays(mode, startVertex, numVertices); + setupStereoSide(1); + glDrawArrays(mode, startVertex, numVertices); + +#endif + _stats._DSNumTriangles += 2 * numVertices / 3; + _stats._DSNumDrawcalls += 2; + + } else { + glDrawArrays(mode, startVertex, numVertices); + _stats._DSNumTriangles += numVertices / 3; + _stats._DSNumDrawcalls++; + } + _stats._DSNumAPIDrawcalls++; + + (void) CHECK_GL_ERROR(); +} + +void GLESBackend::do_drawIndexed(const Batch& batch, size_t paramOffset) { + Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; + uint32 numIndices = batch._params[paramOffset + 1]._uint; + uint32 startIndex = batch._params[paramOffset + 0]._uint; + + GLenum glType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType]; + + auto typeByteSize = TYPE_SIZE[_input._indexBufferType]; + GLvoid* indexBufferByteOffset = reinterpret_cast(startIndex * typeByteSize + _input._indexBufferOffset); + + if (isStereo()) { +#ifdef GPU_STEREO_DRAWCALL_INSTANCED + glDrawElementsInstanced(mode, numIndices, glType, indexBufferByteOffset, 2); +#else + setupStereoSide(0); + glDrawElements(mode, numIndices, glType, indexBufferByteOffset); + setupStereoSide(1); + glDrawElements(mode, numIndices, glType, indexBufferByteOffset); +#endif + _stats._DSNumTriangles += 2 * numIndices / 3; + _stats._DSNumDrawcalls += 2; + } else { + //qDebug() << "GLESBackend::do_drawIndexed glDrawElements " << numIndices; + glDrawElements(mode, numIndices, glType, indexBufferByteOffset); + _stats._DSNumTriangles += numIndices / 3; + _stats._DSNumDrawcalls++; + } + _stats._DSNumAPIDrawcalls++; + + (void) CHECK_GL_ERROR(); +} + +void GLESBackend::do_drawInstanced(const Batch& batch, size_t paramOffset) { + GLint numInstances = batch._params[paramOffset + 4]._uint; + Primitive primitiveType = (Primitive)batch._params[paramOffset + 3]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; + uint32 numVertices = batch._params[paramOffset + 2]._uint; + uint32 startVertex = batch._params[paramOffset + 1]._uint; + + + if (isStereo()) { + GLint trueNumInstances = 2 * numInstances; + +#ifdef GPU_STEREO_DRAWCALL_INSTANCED + glDrawArraysInstanced(mode, startVertex, numVertices, trueNumInstances); +#else + setupStereoSide(0); + glDrawArraysInstanced(mode, startVertex, numVertices, numInstances); + setupStereoSide(1); + glDrawArraysInstanced(mode, startVertex, numVertices, numInstances); +#endif + _stats._DSNumTriangles += (trueNumInstances * numVertices) / 3; + _stats._DSNumDrawcalls += trueNumInstances; + } else { + //qDebug() << "GLESBackend::do_drawInstanced glDrawArraysInstancedEXT " << numVertices << "," << numInstances; + glDrawArraysInstanced(mode, startVertex, numVertices, numInstances); + _stats._DSNumTriangles += (numInstances * numVertices) / 3; + _stats._DSNumDrawcalls += numInstances; + } + _stats._DSNumAPIDrawcalls++; + + (void) CHECK_GL_ERROR(); +} + +void glbackend_glDrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices, GLsizei primcount, GLint basevertex, GLuint baseinstance) { +//#if (GPU_INPUT_PROFILE == GPU_CORE_43) + //glDrawElementsInstancedBaseVertexBaseInstance(mode, count, type, indices, primcount, basevertex, baseinstance); +//#else + glDrawElementsInstanced(mode, count, type, indices, primcount); +//#endif +} + +void GLESBackend::do_drawIndexedInstanced(const Batch& batch, size_t paramOffset) { + GLint numInstances = batch._params[paramOffset + 4]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 3]._uint]; + uint32 numIndices = batch._params[paramOffset + 2]._uint; + uint32 startIndex = batch._params[paramOffset + 1]._uint; + // FIXME glDrawElementsInstancedBaseVertexBaseInstance is only available in GL 4.3 + // and higher, so currently we ignore this field + uint32 startInstance = batch._params[paramOffset + 0]._uint; + GLenum glType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType]; + + auto typeByteSize = TYPE_SIZE[_input._indexBufferType]; + GLvoid* indexBufferByteOffset = reinterpret_cast(startIndex * typeByteSize + _input._indexBufferOffset); + + if (isStereo()) { + GLint trueNumInstances = 2 * numInstances; + +#ifdef GPU_STEREO_DRAWCALL_INSTANCED + glbackend_glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, trueNumInstances, 0, startInstance); +#else + setupStereoSide(0); + glbackend_glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); + setupStereoSide(1); + glbackend_glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); +#endif + + _stats._DSNumTriangles += (trueNumInstances * numIndices) / 3; + _stats._DSNumDrawcalls += trueNumInstances; + } else { + //qDebug() << "GLESBackend::do_drawIndexedInstanced glbackend_glDrawElementsInstancedBaseVertexBaseInstance " << numInstances; + glbackend_glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); + _stats._DSNumTriangles += (numInstances * numIndices) / 3; + _stats._DSNumDrawcalls += numInstances; + } + + _stats._DSNumAPIDrawcalls++; + + (void)CHECK_GL_ERROR(); +} + + +void GLESBackend::do_multiDrawIndirect(const Batch& batch, size_t paramOffset) { +#if (GPU_INPUT_PROFILE == GPU_CORE_43) + uint commandCount = batch._params[paramOffset + 0]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; + + //glMultiDrawArraysIndirect(mode, reinterpret_cast(_input._indirectBufferOffset), commandCount, (GLsizei)_input._indirectBufferStride); + qDebug() << "TODO: GLESBackend.cpp:do_multiDrawIndirect do_multiDrawIndirect"; + _stats._DSNumDrawcalls += commandCount; + _stats._DSNumAPIDrawcalls++; + +#else + // FIXME implement the slow path +#endif + (void)CHECK_GL_ERROR(); + +} + +void GLESBackend::do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOffset) { +//#if (GPU_INPUT_PROFILE == GPU_CORE_43) + uint commandCount = batch._params[paramOffset + 0]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; + GLenum indexType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType]; + + //glMultiDrawElementsIndirect(mode, indexType, reinterpret_cast(_input._indirectBufferOffset), commandCount, (GLsizei)_input._indirectBufferStride); + qDebug() << "TODO: GLESBackend.cpp:do_multiDrawIndexedIndirect glMultiDrawElementsIndirect"; + _stats._DSNumDrawcalls += commandCount; + _stats._DSNumAPIDrawcalls++; +//#else + // FIXME implement the slow path +//#endif + (void)CHECK_GL_ERROR(); +} diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackend.h b/libraries/gpu-gles/src/gpu/gles/GLESBackend.h new file mode 100644 index 0000000000..69a417d952 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackend.h @@ -0,0 +1,99 @@ +// +// GLESBackend.h +// libraries/gpu-gl-android/src/gpu/gles +// +// Created by Gabriel Calero & Cristian Duarte on 9/27/2016. +// Copyright 2016 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_gpu_gles_GLESBackend_h +#define hifi_gpu_gles_GLESBackend_h + +#include + +#include "../gl/GLBackend.h" +#include "../gl/GLTexture.h" + + +namespace gpu { namespace gles { + +using namespace gpu::gl; + +class GLESBackend : public GLBackend { + using Parent = GLBackend; + // Context Backend static interface required + friend class Context; + +public: + explicit GLESBackend(bool syncCache) : Parent(syncCache) {} + GLESBackend() : Parent() {} + virtual ~GLESBackend() { + // call resetStages here rather than in ~GLBackend dtor because it will call releaseResourceBuffer + // which is pure virtual from GLBackend's dtor. + resetStages(); + } + + static const std::string GLES_VERSION; + const std::string& getVersion() const override { return GLES_VERSION; } + + + class GLESTexture : public GLTexture { + using Parent = GLTexture; + GLuint allocate(); + public: + GLESTexture(const std::weak_ptr& backend, const Texture& buffer, GLuint externalId); + GLESTexture(const std::weak_ptr& backend, const Texture& buffer, bool transferrable); + + protected: + void transferMip(uint16_t mipLevel, uint8_t face) const; + void startTransfer() override; + void allocateStorage() const override; + void updateSize() const override; + void syncSampler() const override; + void generateMips() const override; + }; + + +protected: + GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; + GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; + + GLuint getBufferID(const Buffer& buffer) override; + GLBuffer* syncGPUObject(const Buffer& buffer) override; + + GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; + GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; + + GLuint getQueryID(const QueryPointer& query) override; + GLQuery* syncGPUObject(const Query& query) override; + + // Draw Stage + void do_draw(const Batch& batch, size_t paramOffset) override; + void do_drawIndexed(const Batch& batch, size_t paramOffset) override; + void do_drawInstanced(const Batch& batch, size_t paramOffset) override; + void do_drawIndexedInstanced(const Batch& batch, size_t paramOffset) override; + void do_multiDrawIndirect(const Batch& batch, size_t paramOffset) override; + void do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOffset) override; + + // Input Stage + void updateInput() override; + void resetInputStage() override; + + // Synchronize the state cache of this Backend with the actual real state of the GL Context + void transferTransformState(const Batch& batch) const override; + void initTransform() override; + void updateTransform(const Batch& batch); + void resetTransformStage(); + + // Output stage + void do_blit(const Batch& batch, size_t paramOffset) override; +}; + +} } + +Q_DECLARE_LOGGING_CATEGORY(gpugleslogging) + + +#endif \ No newline at end of file diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendBuffer.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendBuffer.cpp new file mode 100644 index 0000000000..f6bdea45af --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendBuffer.cpp @@ -0,0 +1,69 @@ +// +// Created by Gabriel Calero & Cristian Duarte on 09/27/2016 +// Copyright 2013-2016 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 "GLESBackend.h" +#include "../gl/GLBuffer.h" + +namespace gpu { + namespace gles { + + class GLESBuffer : public gpu::gl::GLBuffer { + using Parent = gpu::gl::GLBuffer; + static GLuint allocate() { + GLuint result; + glGenBuffers(1, &result); + return result; + } + + public: + GLESBuffer(const std::weak_ptr& backend, const Buffer& buffer, GLESBuffer* original) : Parent(backend, buffer, allocate()) { + glBindBuffer(GL_ARRAY_BUFFER, _buffer); + glBufferData(GL_ARRAY_BUFFER, _size, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + if (original && original->_size) { + glBindBuffer(GL_COPY_WRITE_BUFFER, _buffer); + glBindBuffer(GL_COPY_READ_BUFFER, original->_buffer); + glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, original->_size); + glBindBuffer(GL_COPY_WRITE_BUFFER, 0); + glBindBuffer(GL_COPY_READ_BUFFER, 0); + (void)CHECK_GL_ERROR(); + } + Backend::setGPUObject(buffer, this); + } + + void transfer() override { + glBindBuffer(GL_ARRAY_BUFFER, _buffer); + (void)CHECK_GL_ERROR(); + Size offset; + Size size; + Size currentPage { 0 }; + auto data = _gpuObject._renderSysmem.readData(); + while (_gpuObject._renderPages.getNextTransferBlock(offset, size, currentPage)) { + glBufferSubData(GL_ARRAY_BUFFER, offset, size, data + offset); + (void)CHECK_GL_ERROR(); + } + glBindBuffer(GL_ARRAY_BUFFER, 0); + (void)CHECK_GL_ERROR(); + _gpuObject._renderPages._flags &= ~PageManager::DIRTY; + } + }; + } +} + +using namespace gpu; +using namespace gpu::gl; +using namespace gpu::gles; + + +GLuint GLESBackend::getBufferID(const Buffer& buffer) { + return GLESBuffer::getId(*this, buffer); +} + +GLBuffer* GLESBackend::syncGPUObject(const Buffer& buffer) { + return GLESBuffer::sync(*this, buffer); +} diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendInput.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendInput.cpp new file mode 100644 index 0000000000..d37a01eb90 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendInput.cpp @@ -0,0 +1,29 @@ +// +// GLESBackendInput.cpp +// libraries/gpu-gl-android/src/gpu/gles +// +// Created by Cristian Duarte & Gabriel Calero on 10/7/2016. +// 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 "GLESBackend.h" + +using namespace gpu; +using namespace gpu::gles; + +void GLESBackend::updateInput() { + Parent::updateInput(); +} + + +void GLESBackend::resetInputStage() { + Parent::resetInputStage(); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + for (uint32_t i = 0; i < _input._attributeActivation.size(); i++) { + glDisableVertexAttribArray(i); + glVertexAttribPointer(i, 4, GL_FLOAT, GL_FALSE, 0, 0); + } +} \ No newline at end of file diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp new file mode 100644 index 0000000000..8bf9267fde --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp @@ -0,0 +1,169 @@ +// +// GLESBackendOutput.cpp +// libraries/gpu-gl-android/src/gpu/gles +// +// Created by Gabriel Calero & Cristian Duarte on 9/27/2016. +// Copyright 2016 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 "GLESBackend.h" + +#include + +#include "../gl/GLFramebuffer.h" +#include "../gl/GLTexture.h" + +namespace gpu { namespace gles { + +class GLESFramebuffer : public gl::GLFramebuffer { + using Parent = gl::GLFramebuffer; + static GLuint allocate() { + GLuint result; + glGenFramebuffers(1, &result); + return result; + } +public: + void update() override { + GLint currentFBO = -1; + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, ¤tFBO); + glBindFramebuffer(GL_FRAMEBUFFER, _fbo); + gl::GLTexture* gltexture = nullptr; + TexturePointer surface; + if (_gpuObject.getColorStamps() != _colorStamps) { + if (_gpuObject.hasColor()) { + _colorBuffers.clear(); + static const GLenum colorAttachments[] = { + GL_COLOR_ATTACHMENT0, + GL_COLOR_ATTACHMENT1, + GL_COLOR_ATTACHMENT2, + GL_COLOR_ATTACHMENT3, + GL_COLOR_ATTACHMENT4, + GL_COLOR_ATTACHMENT5, + GL_COLOR_ATTACHMENT6, + GL_COLOR_ATTACHMENT7, + GL_COLOR_ATTACHMENT8, + GL_COLOR_ATTACHMENT9, + GL_COLOR_ATTACHMENT10, + GL_COLOR_ATTACHMENT11, + GL_COLOR_ATTACHMENT12, + GL_COLOR_ATTACHMENT13, + GL_COLOR_ATTACHMENT14, + GL_COLOR_ATTACHMENT15 }; + + int unit = 0; + for (auto& b : _gpuObject.getRenderBuffers()) { + surface = b._texture; + if (surface) { + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + } else { + gltexture = nullptr; + } + + if (gltexture) { + glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, gltexture->_texture, 0); + _colorBuffers.push_back(colorAttachments[unit]); + } else { + glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, 0, 0); + } + unit++; + } + } + _colorStamps = _gpuObject.getColorStamps(); + } + + GLenum attachement = GL_DEPTH_STENCIL_ATTACHMENT; + if (!_gpuObject.hasStencil()) { + attachement = GL_DEPTH_ATTACHMENT; + } else if (!_gpuObject.hasDepth()) { + attachement = GL_STENCIL_ATTACHMENT; + } + + if (_gpuObject.getDepthStamp() != _depthStamp) { + auto surface = _gpuObject.getDepthStencilBuffer(); + if (_gpuObject.hasDepthStencil() && surface) { + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + } + + if (gltexture) { + glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); + } else { + glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, 0, 0); + } + _depthStamp = _gpuObject.getDepthStamp(); + } + + + // Last but not least, define where we draw + if (!_colorBuffers.empty()) { + glDrawBuffers((GLsizei)_colorBuffers.size(), _colorBuffers.data()); + } else { + glDrawBuffers(1, {GL_NONE}); + } + + // Now check for completness + _status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + + // restore the current framebuffer + if (currentFBO != -1) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO); + } + + checkStatus(GL_DRAW_FRAMEBUFFER); + } + + +public: + GLESFramebuffer(const std::weak_ptr& backend, const gpu::Framebuffer& framebuffer) + : Parent(backend, framebuffer, allocate()) { } +}; + +gl::GLFramebuffer* gpu::gles::GLESBackend::syncGPUObject(const Framebuffer& framebuffer) { + return GLESFramebuffer::sync(*this, framebuffer); +} + +GLuint GLESBackend::getFramebufferID(const FramebufferPointer& framebuffer) { + return framebuffer ? GLESFramebuffer::getId(*this, *framebuffer) : 0; +} + +void GLESBackend::do_blit(const Batch& batch, size_t paramOffset) { + auto srcframebuffer = batch._framebuffers.get(batch._params[paramOffset]._uint); + Vec4i srcvp; + for (auto i = 0; i < 4; ++i) { + srcvp[i] = batch._params[paramOffset + 1 + i]._int; + } + + auto dstframebuffer = batch._framebuffers.get(batch._params[paramOffset + 5]._uint); + Vec4i dstvp; + for (auto i = 0; i < 4; ++i) { + dstvp[i] = batch._params[paramOffset + 6 + i]._int; + } + + // Assign dest framebuffer if not bound already + auto newDrawFBO = getFramebufferID(dstframebuffer); + if (_output._drawFBO != newDrawFBO) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, newDrawFBO); + } + + // always bind the read fbo + glBindFramebuffer(GL_READ_FRAMEBUFFER, getFramebufferID(srcframebuffer)); + + // Blit! + glBlitFramebuffer(srcvp.x, srcvp.y, srcvp.z, srcvp.w, + dstvp.x, dstvp.y, dstvp.z, dstvp.w, + GL_COLOR_BUFFER_BIT, GL_LINEAR); + + // Always clean the read fbo to 0 + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + + // Restore draw fbo if changed + if (_output._drawFBO != newDrawFBO) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _output._drawFBO); + } + + (void) CHECK_GL_ERROR(); +} + + +} } diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendQuery.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendQuery.cpp new file mode 100644 index 0000000000..db541b07bc --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendQuery.cpp @@ -0,0 +1,38 @@ +// +// GLESBackendQuery.cpp +// libraries/gpu-gl-android/src/gpu/gles +// +// Created by Gabriel Calero & Cristian Duarte on 9/27/2016 +// Copyright 2016 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 "GLESBackend.h" + +#include "../gl/GLQuery.h" + +using namespace gpu; +using namespace gpu::gl; +using namespace gpu::gles; + +class GLESQuery : public GLQuery { + using Parent = GLQuery; +public: + static GLuint allocateQuery() { + GLuint result; + glGenQueries(1, &result); + return result; + } + + GLESQuery(const std::weak_ptr& backend, const Query& query) + : Parent(backend, query, allocateQuery(), allocateQuery()) { } +}; + +GLQuery* GLESBackend::syncGPUObject(const Query& query) { + return GLESQuery::sync(*this, query); +} + +GLuint GLESBackend::getQueryID(const QueryPointer& query) { + return GLESQuery::getId(*this, query); +} diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp new file mode 100644 index 0000000000..31a98edd12 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp @@ -0,0 +1,176 @@ +// +// GLESBackendTexture.cpp +// libraries/gpu-gl-android/src/gpu/gles +// +// Created by Gabriel Calero & Cristian Duarte on 9/27/2016. +// Copyright 2016 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 "GLESBackend.h" + +#include +#include +#include + +// #include "../gl/GLTexelFormat.h" + +using namespace gpu; +using namespace gpu::gl; +using namespace gpu::gles; + +//using GL41TexelFormat = GLTexelFormat; +using GLESTexture = GLESBackend::GLESTexture; + +GLuint GLESTexture::allocate() { + //CLIMAX_MERGE_START + //Backend::incrementTextureGPUCount(); + //CLIMAX_MERGE_END + GLuint result; + glGenTextures(1, &result); + return result; +} + +GLuint GLESBackend::getTextureID(const TexturePointer& texture, bool transfer) { + return GLESTexture::getId(*this, texture, transfer); +} + +GLTexture* GLESBackend::syncGPUObject(const TexturePointer& texture, bool transfer) { + return GLESTexture::sync(*this, texture, transfer); +} + +GLESTexture::GLESTexture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) + : GLTexture(backend, texture, externalId) { +} + +GLESTexture::GLESTexture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) + : GLTexture(backend, texture, allocate(), transferrable) { +} + +void GLESTexture::generateMips() const { + withPreservedTexture([&] { + glGenerateMipmap(_target); + }); + (void)CHECK_GL_ERROR(); +} + +void GLESTexture::allocateStorage() const { + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0); + (void)CHECK_GL_ERROR(); + glTexParameteri(_target, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); + (void)CHECK_GL_ERROR(); +/* if (GLEW_VERSION_4_2 && !_gpuObject.getTexelFormat().isCompressed()) { + // Get the dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip); + glTexStorage2D(_target, usedMipLevels(), texelFormat.internalFormat, dimensions.x, dimensions.y); + (void)CHECK_GL_ERROR(); + } else {*/ + for (uint16_t l = _minMip; l <= _maxMip; l++) { + // Get the mip level dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(l); + for (GLenum target : getFaceTargets(_target)) { + glTexImage2D(target, l - _minMip, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, NULL); + (void)CHECK_GL_ERROR(); + } + } + //} +} + +void GLESTexture::updateSize() const { + setSize(_virtualSize); + if (!_id) { + return; + } + + if (_gpuObject.getTexelFormat().isCompressed()) { + GLenum proxyType = GL_TEXTURE_2D; + GLuint numFaces = 1; + if (_gpuObject.getType() == gpu::Texture::TEX_CUBE) { + proxyType = CUBE_FACE_LAYOUT[0]; + numFaces = (GLuint)CUBE_NUM_FACES; + } + GLint gpuSize{ 0 }; + glGetTexLevelParameteriv(proxyType, 0, GL_TEXTURE_COMPRESSED, &gpuSize); + (void)CHECK_GL_ERROR(); + + if (gpuSize) { + for (GLuint level = _minMip; level < _maxMip; level++) { + GLint levelSize{ 0 }; + //glGetTexLevelParameteriv(proxyType, level, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &levelSize); + //qDebug() << "TODO: GLBackendTexture.cpp:updateSize GL_TEXTURE_COMPRESSED_IMAGE_SIZE"; + levelSize *= numFaces; + + if (levelSize <= 0) { + break; + } + gpuSize += levelSize; + } + (void)CHECK_GL_ERROR(); + setSize(gpuSize); + return; + } + } +} + +// Move content bits from the CPU to the GPU for a given mip / face +void GLESTexture::transferMip(uint16_t mipLevel, uint8_t face) const { + auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); + //GLenum target = getFaceTargets()[face]; + GLenum target = _target == GL_TEXTURE_2D ? GL_TEXTURE_2D : CUBE_FACE_LAYOUT[face]; + auto size = _gpuObject.evalMipDimensions(mipLevel); + glTexSubImage2D(target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + (void)CHECK_GL_ERROR(); +} + +void GLESTexture::startTransfer() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + Parent::startTransfer(); + + glBindTexture(_target, _id); + (void)CHECK_GL_ERROR(); + + // transfer pixels from each faces + uint8_t numFaces = (Texture::TEX_CUBE == _gpuObject.getType()) ? CUBE_NUM_FACES : 1; + for (uint8_t f = 0; f < numFaces; f++) { + for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i, f)) { + transferMip(i, f); + } + } + } +} + +void GLESBackend::GLESTexture::syncSampler() const { + const Sampler& sampler = _gpuObject.getSampler(); + const auto& fm = FILTER_MODES[sampler.getFilter()]; + glTexParameteri(_target, GL_TEXTURE_MIN_FILTER, fm.minFilter); + glTexParameteri(_target, GL_TEXTURE_MAG_FILTER, fm.magFilter); + + if (sampler.doComparison()) { + glTexParameteri(_target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); + glTexParameteri(_target, GL_TEXTURE_COMPARE_FUNC, COMPARISON_TO_GL[sampler.getComparisonFunction()]); + } else { + glTexParameteri(_target, GL_TEXTURE_COMPARE_MODE, GL_NONE); + } + + glTexParameteri(_target, GL_TEXTURE_WRAP_S, WRAP_MODES[sampler.getWrapModeU()]); + glTexParameteri(_target, GL_TEXTURE_WRAP_T, WRAP_MODES[sampler.getWrapModeV()]); + glTexParameteri(_target, GL_TEXTURE_WRAP_R, WRAP_MODES[sampler.getWrapModeW()]); + + glTexParameterfv(_target, GL_TEXTURE_BORDER_COLOR_EXT, (const float*)&sampler.getBorderColor()); + + + glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, (uint16)sampler.getMipOffset()); + glTexParameterf(_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); + glTexParameterf(_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); + (void)CHECK_GL_ERROR(); + //qDebug() << "[GPU-GL-GLBackend] syncSampler 12 " << _target << "," << sampler.getMaxAnisotropy(); + //glTexParameterf(_target, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); + //(void)CHECK_GL_ERROR(); + //qDebug() << "[GPU-GL-GLBackend] syncSampler end"; + +} + diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendTransform.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendTransform.cpp new file mode 100644 index 0000000000..5050db6edd --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendTransform.cpp @@ -0,0 +1,67 @@ +// +// GLESBackendTransform.cpp +// libraries/gpu-gl-android/src/gpu/gles +// +// Created by Gabriel Calero & Cristian Duarte on 9/28/2016. +// Copyright 2016 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 "GLESBackend.h" + +using namespace gpu; +using namespace gpu::gles; + +void GLESBackend::initTransform() { + glGenBuffers(1, &_transform._objectBuffer); + glGenBuffers(1, &_transform._cameraBuffer); + glGenBuffers(1, &_transform._drawCallInfoBuffer); + glGenTextures(1, &_transform._objectBufferTexture); + size_t cameraSize = sizeof(TransformStageState::CameraBufferElement); + while (_transform._cameraUboSize < cameraSize) { + _transform._cameraUboSize += _uboAlignment; + } +} + +void GLESBackend::transferTransformState(const Batch& batch) const { + // FIXME not thread safe + static std::vector bufferData; + if (!_transform._cameras.empty()) { + bufferData.resize(_transform._cameraUboSize * _transform._cameras.size()); + for (size_t i = 0; i < _transform._cameras.size(); ++i) { + memcpy(bufferData.data() + (_transform._cameraUboSize * i), &_transform._cameras[i], sizeof(TransformStageState::CameraBufferElement)); + } + glBindBuffer(GL_UNIFORM_BUFFER, _transform._cameraBuffer); + glBufferData(GL_UNIFORM_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); + glBindBuffer(GL_UNIFORM_BUFFER, 0); + } + + if (!batch._objects.empty()) { + glBindBuffer(GL_SHADER_STORAGE_BUFFER, _transform._objectBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, batch._objects.size() * sizeof(Batch::TransformObject), batch._objects.data(), GL_STREAM_DRAW); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + } + + if (!batch._namedData.empty()) { + bufferData.clear(); + for (auto& data : batch._namedData) { + auto currentSize = bufferData.size(); + auto bytesToCopy = data.second.drawCallInfos.size() * sizeof(Batch::DrawCallInfo); + bufferData.resize(currentSize + bytesToCopy); + memcpy(bufferData.data() + currentSize, data.second.drawCallInfos.data(), bytesToCopy); + _transform._drawCallInfoOffsets[data.first] = (GLvoid*)currentSize; + } + + glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); + glBufferData(GL_ARRAY_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + } + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, TRANSFORM_OBJECT_SLOT, _transform._objectBuffer); + + CHECK_GL_ERROR(); + + // Make sure the current Camera offset is unknown before render Draw + _transform._currentCameraOffset = INVALID_OFFSET; +} diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h index c5df94235c..01cc652fd1 100644 --- a/libraries/gpu/src/gpu/Buffer.h +++ b/libraries/gpu/src/gpu/Buffer.h @@ -159,6 +159,7 @@ protected: friend class gl::GLBuffer; friend class gl41::GL41Buffer; friend class gl45::GL45Buffer; + friend class gles::GLESBuffer; }; using BufferUpdates = std::vector; diff --git a/libraries/gpu/src/gpu/DrawTexcoordRectTransformUnitQuad.slv b/libraries/gpu/src/gpu/DrawTexcoordRectTransformUnitQuad.slv index c9cc9e9dfa..1f788051bc 100755 --- a/libraries/gpu/src/gpu/DrawTexcoordRectTransformUnitQuad.slv +++ b/libraries/gpu/src/gpu/DrawTexcoordRectTransformUnitQuad.slv @@ -35,5 +35,5 @@ void main(void) { TransformObject obj = getTransformObject(); <$transformModelToClipPos(cam, obj, pos, gl_Position)$> - varTexCoord0 = ((pos.xy + 1) * 0.5) * texcoordRect.zw + texcoordRect.xy; + varTexCoord0 = ((pos.xy + 1.0) * 0.5) * texcoordRect.zw + texcoordRect.xy; } diff --git a/libraries/gpu/src/gpu/DrawTransformUnitQuad.slv b/libraries/gpu/src/gpu/DrawTransformUnitQuad.slv index f55abf1089..845cf0326d 100755 --- a/libraries/gpu/src/gpu/DrawTransformUnitQuad.slv +++ b/libraries/gpu/src/gpu/DrawTransformUnitQuad.slv @@ -32,5 +32,5 @@ void main(void) { TransformObject obj = getTransformObject(); <$transformModelToClipPos(cam, obj, pos, gl_Position)$> - varTexCoord0 = (pos.xy + 1) * 0.5; + varTexCoord0 = (pos.xy + 1.0) * 0.5; } diff --git a/libraries/gpu/src/gpu/DrawUnitQuadTexcoord.slv b/libraries/gpu/src/gpu/DrawUnitQuadTexcoord.slv index 1426ae48fd..289d8f96b1 100644 --- a/libraries/gpu/src/gpu/DrawUnitQuadTexcoord.slv +++ b/libraries/gpu/src/gpu/DrawUnitQuadTexcoord.slv @@ -23,7 +23,7 @@ void main(void) { ); vec4 pos = UNIT_QUAD[gl_VertexID]; - varTexCoord0 = (pos.xy + 1) * 0.5; + varTexCoord0 = (pos.xy + 1.0) * 0.5; gl_Position = pos; } diff --git a/libraries/gpu/src/gpu/DrawViewportQuadTransformTexcoord.slv b/libraries/gpu/src/gpu/DrawViewportQuadTransformTexcoord.slv index 1f9bc13700..554728417b 100755 --- a/libraries/gpu/src/gpu/DrawViewportQuadTransformTexcoord.slv +++ b/libraries/gpu/src/gpu/DrawViewportQuadTransformTexcoord.slv @@ -28,7 +28,7 @@ void main(void) { vec4 pos = UNIT_QUAD[gl_VertexID]; // standard transform but applied to the Texcoord - vec4 tc = vec4((pos.xy + 1) * 0.5, pos.zw); + vec4 tc = vec4((pos.xy + 1.0) * 0.5, pos.zw); TransformObject obj = getTransformObject(); <$transformModelToWorldPos(obj, tc, tc)$> diff --git a/libraries/gpu/src/gpu/Forward.h b/libraries/gpu/src/gpu/Forward.h index 88800652a5..8100efafaf 100644 --- a/libraries/gpu/src/gpu/Forward.h +++ b/libraries/gpu/src/gpu/Forward.h @@ -137,6 +137,11 @@ namespace gpu { class GL45Backend; class GL45Buffer; } + + namespace gles { + class GLESBackend; + class GLESBuffer; + } } #endif diff --git a/libraries/gpu/src/gpu/Frame.cpp b/libraries/gpu/src/gpu/Frame.cpp index 4854559d61..58ab7257f7 100644 --- a/libraries/gpu/src/gpu/Frame.cpp +++ b/libraries/gpu/src/gpu/Frame.cpp @@ -11,6 +11,10 @@ using namespace gpu; +Frame::Frame() { + batches.reserve(16); +} + Frame::~Frame() { if (framebuffer && framebufferRecycler) { framebufferRecycler(framebuffer); diff --git a/libraries/gpu/src/gpu/Frame.h b/libraries/gpu/src/gpu/Frame.h index bfebe85753..69872906e3 100644 --- a/libraries/gpu/src/gpu/Frame.h +++ b/libraries/gpu/src/gpu/Frame.h @@ -20,8 +20,8 @@ namespace gpu { friend class Context; public: + Frame(); virtual ~Frame(); - using Batches = std::vector; using FramebufferRecycler = std::function; using OverlayRecycler = std::function; diff --git a/libraries/gpu/src/gpu/Framebuffer.cpp b/libraries/gpu/src/gpu/Framebuffer.cpp index b49c681889..f1257e7c83 100755 --- a/libraries/gpu/src/gpu/Framebuffer.cpp +++ b/libraries/gpu/src/gpu/Framebuffer.cpp @@ -13,6 +13,7 @@ #include #include +#include #include using namespace gpu; diff --git a/libraries/gpu/src/gpu/Framebuffer.h b/libraries/gpu/src/gpu/Framebuffer.h index a65aaf765b..b3a500d68f 100755 --- a/libraries/gpu/src/gpu/Framebuffer.h +++ b/libraries/gpu/src/gpu/Framebuffer.h @@ -132,7 +132,11 @@ public: float getAspectRatio() const { return getWidth() / (float) getHeight() ; } +#ifndef ANDROID static const uint32 MAX_NUM_RENDER_BUFFERS = 8; +#else + static const uint32 MAX_NUM_RENDER_BUFFERS = 4; +#endif static uint32 getMaxNumRenderBuffers() { return MAX_NUM_RENDER_BUFFERS; } const GPUObjectPointer gpuObject {}; diff --git a/libraries/gpu/src/gpu/Query.cpp b/libraries/gpu/src/gpu/Query.cpp index 38a9d6db8c..03312e0db9 100644 --- a/libraries/gpu/src/gpu/Query.cpp +++ b/libraries/gpu/src/gpu/Query.cpp @@ -45,8 +45,8 @@ void Query::triggerReturnHandler(uint64_t queryResult, uint64_t batchElapsedTime RangeTimer::RangeTimer(const std::string& name) : _name(name) { for (int i = 0; i < QUERY_QUEUE_SIZE; i++) { - _timerQueries.push_back(std::make_shared([&, i] (const Query& query) { - _tailIndex ++; + _timerQueries.push_back(std::make_shared([this] (const Query& query) { + _tailIndex++; _movingAverageGPU.addSample(query.getGPUElapsedTime()); _movingAverageBatch.addSample(query.getBatchElapsedTime()); diff --git a/libraries/gpu/src/gpu/Transform.slh b/libraries/gpu/src/gpu/Transform.slh index b786222198..9feca4a3c9 100644 --- a/libraries/gpu/src/gpu/Transform.slh +++ b/libraries/gpu/src/gpu/Transform.slh @@ -43,16 +43,21 @@ layout(location=14) in int _inStereoSide; flat out int _stereoSide; // In stereo drawcall mode Instances are drawn twice (left then right) hence the true InstanceID is the gl_InstanceID / 2 -int gpu_InstanceID = gl_InstanceID >> 1; +int gpu_InstanceID() { + return gl_InstanceID >> 1; +} #else -int gpu_InstanceID = gl_InstanceID; - +int gpu_InstanceID() { + return gl_InstanceID; +} #endif #else -int gpu_InstanceID = gl_InstanceID; +int gpu_InstanceID() { + return gl_InstanceID; +} #endif @@ -166,7 +171,7 @@ TransformObject getTransformObject() { #ifdef GPU_TRANSFORM_STEREO_SPLIT_SCREEN vec4 eyeClipEdge[2]= vec4[2](vec4(-1,0,0,1), vec4(1,0,0,1)); vec2 eyeOffsetScale = vec2(-0.5, +0.5); - uint eyeIndex = _stereoSide; + uint eyeIndex = uint(_stereoSide); gl_ClipDistance[0] = dot(<$clipPos$>, eyeClipEdge[eyeIndex]); float newClipPosX = <$clipPos$>.x * 0.5 + eyeOffsetScale[eyeIndex] * <$clipPos$>.w; <$clipPos$>.x = newClipPosX; diff --git a/libraries/image/CMakeLists.txt b/libraries/image/CMakeLists.txt index 85d3d8f1ae..442fa714b3 100644 --- a/libraries/image/CMakeLists.txt +++ b/libraries/image/CMakeLists.txt @@ -2,10 +2,10 @@ set(TARGET_NAME image) setup_hifi_library() link_hifi_libraries(shared gpu) -target_glm() - -add_dependency_external_projects(nvtt) -find_package(NVTT REQUIRED) -target_include_directories(${TARGET_NAME} PRIVATE ${NVTT_INCLUDE_DIRS}) -target_link_libraries(${TARGET_NAME} ${NVTT_LIBRARIES}) -add_paths_to_fixup_libs(${NVTT_DLL_PATH}) +if (NOT ANDROID) + add_dependency_external_projects(nvtt) + find_package(NVTT REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE ${NVTT_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${NVTT_LIBRARIES}) + add_paths_to_fixup_libs(${NVTT_DLL_PATH}) +endif() \ No newline at end of file diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index f274dc54f8..e9fb447e99 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -11,7 +11,8 @@ #include "Image.h" -#include +#include + #include #include @@ -28,7 +29,13 @@ using namespace gpu; +#if defined(Q_OS_ANDROID) +#define CPU_MIPMAPS 0 +#else #define CPU_MIPMAPS 1 +#include +#endif + static std::mutex settingsMutex; static Setting::Handle compressColorTextures("hifi.graphics.compressColorTextures", false); @@ -292,6 +299,7 @@ QImage processSourceImage(const QImage& srcImage, bool cubemap) { return srcImage; } +#if defined(NVTT_API) struct MyOutputHandler : public nvtt::OutputHandler { MyOutputHandler(gpu::Texture* texture, int face) : _texture(texture), _face(face) {} @@ -330,6 +338,7 @@ struct MyErrorHandler : public nvtt::ErrorHandler { qCWarning(imagelogging) << "Texture compression error:" << nvtt::errorString(e); } }; +#endif void generateMips(gpu::Texture* texture, QImage& image, int face = -1) { #if CPU_MIPMAPS @@ -450,7 +459,7 @@ void generateMips(gpu::Texture* texture, QImage& image, int face = -1) { nvtt::Compressor compressor; compressor.process(inputOptions, compressionOptions, outputOptions); #else - texture->autoGenerateMips(-1); + texture->setAutoGenerateMips(true); #endif } diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h index 02dcbe4664..c0cca200d7 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h @@ -30,7 +30,7 @@ public: TOUCH_AXIS_Y_NEG, }; - enum TouchGestureAxisChannel { + enum TouchGestureAxisChannel { TOUCH_GESTURE_PINCH_POS = TOUCH_AXIS_Y_NEG + 1, TOUCH_GESTURE_PINCH_NEG, }; diff --git a/libraries/ktx/CMakeLists.txt b/libraries/ktx/CMakeLists.txt index 967ea71eb6..efd8b76567 100644 --- a/libraries/ktx/CMakeLists.txt +++ b/libraries/ktx/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME ktx) setup_hifi_library() -include_hifi_library_headers(shared) \ No newline at end of file +link_hifi_libraries(shared) diff --git a/libraries/midi/src/Midi.h b/libraries/midi/src/Midi.h index 0ffa27986d..013ec056e3 100644 --- a/libraries/midi/src/Midi.h +++ b/libraries/midi/src/Midi.h @@ -66,7 +66,7 @@ Q_INVOKABLE void thruModeEnable(bool enable); public: Midi(); - virtual ~Midi(); + virtual ~Midi(); }; #endif // hifi_Midi_h diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 704455c981..2f29f47864 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -173,9 +173,9 @@ void GeometryReader::run() { QString urlname = _url.path().toLower(); if (!urlname.isEmpty() && !_url.path().isEmpty() && - (_url.path().toLower().endsWith(".fbx") || - _url.path().toLower().endsWith(".obj") || - _url.path().toLower().endsWith(".obj.gz"))) { + (_url.path().toLower().endsWith(".fbx") || + _url.path().toLower().endsWith(".obj") || + _url.path().toLower().endsWith(".obj.gz"))) { FBXGeometry::Pointer fbxGeometry; if (_url.path().toLower().endsWith(".fbx")) { @@ -185,15 +185,15 @@ void GeometryReader::run() { } } else if (_url.path().toLower().endsWith(".obj")) { fbxGeometry.reset(OBJReader().readOBJ(_data, _mapping, _combineParts, _url)); - } else if (_url.path().toLower().endsWith(".obj.gz")) { - QByteArray uncompressedData; - if (gunzip(_data, uncompressedData)){ - fbxGeometry.reset(OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url)); - } else { - throw QString("failed to decompress .obj.gz" ); - } + } else if (_url.path().toLower().endsWith(".obj.gz")) { + QByteArray uncompressedData; + if (gunzip(_data, uncompressedData)){ + fbxGeometry.reset(OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url)); + } else { + throw QString("failed to decompress .obj.gz" ); + } - } else { + } else { throw QString("unsupported format"); } diff --git a/libraries/model/src/model/Light.slh b/libraries/model/src/model/Light.slh index 67946abea0..093a87adc8 100644 --- a/libraries/model/src/model/Light.slh +++ b/libraries/model/src/model/Light.slh @@ -41,7 +41,7 @@ SphericalHarmonics getLightAmbientSphere(LightAmbient l) { return l._ambientSphe float getLightAmbientIntensity(LightAmbient l) { return l._ambient.x; } -bool getLightHasAmbientMap(LightAmbient l) { return l._ambient.y > 0; } +bool getLightHasAmbientMap(LightAmbient l) { return l._ambient.y > 0.0; } float getLightAmbientMapNumMips(LightAmbient l) { return l._ambient.y; } <@func declareLightBuffer(N)@> diff --git a/libraries/model/src/model/LightIrradiance.shared.slh b/libraries/model/src/model/LightIrradiance.shared.slh index 4a2ee40c9d..13d6cf4b93 100644 --- a/libraries/model/src/model/LightIrradiance.shared.slh +++ b/libraries/model/src/model/LightIrradiance.shared.slh @@ -37,7 +37,7 @@ float lightIrradiance_evalLightAttenuation(LightIrradiance li, float d) { // "Fade" the edges of light sources to make things look a bit more attractive. // Note: this tends to look a bit odd at lower exponents. - attenuation *= min(1, max(0, -(d - cutoff))); + attenuation *= min(1.0, max(0.0, -(d - cutoff))); return attenuation; } diff --git a/libraries/networking/CMakeLists.txt b/libraries/networking/CMakeLists.txt index 288e98d5a5..c3592c5da2 100644 --- a/libraries/networking/CMakeLists.txt +++ b/libraries/networking/CMakeLists.txt @@ -2,44 +2,18 @@ set(TARGET_NAME networking) setup_hifi_library(Network) link_hifi_libraries(shared) -target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/includes") +target_openssl() +target_tbb() if (WIN32) - # we need ws2_32.lib on windows, but it's static so we don't bubble it up - target_link_libraries(${TARGET_NAME} ws2_32.lib) -endif () - -add_dependency_external_projects(tbb) - -# find required dependencies -find_package(OpenSSL REQUIRED) -find_package(TBB REQUIRED) - -if (APPLE) + # we need ws2_32.lib on windows, but it's static so we don't bubble it up + target_link_libraries(${TARGET_NAME} ws2_32.lib) +elseif(APPLE) + # IOKit is needed for getting machine fingerprint find_library(FRAMEWORK_IOKIT IOKit) find_library(CORE_FOUNDATION CoreFoundation) -endif () - -if (APPLE AND ${OPENSSL_INCLUDE_DIR} STREQUAL "/usr/include") - # this is a user on OS X using system OpenSSL, which is going to throw warnings since they're deprecating for their common crypto - message(WARNING "The found version of OpenSSL is the OS X system version. This will produce deprecation warnings." - "\nWe recommend you install a newer version (at least 1.0.1h) in a different directory and set OPENSSL_ROOT_DIR in your env so Cmake can find it.") -endif () - -include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}") - -# append OpenSSL to our list of libraries to link -target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES} ${TBB_LIBRARIES}) - -# IOKit is needed for getting machine fingerprint -if (APPLE) target_link_libraries(${TARGET_NAME} ${FRAMEWORK_IOKIT} ${CORE_FOUNDATION}) -endif (APPLE) - -# libcrypto uses dlopen in libdl -if (UNIX) - target_link_libraries(${TARGET_NAME} ${CMAKE_DL_LIBS}) -endif (UNIX) - -# append tbb includes to our list of includes to bubble -target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${TBB_INCLUDE_DIRS}) +elseif(UNIX) + # libcrypto uses dlopen in libdl + target_link_libraries(${TARGET_NAME} ${CMAKE_DL_LIBS}) +endif () diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 99e1962387..2376a3b2b6 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -29,11 +29,15 @@ #include "UserActivityLogger.h" #include "udt/PacketHeaders.h" +#ifdef Q_OS_ANDROID +const QString DEFAULT_HIFI_ADDRESS = "hifi://android/0.0,0.0,-200"; +#else #if USE_STABLE_GLOBAL_SERVICES const QString DEFAULT_HIFI_ADDRESS = "hifi://welcome/hello"; #else const QString DEFAULT_HIFI_ADDRESS = "hifi://dev-welcome/hello"; #endif +#endif const QString ADDRESS_MANAGER_SETTINGS_GROUP = "AddressManager"; const QString SETTINGS_CURRENT_ADDRESS_KEY = "address"; diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index cb0b620a54..ea66d018e5 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -59,7 +59,11 @@ void AssetClient::init() { auto& networkAccessManager = NetworkAccessManager::getInstance(); if (!networkAccessManager.cache()) { if (_cacheDir.isEmpty()) { +#ifdef Q_OS_ANDROID + QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); +#else QString cachePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation); +#endif _cacheDir = !cachePath.isEmpty() ? cachePath : "interfaceCache"; } QNetworkDiskCache* cache = new QNetworkDiskCache(); diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index a41283cc0d..eb3a75d14f 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -187,7 +187,7 @@ void AssetResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytes emit progress(bytesReceived, bytesTotal); auto now = p_high_resolution_clock::now(); - + // Recording ATP bytes downloaded in stats DependencyManager::get()->updateStat(STAT_ATP_RESOURCE_TOTAL_BYTES, bytesReceived); diff --git a/libraries/networking/src/HTTPResourceRequest.cpp b/libraries/networking/src/HTTPResourceRequest.cpp index c6d0370a70..edba520bd5 100644 --- a/libraries/networking/src/HTTPResourceRequest.cpp +++ b/libraries/networking/src/HTTPResourceRequest.cpp @@ -201,11 +201,11 @@ void HTTPResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytesT _sendTimer->start(); emit progress(bytesReceived, bytesTotal); - + // Recording HTTP bytes downloaded in stats DependencyManager::get()->updateStat(STAT_HTTP_RESOURCE_TOTAL_BYTES, bytesReceived); - - + + } void HTTPResourceRequest::onTimeout() { diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 954b9685af..d0cb0109c7 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -668,7 +668,11 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t } // insert the new node and release our read lock +#ifdef Q_OS_ANDROID + _nodeHash.insert(UUIDNodePair(newNode->getUUID(), newNodePointer)); +#else _nodeHash.emplace(newNode->getUUID(), newNodePointer); +#endif readLocker.unlock(); qCDebug(networking) << "Added" << *newNode; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 48e5c8a62c..fcf369f786 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -809,7 +809,7 @@ void NodeList::sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationN void NodeList::ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled) { // enumerate the nodes to send a reliable ignore packet to each that can leverage it if (!nodeID.isNull() && _sessionUUID != nodeID) { - eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool { + eachMatchingNode([](const SharedNodePointer& node)->bool { if (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer) { return true; } else { diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp index e94c43b6fb..67359ee862 100644 --- a/libraries/networking/src/NodePermissions.cpp +++ b/libraries/networking/src/NodePermissions.cpp @@ -9,9 +9,28 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "NodePermissions.h" + +#include #include #include -#include "NodePermissions.h" + + + +size_t std::hash::operator()(const NodePermissionsKey& key) const { + size_t result = qHash(key.first); + result <<= sizeof(size_t) / 2; + +#if (QT_POINTER_SIZE == 8) + const uint MASK = 0x00FF; +#else + const uint MASK = 0xFFFF; +#endif + + result |= (qHash(key.second) & MASK); + return result; +} + NodePermissionsKey NodePermissions::standardNameLocalhost = NodePermissionsKey("localhost", 0); NodePermissionsKey NodePermissions::standardNameLoggedIn = NodePermissionsKey("logged-in", 0); diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index 129d7e5c08..8f7bdaebfe 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -29,13 +29,8 @@ using NodePermissionsKeyList = QList>; namespace std { template<> - struct hash { - size_t operator()(const NodePermissionsKey& key) const { - size_t result = qHash(key.first); - result <<= 32; - result |= qHash(key.second); - return result; - } + struct hash { + size_t operator()(const NodePermissionsKey& key) const; }; } diff --git a/libraries/networking/src/udt/CongestionControl.h b/libraries/networking/src/udt/CongestionControl.h index 3a93134a5e..e6a462651e 100644 --- a/libraries/networking/src/udt/CongestionControl.h +++ b/libraries/networking/src/udt/CongestionControl.h @@ -123,9 +123,9 @@ private: p_high_resolution_clock::time_point _lastRCTime = p_high_resolution_clock::now(); // last rate increase time - bool _slowStart { true }; // if in slow start phase + bool _slowStart { true }; // if in slow start phase SequenceNumber _lastACK; // last ACKed sequence number from previous - bool _loss { false }; // if loss happened since last rate increase + bool _loss { false }; // if loss happened since last rate increase SequenceNumber _lastDecreaseMaxSeq; // max pkt seq num sent out when last decrease happened double _lastDecreasePeriod { 1 }; // value of _packetSendPeriod when last decrease happened int _nakCount { 0 }; // number of NAKs in congestion epoch diff --git a/libraries/networking/src/udt/ControlPacket.cpp b/libraries/networking/src/udt/ControlPacket.cpp index 0d5d3e8c25..6fdefd36f6 100644 --- a/libraries/networking/src/udt/ControlPacket.cpp +++ b/libraries/networking/src/udt/ControlPacket.cpp @@ -68,7 +68,7 @@ ControlPacket::ControlPacket(std::unique_ptr data, qint64 size, const Hi } ControlPacket::ControlPacket(ControlPacket&& other) : - BasePacket(std::move(other)) + BasePacket(std::move(other)) { _type = other._type; } diff --git a/libraries/physics/src/CharacterRayResult.h b/libraries/physics/src/CharacterRayResult.h index e8b0bb7f99..239ab9570a 100644 --- a/libraries/physics/src/CharacterRayResult.h +++ b/libraries/physics/src/CharacterRayResult.h @@ -26,19 +26,19 @@ public: protected: const CharacterGhostObject* _character; - // Note: Public data members inherited from ClosestRayResultCallback - // - // btVector3 m_rayFromWorld;//used to calculate hitPointWorld from hitFraction - // btVector3 m_rayToWorld; - // btVector3 m_hitNormalWorld; - // btVector3 m_hitPointWorld; - // - // Note: Public data members inherited from RayResultCallback - // - // btScalar m_closestHitFraction; - // const btCollisionObject* m_collisionObject; - // short int m_collisionFilterGroup; - // short int m_collisionFilterMask; + // Note: Public data members inherited from ClosestRayResultCallback + // + // btVector3 m_rayFromWorld;//used to calculate hitPointWorld from hitFraction + // btVector3 m_rayToWorld; + // btVector3 m_hitNormalWorld; + // btVector3 m_hitPointWorld; + // + // Note: Public data members inherited from RayResultCallback + // + // btScalar m_closestHitFraction; + // const btCollisionObject* m_collisionObject; + // short int m_collisionFilterGroup; + // short int m_collisionFilterMask; }; #endif // hifi_CharacterRayResult_h diff --git a/libraries/physics/src/CharacterSweepResult.cpp b/libraries/physics/src/CharacterSweepResult.cpp index a5c4092b1d..36e36185d3 100755 --- a/libraries/physics/src/CharacterSweepResult.cpp +++ b/libraries/physics/src/CharacterSweepResult.cpp @@ -25,7 +25,7 @@ CharacterSweepResult::CharacterSweepResult(const CharacterGhostObject* character } btScalar CharacterSweepResult::addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool useWorldFrame) { - // skip objects that we shouldn't collide with + // skip objects that we shouldn't collide with if (!convexResult.m_hitCollisionObject->hasContactResponse()) { return btScalar(1.0); } @@ -37,6 +37,6 @@ btScalar CharacterSweepResult::addSingleResult(btCollisionWorld::LocalConvexResu } void CharacterSweepResult::resetHitHistory() { - m_hitCollisionObject = nullptr; - m_closestHitFraction = btScalar(1.0f); + m_hitCollisionObject = nullptr; + m_closestHitFraction = btScalar(1.0f); } diff --git a/libraries/physics/src/CharacterSweepResult.h b/libraries/physics/src/CharacterSweepResult.h index 1e2898a3cf..3c7bb66282 100644 --- a/libraries/physics/src/CharacterSweepResult.h +++ b/libraries/physics/src/CharacterSweepResult.h @@ -22,13 +22,13 @@ class CharacterSweepResult : public btCollisionWorld::ClosestConvexResultCallbac public: CharacterSweepResult(const CharacterGhostObject* character); virtual btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool useWorldFrame) override; - void resetHitHistory(); + void resetHitHistory(); protected: const CharacterGhostObject* _character; // NOTE: Public data members inherited from ClosestConvexResultCallback: // - // btVector3 m_convexFromWorld; // unused except by btClosestNotMeConvexResultCallback + // btVector3 m_convexFromWorld; // unused except by btClosestNotMeConvexResultCallback // btVector3 m_convexToWorld; // unused except by btClosestNotMeConvexResultCallback // btVector3 m_hitNormalWorld; // btVector3 m_hitPointWorld; diff --git a/libraries/physics/src/CollisionRenderMeshCache.h b/libraries/physics/src/CollisionRenderMeshCache.h index 6a6857a5ae..10b2440db2 100644 --- a/libraries/physics/src/CollisionRenderMeshCache.h +++ b/libraries/physics/src/CollisionRenderMeshCache.h @@ -21,7 +21,7 @@ class CollisionRenderMeshCache { public: - using Key = const void*; // must actually be a const btCollisionShape* + using Key = const void*; // must actually be a const btCollisionShape* CollisionRenderMeshCache(); ~CollisionRenderMeshCache(); diff --git a/libraries/procedural/CMakeLists.txt b/libraries/procedural/CMakeLists.txt index 3ebd0f3d14..daf6fefccc 100644 --- a/libraries/procedural/CMakeLists.txt +++ b/libraries/procedural/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME procedural) AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() -link_hifi_libraries(shared gpu gpu-gl networking model model-networking ktx image) +link_hifi_libraries(shared gpu networking model model-networking ktx image) diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index 0079ffae66..94232c81b2 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -3,7 +3,7 @@ AUTOSCRIBE_SHADER_LIB(gpu model render) # pull in the resources.qrc file qt5_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") setup_hifi_library(Widgets OpenGL Network Qml Quick Script) -link_hifi_libraries(shared ktx gpu model model-networking render animation fbx entities image procedural) +link_hifi_libraries(shared ktx gpu model model-networking render animation fbx image procedural) include_hifi_library_headers(networking) include_hifi_library_headers(octree) include_hifi_library_headers(audio) diff --git a/libraries/render-utils/src/DeferredBufferRead.slh b/libraries/render-utils/src/DeferredBufferRead.slh index 315de30fea..fbca241bb9 100644 --- a/libraries/render-utils/src/DeferredBufferRead.slh +++ b/libraries/render-utils/src/DeferredBufferRead.slh @@ -90,7 +90,7 @@ DeferredFragment unpackDeferredFragmentNoPositionNoAmbient(vec2 texcoord) { vec4 diffuseVal; DeferredFragment frag; - frag.depthVal = -1; + frag.depthVal = -1.0; normalVal = texture(normalMap, texcoord); diffuseVal = texture(albedoMap, texcoord); diff --git a/libraries/render-utils/src/FadeEffect.h b/libraries/render-utils/src/FadeEffect.h index 4b4e401332..e4a7debd1e 100644 --- a/libraries/render-utils/src/FadeEffect.h +++ b/libraries/render-utils/src/FadeEffect.h @@ -47,8 +47,8 @@ private: glm::vec3 _lastBaseOffset { 0.f, 0.f, 0.f }; glm::vec3 _lastBaseInvSize { 1.f, 1.f, 1.f }; - explicit FadeEffect(); - virtual ~FadeEffect() { } + explicit FadeEffect(); + virtual ~FadeEffect() { } }; #endif // hifi_render_utils_FadeEffect_h diff --git a/libraries/render-utils/src/ForwardBuffer.slh b/libraries/render-utils/src/ForwardBuffer.slh new file mode 100644 index 0000000000..4d1dc89aa4 --- /dev/null +++ b/libraries/render-utils/src/ForwardBuffer.slh @@ -0,0 +1,68 @@ + +<@if not FORWARD_BUFFER_SLH@> +<@def FORWARD_BUFFER_SLH@> + +<@include gpu/PackedNormal.slh@> + +// Unpack the metallic-mode value +const float FRAG_PACK_SHADED_NON_METALLIC = 0.0; +const float FRAG_PACK_SHADED_METALLIC = 0.1; +const float FRAG_PACK_SHADED_RANGE_INV = 1.0 / (FRAG_PACK_SHADED_METALLIC - FRAG_PACK_SHADED_NON_METALLIC); + +const float FRAG_PACK_LIGHTMAPPED_NON_METALLIC = 0.2; +const float FRAG_PACK_LIGHTMAPPED_METALLIC = 0.3; +const float FRAG_PACK_LIGHTMAPPED_RANGE_INV = 1.0 / (FRAG_PACK_LIGHTMAPPED_METALLIC - FRAG_PACK_LIGHTMAPPED_NON_METALLIC); + +const float FRAG_PACK_SCATTERING_NON_METALLIC = 0.4; +const float FRAG_PACK_SCATTERING_METALLIC = 0.5; +const float FRAG_PACK_SCATTERING_RANGE_INV = 1.0 / (FRAG_PACK_SCATTERING_METALLIC - FRAG_PACK_SCATTERING_NON_METALLIC); + +const float FRAG_PACK_UNLIT = 0.6; + +const int FRAG_MODE_UNLIT = 0; +const int FRAG_MODE_SHADED = 1; +const int FRAG_MODE_LIGHTMAPPED = 2; +const int FRAG_MODE_SCATTERING = 3; + +void unpackModeMetallic(float rawValue, out int mode, out float metallic) { + if (rawValue <= FRAG_PACK_SHADED_METALLIC) { + mode = FRAG_MODE_SHADED; + metallic = clamp((rawValue - FRAG_PACK_SHADED_NON_METALLIC) * FRAG_PACK_SHADED_RANGE_INV, 0.0, 1.0); + } else if (rawValue <= FRAG_PACK_LIGHTMAPPED_METALLIC) { + mode = FRAG_MODE_LIGHTMAPPED; + metallic = clamp((rawValue - FRAG_PACK_LIGHTMAPPED_NON_METALLIC) * FRAG_PACK_LIGHTMAPPED_RANGE_INV, 0.0, 1.0); + } else if (rawValue <= FRAG_PACK_SCATTERING_METALLIC) { + mode = FRAG_MODE_SCATTERING; + metallic = clamp((rawValue - FRAG_PACK_SCATTERING_NON_METALLIC) * FRAG_PACK_SCATTERING_RANGE_INV, 0.0, 1.0); + } else if (rawValue >= FRAG_PACK_UNLIT) { + mode = FRAG_MODE_UNLIT; + metallic = 0.0; + } +} + +float packShadedMetallic(float metallic) { + return mix(FRAG_PACK_SHADED_NON_METALLIC, FRAG_PACK_SHADED_METALLIC, metallic); +} + +float packLightmappedMetallic(float metallic) { + return mix(FRAG_PACK_LIGHTMAPPED_NON_METALLIC, FRAG_PACK_LIGHTMAPPED_METALLIC, metallic); +} + +float packScatteringMetallic(float metallic) { + return mix(FRAG_PACK_SCATTERING_NON_METALLIC, FRAG_PACK_SCATTERING_METALLIC, metallic); +} + +float packUnlit() { + return FRAG_PACK_UNLIT; +} + +<@endif@> diff --git a/libraries/render-utils/src/ForwardBufferWrite.slh b/libraries/render-utils/src/ForwardBufferWrite.slh new file mode 100644 index 0000000000..873514d51f --- /dev/null +++ b/libraries/render-utils/src/ForwardBufferWrite.slh @@ -0,0 +1,63 @@ + +<@if not FORWARD_BUFFER_WRITE_SLH@> +<@def FORWARD_BUFFER_WRITE_SLH@> + +<@include ForwardBuffer.slh@> + + +layout(location = 0) out vec4 _fragColor0; + +// the alpha threshold +const float alphaThreshold = 0.5; +float evalOpaqueFinalAlpha(float alpha, float mapAlpha) { + return mix(alpha, 1.0 - alpha, step(mapAlpha, alphaThreshold)); +} + +const float DEFAULT_ROUGHNESS = 0.9; +const float DEFAULT_SHININESS = 10.0; +const float DEFAULT_METALLIC = 0.0; +const vec3 DEFAULT_SPECULAR = vec3(0.1); +const vec3 DEFAULT_EMISSIVE = vec3(0.0); +const float DEFAULT_OCCLUSION = 1.0; +const float DEFAULT_SCATTERING = 0.0; +const vec3 DEFAULT_FRESNEL = DEFAULT_EMISSIVE; + +void packForwardFragment(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 emissive, float occlusion, float scattering) { + if (alpha != 1.0) { + discard; + } + _fragColor0 = vec4(albedo, ((scattering > 0.0) ? packScatteringMetallic(metallic) : packShadedMetallic(metallic))); +} + +void packForwardFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 fresnel, vec3 lightmap) { + if (alpha != 1.0) { + discard; + } + + _fragColor0 = vec4(albedo, packLightmappedMetallic(metallic)); +} + +void packForwardFragmentUnlit(vec3 normal, float alpha, vec3 color) { + if (alpha != 1.0) { + discard; + } + _fragColor0 = vec4(color, packUnlit()); +} + +void packForwardFragmentTranslucent(vec3 normal, float alpha, vec3 albedo, vec3 fresnel, float roughness) { + if (alpha <= 0.0) { + discard; + } + _fragColor0 = vec4(albedo.rgb, alpha); +} + +<@endif@> diff --git a/libraries/render-utils/src/ForwardGlobalLight.slh b/libraries/render-utils/src/ForwardGlobalLight.slh new file mode 100644 index 0000000000..aba0498ef5 --- /dev/null +++ b/libraries/render-utils/src/ForwardGlobalLight.slh @@ -0,0 +1,198 @@ + +<@if not DEFERRED_GLOBAL_LIGHT_SLH@> +<@def DEFERRED_GLOBAL_LIGHT_SLH@> + +<@include model/Light.slh@> + +<@include LightingModel.slh@> +<$declareLightBuffer()$> +<$declareLightAmbientBuffer()$> + +<@include LightAmbient.slh@> +<@include LightDirectional.slh@> + + +<@func prepareGlobalLight(isScattering)@> + // prepareGlobalLight + // Transform directions to worldspace + vec3 fragNormal = vec3((normal)); + vec3 fragEyeVector = vec3(invViewMat * vec4(-1.0*position, 0.0)); + vec3 fragEyeDir = normalize(fragEyeVector); + + // Get light + Light light = getLight(); + LightAmbient lightAmbient = getLightAmbient(); + + vec3 lightDirection = getLightDirection(light); + vec3 lightIrradiance = getLightIrradiance(light); + + vec3 color = vec3(0.0); + +<@endfunc@> + + +<@func declareEvalAmbientGlobalColor()@> +vec3 evalAmbientGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, vec3 fresnel, float metallic, float roughness) { + <$prepareGlobalLight()$> + color += albedo * getLightColor(light) * obscurance * getLightAmbientIntensity(lightAmbient); + return color; +} +<@endfunc@> + +<@func declareEvalAmbientSphereGlobalColor(supportScattering)@> + +<$declareLightingAmbient(1, _SCRIBE_NULL, _SCRIBE_NULL, $supportScattering$)$> +<$declareLightingDirectional($supportScattering$)$> + +<@if supportScattering@> +<$declareDeferredCurvature()$> +<@endif@> + +vec3 evalAmbientSphereGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, +vec3 albedo, vec3 fresnel, float metallic, float roughness +<@if supportScattering@> + , float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature +<@endif@> ) { + + <$prepareGlobalLight($supportScattering$)$> + + // Ambient + vec3 ambientDiffuse; + vec3 ambientSpecular; + evalLightingAmbient(ambientDiffuse, ambientSpecular, lightAmbient, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance +<@if supportScattering@> + ,scattering, midNormalCurvature, lowNormalCurvature +<@endif@> ); + color += ambientDiffuse; + color += ambientSpecular; + + + // Directional + vec3 directionalDiffuse; + vec3 directionalSpecular; + evalLightingDirectional(directionalDiffuse, directionalSpecular, lightDirection, lightIrradiance, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation +<@if supportScattering@> + ,scattering, midNormalCurvature, lowNormalCurvature +<@endif@> ); + color += directionalDiffuse; + color += directionalSpecular; + + return color; +} + +<@endfunc@> + + +<@func declareEvalSkyboxGlobalColor(supportScattering)@> + +<$declareLightingAmbient(_SCRIBE_NULL, 1, _SCRIBE_NULL, $supportScattering$)$> +<$declareLightingDirectional($supportScattering$)$> + +<@if supportScattering@> +<$declareDeferredCurvature()$> +<@endif@> + +vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, + vec3 albedo, vec3 fresnel, float metallic, float roughness +<@if supportScattering@> + , float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature +<@endif@> + ) { + <$prepareGlobalLight($supportScattering$)$> + + // Ambient + vec3 ambientDiffuse; + vec3 ambientSpecular; + evalLightingAmbient(ambientDiffuse, ambientSpecular, lightAmbient, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance +<@if supportScattering@> + ,scattering, midNormalCurvature, lowNormalCurvature +<@endif@> + ); + color += ambientDiffuse; + color += ambientSpecular; + + + // Directional + vec3 directionalDiffuse; + vec3 directionalSpecular; + evalLightingDirectional(directionalDiffuse, directionalSpecular, lightDirection, lightIrradiance, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation +<@if supportScattering@> + ,scattering, midNormalCurvature, lowNormalCurvature +<@endif@> + ); + color += directionalDiffuse; + color += directionalSpecular; + + return color; +} + +<@endfunc@> + +<@func declareEvalLightmappedColor()@> +vec3 evalLightmappedColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 normal, vec3 albedo, vec3 lightmap) { + Light light = getLight(); + LightAmbient ambient = getLightAmbient(); + + // Catch normals perpendicular to the projection plane, hence the magic number for the threshold + // It should be just 0, but we have inaccuracy so we overshoot + const float PERPENDICULAR_THRESHOLD = -0.005; + vec3 fragNormal = vec3(invViewMat * vec4(normal, 0.0)); // transform to worldspace + float diffuseDot = dot(fragNormal, -getLightDirection(light)); + float facingLight = step(PERPENDICULAR_THRESHOLD, diffuseDot); + + // Reevaluate the shadow attenuation for light facing fragments + float lightAttenuation = (1.0 - facingLight) + facingLight * shadowAttenuation; + + // Diffuse light is the lightmap dimmed by shadow + vec3 diffuseLight = lightAttenuation * lightmap; + + // Ambient light is the lightmap when in shadow + vec3 ambientLight = (1.0 - lightAttenuation) * lightmap * getLightAmbientIntensity(ambient); + + return isLightmapEnabled() * obscurance * albedo * (diffuseLight + ambientLight); +} +<@endfunc@> + + + + +<@func declareEvalGlobalLightingAlphaBlended()@> + +<$declareLightingAmbient(1, 1, 1)$> +<$declareLightingDirectional()$> + +vec3 evalGlobalLightingAlphaBlended(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, vec3 fresnel, float metallic, vec3 emissive, float roughness, float opacity) { + <$prepareGlobalLight()$> + + color += emissive * isEmissiveEnabled(); + + // Ambient + vec3 ambientDiffuse; + vec3 ambientSpecular; + evalLightingAmbient(ambientDiffuse, ambientSpecular, lightAmbient, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance); + color += ambientDiffuse; + color += ambientSpecular / opacity; + + // Directional + vec3 directionalDiffuse; + vec3 directionalSpecular; + evalLightingDirectional(directionalDiffuse, directionalSpecular, lightDirection, lightIrradiance, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation); + color += directionalDiffuse; + color += directionalSpecular / opacity; + + return color; +} + +<@endfunc@> + + +<@endif@> diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 3bf83d08c9..02e4ca9748 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -18,9 +18,11 @@ #include #include +#include +#include + #include #include -#include #include "TextureCache.h" #include "RenderUtilsLogging.h" diff --git a/libraries/render-utils/src/LightClusterGrid_shared.slh b/libraries/render-utils/src/LightClusterGrid_shared.slh index 50499cf31a..6d43e71920 100644 --- a/libraries/render-utils/src/LightClusterGrid_shared.slh +++ b/libraries/render-utils/src/LightClusterGrid_shared.slh @@ -36,7 +36,7 @@ vec3 frustumGrid_volumeToGrid(vec3 vpos, ivec3 dims) { vec4 frustumGrid_volumeToClip(vec3 vpos, float rangeNear, float rangeFar) { vec3 ndcPos = vec3(-1.0f + 2.0f * vpos.x, -1.0f + 2.0f * vpos.y, vpos.z); - float depth = rangeNear * (1 - ndcPos.z) + rangeFar * (ndcPos.z); + float depth = rangeNear * (1.0f - ndcPos.z) + rangeFar * (ndcPos.z); vec4 clipPos = vec4(ndcPos.x * depth, ndcPos.y * depth, 1.0f, depth); return clipPos; } @@ -114,14 +114,14 @@ int frustumGrid_eyeDepthToClusterLayer(float eyeZ) { float volumeZ = frustumGrid_eyeToVolumeDepth(eyeZ, frustumGrid.rangeNear, frustumGrid.rangeFar); - float gridZ = frustumGrid_volumeToGridDepth(volumeZ, frustumGrid.dims); + int gridZ = int(frustumGrid_volumeToGridDepth(volumeZ, frustumGrid.dims)); if (gridZ >= frustumGrid.dims.z) { gridZ = frustumGrid.dims.z; } - return int(gridZ); + return gridZ; } ivec3 frustumGrid_eyeToClusterPos(vec3 eyePos) { @@ -134,8 +134,8 @@ ivec3 frustumGrid_eyeToClusterPos(vec3 eyePos) { vec3 gridPos = frustumGrid_volumeToGrid(volumePos, frustumGrid.dims); - if (gridPos.z >= frustumGrid.dims.z) { - gridPos.z = frustumGrid.dims.z; + if (gridPos.z >= float(frustumGrid.dims.z)) { + gridPos.z = float(frustumGrid.dims.z); } ivec3 igridPos = ivec3(floor(gridPos)); @@ -154,7 +154,7 @@ ivec3 frustumGrid_eyeToClusterPos(vec3 eyePos) { int frustumGrid_eyeToClusterDirH(vec3 eyeDir) { if (eyeDir.z >= 0.0f) { - return (eyeDir.x > 0 ? frustumGrid.dims.x : -1); + return (eyeDir.x > 0.0f ? frustumGrid.dims.x : -1); } float eyeDepth = -eyeDir.z; @@ -168,7 +168,7 @@ int frustumGrid_eyeToClusterDirH(vec3 eyeDir) { int frustumGrid_eyeToClusterDirV(vec3 eyeDir) { if (eyeDir.z >= 0.0f) { - return (eyeDir.y > 0 ? frustumGrid.dims.y : -1); + return (eyeDir.y > 0.0f ? frustumGrid.dims.y : -1); } float eyeDepth = -eyeDir.z; diff --git a/libraries/render-utils/src/LightPoint.slh b/libraries/render-utils/src/LightPoint.slh index ac1e415d9d..7e389e11f6 100644 --- a/libraries/render-utils/src/LightPoint.slh +++ b/libraries/render-utils/src/LightPoint.slh @@ -59,12 +59,12 @@ bool evalLightPointEdge(out vec3 color, Light light, vec4 fragLightDirLen, vec3 // Show edges float edge = abs(2.0 * ((lightVolume_getRadius(light.volume) - fragLightDistance) / (0.1)) - 1.0); - if (edge < 1) { + if (edge < 1.0) { float edgeCoord = exp2(-8.0*edge*edge); color = vec3(edgeCoord * edgeCoord * getLightColor(light)); } - return (edge < 1); + return (edge < 1.0); } <@endfunc@> diff --git a/libraries/render-utils/src/LightSpot.slh b/libraries/render-utils/src/LightSpot.slh index a38851b039..8627dae0eb 100644 --- a/libraries/render-utils/src/LightSpot.slh +++ b/libraries/render-utils/src/LightSpot.slh @@ -66,12 +66,12 @@ bool evalLightSpotEdge(out vec3 color, Light light, vec4 fragLightDirLen, float float edgeDistS = dot(fragLightDistance * vec2(cosSpotAngle, sqrt(1.0 - cosSpotAngle * cosSpotAngle)), -lightVolume_getSpotOutsideNormal2(light.volume)); float edgeDist = min(edgeDistR, edgeDistS); float edge = abs(2.0 * (edgeDist / (0.1)) - 1.0); - if (edge < 1) { + if (edge < 1.0) { float edgeCoord = exp2(-8.0*edge*edge); color = vec3(edgeCoord * edgeCoord * getLightColor(light)); } - return (edge < 1); + return (edge < 1.0); } <@endfunc@> diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index 209a1f38d6..521c4894dc 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -157,7 +157,7 @@ vec4 evalPBRShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float m float power = specularDistribution(roughness, fragNormal, halfDir); vec3 specular = fresnelColor * power * diffuse; - return vec4(specular, (1.0 - metallic) * diffuse * (1 - fresnelColor.x)); + return vec4(specular, (1.0 - metallic) * diffuse * (1.0 - fresnelColor.x)); } // Frag Shading returns the diffuse amount as W and the specular rgb as xyz @@ -171,7 +171,7 @@ vec4 evalPBRShadingDielectric(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDi float power = specularDistribution(roughness, fragNormal, halfDir); vec3 specular = vec3(fresnelScalar) * power * diffuse; - return vec4(specular, diffuse * (1 - fresnelScalar)); + return vec4(specular, diffuse * (1.0 - fresnelScalar)); } vec4 evalPBRShadingMetallic(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, float roughness, vec3 fresnel) { diff --git a/libraries/render-utils/src/MaterialTextures.slh b/libraries/render-utils/src/MaterialTextures.slh index e694935361..6c77fc4a91 100644 --- a/libraries/render-utils/src/MaterialTextures.slh +++ b/libraries/render-utils/src/MaterialTextures.slh @@ -66,7 +66,7 @@ vec3 fetchNormalMap(vec2 uv) { // unpack normal, swizzle to get into hifi tangent space with Y axis pointing out vec2 t = 2.0 * (texture(normalMap, uv).rg - vec2(0.5, 0.5)); vec2 t2 = t*t; - return vec3(t.x, sqrt(1 - t2.x - t2.y), t.y); + return vec3(t.x, sqrt(1.0 - t2.x - t2.y), t.y); } <@endif@> @@ -163,7 +163,7 @@ vec3 fetchLightmapMap(vec2 uv) { vec3 normalizedBitangent = normalize(cross(normalizedNormal, normalizedTangent)); // attenuate the normal map divergence from the mesh normal based on distance // THe attenuation range [20,100] meters from the eye is arbitrary for now - vec3 localNormal = mix(<$fetchedNormal$>, vec3(0.0, 1.0, 0.0), smoothstep(20, 100, (-<$fragPos$>).z)); + vec3 localNormal = mix(<$fetchedNormal$>, vec3(0.0, 1.0, 0.0), smoothstep(20.0, 100.0, (-<$fragPos$>).z)); <$normal$> = vec3(normalizedTangent * localNormal.x + normalizedNormal * localNormal.y + normalizedBitangent * localNormal.z); } <@endfunc@> diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 517fe97dba..e640f54fd4 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -14,7 +14,6 @@ #include #include "DeferredLightingEffect.h" -#include "EntityItem.h" using namespace render; diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 63aeacf80c..3b55cac0b8 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -382,8 +382,8 @@ protected: QVector> _collisionRenderItems; QMap _collisionRenderItemsMap; - QVector> _modelMeshRenderItems; - QMap _modelMeshRenderItemsMap; + QVector> _modelMeshRenderItems; + QMap _modelMeshRenderItemsMap; render::ItemIDs _modelMeshRenderItemIDs; diff --git a/libraries/render-utils/src/PickItemsJob.cpp b/libraries/render-utils/src/PickItemsJob.cpp index 48ba605d7b..860262a969 100644 --- a/libraries/render-utils/src/PickItemsJob.cpp +++ b/libraries/render-utils/src/PickItemsJob.cpp @@ -31,26 +31,26 @@ void PickItemsJob::run(const render::RenderContextPointer& renderContext, const } render::ItemBound PickItemsJob::findNearestItem(const render::RenderContextPointer& renderContext, const render::ItemBounds& inputs, float& minIsectDistance) const { - const glm::vec3 rayOrigin = renderContext->args->getViewFrustum().getPosition(); - const glm::vec3 rayDirection = renderContext->args->getViewFrustum().getDirection(); - BoxFace face; - glm::vec3 normal; - float isectDistance; - render::ItemBound nearestItem( render::Item::INVALID_ITEM_ID ); - const float minDistance = 0.2f; - const float maxDistance = 50.f; + const glm::vec3 rayOrigin = renderContext->args->getViewFrustum().getPosition(); + const glm::vec3 rayDirection = renderContext->args->getViewFrustum().getDirection(); + BoxFace face; + glm::vec3 normal; + float isectDistance; + render::ItemBound nearestItem( render::Item::INVALID_ITEM_ID ); + const float minDistance = 0.2f; + const float maxDistance = 50.f; render::ItemKey itemKey; - for (const auto& itemBound : inputs) { - if (!itemBound.bound.contains(rayOrigin) && itemBound.bound.findRayIntersection(rayOrigin, rayDirection, isectDistance, face, normal)) { - auto& item = renderContext->_scene->getItem(itemBound.id); + for (const auto& itemBound : inputs) { + if (!itemBound.bound.contains(rayOrigin) && itemBound.bound.findRayIntersection(rayOrigin, rayDirection, isectDistance, face, normal)) { + auto& item = renderContext->_scene->getItem(itemBound.id); itemKey = item.getKey(); - if (itemKey.isWorldSpace() && isectDistance>minDistance && isectDistance < minIsectDistance && isectDistanceminDistance && isectDistance < minIsectDistance && isectDistance; + using Config = PickItemsConfig; + using Input = render::ItemBounds; + using Output = render::ItemBounds; + using JobModel = render::Job::ModelIO; PickItemsJob(render::ItemKey::Flags validKeys = render::ItemKey::Builder().withTypeMeta().withTypeShape().build()._flags, render::ItemKey::Flags excludeKeys = 0); - void configure(const Config& config); - void run(const render::RenderContextPointer& renderContext, const PickItemsJob::Input& input, PickItemsJob::Output& output); + void configure(const Config& config); + void run(const render::RenderContextPointer& renderContext, const PickItemsJob::Input& input, PickItemsJob::Output& output); private: @@ -47,7 +47,7 @@ private: render::ItemKey::Flags _excludeKeys; bool _isEnabled{ false }; - render::ItemBound findNearestItem(const render::RenderContextPointer& renderContext, const render::ItemBounds& inputs, float& minIsectDistance) const; + render::ItemBound findNearestItem(const render::RenderContextPointer& renderContext, const render::ItemBounds& inputs, float& minIsectDistance) const; }; #endif // hifi_render_utils_PickItemsJob_h diff --git a/libraries/render-utils/src/SubsurfaceScattering.slh b/libraries/render-utils/src/SubsurfaceScattering.slh index 42ffafd9ff..201ec2291a 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.slh +++ b/libraries/render-utils/src/SubsurfaceScattering.slh @@ -105,7 +105,7 @@ vec3 integrate(float cosTheta, float skinRadius) { vec3 totalLight = vec3(0.0); const float _PI = 3.14159265358979523846; - const float step = 2.0 * _PI / <$NumIntegrationSteps$>; + const float step = 2.0 * _PI / float(<$NumIntegrationSteps$>); float a = -(_PI); diff --git a/libraries/render-utils/src/forward_model.slf b/libraries/render-utils/src/forward_model.slf index daeead65ec..7b708a1d24 100644 --- a/libraries/render-utils/src/forward_model.slf +++ b/libraries/render-utils/src/forward_model.slf @@ -1,7 +1,7 @@ <@include gpu/Config.slh@> <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> -// model.frag +// forward_model.frag // fragment shader // // Created by Andrzej Kapolka on 10/14/13. @@ -11,12 +11,18 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include DeferredBufferWrite.slh@> + !> + +<@include ForwardGlobalLight.slh@> +<$declareEvalSkyboxGlobalColor()$> <@include model/Material.slh@> +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + <@include MaterialTextures.slh@> -<$declareMaterialTextures(ALBEDO, ROUGHNESS, _SCRIBE_NULL, _SCRIBE_NULL, EMISSIVE, OCCLUSION)$> +<$declareMaterialTextures(ALBEDO, ROUGHNESS, _SCRIBE_NULL, _SCRIBE_NULL, EMISSIVE, _SCRIBE_NULL)$> in vec4 _position; in vec3 _normal; @@ -24,12 +30,13 @@ in vec3 _color; in vec2 _texCoord0; in vec2 _texCoord1; +out vec4 _fragColor; void main(void) { Material mat = getMaterial(); int matKey = getMaterialKey(mat); <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, roughnessTex, _SCRIBE_NULL, _SCRIBE_NULL, emissiveTex)$> - <$fetchMaterialTexturesCoord1(matKey, _texCoord1, occlusionTex)$> + !> float opacity = 1.0; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; @@ -39,21 +46,43 @@ void main(void) { <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; albedo *= _color; + float metallic = getMaterialMetallic(mat); + vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value + if (metallic <= 0.5) { + metallic = 0.0; + } else { + fresnel = albedo; + metallic = 1.0; + } + float roughness = getMaterialRoughness(mat); <$evalMaterialRoughness(roughnessTex, roughness, matKey, roughness)$>; vec3 emissive = getMaterialEmissive(mat); <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; - float scattering = getMaterialScattering(mat); - packDeferredFragment( - normalize(_normal.xyz), - opacity, + vec3 fragPosition = _position.xyz; + + TransformCamera cam = getTransformCamera(); + vec3 fragNormal; + <$transformEyeToWorldDir(cam, _normal, fragNormal)$> + + /* vec4 color = vec4(evalSkyboxGlobalColor( + cam._viewInverse, + 1.0, + 1.0, + fragPosition, + fragNormal, albedo, - roughness, - getMaterialMetallic(mat), - emissive, - occlusionTex, - scattering); + fresnel, + metallic, + roughness), + opacity); + color.rgb += emissive * isEmissiveEnabled(); + + */ + + _fragColor = vec4(albedo, opacity); + } diff --git a/libraries/render-utils/src/forward_model_normal_map.slf b/libraries/render-utils/src/forward_model_normal_map.slf index 5cc1a1859f..a199483b9f 100644 --- a/libraries/render-utils/src/forward_model_normal_map.slf +++ b/libraries/render-utils/src/forward_model_normal_map.slf @@ -2,7 +2,7 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// model_normal_map.frag +// forward_model_normal_map.frag // fragment shader // // Created by Andrzej Kapolka on 10/29/13. @@ -12,7 +12,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include DeferredBufferWrite.slh@> +<@include ForwardBufferWrite.slh@> <@include model/Material.slh@> @@ -47,12 +47,12 @@ void main(void) { <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; vec3 viewNormal; - <$tangentToViewSpaceLOD(_position, normalTex, _normal, _tangent, viewNormal)$> + <$tangentToViewSpace(normalTex, _normal, _tangent, viewNormal)$> float scattering = getMaterialScattering(mat); <$evalMaterialScattering(scatteringTex, scattering, matKey, scattering)$>; - packDeferredFragment( + packForwardFragment( viewNormal, opacity, albedo, diff --git a/libraries/render-utils/src/forward_model_normal_specular_map.slf b/libraries/render-utils/src/forward_model_normal_specular_map.slf index 9e079b33a0..ca6bbec3da 100644 --- a/libraries/render-utils/src/forward_model_normal_specular_map.slf +++ b/libraries/render-utils/src/forward_model_normal_specular_map.slf @@ -2,7 +2,7 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// model_normal_specular_map.frag +// forward_model_normal_specular_map.frag // fragment shader // // Created by Andrzej Kapolka on 5/6/14. @@ -12,7 +12,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include DeferredBufferWrite.slh@> +<@include ForwardBufferWrite.slh@> <@include model/Material.slh@> @@ -47,14 +47,14 @@ void main(void) { <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; vec3 viewNormal; - <$tangentToViewSpaceLOD(_position, normalTex, _normal, _tangent, viewNormal)$> + <$tangentToViewSpace(normalTex, _normal, _tangent, viewNormal)$> float metallic = getMaterialMetallic(mat); <$evalMaterialMetallic(metallicTex, metallic, matKey, metallic)$>; float scattering = getMaterialScattering(mat); - packDeferredFragment( + packForwardFragment( normalize(viewNormal.xyz), opacity, albedo, diff --git a/libraries/render-utils/src/forward_model_specular_map.slf b/libraries/render-utils/src/forward_model_specular_map.slf index 47b5e3389d..d2fdd18794 100644 --- a/libraries/render-utils/src/forward_model_specular_map.slf +++ b/libraries/render-utils/src/forward_model_specular_map.slf @@ -2,7 +2,7 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// model_specular_map.frag +// forward_model_specular_map.frag // fragment shader // // Created by Andrzej Kapolka on 5/6/14. @@ -12,7 +12,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include DeferredBufferWrite.slh@> +<@include ForwardBufferWrite.slh@> <@include model/Material.slh@> @@ -51,7 +51,7 @@ void main(void) { float scattering = getMaterialScattering(mat); - packDeferredFragment( + packForwardFragment( normalize(_normal), opacity, albedo, diff --git a/libraries/render-utils/src/forward_model_translucent.slf b/libraries/render-utils/src/forward_model_translucent.slf new file mode 100644 index 0000000000..52e8ce50e7 --- /dev/null +++ b/libraries/render-utils/src/forward_model_translucent.slf @@ -0,0 +1,81 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// forward_model_translucent.frag +// fragment shader +// +// Created by Sam Gateau on 2/15/2016. +// Copyright 2014 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 model/Material.slh@> + +<@include ForwardGlobalLight.slh@> + +<$declareEvalGlobalLightingAlphaBlended()$> + +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + +<@include MaterialTextures.slh@> +<$declareMaterialTextures(ALBEDO, ROUGHNESS, _SCRIBE_NULL, _SCRIBE_NULL, EMISSIVE, OCCLUSION)$> + +in vec2 _texCoord0; +in vec2 _texCoord1; +in vec4 _position; +in vec3 _normal; +in vec3 _color; +in float _alpha; + +out vec4 _fragColor; + +void main(void) { + Material mat = getMaterial(); + int matKey = getMaterialKey(mat); + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, roughnessTex, _SCRIBE_NULL, _SCRIBE_NULL, emissiveTex)$> + <$fetchMaterialTexturesCoord1(matKey, _texCoord1, occlusionTex)$> + + float opacity = getMaterialOpacity(mat) * _alpha; + <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + + vec3 albedo = getMaterialAlbedo(mat); + <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; + albedo *= _color; + + float roughness = getMaterialRoughness(mat); + <$evalMaterialRoughness(roughnessTex, roughness, matKey, roughness)$>; + + float metallic = getMaterialMetallic(mat); + vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value + if (metallic <= 0.5) { + metallic = 0.0; + } else { + fresnel = albedo; + metallic = 1.0; + } + + vec3 emissive = getMaterialEmissive(mat); + <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; + + vec3 fragPosition = _position.xyz; + vec3 fragNormal = normalize(_normal); + + TransformCamera cam = getTransformCamera(); + + _fragColor = vec4(evalGlobalLightingAlphaBlended( + cam._viewInverse, + 1.0, + occlusionTex, + fragPosition, + fragNormal, + albedo, + fresnel, + metallic, + emissive, + roughness, opacity), + opacity); +} diff --git a/libraries/render-utils/src/forward_model_unlit.slf b/libraries/render-utils/src/forward_model_unlit.slf index 750b51fe8c..fb760467c9 100644 --- a/libraries/render-utils/src/forward_model_unlit.slf +++ b/libraries/render-utils/src/forward_model_unlit.slf @@ -2,7 +2,7 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// material_opaque_unlit.frag +// forward_model_unlit.frag // fragment shader // // Created by Sam Gateau on 5/5/2016. @@ -12,7 +12,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include DeferredBufferWrite.slh@> +<@include ForwardBufferWrite.slh@> <@include LightingModel.slh@> <@include model/Material.slh@> @@ -38,7 +38,7 @@ void main(void) { <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; albedo *= _color; - packDeferredFragmentUnlit( + packForwardFragmentUnlit( normalize(_normal), opacity, albedo * isUnlitEnabled()); diff --git a/libraries/render-utils/src/glowLine.slf b/libraries/render-utils/src/glowLine.slf index c0af97930a..6f0281c5ec 100644 --- a/libraries/render-utils/src/glowLine.slf +++ b/libraries/render-utils/src/glowLine.slf @@ -20,7 +20,7 @@ void main(void) { float alpha = 1.0 - abs(inColor.a); // Convert from a linear alpha curve to a sharp peaked one - alpha = pow(alpha, 10); + alpha = pow(alpha, 10.0); // Drop everything where the curve falls off to nearly nothing if (alpha <= 0.05) { diff --git a/libraries/render-utils/src/lightClusters_drawClusterContent.slv b/libraries/render-utils/src/lightClusters_drawClusterContent.slv index c570957dfb..b88e2e9ee2 100644 --- a/libraries/render-utils/src/lightClusters_drawClusterContent.slv +++ b/libraries/render-utils/src/lightClusters_drawClusterContent.slv @@ -53,12 +53,12 @@ void main(void) { - ivec3 cluster = clusterGrid_getCluster(gpu_InstanceID); + ivec3 cluster = clusterGrid_getCluster(gpu_InstanceID()); int numLights = cluster.x + cluster.y; float numLightsScale = clamp(numLights * 0.1, 0.0, 1.0); - ivec3 clusterPos = frustumGrid_indexToCluster(gpu_InstanceID); + ivec3 clusterPos = frustumGrid_indexToCluster(gpu_InstanceID()); float boxScale = 0.99; vec3 eyePos = frustumGrid_clusterPosToEye(clusterPos, vec3((1.0 - boxScale) * 0.5 + (1.0 - numLightsScale) * boxScale * 0.5) + numLightsScale * boxScale * pos.xyz); @@ -69,5 +69,5 @@ void main(void) { TransformCamera cam = getTransformCamera(); <$transformWorldToClipPos(cam, worldPos, gl_Position)$> - varColor = vec4(colorWheel(fract(float(gpu_InstanceID) / float(frustumGrid_numClusters()))), (numLights >0 ? 0.9 : 0.1)); + varColor = vec4(colorWheel(fract(float(gpu_InstanceID()) / float(frustumGrid_numClusters()))), (numLights >0 ? 0.9 : 0.1)); } \ No newline at end of file diff --git a/libraries/render-utils/src/lightClusters_drawClusterFromDepth.slv b/libraries/render-utils/src/lightClusters_drawClusterFromDepth.slv index 5840f11d0a..912c39f93c 100644 --- a/libraries/render-utils/src/lightClusters_drawClusterFromDepth.slv +++ b/libraries/render-utils/src/lightClusters_drawClusterFromDepth.slv @@ -53,7 +53,7 @@ void main(void) { ); vec4 pos = UNIT_BOX[UNIT_BOX_LINE_INDICES[gl_VertexID]]; - ivec3 clusterPos = frustumGrid_indexToCluster(gpu_InstanceID); + ivec3 clusterPos = frustumGrid_indexToCluster(gpu_InstanceID()); vec3 eyePos = frustumGrid_clusterPosToEye(clusterPos, vec3(0.05) + 0.9 * pos.xyz); vec4 worldPos = frustumGrid_eyeToWorld(vec4(eyePos.xyz, 1.0)); @@ -62,5 +62,5 @@ void main(void) { TransformCamera cam = getTransformCamera(); <$transformWorldToClipPos(cam, worldPos, gl_Position)$> - varColor = vec4(colorWheel(fract(float(gpu_InstanceID) / float(frustumGrid_numClusters()))), 0.9); + varColor = vec4(colorWheel(fract(float(gpu_InstanceID()) / float(frustumGrid_numClusters()))), 0.9); } \ No newline at end of file diff --git a/libraries/render-utils/src/lightClusters_drawGrid.slv b/libraries/render-utils/src/lightClusters_drawGrid.slv index 1475666ec6..aac7fe59a5 100644 --- a/libraries/render-utils/src/lightClusters_drawGrid.slv +++ b/libraries/render-utils/src/lightClusters_drawGrid.slv @@ -54,10 +54,10 @@ void main(void) { vec4 pos = UNIT_BOX[UNIT_BOX_LINE_INDICES[gl_VertexID]]; - ivec3 cluster = clusterGrid_getCluster(gpu_InstanceID); + ivec3 cluster = clusterGrid_getCluster(gpu_InstanceID()); int numLights = cluster.x + cluster.y; - ivec3 clusterPos = frustumGrid_indexToCluster(gpu_InstanceID); + ivec3 clusterPos = frustumGrid_indexToCluster(gpu_InstanceID()); float boxScale = 1.0; @@ -69,5 +69,5 @@ void main(void) { TransformCamera cam = getTransformCamera(); <$transformWorldToClipPos(cam, worldPos, gl_Position)$> - varColor = vec4(colorWheel(fract(float(gpu_InstanceID) / float(frustumGrid_numClusters()))), (numLights > 0 ? 0.9 : 0.0)); + varColor = vec4(colorWheel(fract(float(gpu_InstanceID()) / float(frustumGrid_numClusters()))), (numLights > 0 ? 0.9 : 0.0)); } \ No newline at end of file diff --git a/libraries/render-utils/src/simple.slf b/libraries/render-utils/src/simple.slf index 228560f394..1e3b6908cd 100644 --- a/libraries/render-utils/src/simple.slf +++ b/libraries/render-utils/src/simple.slf @@ -72,7 +72,7 @@ void main(void) { normal, 1.0, diffuse, - max(0, 1.0 - shininess / 128.0), + max(0.0, 1.0 - shininess / 128.0), DEFAULT_METALLIC, specular, specular); @@ -81,7 +81,7 @@ void main(void) { normal, 1.0, diffuse, - max(0, 1.0 - shininess / 128.0), + max(0.0, 1.0 - shininess / 128.0), length(specular), DEFAULT_EMISSIVE, DEFAULT_OCCLUSION, diff --git a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf index e96ac60b45..ecbc60b648 100644 --- a/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf +++ b/libraries/render-utils/src/surfaceGeometry_makeCurvature.slf @@ -123,7 +123,7 @@ void main(void) { // Calculate dF/du and dF/dv vec2 viewportScale = perspectiveScale * getInvWidthHeight(); - vec2 du = vec2( viewportScale.x * (stereoSide.w > 0.0 ? 0.5 : 1.0), 0.0f ); + vec2 du = vec2( viewportScale.x * (float(stereoSide.w) > 0.0 ? 0.5 : 1.0), 0.0f ); vec2 dv = vec2( 0.0f, viewportScale.y ); vec4 dFdu = vec4(getWorldNormalDiff(frameTexcoordPos, du), getEyeDepthDiff(frameTexcoordPos, du)); diff --git a/libraries/render/CMakeLists.txt b/libraries/render/CMakeLists.txt index 561dff4290..8fd05bd320 100644 --- a/libraries/render/CMakeLists.txt +++ b/libraries/render/CMakeLists.txt @@ -2,8 +2,7 @@ set(TARGET_NAME render) AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() -link_hifi_libraries(shared ktx gpu model) # render needs octree only for getAccuracyAngle(float, int) -include_hifi_library_headers(octree) +link_hifi_libraries(shared ktx gpu model octree) target_nsight() diff --git a/libraries/render/src/render/Engine.h b/libraries/render/src/render/Engine.h index 240693b422..1650d09c5d 100644 --- a/libraries/render/src/render/Engine.h +++ b/libraries/render/src/render/Engine.h @@ -16,7 +16,7 @@ #include "Scene.h" #include "../task/Task.h" -#include "gpu/Batch.h" +#include namespace render { diff --git a/libraries/render/src/render/Item.h b/libraries/render/src/render/Item.h index 21638fbb70..96faf9719e 100644 --- a/libraries/render/src/render/Item.h +++ b/libraries/render/src/render/Item.h @@ -25,7 +25,7 @@ #include "Args.h" -#include "model/Material.h" +#include #include "ShapePipeline.h" namespace render { diff --git a/libraries/render/src/render/TransitionStage.h b/libraries/render/src/render/TransitionStage.h index 8dfef1b78e..226d531d8b 100644 --- a/libraries/render/src/render/TransitionStage.h +++ b/libraries/render/src/render/TransitionStage.h @@ -18,19 +18,19 @@ namespace render { - // Transition stage to set up Transition-related effects - class TransitionStage : public render::Stage { - public: + // Transition stage to set up Transition-related effects + class TransitionStage : public render::Stage { + public: static const std::string& getName() { return _name; } - using Index = indexed_container::Index; - static const Index INVALID_INDEX{ indexed_container::INVALID_INDEX }; + using Index = indexed_container::Index; + static const Index INVALID_INDEX{ indexed_container::INVALID_INDEX }; using TransitionIdList = indexed_container::Indices; static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } - bool checkTransitionId(Index index) const { return _transitions.checkIndex(index); } + bool checkTransitionId(Index index) const { return _transitions.checkIndex(index); } const Transition& getTransition(Index TransitionId) const { return _transitions.get(TransitionId); } @@ -48,10 +48,10 @@ namespace render { static std::string _name; - Transitions _transitions; + Transitions _transitions; TransitionIdList _activeTransitionIds; - }; - using TransitionStagePointer = std::shared_ptr; + }; + using TransitionStagePointer = std::shared_ptr; class TransitionStageSetup { public: diff --git a/libraries/render/src/render/drawItemBounds.slf b/libraries/render/src/render/drawItemBounds.slf index 4fb23df8f6..e01d1607bd 100644 --- a/libraries/render/src/render/drawItemBounds.slf +++ b/libraries/render/src/render/drawItemBounds.slf @@ -19,7 +19,7 @@ out vec4 outFragColor; void main(void) { float var = step(fract(varTexcoord.x * varTexcoord.y * 1.0), 0.5); - if (varColor.a == 0) { + if (varColor.a == 0.0) { outFragColor = vec4(mix(vec3(0.0), varColor.xyz, var), mix(0.0, 1.0, var)); } else { diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 6dc3188b3f..fef11c12e9 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -33,7 +33,7 @@ bool UsersScriptingInterface::getIgnoreStatus(const QUuid& nodeID) { void UsersScriptingInterface::personalMute(const QUuid& nodeID, bool muteEnabled) { // ask the NodeList to mute the user with the given session ID - // "Personal Mute" only applies one way and is not global + // "Personal Mute" only applies one way and is not global DependencyManager::get()->personalMuteNodeBySessionID(nodeID, muteEnabled); } diff --git a/libraries/script-engine/src/Vec3.cpp b/libraries/script-engine/src/Vec3.cpp index a156f56d96..c21f96cd47 100644 --- a/libraries/script-engine/src/Vec3.cpp +++ b/libraries/script-engine/src/Vec3.cpp @@ -90,6 +90,6 @@ glm::vec3 Vec3::fromPolar(float elevation, float azimuth) { } float Vec3::getAngle(const glm::vec3& v1, const glm::vec3& v2) { - return glm::acos(glm::dot(glm::normalize(v1), glm::normalize(v2))); + return glm::acos(glm::dot(glm::normalize(v1), glm::normalize(v2))); } diff --git a/libraries/script-engine/src/WebSocketClass.cpp b/libraries/script-engine/src/WebSocketClass.cpp index 19148b26e9..76faaab415 100644 --- a/libraries/script-engine/src/WebSocketClass.cpp +++ b/libraries/script-engine/src/WebSocketClass.cpp @@ -25,7 +25,7 @@ WebSocketClass::WebSocketClass(QScriptEngine* engine, QString url) : WebSocketClass::WebSocketClass(QScriptEngine* engine, QWebSocket* qWebSocket) : _webSocket(qWebSocket), - _engine(engine) + _engine(engine) { initialize(); } diff --git a/libraries/shared/CMakeLists.txt b/libraries/shared/CMakeLists.txt index 1fefda06b3..f9b835df5c 100644 --- a/libraries/shared/CMakeLists.txt +++ b/libraries/shared/CMakeLists.txt @@ -3,8 +3,6 @@ set(TARGET_NAME shared) # TODO: there isn't really a good reason to have Script linked here - let's get what is requiring it out (RegisteredMetaTypes.cpp) setup_hifi_library(Gui Network Script Widgets) -target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/includes") - if (WIN32) target_link_libraries(${TARGET_NAME} Wbemuuid.lib) endif() diff --git a/libraries/shared/src/DependencyManager.h b/libraries/shared/src/DependencyManager.h index 7a453e63c3..e6fc7ce96b 100644 --- a/libraries/shared/src/DependencyManager.h +++ b/libraries/shared/src/DependencyManager.h @@ -12,6 +12,7 @@ #ifndef hifi_DependencyManager_h #define hifi_DependencyManager_h +#include #include #include #include @@ -64,6 +65,15 @@ public: template static void registerInheritance(); + template + static size_t typeHash() { +#ifdef Q_OS_ANDROID + size_t hashCode = std::hash{}( typeid(T).name() ); +#else + size_t hashCode = typeid(T).hash_code(); +#endif + return hashCode; + } private: static DependencyManager& manager(); @@ -134,14 +144,14 @@ void DependencyManager::destroy() { template void DependencyManager::registerInheritance() { - size_t baseHashCode = typeid(Base).hash_code(); - size_t derivedHashCode = typeid(Derived).hash_code(); + size_t baseHashCode = typeHash(); + size_t derivedHashCode = typeHash(); manager()._inheritanceHash.insert(baseHashCode, derivedHashCode); } template size_t DependencyManager::getHashCode() { - size_t hashCode = typeid(T).hash_code(); + size_t hashCode = typeHash(); auto derivedHashCode = _inheritanceHash.find(hashCode); while (derivedHashCode != _inheritanceHash.end()) { diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 0636411f51..cd7a6530f9 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "PathUtils.h" + #include #include #include @@ -16,7 +18,6 @@ #include #include #include -#include "PathUtils.h" #include #include // std::once #include "shared/GlobalAppProperties.h" @@ -92,18 +93,17 @@ QUrl PathUtils::defaultScriptsLocation(const QString& newDefaultPath) { if (!overriddenDefaultScriptsLocation.isEmpty()) { path = overriddenDefaultScriptsLocation; } else { -#ifdef Q_OS_WIN - path = QCoreApplication::applicationDirPath() + "/scripts"; -#elif defined(Q_OS_OSX) +#if defined(Q_OS_OSX) path = QCoreApplication::applicationDirPath() + "/../Resources/scripts"; +#elif defined(Q_OS_ANDROID) + path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/scripts"; #else path = QCoreApplication::applicationDirPath() + "/scripts"; #endif } // turn the string into a legit QUrl - QFileInfo fileInfo(path); - return QUrl::fromLocalFile(fileInfo.canonicalFilePath()); + return QUrl::fromLocalFile(QFileInfo(path).canonicalFilePath()); } QString PathUtils::stripFilename(const QUrl& url) { diff --git a/libraries/shared/src/Preferences.h b/libraries/shared/src/Preferences.h index 6fa2cb9b1f..a243a6d58d 100644 --- a/libraries/shared/src/Preferences.h +++ b/libraries/shared/src/Preferences.h @@ -257,12 +257,12 @@ public: }; class SpinnerSliderPreference : public FloatPreference { - Q_OBJECT + Q_OBJECT public: - SpinnerSliderPreference(const QString& category, const QString& name, Getter getter, Setter setter) - : FloatPreference(category, name, getter, setter) { } + SpinnerSliderPreference(const QString& category, const QString& name, Getter getter, Setter setter) + : FloatPreference(category, name, getter, setter) { } - Type getType() override { return SpinnerSlider; } + Type getType() override { return SpinnerSlider; } }; class IntSpinnerPreference : public IntPreference { diff --git a/libraries/shared/src/SettingHandle.h b/libraries/shared/src/SettingHandle.h index 341a4cb101..e77ee00b05 100644 --- a/libraries/shared/src/SettingHandle.h +++ b/libraries/shared/src/SettingHandle.h @@ -39,7 +39,7 @@ public: QStringList childKeys() const; QStringList allKeys() const; bool contains(const QString& key) const; - int beginReadArray(const QString & prefix); + int beginReadArray(const QString & prefix); void beginWriteArray(const QString& prefix, int size = -1); void endArray(); void setArrayIndex(int i); diff --git a/libraries/shared/src/shared/PlatformHacks.h b/libraries/shared/src/shared/PlatformHacks.h new file mode 100644 index 0000000000..909f0bac9a --- /dev/null +++ b/libraries/shared/src/shared/PlatformHacks.h @@ -0,0 +1,18 @@ +// +// Created by Bradley Austin Davis on 2017/09/15 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once +#ifndef hifi_shared_PlatformHacks_h +#define hifi_shared_PlatformHacks_h + +#include + +#if defined(Q_OS_ANDROID) +#include "platform/AndroidHacks.h" +#endif + +#endif diff --git a/libraries/shared/src/shared/platform/AndroidHacks.h b/libraries/shared/src/shared/platform/AndroidHacks.h new file mode 100644 index 0000000000..29ab013f98 --- /dev/null +++ b/libraries/shared/src/shared/platform/AndroidHacks.h @@ -0,0 +1,50 @@ +// +// androidhacks.h +// interface/src +// +// Created by Cristian Duarte & Gabriel Calero on 1/4/17. +// Copyright 2017 High Fidelity, Inc. +// +// hacks to get android to compile +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#pragma once +#ifndef hifi_shared_platform_androidhacks_h +#define hifi_shared_platform_androidhacks_h + +#include +#include + +#include + +// Only for gnu stl, so checking if using llvm +// (If there is a better check than this http://stackoverflow.com/questions/31657499/how-to-detect-stdlib-libc-in-the-preprocessor, improve this one) +#if (_LIBCPP_VERSION) + // NOTHING, these functions are well defined on libc++ +#else + +using namespace std; +namespace std +{ + // to_string impl + // error: no member named 'to_string' in namespace 'std' + // http://stackoverflow.com/questions/26095886/error-to-string-is-not-a-member-of-std + template + inline std::string to_string(T value) { + std::ostringstream os; + os << value; + return os.str(); + } + + inline float stof(std::string str) { + return atof(str.c_str()); + } +} + +#endif // _LIBCPP_VERSION + +#endif // hifi_androidhacks_h diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt index 68a6fd25b9..f28157ff97 100644 --- a/libraries/ui/CMakeLists.txt +++ b/libraries/ui/CMakeLists.txt @@ -2,7 +2,5 @@ set(TARGET_NAME ui) setup_hifi_library(OpenGL Network Qml Quick Script WebChannel WebEngine WebSockets XmlPatterns) link_hifi_libraries(shared networking gl audio) -if (NOT ANDROID) - # Required for some low level GL interaction in the OffscreenQMLSurface - target_glew() -endif () +# Required for some low level GL interaction in the OffscreenQMLSurface +target_opengl() diff --git a/libraries/ui/src/VrMenu.cpp b/libraries/ui/src/VrMenu.cpp index 70daff944a..12cb7e2e4b 100644 --- a/libraries/ui/src/VrMenu.cpp +++ b/libraries/ui/src/VrMenu.cpp @@ -170,7 +170,7 @@ void VrMenu::addMenu(QMenu* menu) { QObject* parent = menu->parent(); QObject* qmlParent = nullptr; QMenu* parentMenu = dynamic_cast(parent); - if (parentMenu) { + if (parentMenu && menu->menuAction()) { MenuUserData* userData = MenuUserData::forObject(parentMenu->menuAction()); if (!userData) { return; diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index d03842d45a..6125aea15c 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -827,7 +827,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even switch (event->type()) { case QEvent::Resize: { QResizeEvent* resizeEvent = static_cast(event); - QWidget* widget = dynamic_cast(originalDestination); + QWidget* widget = static_cast(originalDestination); if (widget) { this->resize(resizeEvent->size()); } @@ -932,7 +932,7 @@ void OffscreenQmlSurface::focusDestroyed(QObject *obj) { } void OffscreenQmlSurface::onFocusObjectChanged(QObject* object) { - QQuickItem* item = dynamic_cast(object); + QQuickItem* item = static_cast(object); if (!item) { setFocusText(false); _currentFocusItem = nullptr; @@ -1019,6 +1019,10 @@ void OffscreenQmlSurface::synthesizeKeyPress(QString key) { } void OffscreenQmlSurface::setKeyboardRaised(QObject* object, bool raised, bool numeric) { +#if Q_OS_ANDROID + return; +#endif + if (!object) { return; } diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index 18844a1995..3d06a4b223 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -88,7 +88,7 @@ ovrSession acquireOculusSession() { } if (!session) { - ovrInitParams initParams { + ovrInitParams initParams { ovrInit_RequestVersion | ovrInit_MixedRendering, OVR_MINOR_VERSION, nullptr, 0, 0 }; diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 80c8698bb6..804ff7d62e 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -181,7 +181,7 @@ bool OculusLegacyDisplayPlugin::internalActivate() { } void OculusLegacyDisplayPlugin::internalDeactivate() { - Parent::internalDeactivate(); + Parent::internalDeactivate(); ovrHmd_Destroy(_hmd); _hmd = nullptr; ovr_Shutdown(); diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h index 20345467df..36bdd1c792 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h @@ -17,7 +17,7 @@ const float TARGET_RATE_OculusLegacy = 75.0f; class GLWindow; class OculusLegacyDisplayPlugin : public HmdDisplayPlugin { - using Parent = HmdDisplayPlugin; + using Parent = HmdDisplayPlugin; public: OculusLegacyDisplayPlugin(); bool isSupported() const override; diff --git a/tests/shaders/src/main.cpp b/tests/shaders/src/main.cpp index 9847e9f7b9..7c6886ad93 100644 --- a/tests/shaders/src/main.cpp +++ b/tests/shaders/src/main.cpp @@ -19,6 +19,7 @@ #include #include +#include #include @@ -114,13 +115,37 @@ public: } }; + + +const std::string VERTEX_SHADER_DEFINES{ R"GLSL( +#version 410 core +#define GPU_VERTEX_SHADER +#define GPU_TRANSFORM_IS_STEREO +#define GPU_TRANSFORM_STEREO_CAMERA +#define GPU_TRANSFORM_STEREO_CAMERA_INSTANCED +#define GPU_TRANSFORM_STEREO_SPLIT_SCREEN +)GLSL" }; + +const std::string PIXEL_SHADER_DEFINES{ R"GLSL( +#version 410 core +#define GPU_PIXEL_SHADER +#define GPU_TRANSFORM_IS_STEREO +#define GPU_TRANSFORM_STEREO_CAMERA +#define GPU_TRANSFORM_STEREO_CAMERA_INSTANCED +#define GPU_TRANSFORM_STEREO_SPLIT_SCREEN +)GLSL" }; + void testShaderBuild(const char* vs_src, const char * fs_src) { - auto vs = gpu::Shader::createVertex(std::string(vs_src)); - auto fs = gpu::Shader::createPixel(std::string(fs_src)); - auto pr = gpu::Shader::createProgram(vs, fs); - if (!gpu::Shader::makeProgram(*pr)) { + std::string error; + GLuint vs, fs; + if (!gl::compileShader(GL_VERTEX_SHADER, vs_src, VERTEX_SHADER_DEFINES, vs, error) || + !gl::compileShader(GL_FRAGMENT_SHADER, fs_src, PIXEL_SHADER_DEFINES, fs, error)) { throw std::runtime_error("Failed to compile shader"); } + auto pr = gl::compileProgram({ vs, fs }, error); + if (!pr) { + throw std::runtime_error("Failed to link shader"); + } } void QTestWindow::draw() { diff --git a/tools/oven/src/ui/BakeWidget.cpp b/tools/oven/src/ui/BakeWidget.cpp index 23a4822d82..9fb8f2f880 100644 --- a/tools/oven/src/ui/BakeWidget.cpp +++ b/tools/oven/src/ui/BakeWidget.cpp @@ -17,7 +17,7 @@ #include "BakeWidget.h" BakeWidget::BakeWidget(QWidget* parent, Qt::WindowFlags flags) : - QWidget(parent, flags) + QWidget(parent, flags) { } From 41647305ed8412e9b3aaa9ff90f6007296ef0590 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 20 Sep 2017 11:11:18 +1200 Subject: [PATCH 363/722] Rename selection manager object to avoid name clash with new API object --- scripts/vr-edit/modules/selection.js | 10 +++++----- scripts/vr-edit/vr-edit.js | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index c0d3333a77..a01eef5481 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -8,9 +8,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Selection */ +/* global SelectionManager */ -Selection = function (side) { +SelectionManager = function (side) { // Manages set of selected entities. Currently supports just one set of linked entities. "use strict"; @@ -38,8 +38,8 @@ Selection = function (side) { - if (!this instanceof Selection) { - return new Selection(side); + if (!this instanceof SelectionManager) { + return new SelectionManager(side); } function traverseEntityTree(id, selection, selectionProperties) { @@ -781,4 +781,4 @@ Selection = function (side) { }; }; -Selection.prototype = {}; +SelectionManager.prototype = {}; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index e0a54578ff..ea1ac86254 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -59,7 +59,7 @@ Handles, Highlights, Laser, - Selection, + SelectionManager, ToolIcon, ToolsMenu, @@ -400,7 +400,7 @@ return new Editor(); } - selection = new Selection(side); + selection = new SelectionManager(side); highlights = new Highlights(side); handles = new Handles(side); From fd8868f609874680b0254f75cad4a7165e9e5f2e Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 19 Sep 2017 17:30:20 -0700 Subject: [PATCH 364/722] Moving the render transform evaualtion in game loop for Models --- .../src/EntityTreeRenderer.cpp | 2 +- libraries/render-utils/src/Model.cpp | 64 +++---------------- libraries/render-utils/src/Model.h | 1 - 3 files changed, 9 insertions(+), 58 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index e8ad163964..2aa9c20704 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -1026,4 +1026,4 @@ void EntityTreeRenderer::onEntityChanged(const EntityItemID& id) { _changedEntitiesGuard.withWriteLock([&] { _changedEntities.insert(id); }); -} +} \ No newline at end of file diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 42bb91ce94..1f45647c0d 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -233,18 +233,20 @@ void Model::updateRenderItems() { // We need to update them here so we can correctly update the bounding box. self->updateClusterMatrices(); + Transform modelTransform = self->getTransform(); + Transform physicsTransform = modelTransform; + modelTransform.setScale(glm::vec3(1.0f)); + uint32_t deleteGeometryCounter = self->_deleteGeometryCounter; render::Transaction transaction; foreach (auto itemID, self->_modelMeshRenderItemsMap.keys()) { - transaction.updateItem(itemID, [deleteGeometryCounter](ModelMeshPartPayload& data) { + transaction.updateItem(itemID, [deleteGeometryCounter, modelTransform](ModelMeshPartPayload& data) { ModelPointer model = data._model.lock(); if (model && model->isLoaded()) { // Ensure the model geometry was not reset between frames if (deleteGeometryCounter == model->_deleteGeometryCounter) { - Transform modelTransform = model->getTransform(); - modelTransform.setScale(glm::vec3(1.0f)); - + const Model::MeshState& state = model->getMeshState(data._meshIndex); Transform renderTransform = modelTransform; if (state.clusterMatrices.size() == 1) { @@ -259,11 +261,10 @@ void Model::updateRenderItems() { // collision mesh does not share the same unit scale as the FBX file's mesh: only apply offset Transform collisionMeshOffset; collisionMeshOffset.setIdentity(); - Transform modelTransform = self->getTransform(); foreach(auto itemID, self->_collisionRenderItemsMap.keys()) { - transaction.updateItem(itemID, [modelTransform, collisionMeshOffset](MeshPartPayload& data) { + transaction.updateItem(itemID, [physicsTransform, collisionMeshOffset](MeshPartPayload& data) { // update the model transform for this render item. - data.updateTransform(modelTransform, collisionMeshOffset); + data.updateTransform(physicsTransform, collisionMeshOffset); }); } @@ -1311,55 +1312,6 @@ bool Model::isRenderable() const { return !_meshStates.isEmpty() || (isLoaded() && _renderGeometry->getMeshes().empty()); } -bool Model::initWhenReady(const render::ScenePointer& scene) { - // NOTE: this only called by SkeletonModel - if (_addedToScene || !isRenderable()) { - return false; - } - - createRenderItemSet(); - - render::Transaction transaction; - - bool addedTransaction = false; - if (_collisionGeometry) { - foreach (auto renderItem, _collisionRenderItems) { - auto item = scene->allocateID(); - auto renderPayload = std::make_shared(renderItem); - _collisionRenderItemsMap.insert(item, renderPayload); - transaction.resetItem(item, renderPayload); - } - addedTransaction = !_collisionRenderItems.empty(); - } else { - bool hasTransparent = false; - size_t verticesCount = 0; - foreach (auto renderItem, _modelMeshRenderItems) { - auto item = scene->allocateID(); - auto renderPayload = std::make_shared(renderItem); - - hasTransparent = hasTransparent || renderItem.get()->getShapeKey().isTranslucent(); - verticesCount += renderItem.get()->getVerticesCount(); - _modelMeshRenderItemsMap.insert(item, renderPayload); - transaction.resetItem(item, renderPayload); - } - addedTransaction = !_modelMeshRenderItemsMap.empty(); - _renderInfoVertexCount = verticesCount; - _renderInfoDrawCalls = _modelMeshRenderItemsMap.count(); - _renderInfoHasTransparent = hasTransparent; - } - _addedToScene = addedTransaction; - if (addedTransaction) { - scene->enqueueTransaction(transaction); - // NOTE: updateRender items enqueues identical transaction (using a lambda) - // so it looks like we're doing double work here, but I don't want to remove the call - // for fear there is some side effect we'll miss. -- Andrew 2016.07.21 - // TODO: figure out if we really need this call to updateRenderItems() or not. - updateRenderItems(); - } - - return true; -} - class CollisionRenderGeometry : public Geometry { public: CollisionRenderGeometry(model::MeshPointer mesh) { diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 63aeacf80c..87ee754b93 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -87,7 +87,6 @@ public: bool needsFixupInScene() const; bool needsReload() const { return _needsReload; } - bool initWhenReady(const render::ScenePointer& scene); bool addToScene(const render::ScenePointer& scene, render::Transaction& transaction) { auto getters = render::Item::Status::Getters(0); From 809e986738066de9c7302f162def7ecb85c483d7 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 14 Sep 2017 15:49:09 -0700 Subject: [PATCH 365/722] Fixes for lasers on 2d HUD --- interface/src/Application.cpp | 13 ++----------- .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5700bb6a72..8c64a626c2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2637,17 +2637,8 @@ void Application::paintGL() { PerformanceTimer perfTimer("postComposite"); renderArgs._batch = &postCompositeBatch; renderArgs._batch->setViewportTransform(ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height())); - for_each_eye([&](Eye eye) { - - // apply eye offset and IPD scale to the view matrix - mat4 eyeToHead = displayPlugin->getEyeToHeadTransform(eye); - vec3 eyeOffset = glm::vec3(eyeToHead[3]); - mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); - renderArgs._batch->setViewTransform(renderArgs.getViewFrustum().getView() * eyeOffsetTransform); - - renderArgs._batch->setProjectionTransform(eyeProjections[eye]); - _overlays.render3DHUDOverlays(&renderArgs); - }); + renderArgs._batch->setViewTransform(renderArgs.getViewFrustum().getView()); + _overlays.render3DHUDOverlays(&renderArgs); } auto frame = _gpuContext->endFrame(); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index caeba37839..88ec94eefb 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -441,7 +441,7 @@ void HmdDisplayPlugin::OverlayRenderer::updatePipeline() { this->uniformsLocation = program->getUniformBuffers().findLocation("overlayBuffer"); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - state->setDepthTest(gpu::State::DepthTest(true, true, gpu::LESS_EQUAL)); + state->setDepthTest(gpu::State::DepthTest(false, false, gpu::LESS_EQUAL)); state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); From 0873b88347e4f26ce294344749bed38741ba6cde Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 14 Sep 2017 17:09:10 -0700 Subject: [PATCH 366/722] code review feedback --- interface/src/Application.cpp | 2 +- scripts/system/notifications.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8c64a626c2..f0a396672f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6676,7 +6676,7 @@ void Application::addAssetToWorldCheckModelSize() { if (dimensions != DEFAULT_DIMENSIONS) { // Scale model so that its maximum is exactly specific size. - const float MAXIMUM_DIMENSION = 1.0f * getMyAvatar()->getSensorToWorldScale(); + const float MAXIMUM_DIMENSION = getMyAvatar()->getSensorToWorldScale(); auto previousDimensions = dimensions; auto scale = std::min(MAXIMUM_DIMENSION / dimensions.x, std::min(MAXIMUM_DIMENSION / dimensions.y, MAXIMUM_DIMENSION / dimensions.z)); diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 53cffa18d8..ffe93d13e8 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -249,7 +249,7 @@ noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; noticeHeight = notice.height * NOTIFICATION_3D_SCALE; - notice.size = { x: noticeWidth, y: noticeHeight}; + notice.size = { x: noticeWidth, y: noticeHeight }; positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); From 8144841258c7dfe29ea9fcfca4e16d54cf53618a Mon Sep 17 00:00:00 2001 From: Daniela Date: Fri, 15 Sep 2017 12:41:15 +0100 Subject: [PATCH 367/722] Add Edit.js Entities spawn up according to the orientation of the avatar. --- scripts/system/edit.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 569d4812dc..dca07a2fac 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -247,9 +247,12 @@ var toolBar = (function () { direction = MyAvatar.orientation; } direction = Vec3.multiplyQbyV(direction, Vec3.UNIT_Z); - + // Align entity with Avatar orientation. + properties.rotation = MyAvatar.orientation; + var PRE_ADJUST_ENTITY_TYPES = ["Box", "Sphere", "Shape", "Text", "Web"]; if (PRE_ADJUST_ENTITY_TYPES.indexOf(properties.type) !== -1) { + // Adjust position of entity per bounding box prior to creating it. var registration = properties.registration; if (registration === undefined) { @@ -259,7 +262,14 @@ var toolBar = (function () { var orientation = properties.orientation; if (orientation === undefined) { - var DEFAULT_ORIENTATION = Quat.fromPitchYawRollDegrees(0, 0, 0); + properties.orientation = MyAvatar.orientation; + var DEFAULT_ORIENTATION = properties.orientation; + orientation = DEFAULT_ORIENTATION; + } else { + // If the orientation is already defined, we perform the corresponding rotation assuming that + // our start referential is the avatar referential. + properties.orientation = Quat.multiply(MyAvatar.orientation, properties.orientation); + var DEFAULT_ORIENTATION = properties.orientation; orientation = DEFAULT_ORIENTATION; } From a8a5138c18d336d12a4a30ff50810e82c4576cea Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 18 Sep 2017 13:30:34 -0700 Subject: [PATCH 368/722] remove USE_N_SCALE preprocessor check --- interface/src/ui/overlays/ModelOverlay.cpp | 2 -- interface/src/ui/overlays/Sphere3DOverlay.cpp | 2 -- interface/src/ui/overlays/Volume3DOverlay.cpp | 2 -- 3 files changed, 6 deletions(-) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index e2a7df7ae6..8ab44faaf9 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -47,9 +47,7 @@ void ModelOverlay::update(float deltatime) { _updateModel = false; _model->setSnapModelToCenter(true); Transform transform = getTransform(); -#ifndef USE_SN_SCALE transform.setScale(1.0f); // disable inherited scale -#endif if (_scaleToFit) { _model->setScaleToFit(true, transform.getScale() * getDimensions()); } else { diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index 83dc4b0e2b..3b3fe9d2bc 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -41,9 +41,7 @@ void Sphere3DOverlay::render(RenderArgs* args) { if (batch) { // FIXME Start using the _renderTransform instead of calling for Transform and Dimensions from here, do the custom things needed in evalRenderTransform() Transform transform = getTransform(); -#ifndef USE_SN_SCALE transform.setScale(1.0f); // ignore inherited scale from SpatiallyNestable -#endif transform.postScale(getDimensions() * SPHERE_OVERLAY_SCALE); batch->setModelTransform(transform); diff --git a/interface/src/ui/overlays/Volume3DOverlay.cpp b/interface/src/ui/overlays/Volume3DOverlay.cpp index 5e3e4ccee7..3f94c1fa7b 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.cpp +++ b/interface/src/ui/overlays/Volume3DOverlay.cpp @@ -57,9 +57,7 @@ bool Volume3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::ve // extents is the entity relative, scaled, centered extents of the entity glm::mat4 worldToEntityMatrix; Transform transform = getTransform(); -#ifndef USE_SN_SCALE transform.setScale(1.0f); // ignore any inherited scale from SpatiallyNestable -#endif transform.getInverseMatrix(worldToEntityMatrix); glm::vec3 overlayFrameOrigin = glm::vec3(worldToEntityMatrix * glm::vec4(origin, 1.0f)); From c17ca955dbdadbd484276daeb3347faaf7c15567 Mon Sep 17 00:00:00 2001 From: vladest Date: Wed, 20 Sep 2017 20:03:53 +0200 Subject: [PATCH 369/722] Listener disconnects in C++ now --- scripts/system/edit.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index a6d09f3ea6..350d1bd8a3 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1463,7 +1463,6 @@ function onFileSaveChanged(filename) { } function onFileOpenChanged(filename) { - Window.openFileChanged.disconnect(onFileOpenChanged); var importURL = null; if (filename !== "") { importURL = "file:///" + filename; From 6e3cfe81ab5288cbef675e8b9f492e66900c1b56 Mon Sep 17 00:00:00 2001 From: vladest Date: Wed, 20 Sep 2017 21:01:16 +0200 Subject: [PATCH 370/722] Fix crash on second bookmark saving --- interface/src/Bookmarks.cpp | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/interface/src/Bookmarks.cpp b/interface/src/Bookmarks.cpp index 0bd6b01128..f48b5e1f5b 100644 --- a/interface/src/Bookmarks.cpp +++ b/interface/src/Bookmarks.cpp @@ -58,20 +58,25 @@ void Bookmarks::addBookmarkToFile(const QString& bookmarkName, const QVariant& b Menu* menubar = Menu::getInstance(); if (contains(bookmarkName)) { auto offscreenUi = DependencyManager::get(); - auto duplicateBookmarkMessage = offscreenUi->createMessageBox(OffscreenUi::ICON_WARNING, "Duplicate Bookmark", - "The bookmark name you entered already exists in your list.", - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - duplicateBookmarkMessage->setProperty("informativeText", "Would you like to overwrite it?"); - auto result = offscreenUi->waitForMessageBoxResult(duplicateBookmarkMessage); - if (result != QMessageBox::Yes) { - return; - } - removeBookmarkFromMenu(menubar, bookmarkName); - } + ModalDialogListener* dlg = OffscreenUi::asyncWarning("Duplicate Bookmark", + "The bookmark name you entered already exists in your list.", + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + dlg->setProperty("informativeText", "Would you like to overwrite it?"); + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - addBookmarkToMenu(menubar, bookmarkName, bookmark); - insert(bookmarkName, bookmark); // Overwrites any item with the same bookmarkName. - enableMenuItems(true); + if (QMessageBox::Yes == static_cast(answer.toInt())) { + removeBookmarkFromMenu(menubar, bookmarkName); + addBookmarkToMenu(menubar, bookmarkName, bookmark); + insert(bookmarkName, bookmark); // Overwrites any item with the same bookmarkName. + enableMenuItems(true); + } + }); + } else { + addBookmarkToMenu(menubar, bookmarkName, bookmark); + insert(bookmarkName, bookmark); // Overwrites any item with the same bookmarkName. + enableMenuItems(true); + } } void Bookmarks::insert(const QString& name, const QVariant& bookmark) { From 09c61deda835ccb8e66ccd779ade9be4a33b0dd6 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 20 Sep 2017 15:41:49 -0700 Subject: [PATCH 371/722] Fixes for Vive puck calibration --- interface/src/avatar/MyAvatar.cpp | 40 ++++++------ libraries/shared/src/GLMHelpers.cpp | 8 +++ libraries/shared/src/GLMHelpers.h | 1 + plugins/openvr/src/ViveControllerManager.cpp | 66 ++++++++++++-------- plugins/openvr/src/ViveControllerManager.h | 4 +- 5 files changed, 70 insertions(+), 49 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index f7116a60db..1bec40c35a 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3035,9 +3035,9 @@ glm::mat4 MyAvatar::getCenterEyeCalibrationMat() const { if (rightEyeIndex >= 0 && leftEyeIndex >= 0) { auto centerEyePos = (getAbsoluteDefaultJointTranslationInObjectFrame(rightEyeIndex) + getAbsoluteDefaultJointTranslationInObjectFrame(leftEyeIndex)) * 0.5f; auto centerEyeRot = Quaternions::Y_180; - return createMatFromQuatAndPos(centerEyeRot, centerEyePos); + return createMatFromQuatAndPos(centerEyeRot, centerEyePos / getSensorToWorldScale()); } else { - return createMatFromQuatAndPos(DEFAULT_AVATAR_MIDDLE_EYE_ROT, DEFAULT_AVATAR_MIDDLE_EYE_POS); + return createMatFromQuatAndPos(DEFAULT_AVATAR_MIDDLE_EYE_ROT, DEFAULT_AVATAR_MIDDLE_EYE_POS / getSensorToWorldScale()); } } @@ -3047,9 +3047,9 @@ glm::mat4 MyAvatar::getHeadCalibrationMat() const { if (headIndex >= 0) { auto headPos = getAbsoluteDefaultJointTranslationInObjectFrame(headIndex); auto headRot = getAbsoluteDefaultJointRotationInObjectFrame(headIndex); - return createMatFromQuatAndPos(headRot, headPos); + return createMatFromQuatAndPos(headRot, headPos / getSensorToWorldScale()); } else { - return createMatFromQuatAndPos(DEFAULT_AVATAR_HEAD_ROT, DEFAULT_AVATAR_HEAD_POS); + return createMatFromQuatAndPos(DEFAULT_AVATAR_HEAD_ROT, DEFAULT_AVATAR_HEAD_POS / getSensorToWorldScale()); } } @@ -3059,9 +3059,9 @@ glm::mat4 MyAvatar::getSpine2CalibrationMat() const { if (spine2Index >= 0) { auto spine2Pos = getAbsoluteDefaultJointTranslationInObjectFrame(spine2Index); auto spine2Rot = getAbsoluteDefaultJointRotationInObjectFrame(spine2Index); - return createMatFromQuatAndPos(spine2Rot, spine2Pos); + return createMatFromQuatAndPos(spine2Rot, spine2Pos / getSensorToWorldScale()); } else { - return createMatFromQuatAndPos(DEFAULT_AVATAR_SPINE2_ROT, DEFAULT_AVATAR_SPINE2_POS); + return createMatFromQuatAndPos(DEFAULT_AVATAR_SPINE2_ROT, DEFAULT_AVATAR_SPINE2_POS / getSensorToWorldScale()); } } @@ -3071,9 +3071,9 @@ glm::mat4 MyAvatar::getHipsCalibrationMat() const { if (hipsIndex >= 0) { auto hipsPos = getAbsoluteDefaultJointTranslationInObjectFrame(hipsIndex); auto hipsRot = getAbsoluteDefaultJointRotationInObjectFrame(hipsIndex); - return createMatFromQuatAndPos(hipsRot, hipsPos); + return createMatFromQuatAndPos(hipsRot, hipsPos / getSensorToWorldScale()); } else { - return createMatFromQuatAndPos(DEFAULT_AVATAR_HIPS_ROT, DEFAULT_AVATAR_HIPS_POS); + return createMatFromQuatAndPos(DEFAULT_AVATAR_HIPS_ROT, DEFAULT_AVATAR_HIPS_POS / getSensorToWorldScale()); } } @@ -3083,9 +3083,9 @@ glm::mat4 MyAvatar::getLeftFootCalibrationMat() const { if (leftFootIndex >= 0) { auto leftFootPos = getAbsoluteDefaultJointTranslationInObjectFrame(leftFootIndex); auto leftFootRot = getAbsoluteDefaultJointRotationInObjectFrame(leftFootIndex); - return createMatFromQuatAndPos(leftFootRot, leftFootPos); + return createMatFromQuatAndPos(leftFootRot, leftFootPos / getSensorToWorldScale()); } else { - return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTFOOT_ROT, DEFAULT_AVATAR_LEFTFOOT_POS); + return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTFOOT_ROT, DEFAULT_AVATAR_LEFTFOOT_POS / getSensorToWorldScale()); } } @@ -3095,9 +3095,9 @@ glm::mat4 MyAvatar::getRightFootCalibrationMat() const { if (rightFootIndex >= 0) { auto rightFootPos = getAbsoluteDefaultJointTranslationInObjectFrame(rightFootIndex); auto rightFootRot = getAbsoluteDefaultJointRotationInObjectFrame(rightFootIndex); - return createMatFromQuatAndPos(rightFootRot, rightFootPos); + return createMatFromQuatAndPos(rightFootRot, rightFootPos / getSensorToWorldScale()); } else { - return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTFOOT_ROT, DEFAULT_AVATAR_RIGHTFOOT_POS); + return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTFOOT_ROT, DEFAULT_AVATAR_RIGHTFOOT_POS / getSensorToWorldScale()); } } @@ -3107,9 +3107,9 @@ glm::mat4 MyAvatar::getRightArmCalibrationMat() const { if (rightArmIndex >= 0) { auto rightArmPos = getAbsoluteDefaultJointTranslationInObjectFrame(rightArmIndex); auto rightArmRot = getAbsoluteDefaultJointRotationInObjectFrame(rightArmIndex); - return createMatFromQuatAndPos(rightArmRot, rightArmPos); + return createMatFromQuatAndPos(rightArmRot, rightArmPos / getSensorToWorldScale()); } else { - return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTARM_ROT, DEFAULT_AVATAR_RIGHTARM_POS); + return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTARM_ROT, DEFAULT_AVATAR_RIGHTARM_POS / getSensorToWorldScale()); } } @@ -3118,9 +3118,9 @@ glm::mat4 MyAvatar::getLeftArmCalibrationMat() const { if (leftArmIndex >= 0) { auto leftArmPos = getAbsoluteDefaultJointTranslationInObjectFrame(leftArmIndex); auto leftArmRot = getAbsoluteDefaultJointRotationInObjectFrame(leftArmIndex); - return createMatFromQuatAndPos(leftArmRot, leftArmPos); + return createMatFromQuatAndPos(leftArmRot, leftArmPos / getSensorToWorldScale()); } else { - return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTARM_ROT, DEFAULT_AVATAR_LEFTARM_POS); + return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTARM_ROT, DEFAULT_AVATAR_LEFTARM_POS / getSensorToWorldScale()); } } @@ -3129,9 +3129,9 @@ glm::mat4 MyAvatar::getRightHandCalibrationMat() const { if (rightHandIndex >= 0) { auto rightHandPos = getAbsoluteDefaultJointTranslationInObjectFrame(rightHandIndex); auto rightHandRot = getAbsoluteDefaultJointRotationInObjectFrame(rightHandIndex); - return createMatFromQuatAndPos(rightHandRot, rightHandPos); + return createMatFromQuatAndPos(rightHandRot, rightHandPos / getSensorToWorldScale()); } else { - return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTHAND_ROT, DEFAULT_AVATAR_RIGHTHAND_POS); + return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTHAND_ROT, DEFAULT_AVATAR_RIGHTHAND_POS / getSensorToWorldScale()); } } @@ -3140,9 +3140,9 @@ glm::mat4 MyAvatar::getLeftHandCalibrationMat() const { if (leftHandIndex >= 0) { auto leftHandPos = getAbsoluteDefaultJointTranslationInObjectFrame(leftHandIndex); auto leftHandRot = getAbsoluteDefaultJointRotationInObjectFrame(leftHandIndex); - return createMatFromQuatAndPos(leftHandRot, leftHandPos); + return createMatFromQuatAndPos(leftHandRot, leftHandPos / getSensorToWorldScale()); } else { - return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTHAND_ROT, DEFAULT_AVATAR_LEFTHAND_POS); + return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTHAND_ROT, DEFAULT_AVATAR_LEFTHAND_POS / getSensorToWorldScale()); } } diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index 394517aac4..39fec45d90 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -473,6 +473,14 @@ glm::mat4 createMatFromScaleQuatAndPos(const glm::vec3& scale, const glm::quat& glm::vec4(zAxis, 0.0f), glm::vec4(trans, 1.0f)); } +glm::mat4 createMatFromScale(const glm::vec3& scale) { + glm::vec3 xAxis = glm::vec3(scale.x, 0.0f, 0.0f); + glm::vec3 yAxis = glm::vec3(0.0f, scale.y, 0.0f); + glm::vec3 zAxis = glm::vec3(0.0f, 0.0f, scale.z); + return glm::mat4(glm::vec4(xAxis, 0.0f), glm::vec4(yAxis, 0.0f), + glm::vec4(zAxis, 0.0f), glm::vec4(Vectors::ZERO, 1.0f)); +} + // cancel out roll glm::quat cancelOutRoll(const glm::quat& q) { glm::vec3 forward = q * Vectors::FRONT; diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index 3386ea2c22..7248f4cb46 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -231,6 +231,7 @@ glm::tvec4 lerp(const glm::tvec4& x, const glm::tvec4& y, T a) glm::mat4 createMatFromQuatAndPos(const glm::quat& q, const glm::vec3& p); glm::mat4 createMatFromScaleQuatAndPos(const glm::vec3& scale, const glm::quat& rot, const glm::vec3& trans); +glm::mat4 createMatFromScale(const glm::vec3& scale); glm::quat cancelOutRoll(const glm::quat& q); glm::quat cancelOutRollAndPitch(const glm::quat& q); glm::mat4 cancelOutRollAndPitch(const glm::mat4& m); diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 5a1c23839e..81173722fb 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -375,7 +375,7 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle calibrateFromHandController(inputCalibrationData); calibrateFromUI(inputCalibrationData); - updateCalibratedLimbs(); + updateCalibratedLimbs(inputCalibrationData); _lastSimPoseData = _nextSimPoseData; } @@ -676,40 +676,55 @@ void ViveControllerManager::InputDevice::uncalibrate() { _overrideHands = false; } -void ViveControllerManager::InputDevice::updateCalibratedLimbs() { - _poseStateMap[controller::LEFT_FOOT] = addOffsetToPuckPose(controller::LEFT_FOOT); - _poseStateMap[controller::RIGHT_FOOT] = addOffsetToPuckPose(controller::RIGHT_FOOT); - _poseStateMap[controller::HIPS] = addOffsetToPuckPose(controller::HIPS); - _poseStateMap[controller::SPINE2] = addOffsetToPuckPose(controller::SPINE2); - _poseStateMap[controller::RIGHT_ARM] = addOffsetToPuckPose(controller::RIGHT_ARM); - _poseStateMap[controller::LEFT_ARM] = addOffsetToPuckPose(controller::LEFT_ARM); +void ViveControllerManager::InputDevice::updateCalibratedLimbs(const controller::InputCalibrationData& inputCalibration) { + _poseStateMap[controller::LEFT_FOOT] = addOffsetToPuckPose(inputCalibration, controller::LEFT_FOOT); + _poseStateMap[controller::RIGHT_FOOT] = addOffsetToPuckPose(inputCalibration, controller::RIGHT_FOOT); + _poseStateMap[controller::HIPS] = addOffsetToPuckPose(inputCalibration, controller::HIPS); + _poseStateMap[controller::SPINE2] = addOffsetToPuckPose(inputCalibration, controller::SPINE2); + _poseStateMap[controller::RIGHT_ARM] = addOffsetToPuckPose(inputCalibration, controller::RIGHT_ARM); + _poseStateMap[controller::LEFT_ARM] = addOffsetToPuckPose(inputCalibration, controller::LEFT_ARM); if (_overrideHead) { - _poseStateMap[controller::HEAD] = addOffsetToPuckPose(controller::HEAD); + _poseStateMap[controller::HEAD] = addOffsetToPuckPose(inputCalibration, controller::HEAD); } if (_overrideHands) { - _poseStateMap[controller::LEFT_HAND] = addOffsetToPuckPose(controller::LEFT_HAND); - _poseStateMap[controller::RIGHT_HAND] = addOffsetToPuckPose(controller::RIGHT_HAND); + _poseStateMap[controller::LEFT_HAND] = addOffsetToPuckPose(inputCalibration, controller::LEFT_HAND); + _poseStateMap[controller::RIGHT_HAND] = addOffsetToPuckPose(inputCalibration, controller::RIGHT_HAND); } } -controller::Pose ViveControllerManager::InputDevice::addOffsetToPuckPose(int joint) const { +controller::Pose ViveControllerManager::InputDevice::addOffsetToPuckPose(const controller::InputCalibrationData& inputCalibration, int joint) const { auto puck = _jointToPuckMap.find(joint); if (puck != _jointToPuckMap.end()) { uint32_t puckIndex = puck->second; - auto puckPose = _poseStateMap.find(puckIndex); - auto puckPostOffset = _pucksPostOffset.find(puckIndex); - auto puckPreOffset = _pucksPreOffset.find(puckIndex); - if (puckPose != _poseStateMap.end()) { - if (puckPreOffset != _pucksPreOffset.end() && puckPostOffset != _pucksPostOffset.end()) { - return puckPose->second.postTransform(puckPostOffset->second).transform(puckPreOffset->second); - } else if (puckPostOffset != _pucksPostOffset.end()) { - return puckPose->second.postTransform(puckPostOffset->second); - } else if (puckPreOffset != _pucksPreOffset.end()) { - return puckPose->second.transform(puckPreOffset->second); + // use sensor space pose. + auto puckPoseIter = _validTrackedObjects.begin(); + while (puckPoseIter != _validTrackedObjects.end()) { + if (puckPoseIter->first == puckIndex) { + break; } + puckPoseIter++; + } + + //auto puckPoseIter = _poseStateMap.find(puckIndex); + + if (puckPoseIter != _validTrackedObjects.end()) { + + glm::mat4 postMat; // identity + auto postIter = _pucksPostOffset.find(puckIndex); + if (postIter != _pucksPostOffset.end()) { + postMat = postIter->second; + } + + glm::mat4 preMat = glm::inverse(inputCalibration.avatarMat) * inputCalibration.sensorToWorldMat; + auto preIter = _pucksPreOffset.find(puckIndex); + if (preIter != _pucksPreOffset.end()) { + preMat = preMat * preIter->second; + } + + return puckPoseIter->second.postTransform(postMat).transform(preMat); } } return controller::Pose(); @@ -924,15 +939,12 @@ void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint void ViveControllerManager::InputDevice::handleHeadPoseEvent(const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity) { - //perform a 180 flip to make the HMD face the +z instead of -z, beacuse the head faces +z glm::mat4 matYFlip = mat * Matrices::Y_180; controller::Pose pose(extractTranslation(matYFlip), glmExtractRotation(matYFlip), linearVelocity, angularVelocity); - - glm::mat4 sensorToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; glm::mat4 defaultHeadOffset = glm::inverse(inputCalibrationData.defaultCenterEyeMat) * inputCalibrationData.defaultHeadMat; - controller::Pose hmdHeadPose = pose.transform(sensorToAvatar); - _poseStateMap[controller::HEAD] = hmdHeadPose.postTransform(defaultHeadOffset); + glm::mat4 sensorToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; + _poseStateMap[controller::HEAD] = pose.postTransform(defaultHeadOffset).transform(sensorToAvatar); } void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 9a7b2cbc93..4a7fcaf85e 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -79,10 +79,10 @@ private: void sendUserActivityData(QString activity); void configureCalibrationSettings(const QJsonObject configurationSettings); QJsonObject configurationSettings(); - controller::Pose addOffsetToPuckPose(int joint) const; + controller::Pose addOffsetToPuckPose(const controller::InputCalibrationData& inputCalibration, int joint) const; glm::mat4 calculateDefaultToReferenceForHeadPuck(const controller::InputCalibrationData& inputCalibration); glm::mat4 calculateDefaultToReferenceForHmd(const controller::InputCalibrationData& inputCalibration); - void updateCalibratedLimbs(); + void updateCalibratedLimbs(const controller::InputCalibrationData& inputCalibration); bool checkForCalibrationEvent(); void handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand); void handleHmd(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData); From 5e93c13856ae0fa8eda25c68ffca55184bdfc305 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 20 Sep 2017 16:36:33 -0700 Subject: [PATCH 372/722] warning fix --- interface/src/Application.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f0a396672f..b5274f1d34 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2580,7 +2580,6 @@ void Application::paintGL() { // scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. ipdScale *= sensorToWorldScale; - mat4 eyeProjections[2]; { PROFILE_RANGE(render, "/mainRender"); PerformanceTimer perfTimer("mainRender"); From d112ffd8d657a259ed85a7096812cfb8eca40d7b Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 20 Sep 2017 18:06:49 -0700 Subject: [PATCH 373/722] Moving all the 3D overlay transform evaluation out of the render thread to the game loop, need to do something for the line3d --- interface/src/ui/overlays/Base3DOverlay.cpp | 11 ++++---- interface/src/ui/overlays/Base3DOverlay.h | 2 +- .../src/ui/overlays/Billboard3DOverlay.cpp | 11 ++++++++ .../src/ui/overlays/Billboard3DOverlay.h | 2 ++ interface/src/ui/overlays/Circle3DOverlay.cpp | 4 +++ interface/src/ui/overlays/Circle3DOverlay.h | 2 ++ interface/src/ui/overlays/Cube3DOverlay.cpp | 27 +++++++++++-------- interface/src/ui/overlays/Cube3DOverlay.h | 3 +++ interface/src/ui/overlays/Grid3DOverlay.cpp | 12 ++++++--- interface/src/ui/overlays/Grid3DOverlay.h | 3 +++ interface/src/ui/overlays/Image3DOverlay.cpp | 18 +++++-------- interface/src/ui/overlays/Image3DOverlay.h | 4 +++ interface/src/ui/overlays/Line3DOverlay.cpp | 4 +++ interface/src/ui/overlays/Line3DOverlay.h | 3 +++ interface/src/ui/overlays/ModelOverlay.cpp | 9 ++++--- interface/src/ui/overlays/ModelOverlay.h | 2 ++ interface/src/ui/overlays/Planar3DOverlay.cpp | 2 +- interface/src/ui/overlays/Planar3DOverlay.h | 4 +-- .../src/ui/overlays/Rectangle3DOverlay.cpp | 5 ++++ .../src/ui/overlays/Rectangle3DOverlay.h | 4 +++ interface/src/ui/overlays/Shape3DOverlay.cpp | 26 +++++++++--------- interface/src/ui/overlays/Shape3DOverlay.h | 3 +++ interface/src/ui/overlays/Sphere3DOverlay.cpp | 20 +++++++++----- interface/src/ui/overlays/Sphere3DOverlay.h | 3 +++ interface/src/ui/overlays/Text3DOverlay.cpp | 10 ++++--- interface/src/ui/overlays/Text3DOverlay.h | 4 +++ interface/src/ui/overlays/Volume3DOverlay.cpp | 8 ++++++ interface/src/ui/overlays/Volume3DOverlay.h | 3 +++ 28 files changed, 146 insertions(+), 63 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 6f55260133..9afab80243 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -268,18 +268,17 @@ void Base3DOverlay::update(float duration) { // In Base3DOverlay, if its location or bound changed, the renderTrasnformDirty flag is true. // then the correct transform used for rendering is computed in the update transaction and assigned. - // TODO: Fix the value to be computed in main thread now and passed by value to the render item. - // This is the simplest fix for the web overlay of the tablet for now if (_renderTransformDirty) { - _renderTransformDirty = false; auto itemID = getRenderItemID(); if (render::Item::isValidID(itemID)) { + _renderTransformDirty = false; + // Capture the render transform value in game loop before + auto latestTransform = evalRenderTransform(); render::ScenePointer scene = qApp->getMain3DScene(); render::Transaction transaction; - transaction.updateItem(itemID, [](Overlay& data) { + transaction.updateItem(itemID, [latestTransform](Overlay& data) { auto overlay3D = dynamic_cast(&data); if (overlay3D) { - auto latestTransform = overlay3D->evalRenderTransform(); overlay3D->setRenderTransform(latestTransform); } }); @@ -292,7 +291,7 @@ void Base3DOverlay::notifyRenderTransformChange() const { _renderTransformDirty = true; } -Transform Base3DOverlay::evalRenderTransform() const { +Transform Base3DOverlay::evalRenderTransform() { return getTransform(); } diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index 55b55ed16f..93a973e60a 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -72,7 +72,7 @@ protected: virtual void parentDeleted() override; mutable Transform _renderTransform; - virtual Transform evalRenderTransform() const; + virtual Transform evalRenderTransform(); virtual void setRenderTransform(const Transform& transform); const Transform& getRenderTransform() const { return _renderTransform; } diff --git a/interface/src/ui/overlays/Billboard3DOverlay.cpp b/interface/src/ui/overlays/Billboard3DOverlay.cpp index f5668caa71..960f0de095 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.cpp +++ b/interface/src/ui/overlays/Billboard3DOverlay.cpp @@ -45,3 +45,14 @@ bool Billboard3DOverlay::applyTransformTo(Transform& transform, bool force) { } return transformChanged; } + +Transform Billboard3DOverlay::evalRenderTransform() { + Transform transform = getTransform(); + bool transformChanged = applyTransformTo(transform, true); + // If the transform is not modified, setting the transform to + // itself will cause drift over time due to floating point errors. + if (transformChanged) { + setTransform(transform); + } + return transform; +} \ No newline at end of file diff --git a/interface/src/ui/overlays/Billboard3DOverlay.h b/interface/src/ui/overlays/Billboard3DOverlay.h index d429537b5b..6b3aa40451 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.h +++ b/interface/src/ui/overlays/Billboard3DOverlay.h @@ -28,6 +28,8 @@ public: protected: virtual bool applyTransformTo(Transform& transform, bool force = false) override; + + Transform evalRenderTransform() override; }; #endif // hifi_Billboard3DOverlay_h diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index 57911c0786..b3e4cba5d9 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -438,3 +438,7 @@ bool Circle3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::ve Circle3DOverlay* Circle3DOverlay::createClone() const { return new Circle3DOverlay(this); } + +Transform Circle3DOverlay::evalRenderTransform() { + return getTransform(); +} diff --git a/interface/src/ui/overlays/Circle3DOverlay.h b/interface/src/ui/overlays/Circle3DOverlay.h index 11c9c9710f..e41c308d9d 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.h +++ b/interface/src/ui/overlays/Circle3DOverlay.h @@ -88,6 +88,8 @@ protected: int _minorTicksVerticesID { 0 }; bool _dirty { true }; + + Transform evalRenderTransform() override; }; diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp index ca7355b86f..0ac9dba34b 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.cpp +++ b/interface/src/ui/overlays/Cube3DOverlay.cpp @@ -53,18 +53,11 @@ void Cube3DOverlay::render(RenderArgs* args) { const float MAX_COLOR = 255.0f; glm::vec4 cubeColor(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha); - // TODO: handle registration point?? - // FIXME Start using the _renderTransform instead of calling for Transform from here, do the custom things needed in evalRenderTransform() - glm::vec3 position = getPosition(); - glm::vec3 dimensions = getDimensions(); - glm::quat rotation = getRotation(); + auto batch = args->_batch; - if (batch) { - Transform transform; - transform.setTranslation(position); - transform.setRotation(rotation); + Transform transform = getRenderTransform(); auto geometryCache = DependencyManager::get(); auto shapePipeline = args->_shapePipeline; if (!shapePipeline) { @@ -72,12 +65,12 @@ void Cube3DOverlay::render(RenderArgs* args) { } if (_isSolid) { - transform.setScale(dimensions); batch->setModelTransform(transform); geometryCache->renderSolidCubeInstance(args, *batch, cubeColor, shapePipeline); } else { geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); if (getIsDashedLine()) { + auto dimensions = transform.getScale(); transform.setScale(1.0f); batch->setModelTransform(transform); @@ -108,7 +101,6 @@ void Cube3DOverlay::render(RenderArgs* args) { geometryCache->renderDashedLine(*batch, bottomRightFar, topRightFar, cubeColor, _geometryIds[11]); } else { - transform.setScale(dimensions); batch->setModelTransform(transform); geometryCache->renderWireCubeInstance(args, *batch, cubeColor, shapePipeline); } @@ -149,3 +141,16 @@ QVariant Cube3DOverlay::getProperty(const QString& property) { return Volume3DOverlay::getProperty(property); } + +Transform Cube3DOverlay::evalRenderTransform() { + // TODO: handle registration point?? + glm::vec3 position = getPosition(); + glm::vec3 dimensions = getDimensions(); + glm::quat rotation = getRotation(); + + Transform transform; + transform.setScale(dimensions); + transform.setTranslation(position); + transform.setRotation(rotation); + return transform; +} diff --git a/interface/src/ui/overlays/Cube3DOverlay.h b/interface/src/ui/overlays/Cube3DOverlay.h index 9289af4de5..e7b58ad911 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.h +++ b/interface/src/ui/overlays/Cube3DOverlay.h @@ -36,6 +36,9 @@ public: void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; +protected: + Transform evalRenderTransform() override; + private: float _borderSize; // edges on a cube diff --git a/interface/src/ui/overlays/Grid3DOverlay.cpp b/interface/src/ui/overlays/Grid3DOverlay.cpp index 3172403731..ca275368cb 100644 --- a/interface/src/ui/overlays/Grid3DOverlay.cpp +++ b/interface/src/ui/overlays/Grid3DOverlay.cpp @@ -79,10 +79,7 @@ void Grid3DOverlay::render(RenderArgs* args) { position += glm::vec3(cameraPosition.x, 0.0f, cameraPosition.z); } - // FIXME Start using the _renderTransform instead of calling for Transform from here, do the custom things needed in evalRenderTransform() - Transform transform; - transform.setRotation(getRotation()); - transform.setScale(glm::vec3(getDimensions(), 1.0f)); + Transform transform = getRenderTransform(); transform.setTranslation(position); batch->setModelTransform(transform); const float MINOR_GRID_EDGE = 0.0025f; @@ -146,3 +143,10 @@ void Grid3DOverlay::updateGrid() { _minorGridRowDivisions = getDimensions().x / _minorGridEvery; _minorGridColDivisions = getDimensions().y / _minorGridEvery; } + +Transform Grid3DOverlay::evalRenderTransform() { + Transform transform; + transform.setRotation(getRotation()); + transform.setScale(glm::vec3(getDimensions(), 1.0f)); + return transform; +} diff --git a/interface/src/ui/overlays/Grid3DOverlay.h b/interface/src/ui/overlays/Grid3DOverlay.h index 0d042af6ca..5a67b21e07 100644 --- a/interface/src/ui/overlays/Grid3DOverlay.h +++ b/interface/src/ui/overlays/Grid3DOverlay.h @@ -37,6 +37,9 @@ public: // Grids are UI tools, and may not be intersected (pickable) virtual bool findRayIntersection(const glm::vec3&, const glm::vec3&, float&, BoxFace&, glm::vec3&) override { return false; } +protected: + Transform evalRenderTransform() override; + private: void updateGrid(); diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index c79d363811..22beb2be20 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -116,18 +116,8 @@ void Image3DOverlay::render(RenderArgs* args) { const float MAX_COLOR = 255.0f; xColor color = getColor(); float alpha = getAlpha(); - - // FIXME Start using the _renderTransform instead of calling for Transform from here, do the custom things needed in evalRenderTransform() - Transform transform = getTransform(); - bool transformChanged = applyTransformTo(transform, true); - // If the transform is not modified, setting the transform to - // itself will cause drift over time due to floating point errors. - if (transformChanged) { - setTransform(transform); - } - transform.postScale(glm::vec3(getDimensions(), 1.0f)); - batch->setModelTransform(transform); + batch->setModelTransform(getRenderTransform()); batch->setResourceTexture(0, _texture->getGPUTexture()); DependencyManager::get()->renderQuad( @@ -249,3 +239,9 @@ bool Image3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec Image3DOverlay* Image3DOverlay::createClone() const { return new Image3DOverlay(this); } + +Transform Image3DOverlay::evalRenderTransform() { + auto transform = Parent::evalRenderTransform(); + transform.postScale(glm::vec3(getDimensions(), 1.0f)); + return transform; +} diff --git a/interface/src/ui/overlays/Image3DOverlay.h b/interface/src/ui/overlays/Image3DOverlay.h index 4f813e7368..aa802a82a9 100644 --- a/interface/src/ui/overlays/Image3DOverlay.h +++ b/interface/src/ui/overlays/Image3DOverlay.h @@ -19,6 +19,7 @@ class Image3DOverlay : public Billboard3DOverlay { Q_OBJECT + using Parent = Billboard3DOverlay; public: static QString const TYPE; @@ -46,6 +47,9 @@ public: virtual Image3DOverlay* createClone() const override; +protected: + Transform evalRenderTransform() override; + private: QString _url; NetworkTexturePointer _texture; diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index 534261c839..3bee13269b 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -268,3 +268,7 @@ QVariant Line3DOverlay::getProperty(const QString& property) { Line3DOverlay* Line3DOverlay::createClone() const { return new Line3DOverlay(this); } + +Transform Line3DOverlay::evalRenderTransform() { + return getTransform(); +} diff --git a/interface/src/ui/overlays/Line3DOverlay.h b/interface/src/ui/overlays/Line3DOverlay.h index 9abc2f1a8d..7e8be48cd6 100644 --- a/interface/src/ui/overlays/Line3DOverlay.h +++ b/interface/src/ui/overlays/Line3DOverlay.h @@ -56,6 +56,9 @@ public: QUuid getEndParentID() const { return _endParentID; } quint16 getEndJointIndex() const { return _endParentJointIndex; } +protected: + Transform evalRenderTransform() override; + private: QUuid _endParentID; quint16 _endParentJointIndex { INVALID_JOINT_INDEX }; diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index e2a7df7ae6..cc55165004 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -46,10 +46,7 @@ void ModelOverlay::update(float deltatime) { if (_updateModel) { _updateModel = false; _model->setSnapModelToCenter(true); - Transform transform = getTransform(); -#ifndef USE_SN_SCALE - transform.setScale(1.0f); // disable inherited scale -#endif + Transform transform = evalRenderTransform(); if (_scaleToFit) { _model->setScaleToFit(true, transform.getScale() * getDimensions()); } else { @@ -282,6 +279,10 @@ ModelOverlay* ModelOverlay::createClone() const { return new ModelOverlay(this); } +Transform ModelOverlay::evalRenderTransform() { + return getTransform(); +} + void ModelOverlay::locationChanged(bool tellPhysics) { Base3DOverlay::locationChanged(tellPhysics); diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index 59548dfe62..8d8429b29e 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -46,6 +46,8 @@ public: float getLoadPriority() const { return _loadPriority; } protected: + Transform evalRenderTransform() override; + // helper to extract metadata from our Model's rigged joints template using mapFunction = std::function; template diff --git a/interface/src/ui/overlays/Planar3DOverlay.cpp b/interface/src/ui/overlays/Planar3DOverlay.cpp index e865714e58..cfcea542e3 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.cpp +++ b/interface/src/ui/overlays/Planar3DOverlay.cpp @@ -67,7 +67,7 @@ bool Planar3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::ve return findRayRectangleIntersection(origin, direction, getRotation(), getPosition(), getDimensions(), distance); } -Transform Planar3DOverlay::evalRenderTransform() const { +Transform Planar3DOverlay::evalRenderTransform() { auto transform = getTransform(); transform.setScale(1.0f); // ignore inherited scale factor from parents if (glm::length2(getDimensions()) != 1.0f) { diff --git a/interface/src/ui/overlays/Planar3DOverlay.h b/interface/src/ui/overlays/Planar3DOverlay.h index 2ed90ab4ed..7cf4e0221d 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.h +++ b/interface/src/ui/overlays/Planar3DOverlay.h @@ -33,10 +33,10 @@ public: virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) override; - Transform evalRenderTransform() const override; - protected: glm::vec2 _dimensions; + + Transform evalRenderTransform() override; }; diff --git a/interface/src/ui/overlays/Rectangle3DOverlay.cpp b/interface/src/ui/overlays/Rectangle3DOverlay.cpp index dc8badbbd1..47d47b76a5 100644 --- a/interface/src/ui/overlays/Rectangle3DOverlay.cpp +++ b/interface/src/ui/overlays/Rectangle3DOverlay.cpp @@ -124,3 +124,8 @@ void Rectangle3DOverlay::setProperties(const QVariantMap& properties) { Rectangle3DOverlay* Rectangle3DOverlay::createClone() const { return new Rectangle3DOverlay(this); } + +Transform Rectangle3DOverlay::evalRenderTransform() { + return getTransform(); +} + diff --git a/interface/src/ui/overlays/Rectangle3DOverlay.h b/interface/src/ui/overlays/Rectangle3DOverlay.h index a0c342a25b..a26ed524fc 100644 --- a/interface/src/ui/overlays/Rectangle3DOverlay.h +++ b/interface/src/ui/overlays/Rectangle3DOverlay.h @@ -28,6 +28,10 @@ public: void setProperties(const QVariantMap& properties) override; virtual Rectangle3DOverlay* createClone() const override; + +protected: + Transform evalRenderTransform() override; + private: int _geometryCacheID; std::array _rectGeometryIds; diff --git a/interface/src/ui/overlays/Shape3DOverlay.cpp b/interface/src/ui/overlays/Shape3DOverlay.cpp index fc54cc19ff..2c1df478f6 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.cpp +++ b/interface/src/ui/overlays/Shape3DOverlay.cpp @@ -33,26 +33,15 @@ void Shape3DOverlay::render(RenderArgs* args) { const float MAX_COLOR = 255.0f; glm::vec4 cubeColor(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha); - // FIXME Start using the _renderTransform instead of calling for Transform and Dimensions from here, do the custom things needed in evalRenderTransform() - // TODO: handle registration point?? - glm::vec3 position = getPosition(); - glm::vec3 dimensions = getDimensions(); - glm::quat rotation = getRotation(); - auto batch = args->_batch; - if (batch) { - Transform transform; - transform.setTranslation(position); - transform.setRotation(rotation); auto geometryCache = DependencyManager::get(); auto shapePipeline = args->_shapePipeline; if (!shapePipeline) { shapePipeline = _isSolid ? geometryCache->getOpaqueShapePipeline() : geometryCache->getWireShapePipeline(); } - transform.setScale(dimensions); - batch->setModelTransform(transform); + batch->setModelTransform(getRenderTransform()); if (_isSolid) { geometryCache->renderSolidShapeInstance(args, *batch, _shape, cubeColor, shapePipeline); } else { @@ -129,3 +118,16 @@ QVariant Shape3DOverlay::getProperty(const QString& property) { return Volume3DOverlay::getProperty(property); } + +Transform Shape3DOverlay::evalRenderTransform() { + // TODO: handle registration point?? + glm::vec3 position = getPosition(); + glm::vec3 dimensions = getDimensions(); + glm::quat rotation = getRotation(); + + Transform transform; + transform.setScale(dimensions); + transform.setTranslation(position); + transform.setRotation(rotation); + return transform; +} diff --git a/interface/src/ui/overlays/Shape3DOverlay.h b/interface/src/ui/overlays/Shape3DOverlay.h index 2361001721..e9e26e3c94 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.h +++ b/interface/src/ui/overlays/Shape3DOverlay.h @@ -37,6 +37,9 @@ public: void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; +protected: + Transform evalRenderTransform() override; + private: float _borderSize; GeometryCache::Shape _shape { GeometryCache::Hexagon }; diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index 83dc4b0e2b..d4c04976f5 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -39,13 +39,8 @@ void Sphere3DOverlay::render(RenderArgs* args) { auto batch = args->_batch; if (batch) { - // FIXME Start using the _renderTransform instead of calling for Transform and Dimensions from here, do the custom things needed in evalRenderTransform() - Transform transform = getTransform(); -#ifndef USE_SN_SCALE - transform.setScale(1.0f); // ignore inherited scale from SpatiallyNestable -#endif - transform.postScale(getDimensions() * SPHERE_OVERLAY_SCALE); - batch->setModelTransform(transform); + + batch->setModelTransform(getRenderTransform()); auto geometryCache = DependencyManager::get(); auto shapePipeline = args->_shapePipeline; @@ -75,3 +70,14 @@ const render::ShapeKey Sphere3DOverlay::getShapeKey() { Sphere3DOverlay* Sphere3DOverlay::createClone() const { return new Sphere3DOverlay(this); } + +Transform Sphere3DOverlay::evalRenderTransform() { + // FIXME Start using the _renderTransform instead of calling for Transform and Dimensions from here, do the custom things needed in evalRenderTransform() + Transform transform = getTransform(); +#ifndef USE_SN_SCALE + transform.setScale(1.0f); // ignore inherited scale from SpatiallyNestable +#endif + transform.postScale(getDimensions() * SPHERE_OVERLAY_SCALE); + + return transform; +} diff --git a/interface/src/ui/overlays/Sphere3DOverlay.h b/interface/src/ui/overlays/Sphere3DOverlay.h index 991b2ab51e..ebe6dc8d83 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.h +++ b/interface/src/ui/overlays/Sphere3DOverlay.h @@ -27,6 +27,9 @@ public: virtual const render::ShapeKey getShapeKey() override; virtual Sphere3DOverlay* createClone() const override; + +protected: + Transform evalRenderTransform() override; }; diff --git a/interface/src/ui/overlays/Text3DOverlay.cpp b/interface/src/ui/overlays/Text3DOverlay.cpp index 43a2854206..5813258d94 100644 --- a/interface/src/ui/overlays/Text3DOverlay.cpp +++ b/interface/src/ui/overlays/Text3DOverlay.cpp @@ -96,10 +96,7 @@ void Text3DOverlay::render(RenderArgs* args) { Q_ASSERT(args->_batch); auto& batch = *args->_batch; - // FIXME Start using the _renderTransform instead of calling for Transform and Dimensions from here, do the custom things needed in evalRenderTransform() - Transform transform = getTransform(); - applyTransformTo(transform, true); - setTransform(transform); + auto transform = getRenderTransform(); batch.setModelTransform(transform); const float MAX_COLOR = 255.0f; @@ -249,3 +246,8 @@ bool Text3DOverlay::findRayIntersection(const glm::vec3 &origin, const glm::vec3 setTransform(transform); return Billboard3DOverlay::findRayIntersection(origin, direction, distance, face, surfaceNormal); } + +Transform Text3DOverlay::evalRenderTransform() { + return Parent::evalRenderTransform(); +} + diff --git a/interface/src/ui/overlays/Text3DOverlay.h b/interface/src/ui/overlays/Text3DOverlay.h index e7b09c9040..69b503eb48 100644 --- a/interface/src/ui/overlays/Text3DOverlay.h +++ b/interface/src/ui/overlays/Text3DOverlay.h @@ -19,6 +19,7 @@ class TextRenderer3D; class Text3DOverlay : public Billboard3DOverlay { Q_OBJECT + using Parent = Billboard3DOverlay; public: static QString const TYPE; @@ -63,6 +64,9 @@ public: virtual Text3DOverlay* createClone() const override; +protected: + Transform evalRenderTransform() override; + private: TextRenderer3D* _textRenderer = nullptr; diff --git a/interface/src/ui/overlays/Volume3DOverlay.cpp b/interface/src/ui/overlays/Volume3DOverlay.cpp index 5e3e4ccee7..c42f5ef4d3 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.cpp +++ b/interface/src/ui/overlays/Volume3DOverlay.cpp @@ -69,3 +69,11 @@ bool Volume3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::ve // and testing intersection there. return _localBoundingBox.findRayIntersection(overlayFrameOrigin, overlayFrameDirection, distance, face, surfaceNormal); } + +Transform Volume3DOverlay::evalRenderTransform() { + Transform transform = getTransform(); +#ifndef USE_SN_SCALE + transform.setScale(1.0f); // ignore any inherited scale from SpatiallyNestable +#endif + return transform; +} diff --git a/interface/src/ui/overlays/Volume3DOverlay.h b/interface/src/ui/overlays/Volume3DOverlay.h index 04b694b2f8..53eeae6de0 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.h +++ b/interface/src/ui/overlays/Volume3DOverlay.h @@ -15,6 +15,7 @@ class Volume3DOverlay : public Base3DOverlay { Q_OBJECT + using Parent = Base3DOverlay; public: Volume3DOverlay() {} @@ -35,6 +36,8 @@ public: protected: // Centered local bounding box AABox _localBoundingBox{ vec3(0.0f), 1.0f }; + + Transform evalRenderTransform() override; }; From 906595e4fa95ea4abc0b031c1662a93ca2c5582d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 21 Sep 2017 16:15:06 +1200 Subject: [PATCH 374/722] Automatically return to Tools menu after grouping or ungrouping --- scripts/vr-edit/vr-edit.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index ea1ac86254..2bcd3533cd 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1469,10 +1469,18 @@ case "groupButton": Feedback.play(dominantHand, Feedback.APPLY_PROPERTY); grouping.group(); + grouping.clear(); + toolSelected = TOOL_NONE; + ui.clearTool(); + ui.updateUIOverlays(); break; case "ungroupButton": Feedback.play(dominantHand, Feedback.APPLY_PROPERTY); grouping.ungroup(); + grouping.clear(); + toolSelected = TOOL_NONE; + ui.clearTool(); + ui.updateUIOverlays(); break; case "setColor": From a56a155cc3781fa411f58519a2decf6f2843270f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 21 Sep 2017 17:31:32 +1200 Subject: [PATCH 375/722] Add group selection box buttons --- .../assets/tools/group/cancel-label.svg | 12 ++++ .../tools/group/selection-box-label.svg | 33 ++++++++++ scripts/vr-edit/modules/toolsMenu.js | 63 +++++++++++++++++-- 3 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 scripts/vr-edit/assets/tools/group/cancel-label.svg create mode 100644 scripts/vr-edit/assets/tools/group/selection-box-label.svg diff --git a/scripts/vr-edit/assets/tools/group/cancel-label.svg b/scripts/vr-edit/assets/tools/group/cancel-label.svg new file mode 100644 index 0000000000..66e9ad4afc --- /dev/null +++ b/scripts/vr-edit/assets/tools/group/cancel-label.svg @@ -0,0 +1,12 @@ + + + + CANCEL + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/group/selection-box-label.svg b/scripts/vr-edit/assets/tools/group/selection-box-label.svg new file mode 100644 index 0000000000..21bed98a90 --- /dev/null +++ b/scripts/vr-edit/assets/tools/group/selection-box-label.svg @@ -0,0 +1,33 @@ + + + + group-icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 2b93c215d4..ebe8d1bac4 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -990,12 +990,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { dimensions: { x: UIT.dimensions.buttonDimensions.x, - y: 0.0680, + y: 0.0400, z: UIT.dimensions.buttonDimensions.z }, localPosition: { x: 0, - y: UIT.dimensions.panel.y / 2 - 0.0280 - 0.0680 / 2, + y: UIT.dimensions.panel.y / 2 - 0.0280 - 0.0400 / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 }, color: UIT.colors.baseGrayShadow @@ -1018,16 +1018,15 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { dimensions: { x: UIT.dimensions.buttonDimensions.x, - y: 0.0680, + y: 0.0400, z: UIT.dimensions.buttonDimensions.z }, localPosition: { x: 0, - y: -UIT.dimensions.panel.y / 2 + 0.0120 + 0.0680 / 2, + y: UIT.dimensions.panel.y / 2 - 0.0280 - 0.0400 - 0.0040 - 0.0400 / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 }, color: UIT.colors.baseGrayShadow - }, enabledColor: UIT.colors.redHighlight, highlightColor: UIT.colors.redAccent, @@ -1040,6 +1039,60 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { callback: { method: "ungroupButton" } + }, + { + id: "groupRule2", + type: "horizontalRule", + properties: { + localPosition: { + x: 0, + y: -UIT.dimensions.panel.y / 2 + 0.0603, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } + } + }, + { + id: "groupSelectionBoxButton", + type: "toggleButton", + properties: { + dimensions: { x: 0.1042, y: 0.0400, z: UIT.dimensions.buttonDimensions.z }, + localPosition: { + x: -0.0040 - 0.1042 / 2, + y: -0.0900 + 0.0120 + 0.0400 / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 + } + }, + label: { + url: "../assets/tools/group/selection-box-label.svg", + scale: 0.0161 + }, + command: { + method: "toggleGroupSelectionBox" + } + }, + { + id: "groupsSelectionBoxCancelButton", + type: "button", + properties: { + dimensions: { x: 0.1042, y: 0.0400, z: UIT.dimensions.buttonDimensions.z }, + localPosition: { + x: 0.0040 + 0.1042 / 2, + y: -0.0900 + 0.0120 + 0.0400 / 2, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 + }, + color: UIT.colors.baseGrayShadow + }, + enabledColor: UIT.colors.greenHighlight, + highlightColor: UIT.colors.greenShadow, + label: { + url: "../assets/tools/group/cancel-label.svg", + scale: 0.0380, + color: UIT.colors.baseGray + }, + labelEnabledColor: UIT.colors.white, + command: { + method: "cancelGroupSelectionBox" + } } ], physicsOptions: [ From 2f9a314aa9536cb62225d0fe0272e642138d3efa Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 21 Sep 2017 19:07:13 +1200 Subject: [PATCH 376/722] Implement group selection box button states --- scripts/vr-edit/modules/toolsMenu.js | 54 ++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index ebe8d1bac4..0c3ca9fb02 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -1090,6 +1090,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: UIT.colors.baseGray }, labelEnabledColor: UIT.colors.white, + enabled: false, command: { method: "cancelGroupSelectionBox" } @@ -2423,7 +2424,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { swatchHighlightOverlay = Overlays.addOverlay(UI_ELEMENTS.swatchHighlight.overlay, properties); } - optionsEnabled.push(true); + optionsEnabled.push(optionsItems[i].enabled === undefined || optionsItems[i].enabled); } // Special handling for Group options. @@ -2703,6 +2704,54 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { setColorPicker(parameter); break; + case "toggleGroupSelectionBox": + optionsToggles.groupSelectionBoxButton = !optionsToggles.groupSelectionBoxButton; + + index = optionsOverlaysIDs.indexOf("groupSelectionBoxButton"); + Overlays.editOverlay(optionsOverlays[index], { + color: optionsToggles.groupSelectionBoxButton + ? UI_ELEMENTS[optionsItems[index].type].onHoverColor + : UI_ELEMENTS[optionsItems[index].type].offHoverColor + }); + + index = optionsOverlaysIDs.indexOf("groupsSelectionBoxCancelButton"); + Overlays.editOverlay(optionsOverlays[index], { + color: optionsToggles.groupSelectionBoxButton + ? optionsItems[index].enabledColor + : optionsItems[index].properties.color + }); + Overlays.editOverlay(optionsOverlaysLabels[index], { + color: optionsToggles.groupSelectionBoxButton + ? optionsItems[index].labelEnabledColor + : optionsItems[index].label.color + }); + optionsEnabled[index] = optionsToggles.groupSelectionBoxButton; + + uiCommandCallback("toggleGroupSelectionBoxTool", optionsToggles.groupSelectionBoxButton); + break; + + case "cancelGroupSelectionBox": + optionsToggles.groupSelectionBoxButton = false; + + index = optionsOverlaysIDs.indexOf("groupSelectionBoxButton"); + Overlays.editOverlay(optionsOverlays[index], { + color : optionsToggles.groupSelectionBoxButton + ? UI_ELEMENTS[optionsItems[index].type].onHoverColor + : UI_ELEMENTS[optionsItems[index].type].offHoverColor + }); + + index = optionsOverlaysIDs.indexOf("groupsSelectionBoxCancelButton"); + Overlays.editOverlay(optionsOverlays[index], { + color: optionsItems[index].properties.color + }); + Overlays.editOverlay(optionsOverlaysLabels[index], { + color: optionsItems[index].label.color + }); + optionsEnabled[index] = false; + + uiCommandCallback("cancelGroupSelectionBoxTool"); + break; + case "setGravityOn": case "setGrabOn": case "setCollideOn": @@ -3209,7 +3258,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (pressedItem) { // Unpress previous button. Overlays.editOverlay(pressedSource[pressedItem.index], { - localPosition: isHoveringButtonElement && hoveredItem === pressedItem.index + localPosition: + isHoveringButtonElement && hoveredItem === pressedItem.index && optionsEnabled[pressedItem.index] ? Vec3.sum(pressedItem.localPosition, OPTION_HOVER_DELTA) : pressedItem.localPosition }); From f58b2a3bed5ba07933f7f97f6d9e4e6088212da7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 21 Sep 2017 21:59:19 +1200 Subject: [PATCH 377/722] Add group selection logic --- scripts/vr-edit/vr-edit.js | 76 ++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 2bcd3533cd..c50160f4b4 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -29,10 +29,11 @@ TOOL_SCALE = 1, TOOL_CLONE = 2, TOOL_GROUP = 3, - TOOL_COLOR = 4, - TOOL_PICK_COLOR = 5, - TOOL_PHYSICS = 6, - TOOL_DELETE = 7, + TOOL_GROUP_BOX = 4, + TOOL_COLOR = 5, + TOOL_PICK_COLOR = 6, + TOOL_PHYSICS = 7, + TOOL_DELETE = 8, toolSelected = TOOL_NONE, colorToolColor = { red: 128, green: 128, blue: 128 }, physicsToolPhysics = { userData: { grabbableKey: {} } }, @@ -816,8 +817,17 @@ if (!grouping.includes(rootEntityID)) { highlights.display(false, selection.selection(), null, highlights.GROUP_COLOR); } - Feedback.play(side, Feedback.SELECT_ENTITY); - grouping.toggle(selection.selection()); + if (toolSelected === TOOL_GROUP_BOX) { + if (!grouping.includes(rootEntityID)) { + Feedback.play(side, Feedback.SELECT_ENTITY); + grouping.selectInBox(selection.selection()); + } else { + Feedback.play(side, Feedback.GENERAL_ERROR); + } + } else { + Feedback.play(side, Feedback.SELECT_ENTITY); + grouping.toggle(selection.selection()); + } } function exitEditorGrouping() { @@ -954,7 +964,7 @@ } } else if (toolSelected === TOOL_CLONE) { setState(EDITOR_CLONING); - } else if (toolSelected === TOOL_GROUP) { + } else if (toolSelected === TOOL_GROUP || toolSelected === TOOL_GROUP_BOX) { setState(EDITOR_GROUPING); } else if (toolSelected === TOOL_COLOR) { setState(EDITOR_HIGHLIGHTING); @@ -1043,7 +1053,7 @@ } } else if (toolSelected === TOOL_CLONE) { setState(EDITOR_CLONING); - } else if (toolSelected === TOOL_GROUP) { + } else if (toolSelected === TOOL_GROUP || toolSelected === TOOL_GROUP_BOX) { setState(EDITOR_GROUPING); } else if (toolSelected === TOOL_COLOR) { if (selection.applyColor(colorToolColor, false)) { @@ -1181,6 +1191,7 @@ } break; case EDITOR_GROUPING: + // Immediate transition out of state after updating group data during state entry. if (hand.valid() && isTriggerClicked) { // No transition. break; @@ -1270,6 +1281,7 @@ // Grouping highlights and functions. var groups, + isSelectInBox = false, highlights, exludedLeftRootEntityID = null, exludedrightRootEntityID = null, @@ -1295,6 +1307,33 @@ } } + function updateSelectInBox() { + // TODO: Calculate bounding box. + + // TODO: Select further entities per bounding box. + + // TODO: Update bounding box overlay. + } + + function startSelectInBox() { + isSelectInBox = true; + + // TODO: Create bounding box overlay. + + updateSelectInBox(); + } + + function selectInBox(selection) { + toggle(selection); + updateSelectInBox(); + } + + function stopSelectInBox() { + isSelectInBox = false; + + // TODO: Delete bounding box overlay. + } + function includes(rootEntityID) { return groups.includes(rootEntityID); } @@ -1360,6 +1399,9 @@ return { toggle: toggle, + startSelectInBox: startSelectInBox, + selectInBox: selectInBox, + stopSelectInBox: stopSelectInBox, includes: includes, groupsCount: groupsCount, entitiesCount: entitiesCount, @@ -1482,6 +1524,24 @@ ui.clearTool(); ui.updateUIOverlays(); break; + case "toggleGroupSelectionBoxTool": + toolSelected = parameter ? TOOL_GROUP_BOX : TOOL_GROUP; + if (toolSelected === TOOL_GROUP_BOX) { + grouping.startSelectInBox(); + } else { + grouping.stopSelectInBox(); + } + break; + case "cancelGroupSelectionBoxTool": + if (grouping.groupsCount() > 0) { + Feedback.play(dominantHand, Feedback.SELECT_ENTITY); + } + if (toolSelected === TOOL_GROUP_BOX) { + grouping.stopSelectInBox(); + } + grouping.clear(); + toolSelected = TOOL_GROUP; + break; case "setColor": if (toolSelected === TOOL_PICK_COLOR) { From 8085d0292d41c58c5147dbe1a51fc6ef605a30c0 Mon Sep 17 00:00:00 2001 From: beholder Date: Thu, 21 Sep 2017 16:26:54 +0300 Subject: [PATCH 378/722] 7723 Tablet Should Rotate Faster when using Create app --- scripts/system/libraries/WebTablet.js | 4 ++++ scripts/system/tablet-ui/tabletUI.js | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index c5f8168c30..92a5857390 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -222,6 +222,10 @@ WebTablet.prototype.getTabletTextureResolution = function() { } }; +WebTablet.prototype.getLandscape = function() { + return this.landscape; +} + WebTablet.prototype.setLandscape = function(newLandscapeValue) { if (this.landscape === newLandscapeValue) { return; diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index 63c1cc51aa..cf2a1f1315 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -192,7 +192,9 @@ return; } - if (now - validCheckTime > MSECS_PER_SEC) { + var needInstantUpdate = UIWebTablet && UIWebTablet.getLandscape() !== landscape; + + if ((now - validCheckTime > MSECS_PER_SEC) || needInstantUpdate) { validCheckTime = now; updateTabletWidthFromSettings(); From 65e0c99618bf05b704f97715b516cb53ceb43d9c Mon Sep 17 00:00:00 2001 From: druiz17 Date: Thu, 21 Sep 2017 09:07:49 -0700 Subject: [PATCH 379/722] small change --- scripts/system/controllers/controllerModules/scaleEntity.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/system/controllers/controllerModules/scaleEntity.js b/scripts/system/controllers/controllerModules/scaleEntity.js index 4b504c3733..79b1d18db9 100644 --- a/scripts/system/controllers/controllerModules/scaleEntity.js +++ b/scripts/system/controllers/controllerModules/scaleEntity.js @@ -35,10 +35,7 @@ }; this.bumperPressed = function(controllerData) { - if ( controllerData.secondaryValues[this.hand] > dispatcherUtils.BUMPER_ON_VALUE) { - return true; - } - return false; + return ( controllerData.secondaryValues[this.hand] > dispatcherUtils.BUMPER_ON_VALUE); }; this.getTargetProps = function(controllerData) { From 473db92a8edb255549d79428fda3311616157257 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 21 Sep 2017 11:07:47 -0700 Subject: [PATCH 380/722] fix some parent-grab bugs, re-enable adjusting equipped entities, don't trigger guns right when they are equipped --- .../controllers/controllerDispatcher.js | 3 +- .../controllerModules/equipEntity.js | 40 +++-- .../controllerModules/nearActionGrabEntity.js | 3 +- .../controllerModules/nearParentGrabEntity.js | 155 +++++++++++++----- .../libraries/controllerDispatcherUtils.js | 73 +++++++-- 5 files changed, 201 insertions(+), 73 deletions(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 63657e9b6f..a71d1d8a2a 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -197,7 +197,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); var h; for (h = LEFT_HAND; h <= RIGHT_HAND; h++) { if (controllerLocations[h].valid) { - var nearbyOverlays = Overlays.findOverlays(controllerLocations[h].position, NEAR_MAX_RADIUS * sensorScaleFactor); + var nearbyOverlays = + Overlays.findOverlays(controllerLocations[h].position, NEAR_MAX_RADIUS * sensorScaleFactor); nearbyOverlays.sort(function (a, b) { var aPosition = Overlays.getProperty(a, "position"); var aDistance = Vec3.distance(aPosition, controllerLocations[h].position); diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index fe868493f4..3431f8d3c3 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -254,6 +254,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa this.triggerValue = 0; this.messageGrabEntity = false; this.grabEntityProps = null; + this.shouldSendStart = false; this.parameters = makeDispatcherModuleParameters( 300, @@ -507,8 +508,9 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa return; } - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.targetEntityID, "startEquip", args); + // we don't want to send startEquip message until the trigger is released. otherwise, + // guns etc will fire right as they are equipped. + this.shouldSendStart = true; Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'equip', @@ -588,22 +590,21 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa // if the potentialHotspot is cloneable, clone it and return it // if the potentialHotspot os not cloneable and locked return null - if (potentialEquipHotspot) { - if ((this.triggerSmoothedSqueezed() && !this.waitForTriggerRelease) || this.messageGrabEntity) { - this.grabbedHotspot = potentialEquipHotspot; - this.targetEntityID = this.grabbedHotspot.entityID; - this.startEquipEntity(controllerData); - this.messageGrabEnity = false; - } + if (potentialEquipHotspot && + ((this.triggerSmoothedSqueezed() && !this.waitForTriggerRelease) || this.messageGrabEntity)) { + this.grabbedHotspot = potentialEquipHotspot; + this.targetEntityID = this.grabbedHotspot.entityID; + this.startEquipEntity(controllerData); + this.messageGrabEnity = false; return makeRunningValues(true, [potentialEquipHotspot.entityID], []); } else { return makeRunningValues(false, [], []); } }; - this.isTargetIDValid = function() { - var entityProperties = Entities.getEntityProperties(this.targetEntityID, ["type"]); - return "type" in entityProperties; + this.isTargetIDValid = function(controllerData) { + var entityProperties = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; + return entityProperties && "type" in entityProperties; }; this.isReady = function (controllerData, deltaTime) { @@ -616,7 +617,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var timestamp = Date.now(); this.updateInputs(controllerData); - if (!this.isTargetIDValid()) { + if (!this.isTargetIDValid(controllerData)) { this.endEquipEntity(); return makeRunningValues(false, [], []); } @@ -643,6 +644,13 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var dropDetected = this.dropGestureProcess(deltaTime); if (this.triggerSmoothedReleased()) { + if (this.shouldSendStart) { + // we don't want to send startEquip message until the trigger is released. otherwise, + // guns etc will fire right as they are equipped. + var startArgs = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "startEquip", startArgs); + this.shouldSendStart = false; + } this.waitForTriggerRelease = false; } @@ -674,8 +682,10 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa equipHotspotBuddy.update(deltaTime, timestamp, controllerData); - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.targetEntityID, "continueEquip", args); + if (!this.shouldSendStart) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "continueEquip", args); + } return makeRunningValues(true, [this.targetEntityID], []); }; diff --git a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js index bd7a64572a..41a5202887 100644 --- a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js @@ -182,7 +182,8 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); } if (targetProps) { - if (!propsArePhysical(targetProps) && !propsAreCloneDynamic(targetProps)) { + if ((!propsArePhysical(targetProps) && !propsAreCloneDynamic(targetProps)) || + targetProps.parentID != NULL_UUID) { return makeRunningValues(false, [], []); // let nearParentGrabEntity handle it } else { this.targetEntityID = targetProps.id; diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js index e08b61dbd5..51790f0bfd 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -8,8 +8,10 @@ /* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, AVATAR_SELF_ID, getControllerJointIndex, NULL_UUID, enableDispatcherModule, disableDispatcherModule, propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, - TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS, findGroupParent, - Vec3, cloneEntity, entityIsCloneable, propsAreCloneDynamic, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE + TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS, + findGroupParent, Vec3, cloneEntity, entityIsCloneable, propsAreCloneDynamic, HAPTIC_PULSE_STRENGTH, + HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, findHandChildEntities, TEAR_AWAY_DISTANCE, MSECS_PER_SEC, TEAR_AWAY_CHECK_TIME, + TEAR_AWAY_COUNT, distanceBetweenPointAndEntityBoundingBox */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -28,6 +30,9 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); this.previousParentJointIndex = {}; this.previouslyUnhooked = {}; this.hapticTargetID = null; + this.lastUnequipCheckTime = 0; + this.autoUnequipCounter = 0; + this.lastUnexpectedChildrenCheckTime = 0; this.parameters = makeDispatcherModuleParameters( 500, @@ -40,15 +45,11 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); this.controllerJointIndex = getControllerJointIndex(this.hand); - this.getOtherModule = function() { - return (this.hand === RIGHT_HAND) ? leftNearParentingGrabEntity : rightNearParentingGrabEntity; - }; - - this.otherHandIsParent = function(props) { - return this.getOtherModule().thisHandIsParent(props); - }; - this.thisHandIsParent = function(props) { + if (!props) { + return false; + } + if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== AVATAR_SELF_ID) { return false; } @@ -97,14 +98,8 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); if (this.thisHandIsParent(targetProps)) { // this should never happen, but if it does, don't set previous parent to be this hand. - // this.previousParentID[targetProps.id] = NULL; - // this.previousParentJointIndex[targetProps.id] = -1; - } else if (this.otherHandIsParent(targetProps)) { - // the other hand is parent. Steal the object and information - var otherModule = this.getOtherModule(); - this.previousParentID[targetProps.id] = otherModule.previousParentID[targetProps.id]; - this.previousParentJointIndex[targetProps.id] = otherModule.previousParentJointIndex[targetProps.id]; - otherModule.endNearParentingGrabEntity(); + this.previousParentID[targetProps.id] = null; + this.previousParentJointIndex[targetProps.id] = -1; } else { this.previousParentID[targetProps.id] = targetProps.parentID; this.previousParentJointIndex[targetProps.id] = targetProps.parentJointIndex; @@ -121,20 +116,24 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); this.grabbing = true; }; - this.endNearParentingGrabEntity = function () { - if (this.previousParentID[this.targetEntityID] === NULL_UUID || this.previousParentID === undefined) { - Entities.editEntity(this.targetEntityID, { - parentID: this.previousParentID[this.targetEntityID], - parentJointIndex: this.previousParentJointIndex[this.targetEntityID] - }); - } else { - // we're putting this back as a child of some other parent, so zero its velocity - Entities.editEntity(this.targetEntityID, { - parentID: this.previousParentID[this.targetEntityID], - parentJointIndex: this.previousParentJointIndex[this.targetEntityID], - localVelocity: {x: 0, y: 0, z: 0}, - localAngularVelocity: {x: 0, y: 0, z: 0} - }); + this.endNearParentingGrabEntity = function (controllerData) { + this.hapticTargetID = null; + var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; + if (this.thisHandIsParent(props)) { + if (this.previousParentID[this.targetEntityID] === NULL_UUID || this.previousParentID === undefined) { + Entities.editEntity(this.targetEntityID, { + parentID: this.previousParentID[this.targetEntityID], + parentJointIndex: this.previousParentJointIndex[this.targetEntityID] + }); + } else { + // we're putting this back as a child of some other parent, so zero its velocity + Entities.editEntity(this.targetEntityID, { + parentID: this.previousParentID[this.targetEntityID], + parentJointIndex: this.previousParentJointIndex[this.targetEntityID], + localVelocity: {x: 0, y: 0, z: 0}, + localAngularVelocity: {x: 0, y: 0, z: 0} + }); + } } var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; @@ -143,6 +142,71 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); this.targetEntityID = null; }; + this.checkForChildTooFarAway = function (controllerData) { + var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; + var now = Date.now(); + if (now - this.lastUnequipCheckTime > MSECS_PER_SEC * TEAR_AWAY_CHECK_TIME) { + this.lastUnequipCheckTime = now; + if (props.parentID == AVATAR_SELF_ID) { + var handPosition = controllerData.controllerLocations[this.hand].position; + var dist = distanceBetweenPointAndEntityBoundingBox(handPosition, props); + if (dist > TEAR_AWAY_DISTANCE) { + this.autoUnequipCounter++; + } else { + this.autoUnequipCounter = 0; + } + if (this.autoUnequipCounter >= TEAR_AWAY_COUNT) { + return true; + } + } + } + return false; + }; + + + this.checkForUnexpectedChildren = function (controllerData) { + // sometimes things can get parented to a hand and this script is unaware. Search for such entities and + // unhook them. + + var now = Date.now(); + var UNEXPECTED_CHILDREN_CHECK_TIME = 0.1; // seconds + if (now - this.lastUnexpectedChildrenCheckTime > MSECS_PER_SEC * UNEXPECTED_CHILDREN_CHECK_TIME) { + this.lastUnexpectedChildrenCheckTime = now; + + var children = findHandChildEntities(this.hand); + var _this = this; + + children.forEach(function(childID) { + // we appear to be holding something and this script isn't in a state that would be holding something. + // unhook it. if we previously took note of this entity's parent, put it back where it was. This + // works around some problems that happen when more than one hand or avatar is passing something around. + if (_this.previousParentID[childID]) { + var previousParentID = _this.previousParentID[childID]; + var previousParentJointIndex = _this.previousParentJointIndex[childID]; + + // The main flaw with keeping track of previous parantage in individual scripts is: + // (1) A grabs something (2) B takes it from A (3) A takes it from B (4) A releases it + // now A and B will take turns passing it back to the other. Detect this and stop the loop here... + var UNHOOK_LOOP_DETECT_MS = 200; + if (_this.previouslyUnhooked[childID]) { + if (now - _this.previouslyUnhooked[childID] < UNHOOK_LOOP_DETECT_MS) { + previousParentID = NULL_UUID; + previousParentJointIndex = -1; + } + } + _this.previouslyUnhooked[childID] = now; + + Entities.editEntity(childID, { + parentID: previousParentID, + parentJointIndex: previousParentJointIndex + }); + } else { + Entities.editEntity(childID, { parentID: NULL_UUID }); + } + }); + } + }; + this.getTargetProps = function (controllerData) { // nearbyEntityProperties is already sorted by length from controller var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; @@ -178,11 +242,13 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); var targetProps = this.getTargetProps(controllerData); if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE && controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { + this.checkForUnexpectedChildren(controllerData); return makeRunningValues(false, [], []); } if (targetProps) { - if (propsArePhysical(targetProps) || propsAreCloneDynamic(targetProps)) { + if ((propsArePhysical(targetProps) || propsAreCloneDynamic(targetProps)) && + targetProps.parentID == NULL_UUID) { return makeRunningValues(false, [], []); // let nearActionGrabEntity handle it } else { this.targetEntityID = targetProps.id; @@ -198,16 +264,23 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); if (this.grabbing) { if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE && controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { - this.endNearParentingGrabEntity(); + this.endNearParentingGrabEntity(controllerData); + return makeRunningValues(false, [], []); + } + + var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; + if (!props) { + // entity was deleted + this.grabbing = false; + this.targetEntityID = null; this.hapticTargetID = null; return makeRunningValues(false, [], []); } - var props = Entities.getEntityProperties(this.targetEntityID); - if (!this.thisHandIsParent(props)) { - this.grabbing = false; - this.targetEntityID = null; - this.hapticTargetID = null; + if (this.checkForChildTooFarAway(controllerData)) { + // if the held entity moves too far from the hand, release it + print("nearParentGrabEntity -- autoreleasing held item because it is far from hand"); + this.endNearParentingGrabEntity(controllerData); return makeRunningValues(false, [], []); } @@ -215,7 +288,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args); } else { // still searching / highlighting - var readiness = this.isReady (controllerData); + var readiness = this.isReady(controllerData); if (!readiness.active) { return readiness; } @@ -227,7 +300,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); if (targetCloneable) { var worldEntityProps = controllerData.nearbyEntityProperties[this.hand]; var cloneID = cloneEntity(targetProps, worldEntityProps); - var cloneProps = Entities.getEntityProperties(cloneID); + var cloneProps = controllerData.nearbyEntityPropertiesByID[cloneID]; this.grabbing = true; this.targetEntityID = cloneID; diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 33eec74111..ab3bea3fdd 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -6,7 +6,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global Camera, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays, +/* global module, Camera, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays, Xform, MSECS_PER_SEC:true , LEFT_HAND:true, RIGHT_HAND:true, NULL_UUID:true, AVATAR_SELF_ID:true, FORBIDDEN_GRAB_TYPES:true, HAPTIC_PULSE_STRENGTH:true, HAPTIC_PULSE_DURATION:true, ZERO_VEC:true, ONE_VEC:true, DEFAULT_REGISTRATION_POINT:true, INCHES_TO_METERS:true, @@ -40,7 +40,12 @@ entityHasActions:true, ensureDynamic:true, findGroupParent:true, - BUMPER_ON_VALUE:true + BUMPER_ON_VALUE:true, + findHandChildEntities:true, + TEAR_AWAY_DISTANCE:true, + TEAR_AWAY_COUNT:true, + TEAR_AWAY_CHECK_TIME:true, + distanceBetweenPointAndEntityBoundingBox:true */ MSECS_PER_SEC = 1000.0; @@ -79,6 +84,10 @@ COLORS_GRAB_DISTANCE_HOLD = { red: 238, green: 75, blue: 214 }; NEAR_GRAB_RADIUS = 1.0; +TEAR_AWAY_DISTANCE = 0.1; // ungrab an entity if its bounding-box moves this far from the hand +TEAR_AWAY_COUNT = 2; // multiply by TEAR_AWAY_CHECK_TIME to know how long the item must be away +TEAR_AWAY_CHECK_TIME = 0.15; // seconds, duration between checks + DISPATCHER_PROPERTIES = [ "position", "registrationPoint", @@ -193,17 +202,6 @@ entityIsDistanceGrabbable = function(props) { return false; } - // XXX - // var distance = Vec3.distance(props.position, handPosition); - // this.otherGrabbingUUID = entityIsGrabbedByOther(entityID); - // if (this.otherGrabbingUUID !== null) { - // // don't distance grab something that is already grabbed. - // if (debug) { - // print("distance grab is skipping '" + props.name + "': already grabbed by another."); - // } - // return false; - // } - return true; }; @@ -296,7 +294,7 @@ ensureDynamic = function (entityID) { }; findGroupParent = function (controllerData, targetProps) { - while (targetProps.parentID && targetProps.parentID !== NULL_UUID) { + while (targetProps.parentID && targetProps.parentID !== NULL_UUID && targetProps.parentID !== AVATAR_SELF_ID) { // XXX use controllerData.nearbyEntityPropertiesByID ? var parentProps = Entities.getEntityProperties(targetProps.parentID, DISPATCHER_PROPERTIES); if (!parentProps) { @@ -310,6 +308,50 @@ findGroupParent = function (controllerData, targetProps) { return targetProps; }; + +findHandChildEntities = function(hand) { + // find children of avatar's hand joint + var handJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex); + children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, handJointIndex)); + + // find children of faux controller joint + var controllerJointIndex = getControllerJointIndex(hand); + children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerJointIndex)); + children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerJointIndex)); + + // find children of faux camera-relative controller joint + var controllerCRJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ? + "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : + "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); + children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerCRJointIndex)); + children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerCRJointIndex)); + + return children.filter(function (childID) { + var childType = Entities.getNestableType(childID); + return childType == "entity"; + }); +}; + +distanceBetweenPointAndEntityBoundingBox = function(point, entityProps) { + var entityXform = new Xform(entityProps.rotation, entityProps.position); + var localPoint = entityXform.inv().xformPoint(point); + var minOffset = Vec3.multiplyVbyV(entityProps.registrationPoint, entityProps.dimensions); + var maxOffset = Vec3.multiplyVbyV(Vec3.subtract(ONE_VEC, entityProps.registrationPoint), entityProps.dimensions); + var localMin = Vec3.subtract(entityXform.trans, minOffset); + var localMax = Vec3.sum(entityXform.trans, maxOffset); + + var v = {x: localPoint.x, y: localPoint.y, z: localPoint.z}; + v.x = Math.max(v.x, localMin.x); + v.x = Math.min(v.x, localMax.x); + v.y = Math.max(v.y, localMin.y); + v.y = Math.min(v.y, localMax.y); + v.z = Math.max(v.z, localMin.z); + v.z = Math.min(v.z, localMax.z); + + return Vec3.distance(v, localPoint); +}; + if (typeof module !== 'undefined') { module.exports = { makeDispatcherModuleParameters: makeDispatcherModuleParameters, @@ -318,6 +360,7 @@ if (typeof module !== 'undefined') { makeRunningValues: makeRunningValues, LEFT_HAND: LEFT_HAND, RIGHT_HAND: RIGHT_HAND, - BUMPER_ON_VALUE: BUMPER_ON_VALUE + BUMPER_ON_VALUE: BUMPER_ON_VALUE, + TEAR_AWAY_DISTANCE: TEAR_AWAY_DISTANCE }; } From 24bfb3f3b9941ae5ebcd3cb8bfcde9686c1c08a5 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 21 Sep 2017 11:16:00 -0700 Subject: [PATCH 381/722] oops --- .../controllers/controllerModules/nearParentGrabEntity.js | 1 - scripts/system/libraries/controllerDispatcherUtils.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js index 51790f0bfd..d3548a9356 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -301,7 +301,6 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); var worldEntityProps = controllerData.nearbyEntityProperties[this.hand]; var cloneID = cloneEntity(targetProps, worldEntityProps); var cloneProps = controllerData.nearbyEntityPropertiesByID[cloneID]; - this.grabbing = true; this.targetEntityID = cloneID; this.startNearParentingGrabEntity(controllerData, cloneProps); diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index b7ddf13488..59c529e501 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -361,7 +361,7 @@ if (typeof module !== 'undefined') { LEFT_HAND: LEFT_HAND, RIGHT_HAND: RIGHT_HAND, BUMPER_ON_VALUE: BUMPER_ON_VALUE, - TEAR_AWAY_DISTANCE: TEAR_AWAY_DISTANCE + TEAR_AWAY_DISTANCE: TEAR_AWAY_DISTANCE, projectOntoOverlayXYPlane: projectOntoOverlayXYPlane, projectOntoEntityXYPlane: projectOntoEntityXYPlane }; From 3c22c9ea3a0619d18e947b9697788f41f4010664 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 21 Sep 2017 11:39:14 -0700 Subject: [PATCH 382/722] back out bad change --- scripts/system/controllers/controllerDispatcher.js | 2 ++ .../controllers/controllerModules/nearParentGrabEntity.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index da0a42413e..4bed004e67 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -299,6 +299,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); // activity-slots which this plugin consumes as "in use" _this.runningPluginNames[orderedPluginName] = true; _this.markSlots(candidatePlugin, orderedPluginName); + print("QQQQ running " + orderedPluginName); } if (PROFILE) { Script.endProfileRange("dispatch.isReady." + orderedPluginName); @@ -331,6 +332,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); // of running plugins and mark its activity-slots as "not in use" delete _this.runningPluginNames[runningPluginName]; _this.markSlots(plugin, false); + print("QQQQ stopping " + runningPluginName); } if (PROFILE) { Script.endProfileRange("dispatch.run." + runningPluginName); diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js index d3548a9356..e0bb596253 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -300,7 +300,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); if (targetCloneable) { var worldEntityProps = controllerData.nearbyEntityProperties[this.hand]; var cloneID = cloneEntity(targetProps, worldEntityProps); - var cloneProps = controllerData.nearbyEntityPropertiesByID[cloneID]; + var cloneProps = Entities.getEntityProperties(cloneID); this.grabbing = true; this.targetEntityID = cloneID; this.startNearParentingGrabEntity(controllerData, cloneProps); From fc3e259f6fd1b3cd83e02041226b045c9c99434e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 21 Sep 2017 11:42:30 -0700 Subject: [PATCH 383/722] remove debug prints --- scripts/system/controllers/controllerDispatcher.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 4bed004e67..da0a42413e 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -299,7 +299,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); // activity-slots which this plugin consumes as "in use" _this.runningPluginNames[orderedPluginName] = true; _this.markSlots(candidatePlugin, orderedPluginName); - print("QQQQ running " + orderedPluginName); } if (PROFILE) { Script.endProfileRange("dispatch.isReady." + orderedPluginName); @@ -332,7 +331,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); // of running plugins and mark its activity-slots as "not in use" delete _this.runningPluginNames[runningPluginName]; _this.markSlots(plugin, false); - print("QQQQ stopping " + runningPluginName); } if (PROFILE) { Script.endProfileRange("dispatch.run." + runningPluginName); From 7103864b3705931e4bd71ba85c4b8c55526635d2 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 21 Sep 2017 12:23:53 -0700 Subject: [PATCH 384/722] Also rendering the line3D overlay with start and end captured in game loop --- interface/src/ui/overlays/Line3DOverlay.cpp | 9 +++++---- interface/src/ui/overlays/Line3DOverlay.h | 5 +++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index 3bee13269b..d8a83fad49 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -132,10 +132,9 @@ void Line3DOverlay::render(RenderArgs* args) { glm::vec4 colorv4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha); auto batch = args->_batch; if (batch) { - // FIXME Start using the _renderTransform instead of calling for Transform and start and end from here, do the custom things needed in evalRenderTransform() batch->setModelTransform(Transform()); - glm::vec3 start = getStart(); - glm::vec3 end = getEnd(); + glm::vec3 start = _renderStart; + glm::vec3 end = _renderEnd; auto geometryCache = DependencyManager::get(); if (getIsDashedLine()) { @@ -270,5 +269,7 @@ Line3DOverlay* Line3DOverlay::createClone() const { } Transform Line3DOverlay::evalRenderTransform() { - return getTransform(); + _renderStart = getStart(); + _renderEnd = getEnd(); + return Parent::evalRenderTransform(); } diff --git a/interface/src/ui/overlays/Line3DOverlay.h b/interface/src/ui/overlays/Line3DOverlay.h index 7e8be48cd6..d1c6aa6183 100644 --- a/interface/src/ui/overlays/Line3DOverlay.h +++ b/interface/src/ui/overlays/Line3DOverlay.h @@ -15,6 +15,7 @@ class Line3DOverlay : public Base3DOverlay { Q_OBJECT + using Parent = Base3DOverlay; public: static QString const TYPE; @@ -72,6 +73,10 @@ private: float _glow { 0.0 }; float _glowWidth { 0.0 }; int _geometryCacheID; + + // Similar to the _renderTransform, we capture the start and end pos for render loop in game loop + glm::vec3 _renderStart; + glm::vec3 _renderEnd; }; #endif // hifi_Line3DOverlay_h From 454e167d93aee556bdcc8c69e6a6c1b65e8bc452 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 21 Sep 2017 12:58:56 -0700 Subject: [PATCH 385/722] notifying a render transform / bound change when the dimension property change --- interface/src/ui/overlays/Planar3DOverlay.cpp | 5 +++++ interface/src/ui/overlays/Planar3DOverlay.h | 4 ++-- interface/src/ui/overlays/Volume3DOverlay.cpp | 5 +++++ interface/src/ui/overlays/Volume3DOverlay.h | 4 ++-- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/interface/src/ui/overlays/Planar3DOverlay.cpp b/interface/src/ui/overlays/Planar3DOverlay.cpp index cfcea542e3..ac3fe66ddc 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.cpp +++ b/interface/src/ui/overlays/Planar3DOverlay.cpp @@ -35,6 +35,11 @@ AABox Planar3DOverlay::getBounds() const { return AABox(extents); } +void Planar3DOverlay::setDimensions(const glm::vec2& value) { + _dimensions = value; + notifyRenderTransformChange(); +} + void Planar3DOverlay::setProperties(const QVariantMap& properties) { Base3DOverlay::setProperties(properties); diff --git a/interface/src/ui/overlays/Planar3DOverlay.h b/interface/src/ui/overlays/Planar3DOverlay.h index 7cf4e0221d..1360cccfc2 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.h +++ b/interface/src/ui/overlays/Planar3DOverlay.h @@ -24,8 +24,8 @@ public: virtual glm::vec2 getSize() const { return _dimensions; }; glm::vec2 getDimensions() const { return _dimensions; } - void setDimensions(float value) { _dimensions = glm::vec2(value); } - void setDimensions(const glm::vec2& value) { _dimensions = value; } + void setDimensions(float value) { setDimensions(glm::vec2(value)); } + void setDimensions(const glm::vec2& value); void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; diff --git a/interface/src/ui/overlays/Volume3DOverlay.cpp b/interface/src/ui/overlays/Volume3DOverlay.cpp index c42f5ef4d3..2eeadbe7b3 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.cpp +++ b/interface/src/ui/overlays/Volume3DOverlay.cpp @@ -26,6 +26,11 @@ AABox Volume3DOverlay::getBounds() const { return AABox(extents); } +void Volume3DOverlay::setDimensions(const glm::vec3& value) { + _localBoundingBox.setBox(-value / 2.0f, value); + notifyRenderTransformChange(); +} + void Volume3DOverlay::setProperties(const QVariantMap& properties) { Base3DOverlay::setProperties(properties); diff --git a/interface/src/ui/overlays/Volume3DOverlay.h b/interface/src/ui/overlays/Volume3DOverlay.h index 53eeae6de0..bde8c71aef 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.h +++ b/interface/src/ui/overlays/Volume3DOverlay.h @@ -24,8 +24,8 @@ public: virtual AABox getBounds() const override; const glm::vec3& getDimensions() const { return _localBoundingBox.getDimensions(); } - void setDimensions(float value) { _localBoundingBox.setBox(glm::vec3(-value / 2.0f), value); } - void setDimensions(const glm::vec3& value) { _localBoundingBox.setBox(-value / 2.0f, value); } + void setDimensions(float value) { setDimensions(glm::vec3(value)); } + void setDimensions(const glm::vec3& value); void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; From a757012c10702ca04ddb69141a0d8cbe056323fd Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 22 Sep 2017 08:17:35 +1200 Subject: [PATCH 386/722] Change default color to 100% white --- scripts/vr-edit/modules/toolsMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 2b93c215d4..36bc92f187 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -824,7 +824,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { setting: { key: "VREdit.colorTool.currentColor", property: "color", - defaultValue: { red: 128, green: 128, blue: 128 }, + defaultValue: { red: 255, green: 255, blue: 255 }, command: "setPickColor" } } From e4a126461b2c41c3f5fb90ffad0800b9357174d9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 22 Sep 2017 10:53:06 +1200 Subject: [PATCH 387/722] Add extra Create palette items --- scripts/vr-edit/assets/create/circle.fbx | Bin 0 -> 30268 bytes .../vr-edit/assets/create/dodecahedron.fbx | Bin 0 -> 20700 bytes scripts/vr-edit/assets/create/hexagon.fbx | Bin 0 -> 19516 bytes scripts/vr-edit/assets/create/octagon.fbx | Bin 0 -> 20092 bytes scripts/vr-edit/modules/createPalette.js | 62 ++++++++++++++++-- 5 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 scripts/vr-edit/assets/create/circle.fbx create mode 100644 scripts/vr-edit/assets/create/dodecahedron.fbx create mode 100644 scripts/vr-edit/assets/create/hexagon.fbx create mode 100644 scripts/vr-edit/assets/create/octagon.fbx diff --git a/scripts/vr-edit/assets/create/circle.fbx b/scripts/vr-edit/assets/create/circle.fbx new file mode 100644 index 0000000000000000000000000000000000000000..db43fafab646c8bb978539d1f2142ca674983550 GIT binary patch literal 30268 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 C*V-Kb literal 0 HcmV?d00001 diff --git a/scripts/vr-edit/assets/create/octagon.fbx b/scripts/vr-edit/assets/create/octagon.fbx new file mode 100644 index 0000000000000000000000000000000000000000..8de0f6f6f3b8b14dc8bc751c46c41fa1fdacfb69 GIT binary patch literal 20092 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 literal 0 HcmV?d00001 diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index fff8ac34fb..11229562ef 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -219,8 +219,34 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { color: ENTITY_CREATION_COLOR } }, - // TODO: "Dodecahedron" shape type per edit.js. - // TODO: "Hexagon" shape type per edit.js. + { + icon: { + properties: { + url: "../assets/create/dodecahedron.fbx" + } + }, + entity: { + type: "Shape", + shape: "Dodecahedron", + dimensions: ENTITY_CREATION_DIMENSIONS, + color: ENTITY_CREATION_COLOR + } + }, + { + icon: { + properties: { + url: "../assets/create/hexagon.fbx", + dimensions: { x: 0.02078, y: 0.024, z: 0.024 }, + localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) + } + }, + entity: { + type: "Shape", + shape: "Hexagon", + dimensions: ENTITY_CREATION_DIMENSIONS, + color: ENTITY_CREATION_COLOR + } + }, { icon: { properties: { @@ -235,7 +261,21 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { color: ENTITY_CREATION_COLOR } }, - // TODO: "Octagon" shape type per edit.js. + { + icon: { + properties: { + url: "../assets/create/octagon.fbx", + dimensions: { x: 0.023805, y: 0.024, z: 0.024 }, + localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) + } + }, + entity: { + type: "Shape", + shape: "Octagon", + dimensions: ENTITY_CREATION_DIMENSIONS, + color: ENTITY_CREATION_COLOR + } + }, { icon: { properties: { @@ -263,8 +303,22 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: ENTITY_CREATION_DIMENSIONS, color: ENTITY_CREATION_COLOR } + }, + { + icon: { + properties: { + url: "../assets/create/circle.fbx", + dimensions: { x: 0.024, y: 0.0005, z: 0.024 }, + localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) + } + }, + entity: { + type: "Shape", + shape: "Circle", + dimensions: ENTITY_CREATION_DIMENSIONS, + color: ENTITY_CREATION_COLOR + } } - // TODO: "Circle" shape type per edit.js. ], isDisplaying = false, From 2d369a458647fd18b716ff474bcc608c75feec76 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 21 Sep 2017 16:20:13 -0700 Subject: [PATCH 388/722] fixing the image3D updating its renderTransform from it s update() call --- interface/src/ui/overlays/Image3DOverlay.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index 22beb2be20..998cc312eb 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -58,6 +58,7 @@ void Image3DOverlay::update(float deltatime) { setTransform(transform); } #endif + Parent::update(deltatime); } void Image3DOverlay::render(RenderArgs* args) { From 959d497079b62249fe16645f459cbbb49f129018 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 22 Sep 2017 12:00:12 +1200 Subject: [PATCH 389/722] Change group tool's "Cancel" button to "Clear" --- .../assets/tools/group/cancel-label.svg | 12 ---- .../assets/tools/group/clear-label.svg | 11 ++++ scripts/vr-edit/modules/toolsMenu.js | 66 +++++++++---------- scripts/vr-edit/vr-edit.js | 6 +- 4 files changed, 45 insertions(+), 50 deletions(-) delete mode 100644 scripts/vr-edit/assets/tools/group/cancel-label.svg create mode 100644 scripts/vr-edit/assets/tools/group/clear-label.svg diff --git a/scripts/vr-edit/assets/tools/group/cancel-label.svg b/scripts/vr-edit/assets/tools/group/cancel-label.svg deleted file mode 100644 index 66e9ad4afc..0000000000 --- a/scripts/vr-edit/assets/tools/group/cancel-label.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - CANCEL - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/scripts/vr-edit/assets/tools/group/clear-label.svg b/scripts/vr-edit/assets/tools/group/clear-label.svg new file mode 100644 index 0000000000..8bc5daa8b4 --- /dev/null +++ b/scripts/vr-edit/assets/tools/group/clear-label.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 0c3ca9fb02..fdad65e82c 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -1071,7 +1071,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, { - id: "groupsSelectionBoxCancelButton", + id: "clearGroupingButton", type: "button", properties: { dimensions: { x: 0.1042, y: 0.0400, z: UIT.dimensions.buttonDimensions.z }, @@ -1085,14 +1085,14 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { enabledColor: UIT.colors.greenHighlight, highlightColor: UIT.colors.greenShadow, label: { - url: "../assets/tools/group/cancel-label.svg", - scale: 0.0380, + url: "../assets/tools/group/clear-label.svg", + scale: 0.0314, color: UIT.colors.baseGray }, labelEnabledColor: UIT.colors.white, enabled: false, command: { - method: "cancelGroupSelectionBox" + method: "clearGroupSelection" } } ], @@ -1999,8 +1999,10 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { isGroupButtonEnabled, isUngroupButtonEnabled, + isClearGroupingButtonEnabled, groupButtonIndex, ungroupButtonIndex, + clearGroupingButtonIndex, hsvControl = { hsv: { h: 0, s: 0, v: 0 }, @@ -2431,6 +2433,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (menuItem.toolOptions === "groupOptions") { optionsEnabled[groupButtonIndex] = false; optionsEnabled[ungroupButtonIndex] = false; + optionsEnabled[clearGroupingButtonIndex] = false; } isOptionsOpen = true; @@ -2706,41 +2709,18 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { case "toggleGroupSelectionBox": optionsToggles.groupSelectionBoxButton = !optionsToggles.groupSelectionBoxButton; - index = optionsOverlaysIDs.indexOf("groupSelectionBoxButton"); Overlays.editOverlay(optionsOverlays[index], { color: optionsToggles.groupSelectionBoxButton ? UI_ELEMENTS[optionsItems[index].type].onHoverColor : UI_ELEMENTS[optionsItems[index].type].offHoverColor }); - - index = optionsOverlaysIDs.indexOf("groupsSelectionBoxCancelButton"); - Overlays.editOverlay(optionsOverlays[index], { - color: optionsToggles.groupSelectionBoxButton - ? optionsItems[index].enabledColor - : optionsItems[index].properties.color - }); - Overlays.editOverlay(optionsOverlaysLabels[index], { - color: optionsToggles.groupSelectionBoxButton - ? optionsItems[index].labelEnabledColor - : optionsItems[index].label.color - }); - optionsEnabled[index] = optionsToggles.groupSelectionBoxButton; - uiCommandCallback("toggleGroupSelectionBoxTool", optionsToggles.groupSelectionBoxButton); break; - case "cancelGroupSelectionBox": + case "clearGroupSelection": optionsToggles.groupSelectionBoxButton = false; - - index = optionsOverlaysIDs.indexOf("groupSelectionBoxButton"); - Overlays.editOverlay(optionsOverlays[index], { - color : optionsToggles.groupSelectionBoxButton - ? UI_ELEMENTS[optionsItems[index].type].onHoverColor - : UI_ELEMENTS[optionsItems[index].type].offHoverColor - }); - - index = optionsOverlaysIDs.indexOf("groupsSelectionBoxCancelButton"); + index = clearGroupingButtonIndex; Overlays.editOverlay(optionsOverlays[index], { color: optionsItems[index].properties.color }); @@ -2748,8 +2728,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: optionsItems[index].label.color }); optionsEnabled[index] = false; - - uiCommandCallback("cancelGroupSelectionBoxTool"); + uiCommandCallback("clearGroupSelectionTool"); break; case "setGravityOn": @@ -2974,6 +2953,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { parameterValue, enableGroupButton, enableUngroupButton, + enableClearGroupingButton, sliderProperties, overlayDimensions, basePoint, @@ -3412,7 +3392,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { ? OPTONS_PANELS.groupOptions[groupButtonIndex].labelEnabledColor : OPTONS_PANELS.groupOptions[groupButtonIndex].label.color }); - optionsEnabled[groupButtonIndex] = enableGroupButton; + optionsEnabled[groupButtonIndex] = isGroupButtonEnabled; } enableUngroupButton = groupsCount === 1 && entitiesCount > 1; @@ -3430,7 +3410,23 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { ? OPTONS_PANELS.groupOptions[ungroupButtonIndex].labelEnabledColor : OPTONS_PANELS.groupOptions[ungroupButtonIndex].label.color }); - optionsEnabled[ungroupButtonIndex] = enableUngroupButton; + optionsEnabled[ungroupButtonIndex] = isUngroupButtonEnabled; + } + + enableClearGroupingButton = groupsCount > 0; + if (enableClearGroupingButton !== isClearGroupingButtonEnabled) { + isClearGroupingButtonEnabled = enableClearGroupingButton; + Overlays.editOverlay(optionsOverlays[clearGroupingButtonIndex], { + color: isClearGroupingButtonEnabled + ? optionsItems[clearGroupingButtonIndex].enabledColor + : optionsItems[clearGroupingButtonIndex].properties.color + }); + Overlays.editOverlay(optionsOverlaysLabels[clearGroupingButtonIndex], { + color: isClearGroupingButtonEnabled + ? optionsItems[clearGroupingButtonIndex].labelEnabledColor + : optionsItems[clearGroupingButtonIndex].label.color + }); + optionsEnabled[clearGroupingButtonIndex] = isClearGroupingButtonEnabled; } } @@ -3522,6 +3518,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Special handling for Group options. isGroupButtonEnabled = false; isUngroupButtonEnabled = false; + isClearGroupingButtonEnabled = false; for (i = 0, length = OPTONS_PANELS.groupOptions.length; i < length; i += 1) { id = OPTONS_PANELS.groupOptions[i].id; if (id === "groupButton") { @@ -3530,6 +3527,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (id === "ungroupButton") { ungroupButtonIndex = i; } + if (id === "clearGroupingButton") { + clearGroupingButtonIndex = i; + } } isDisplaying = true; diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index c50160f4b4..5f7b3bb732 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1532,15 +1532,11 @@ grouping.stopSelectInBox(); } break; - case "cancelGroupSelectionBoxTool": + case "clearGroupSelectionTool": if (grouping.groupsCount() > 0) { Feedback.play(dominantHand, Feedback.SELECT_ENTITY); } - if (toolSelected === TOOL_GROUP_BOX) { - grouping.stopSelectInBox(); - } grouping.clear(); - toolSelected = TOOL_GROUP; break; case "setColor": From eed099502aa469bb6536ac222624115192be6f60 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 21 Sep 2017 17:20:32 -0700 Subject: [PATCH 390/722] minimise the changes compared to upstream --- interface/src/ui/overlays/Base3DOverlay.cpp | 5 +++-- interface/src/ui/overlays/Base3DOverlay.h | 1 - .../src/ui/overlays/Billboard3DOverlay.cpp | 1 - interface/src/ui/overlays/ModelOverlay.cpp | 18 +----------------- interface/src/ui/overlays/Overlay.h | 1 - libraries/render-utils/src/Model.cpp | 1 + 6 files changed, 5 insertions(+), 22 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 6fb2bac6ed..f5e43db6b5 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -191,7 +191,7 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { // Communicate changes to the renderItem if needed if (needRenderItemUpdate) { - // notifyRenderTransformChange(); + notifyRenderTransformChange(); auto itemID = getRenderItemID(); if (render::Item::isValidID(itemID)) { @@ -266,7 +266,8 @@ void Base3DOverlay::parentDeleted() { qApp->getOverlays().deleteOverlay(getOverlayID()); } -void Base3DOverlay::update(float duration) { +void Base3DOverlay::update(float duration) { + // In Base3DOverlay, if its location or bound changed, the renderTrasnformDirty flag is true. // then the correct transform used for rendering is computed in the update transaction and assigned. // TODO: Fix the value to be computed in main thread now and passed by value to the render item. diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index df1d3d2875..55b55ed16f 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -72,7 +72,6 @@ protected: virtual void parentDeleted() override; mutable Transform _renderTransform; - virtual Transform evalRenderTransform() const; virtual void setRenderTransform(const Transform& transform); const Transform& getRenderTransform() const { return _renderTransform; } diff --git a/interface/src/ui/overlays/Billboard3DOverlay.cpp b/interface/src/ui/overlays/Billboard3DOverlay.cpp index a333df0821..f5668caa71 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.cpp +++ b/interface/src/ui/overlays/Billboard3DOverlay.cpp @@ -45,4 +45,3 @@ bool Billboard3DOverlay::applyTransformTo(Transform& transform, bool force) { } return transformChanged; } - diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 34bb50994f..74819689e9 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -63,20 +63,6 @@ void ModelOverlay::update(float deltatime) { _model->simulate(deltatime); } _isLoaded = _model->isActive(); - - // check to see if when we added our model to the scene they were ready, if they were not ready, then - // fix them up in the scene - render::ScenePointer scene = qApp->getMain3DScene(); - render::Transaction transaction; - if (_model->needsFixupInScene()) { - _model->removeFromScene(scene, transaction); - _model->addToScene(scene, transaction); - } - - _model->setVisibleInScene(_visible, scene); - _model->setLayeredInFront(getDrawInFront(), scene); - - scene->enqueueTransaction(transaction); } bool ModelOverlay::addToScene(Overlay::Pointer overlay, const render::ScenePointer& scene, render::Transaction& transaction) { @@ -91,7 +77,6 @@ void ModelOverlay::removeFromScene(Overlay::Pointer overlay, const render::Scene } void ModelOverlay::render(RenderArgs* args) { -/* // check to see if when we added our model to the scene they were ready, if they were not ready, then // fix them up in the scene render::ScenePointer scene = qApp->getMain3DScene(); @@ -105,7 +90,6 @@ void ModelOverlay::render(RenderArgs* args) { _model->setLayeredInFront(getDrawInFront(), scene); scene->enqueueTransaction(transaction); - */ } void ModelOverlay::setProperties(const QVariantMap& properties) { @@ -300,11 +284,11 @@ ModelOverlay* ModelOverlay::createClone() const { void ModelOverlay::locationChanged(bool tellPhysics) { Base3DOverlay::locationChanged(tellPhysics); + // FIXME Start using the _renderTransform instead of calling for Transform and Dimensions from here, do the custom things needed in evalRenderTransform() if (_model && _model->isActive()) { _model->setRotation(getRotation()); _model->setTranslation(getPosition()); } - _updateModel = true; } QString ModelOverlay::getName() const { diff --git a/interface/src/ui/overlays/Overlay.h b/interface/src/ui/overlays/Overlay.h index 31846501ec..db2979b4d5 100644 --- a/interface/src/ui/overlays/Overlay.h +++ b/interface/src/ui/overlays/Overlay.h @@ -102,7 +102,6 @@ protected: render::ItemID _renderItemID{ render::Item::INVALID_ITEM_ID }; - bool _isLoaded; float _alpha; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 051858afca..eec3a7b8fe 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -246,6 +246,7 @@ void Model::updateRenderItems() { if (model && model->isLoaded()) { // Ensure the model geometry was not reset between frames if (deleteGeometryCounter == model->_deleteGeometryCounter) { + const Model::MeshState& state = model->getMeshState(data._meshIndex); Transform renderTransform = modelTransform; if (state.clusterMatrices.size() == 1) { From 4a67e0421f4de185f3dec0151ee17c57d1798302 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 21 Sep 2017 17:33:50 -0700 Subject: [PATCH 391/722] minimise the changes compared to upstream --- interface/src/ui/overlays/ModelOverlay.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 74819689e9..e2a7df7ae6 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -77,6 +77,7 @@ void ModelOverlay::removeFromScene(Overlay::Pointer overlay, const render::Scene } void ModelOverlay::render(RenderArgs* args) { + // check to see if when we added our model to the scene they were ready, if they were not ready, then // fix them up in the scene render::ScenePointer scene = qApp->getMain3DScene(); From 53efe6bfb2d80bb9cfdd3f4cf5582a1101bf927f Mon Sep 17 00:00:00 2001 From: druiz17 Date: Thu, 21 Sep 2017 18:17:05 -0700 Subject: [PATCH 392/722] working on fix --- .../src/display-plugins/CompositorHelper.cpp | 16 ++ .../src/display-plugins/CompositorHelper.h | 2 + .../controllerModules/farActionGrabEntity.js | 12 +- .../controllerModules/hudOverlayPointer.js | 255 ++++++++++++++++++ .../controllerModules/scaleAvatar.js | 5 +- .../system/controllers/controllerScripts.js | 7 +- .../controllers/handControllerPointer.js | 10 +- .../libraries/controllerDispatcherUtils.js | 20 +- scripts/system/tablet-ui/tabletUI.js | 30 +++ 9 files changed, 337 insertions(+), 20 deletions(-) create mode 100644 scripts/system/controllers/controllerModules/hudOverlayPointer.js diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index 2f57cc29d0..294a3f6e55 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -279,6 +279,18 @@ bool CompositorHelper::getReticleOverDesktop() const { return _isOverDesktop; } +bool CompositorHelper::isPositionOverDesktop(glm::vec2 position) const { + if (isHMD()) { + glm::vec2 maxOverlayPosition = _currentDisplayPlugin->getRecommendedUiSize(); + static const glm::vec2 minOverlayPosition; + if (glm::any(glm::lessThan(position, minOverlayPosition)) || + glm::any(glm::greaterThan(position, maxOverlayPosition))) { + return true; + } + } + return _isOverDesktop; +} + glm::vec2 CompositorHelper::getReticleMaximumPosition() const { glm::vec2 result; if (isHMD()) { @@ -468,3 +480,7 @@ void ReticleInterface::setScale(float scale) { auto& cursorManager = Cursor::Manager::instance(); cursorManager.setScale(scale); } + +bool ReticleInterface::isPointOnSystemOverlay(QVariant position) { + return !_compositor->isPositionOverDesktop(vec2FromVariant(position)); +} diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index b1d2815f65..8534de6b9d 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -106,6 +106,7 @@ public: /// if the reticle is pointing to a system overlay (a dialog box for example) then the function returns true otherwise false bool getReticleOverDesktop() const; + bool isPositionOverDesktop(glm::vec2 position) const; void setReticleOverDesktop(bool value) { _isOverDesktop = value; } void setDisplayPlugin(const DisplayPluginPointer& displayPlugin) { _currentDisplayPlugin = displayPlugin; } @@ -195,6 +196,7 @@ public: Q_INVOKABLE void setAllowMouseCapture(bool value) { return _compositor->setAllowMouseCapture(value); } Q_INVOKABLE bool isPointingAtSystemOverlay() { return !_compositor->getReticleOverDesktop(); } + Q_INVOKABLE bool isPointOnSystemOverlay(QVariant position); Q_INVOKABLE bool getVisible() { return _compositor->getReticleVisible(); } Q_INVOKABLE void setVisible(bool visible) { _compositor->setReticleVisible(visible); } diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index eb73b0f908..72da3c3f70 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -13,7 +13,7 @@ makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, AVATAR_SELF_ID, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic, - getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Reticle, Overlays + getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Reticle, Overlays, isPointingAtUI */ @@ -21,7 +21,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Script.include("/~/system/libraries/controllers.js"); (function() { - var PICK_WITH_HAND_RAY = true; var halfPath = { @@ -423,12 +422,9 @@ Script.include("/~/system/libraries/controllers.js"); this.isPointingAtUI = function(controllerData) { var hudRayPickInfo = controllerData.hudRayPicks[this.hand]; - var hudPoint2d = HMD.overlayFromWorldPoint(hudRayPickInfo.intersection); - if (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(hudPoint2d || Reticle.position)) { - return true; - } - - return false; + var result = isPointingAtUI(hudRayPickInfo); + print(result); + return result; }; this.run = function (controllerData) { diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js new file mode 100644 index 0000000000..0015a4a2d6 --- /dev/null +++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js @@ -0,0 +1,255 @@ +// +// hudOverlayPointer.js +// +// scripts/system/controllers/controllerModules/ +// +// Created by Dante Ruiz 2017-9-21 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global Script, HMD, WebTablet, UIWebTablet, UserActivityLogger, Settings, Entities, Messages, Tablet, Overlays, + MyAvatar, Menu, AvatarInputs, Vec3 */ +(function() { + Script.include("/~/system/libraries/controllers.js") + var ControllerDispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js"); + var halfPath = { + type: "line3d", + color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawHUDLayer: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + var halfEnd = { + type: "sphere", + solid: true, + color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, + alpha: 0.9, + ignoreRayIntersection: true, + drawHUDLayer: true, // Even when burried inside of something, show it. + visible: true + }; + var fullPath = { + type: "line3d", + color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawHUDLayer: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + var fullEnd = { + type: "sphere", + solid: true, + color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, + alpha: 0.9, + ignoreRayIntersection: true, + drawHUDLayer: true, // Even when burried inside of something, show it. + visible: true + }; + var holdPath = { + type: "line3d", + color: COLORS_GRAB_DISTANCE_HOLD, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawHUDLayer: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + + var renderStates = [ + {name: "half", path: halfPath, end: halfEnd}, + {name: "full", path: fullPath, end: fullEnd}, + {name: "hold", path: holdPath} + ]; + + var defaultRenderStates = [ + {name: "half", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: halfPath}, + {name: "full", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: fullPath}, + {name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: holdPath} + ]; + + + // triggered when stylus presses a web overlay/entity + var HAPTIC_STYLUS_STRENGTH = 1.0; + var HAPTIC_STYLUS_DURATION = 20.0; + var MARGIN = 25; + + function distance2D(a, b) { + var dx = (a.x - b.x); + var dy = (a.y - b.y); + return Math.sqrt(dx * dx + dy * dy); + } + + function HudOverlayPointer(hand) { + var _this = this; + this.hand = hand; + this.reticleMinX = MARGIN; + this.reticleMaxX; + this.reticleMinY = MARGIN; + this.reticleMaxY; + this.clicked = false; + this.triggerClicked = 0; + this.parameters = ControllerDispatcherUtils.makeDispatcherModuleParameters( + 540, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + this.getOtherHandController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; + }; + + this.clicked = function() { + return this.clicked; + }; + + this.getOtherModule = function() { + return (this.hand === RIGHT_HAND) ? leftOverlayLaserInput : rightOverlayLaserInput; + }; + + this.handToController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + this.updateRecommendedArea = function() { + var dims = Controller.getViewportDimensions(); + this.reticleMaxX = dims.x - MARGIN; + this.reticleMaxY = dims.y - MARGIN; + }; + + this.hasNotSentClick = function() { + if (!_this.clicked) { + _this.clicked = true; + return true; + } + return false; + }; + + this.updateLaserPointer = function(controllerData) { + var RADIUS = 0.005; + var dim = { x: RADIUS, y: RADIUS, z: RADIUS }; + + if (this.mode === "full") { + this.fullEnd.dimensions = dim; + LaserPointers.editRenderState(this.laserPointer, this.mode, {path: fullPath, end: this.fullEnd}); + } else if (this.mode === "half") { + this.halfEnd.dimensions = dim; + LaserPointers.editRenderState(this.laserPointer, this.mode, {path: halfPath, end: this.halfEnd}); + } + + LaserPointers.enableLaserPointer(this.laserPointer); + LaserPointers.setRenderState(this.laserPointer, this.mode); + }; + + this.processControllerTriggers = function(controllerData) { + if (controllerData.triggerClicks[this.hand]) { + this.mode = "full"; + } else if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) { + this.clicked = false; + this.mode = "half"; + } else { + this.mode = "none"; + } + }; + + this.calculateNewReticlePosition = function(intersection) { + this.updateRecommendedArea(); + var point2d = HMD.overlayFromWorldPoint(intersection); + point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX)); + point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY)); + return point2d; + }; + + this.setReticlePosition = function(point2d) { + Reticle.setPosition(point2d); + }; + + this.processLaser = function(controllerData) { + var controllerLocation = controllerData.controllerLocations[this.hand]; + if (controllerData.triggerValues[this.hand] < ControllerDispatcherUtils.TRIGGER_OFF_VALUE || !controllerLocation.valid) { + this.exitModule(); + return false; + } + + var hudRayPick = controllerData.hudRayPicks[this.hand]; + var controllerLocation = controllerData.controllerLocations[this.hand]; + var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); + this.setReticlePosition(point2d); + print(Reticle.isPointOnSystemOverlay(point2d)); + if (!Reticle.isPointOnSystemOverlay(point2d)) { + this.exitModule(); + print("----> exiting <------"); + return false; + } + + //this.setReticlePosition(point2d); + + this.clicked = controllerData.triggerClicked[this.hand]; + + this.processControllerTriggers(controllerData); + this.updateLaserPointer(controllerData); + return true; + }; + + this.exitModule = function() { + LaserPointers.disableLaserPointer(this.laserPointer); + }; + + this.isReady = function (controllerData) { + if (this.processLaser(controllerData)) { + return ControllerDispatcherUtils.makeRunningValues(true, [], []); + } else { + return ControllerDispatcherUtils.makeRunningValues(false, [], []); + } + }; + + this.run = function (controllerData, deltaTime) { + return this.isReady(controllerData); + }; + + this.cleanup = function () { + LaserPointers.disableLaserPointer(this.laserPointer); + LaserPointers.removeLaserPointer(this.laserPointer); + }; + + this.halfEnd = halfEnd; + this.fullEnd = fullEnd; + this.laserPointer = LaserPointers.createLaserPointer({ + joint: (this.hand === RIGHT_HAND) ? "_CONTROLLER_RIGHTHAND" : "_CONTROLLER_LEFTHAND", + filter: RayPick.PICK_HUD, + maxDistance: PICK_MAX_DISTANCE, + posOffset: getGrabPointSphereOffset(this.handToController(), true), + renderStates: renderStates, + enabled: true, + defaultRenderStates: defaultRenderStates + }); + } + + + var leftHudOverlayPointer = new HudOverlayPointer(LEFT_HAND); + var rightHudOverlayPointer = new HudOverlayPointer(RIGHT_HAND); + + var clickMapping = Controller.newMapping('HudOverlayPointer-click'); + clickMapping.from(rightHudOverlayPointer.clicked()).when(rightHudOverlayPointer.hasNotSentClick()).to(Controller.Actions.ReticleClick); + clickMapping.from(leftHudOverlayPointer.clicked()).when(leftHudOverlayPointer.hasNotSentClick()).to(Controller.Actions.ReticleClick); + clickMapping.enable(); + + enableDispatcherModule("LeftHudOverlayPointer", leftHudOverlayPointer); + enableDispatcherModule("RightHudOverlayPointer", rightHudOverlayPointer); + + +})(); diff --git a/scripts/system/controllers/controllerModules/scaleAvatar.js b/scripts/system/controllers/controllerModules/scaleAvatar.js index 05804c967b..de0434258c 100644 --- a/scripts/system/controllers/controllerModules/scaleAvatar.js +++ b/scripts/system/controllers/controllerModules/scaleAvatar.js @@ -1,4 +1,4 @@ -// handControllerGrab.js +// scaleAvatar.js // // Created by Dante Ruiz on 9/11/17 // @@ -76,8 +76,9 @@ dispatcherUtils.enableDispatcherModule("LeftScaleAvatar", leftScaleAvatar); dispatcherUtils.enableDispatcherModule("RightScaleAvatar", rightScaleAvatar); - this.cleanup = function() { + function cleanup() { dispatcherUtils.disableDispatcherModule("LeftScaleAvatar"); dispatcherUtils.disableDispatcherModule("RightScaleAvatar"); }; + Script.scriptEnding.connect(cleanup); })(); diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index e8b07c623d..695dea7b2c 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -12,14 +12,14 @@ var CONTOLLER_SCRIPTS = [ "squeezeHands.js", "controllerDisplayManager.js", - "handControllerPointer.js", + //"handControllerPointer.js", "grab.js", "toggleAdvancedMovementForHandControllers.js", "controllerDispatcher.js", "controllerModules/nearParentGrabEntity.js", "controllerModules/nearParentGrabOverlay.js", "controllerModules/nearActionGrabEntity.js", - "controllerModules/farActionGrabEntity.js", + //"controllerModules/farActionGrabEntity.js", "controllerModules/tabletStylusInput.js", "controllerModules/equipEntity.js", "controllerModules/nearTrigger.js", @@ -29,7 +29,8 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/disableOtherModule.js", "controllerModules/farTrigger.js", "controllerModules/teleport.js", - "controllerModules/scaleAvatar.js" + "controllerModules/scaleAvatar.js", + "controllerModules/hudOverlayPointer.js" ]; var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 832fe10d5f..1c988bfd34 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -314,7 +314,7 @@ function activeHudPoint2dGamePad() { var hudPoint2d = overlayFromWorldPoint(hudPoint3d); // We don't know yet if we'll want to make the cursor or laser visble, but we need to move it to see if - // it's pointing at a QML tool (aka system overlay). + // it's pointing at aQML tool (aka system overlay). setReticlePosition(hudPoint2d); return hudPoint2d; @@ -328,7 +328,7 @@ function activeHudPoint2d(activeHand) { // if controller is valid, update reticl } var controllerPosition = controllerPose.position; var controllerDirection = Quat.getUp(controllerPose.rotation); - + var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection, true); if (!hudPoint3d) { if (Menu.isOptionChecked("Overlays")) { // With our hud resetting strategy, hudPoint3d should be valid here @@ -523,21 +523,21 @@ var wantsMenu = 0; clickMapping.from(function () { return wantsMenu; }).to(Controller.Actions.ContextMenu); clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(function (clicked) { if (clicked) { - activeHudPoint2d(Controller.Standard.RightHand); + //activeHudPoint2d(Controller.Standard.RightHand); Messages.sendLocalMessage("toggleHand", Controller.Standard.RightHand); } wantsMenu = clicked; }); clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(function (clicked) { if (clicked) { - activeHudPoint2d(Controller.Standard.LeftHand); + //activeHudPoint2d(Controller.Standard.LeftHand); Messages.sendLocalMessage("toggleHand", Controller.Standard.LeftHand); } wantsMenu = clicked; }); clickMapping.from(Controller.Standard.Start).peek().to(function (clicked) { if (clicked) { - activeHudPoint2dGamePad(); + //activeHudPoint2dGamePad(); var noHands = -1; Messages.sendLocalMessage("toggleHand", Controller.Standard.LeftHand); } diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 10931e4e93..652bd5765b 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -40,7 +40,8 @@ entityHasActions:true, ensureDynamic:true, findGroupParent:true, - BUMPER_ON_VALUE:true + BUMPER_ON_VALUE:true, + isPointingAtUI: true */ MSECS_PER_SEC = 1000.0; @@ -310,6 +311,18 @@ findGroupParent = function (controllerData, targetProps) { return targetProps; }; +isPointingAtUI = function(intersection) { + var MARGIN = 25; + var reticleMinX = MARGIN, reticleMaxX, reticleMinY = MARGIN, reticleMaxY; + var dims = Controller.getViewportDimensions(); + reticleMaxX = dims.x - MARGIN; + reticleMaxY = dims.y - MARGIN; + var point2d = HMD.overlayFromWorldPoint(intersection.intersection); + point2d.x = Math.max(reticleMinX, Math.min(point2d.x, reticleMaxX)); + point2d.y = Math.max(reticleMinY, Math.min(point2d.y, reticleMaxY)); + return point2d; +} + if (typeof module !== 'undefined') { module.exports = { makeDispatcherModuleParameters: makeDispatcherModuleParameters, @@ -318,8 +331,11 @@ if (typeof module !== 'undefined') { makeRunningValues: makeRunningValues, LEFT_HAND: LEFT_HAND, RIGHT_HAND: RIGHT_HAND, + isPointingAtUI: isPointingAtUI, BUMPER_ON_VALUE: BUMPER_ON_VALUE, projectOntoOverlayXYPlane: projectOntoOverlayXYPlane, - projectOntoEntityXYPlane: projectOntoEntityXYPlane + projectOntoEntityXYPlane: projectOntoEntityXYPlane, + TRIGGER_OFF_VALUE: TRIGGER_OFF_VALUE, + TRIGGER_ON_VALUE: TRIGGER_ON_VALUE }; } diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index 63c1cc51aa..9d2382b3f8 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -272,6 +272,36 @@ Messages.subscribe("home"); Messages.messageReceived.connect(handleMessage); + var clickMapping = Controller.newMapping('tabletToggle-click'); + var wantsMenu = 0; + clickMapping.from(function () { return wantsMenu; }).to(Controller.Actions.ContextMenu); + clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(function (clicked) { + if (clicked) { + //activeHudPoint2d(Controller.Standard.RightHand); + Messages.sendLocalMessage("toggleHand", Controller.Standard.RightHand); + } + wantsMenu = clicked; + }); + + clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(function (clicked) { + if (clicked) { + //activeHudPoint2d(Controller.Standard.LeftHand); + Messages.sendLocalMessage("toggleHand", Controller.Standard.LeftHand); + } + wantsMenu = clicked; + }); + + clickMapping.from(Controller.Standard.Start).peek().to(function (clicked) { + if (clicked) { + //activeHudPoint2dGamePad(); + var noHands = -1; + Messages.sendLocalMessage("toggleHand", Controller.Standard.LeftHand); + } + + wantsMenu = clicked; + }); + clickMapping.enable(); + Script.setInterval(updateShowTablet, 100); Script.scriptEnding.connect(function () { From ca000acf2ae753f8e05126b65330a32a8b46197a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 22 Sep 2017 17:40:34 +1200 Subject: [PATCH 393/722] Add space for a footer to put undo/redo buttons in --- scripts/vr-edit/modules/createPalette.js | 8 +--- scripts/vr-edit/modules/toolsMenu.js | 60 ++++++++++++------------ scripts/vr-edit/modules/uit.js | 5 +- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 11229562ef..132be3ab79 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -98,11 +98,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { PALETTE_PANEL_PROPERTIES = { dimensions: UIT.dimensions.panel, - localPosition: { - x: 0, - y: UIT.dimensions.panel.y / 2 - UIT.dimensions.canvas.y / 2, - z: UIT.dimensions.panel.z / 2 - }, + localPosition: { x: 0, y: (UIT.dimensions.panel.y - UIT.dimensions.canvas.y) / 2, z: UIT.dimensions.panel.z / 2 }, localRotation: Quat.ZERO, color: UIT.colors.baseGray, alpha: 1.0, @@ -430,7 +426,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { function itemPosition(index) { // Position relative to palette panel. var ITEMS_PER_ROW = 4, - ROW_ZERO_Y_OFFSET = 0.0580, + ROW_ZERO_Y_OFFSET = 0.0860, ROW_SPACING = 0.0560, COLUMN_ZERO_OFFSET = -0.08415, COLUMN_SPACING = 0.0561, diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 36bc92f187..7ca5d807a9 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -180,7 +180,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { MENU_PANEL_PROPERTIES = { dimensions: UIT.dimensions.panel, - localPosition: { x: 0, y: UIT.dimensions.panel.y / 2 - UIT.dimensions.canvas.y / 2, z: UIT.dimensions.panel.z / 2 }, + localPosition: { x: 0, y: (UIT.dimensions.panel.y - UIT.dimensions.canvas.y) / 2, z: UIT.dimensions.panel.z / 2 }, localRotation: Quat.ZERO, color: UIT.colors.baseGray, alpha: 1.0, @@ -612,7 +612,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: -0.0935, - y: 0.0513, + y: 0.0513 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -633,7 +633,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: -0.0561, - y: 0.0513, + y: 0.0513 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -654,7 +654,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: -0.0935, - y: 0.0153, + y: 0.0153 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -675,7 +675,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: -0.0561, - y: 0.0153, + y: 0.0153 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -696,7 +696,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: -0.0935, - y: -0.0207, + y: -0.0207 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -717,7 +717,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: -0.0561, - y: -0.0207, + y: -0.0207 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -739,7 +739,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.0668, y: 0.001 }, localPosition: { x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0668 / 2, - y: -UIT.dimensions.panel.y / 2 + 0.0481, + y: -UIT.dimensions.panel.y / 2 + 0.0481 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset } } @@ -750,7 +750,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: 0.04675, - y: 0.01655, + y: 0.01655 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -766,7 +766,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: 0.04675, - y: -0.0620, + y: -0.0620 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 }, localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }) @@ -786,7 +786,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.1229, y: 0.001 }, localPosition: { x: 0.04675, - y: -0.0781, + y: -0.0781 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset } } @@ -798,7 +798,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.0294, y: 0.0280, z: UIT.dimensions.buttonDimensions.z }, localPosition: { x: -0.0935, - y: -0.064, + y: -0.064 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -817,7 +817,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.0294, y: 0.0280, z: UIT.dimensions.imageOverlayOffset }, localPosition: { x: -0.0561, - y: -0.064, + y: -0.064 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -892,7 +892,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.0321, y: 0.0320 }, localPosition: { x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0321 / 2, - y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2, + y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2 + UIT.dimensions.footerHeight, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset }, color: UIT.colors.white // Icon SVG is already lightGray color. @@ -904,9 +904,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { url: "../assets/tools/stretch/info-text.svg", scale: 0.1340, - localPosition: { + localPosition: { // Vertically center on info icon. x: -UIT.dimensions.panel.x / 2 + 0.0679 + 0.1340 / 2, - y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2, // Center on info icon. + y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2 + UIT.dimensions.footerHeight, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset }, color: UIT.colors.white @@ -1023,7 +1023,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, localPosition: { x: 0, - y: -UIT.dimensions.panel.y / 2 + 0.0120 + 0.0680 / 2, + y: -UIT.dimensions.panel.y / 2 + 0.0120 + 0.0680 / 2 + UIT.dimensions.footerHeight, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 }, color: UIT.colors.baseGrayShadow @@ -1076,7 +1076,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.0668, y: 0.0280, z: UIT.dimensions.buttonDimensions.z }, localPosition: { x: -0.0748, - y: 0.0480, + y: 0.0480 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -1124,7 +1124,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.0668, y: 0.0280, z: UIT.dimensions.buttonDimensions.z }, localPosition: { x: -0.0748, - y: 0.0120, + y: 0.0120 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -1172,7 +1172,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.0668, y: 0.0280, z: UIT.dimensions.buttonDimensions.z }, localPosition: { x: -0.0748, - y: -0.0240, + y: -0.0240 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -1376,7 +1376,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.1416, y: 0.0280, z: UIT.dimensions.buttonDimensions.z }, localPosition: { x: UIT.dimensions.panel.x / 2 - UIT.dimensions.rightMargin - 0.1416 / 2, - y: 0.0480, + y: 0.0480 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -1435,7 +1435,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.0294, y: 0.1000, z: UIT.dimensions.buttonDimensions.z }, localPosition: { x: -0.0187, - y: -0.0240, + y: -0.0240 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -1464,7 +1464,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.0294, y: 0.1000, z: UIT.dimensions.buttonDimensions.z }, localPosition: { x: 0.0187, - y: -0.0240, + y: -0.0240 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -1493,7 +1493,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.0294, y: 0.1000, z: UIT.dimensions.buttonDimensions.z }, localPosition: { x: 0.0561, - y: -0.0240, + y: -0.0240 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -1522,7 +1522,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.0294, y: 0.1000, z: UIT.dimensions.buttonDimensions.z }, localPosition: { x: 0.0935, - y: -0.0240, + y: -0.0240 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, @@ -1608,7 +1608,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { dimensions: { x: 0.0321, y: 0.0320 }, localPosition: { x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0321 / 2, - y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2, + y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2 + UIT.dimensions.footerHeight, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset }, color: UIT.colors.white // Icon SVG is already lightGray color. @@ -1620,9 +1620,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { url: "../assets/tools/delete/info-text.svg", scale: 0.1416, - localPosition: { + localPosition: { // Vertically off-center w.r.t. info icon. x: -UIT.dimensions.panel.x / 2 + 0.0679 + 0.1340 / 2, - y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0240 / 2 + 0.0063 / 2, // Off-center w.r.t. info icon. + y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0240 / 2 + 0.0063 / 2 + UIT.dimensions.footerHeight, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset }, color: UIT.colors.white @@ -1632,7 +1632,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, MENU_ITEM_XS = [-0.08415, -0.02805, 0.02805, 0.08415], - MENU_ITEM_YS = [0.058, 0.002, -0.054], + MENU_ITEM_YS = [0.086, 0.030, -0.026], MENU_ITEMS = [ { @@ -1998,7 +1998,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { setHand(side); function getOverlayIDs() { - return [menuPanelOverlay, menuHeaderOverlay].concat(menuOverlays).concat(optionsOverlays); + return [menuHeaderOverlay, menuPanelOverlay].concat(menuOverlays).concat(optionsOverlays); } function getIconInfo(tool) { diff --git a/scripts/vr-edit/modules/uit.js b/scripts/vr-edit/modules/uit.js index fa69c9045e..7ad6e67c56 100644 --- a/scripts/vr-edit/modules/uit.js +++ b/scripts/vr-edit/modules/uit.js @@ -32,7 +32,7 @@ UIT = (function () { // Coordinate system: UI lies in x-y plane with the front surface being +z. // Offsets are relative to parents' centers. dimensions: { - canvas: { x: 0.24, y: 0.24 }, // Overall UI size. + canvas: { x: 0.24, y: 0.296 }, // Overall UI size. canvasSeparation: 0.004, // Gap between Tools menu and Create panel. handOffset: 0.085, // Distance from hand (wrist) joint to center of canvas. handLateralOffset: 0.01, // Offset of UI in direction of palm normal. @@ -44,7 +44,8 @@ UIT = (function () { header: { x: 0.24, y: 0.048, z: 0.012 }, headerHeading: { x: 0.24, y: 0.044, z: 0.012 }, headerBar: { x: 0.24, y: 0.004, z: 0.012 }, - panel: { x: 0.24, y: 0.18, z: 0.008 }, + panel: { x: 0.24, y: 0.236, z: 0.008 }, + footerHeight: 0.056, itemCollisionZone: { x: 0.0481, y: 0.0480, z: 0.0040 }, // Cursor intersection zone for Tools and Create items. From edd6310a071ab24288fb2fb50315ce8841a74c64 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 22 Sep 2017 21:46:33 +1200 Subject: [PATCH 394/722] Move undo/redo buttons to footer area and make persistent --- scripts/vr-edit/modules/createPalette.js | 1 - scripts/vr-edit/modules/toolsMenu.js | 190 ++++++++++++++++++----- 2 files changed, 151 insertions(+), 40 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 132be3ab79..e178e41294 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -492,7 +492,6 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.clone(PALETTE_ITEM.properties); properties.parentID = palettePanelOverlay; properties.localPosition = itemPosition(i); - paletteItemOverlays[i] = Overlays.addOverlay(PALETTE_ITEM.overlay, properties); paletteItemPositions[i] = properties.localPosition; diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 7ca5d807a9..b520ccbc3e 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -53,6 +53,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, optionsToggles = {}, // Cater for toggle buttons without a setting. + footerOverlays = [], + footerHoverOverlays = [], + footerIconOverlays = [], + footerLabelOverlays = [], + footerEnabled = [], + swatchHighlightOverlay = null, staticOverlays = [], @@ -766,7 +772,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: 0.04675, - y: -0.0620 + UIT.dimensions.footerHeight / 2, + y: -0.064 + UIT.dimensions.footerHeight / 2, z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 }, localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }) @@ -779,18 +785,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { method: "setColorPerSlider" } }, - { - id: "colorRule3", - type: "horizontalRule", - properties: { - dimensions: { x: 0.1229, y: 0.001 }, - localPosition: { - x: 0.04675, - y: -0.0781 + UIT.dimensions.footerHeight / 2, - z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset - } - } - }, { id: "pickColor", type: "toggleButton", @@ -1632,7 +1626,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, MENU_ITEM_XS = [-0.08415, -0.02805, 0.02805, 0.08415], - MENU_ITEM_YS = [0.086, 0.030, -0.026], + MENU_ITEM_YS = [0.086, 0.030, -0.026, -0.082], MENU_ITEMS = [ { @@ -1863,6 +1857,26 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { callback: { method: "deleteTool" } + } + ], + COLOR_TOOL = 0, // Indexes of corresponding MENU_ITEMS item. + SCALE_TOOL = 1, + CLONE_TOOL = 2, + GROUP_TOOL = 3, + PHYSICS_TOOL = 4, + DELETE_TOOL = 5, + + FOOTER_ITEMS = [ + { + id: "footerRule", + type: "horizontalRule", + properties: { + localPosition: { + x: 0, + y: -UIT.dimensions.panel.y / 2 + 0.0600, + z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset + } + } }, { id: "undoButton", @@ -1870,7 +1884,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: MENU_ITEM_XS[2], - y: MENU_ITEM_YS[2], + y: MENU_ITEM_YS[3] - 0.008, // Allow space for horizontal rule and Line up with Create palette row. z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2 } }, @@ -1896,7 +1910,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties: { localPosition: { x: MENU_ITEM_XS[3], - y: MENU_ITEM_YS[2], + y: MENU_ITEM_YS[3] - 0.008, z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2 } }, @@ -1917,12 +1931,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } ], - COLOR_TOOL = 0, // Indexes of corresponding MENU_ITEMS item. - SCALE_TOOL = 1, - CLONE_TOOL = 2, - GROUP_TOOL = 3, - PHYSICS_TOOL = 4, - DELETE_TOOL = 5, NONE = -1, @@ -1998,7 +2006,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { setHand(side); function getOverlayIDs() { - return [menuHeaderOverlay, menuPanelOverlay].concat(menuOverlays).concat(optionsOverlays); + return [menuHeaderOverlay, menuPanelOverlay].concat(menuOverlays).concat(optionsOverlays).concat(footerOverlays); } function getIconInfo(tool) { @@ -2092,6 +2100,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { menuHoverOverlays = []; menuIconOverlays = []; menuLabelOverlays = []; + menuEnabled = []; pressedItem = null; } @@ -2415,6 +2424,57 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { openMenu(); } + function displayFooter() { + var properties, + itemID, + buttonID, + overlayID, + i, + length; + + // Display footer items. + for (i = 0, length = FOOTER_ITEMS.length; i < length; i += 1) { + properties = Object.clone(UI_ELEMENTS[FOOTER_ITEMS[i].type].properties); + properties = Object.merge(properties, FOOTER_ITEMS[i].properties); + properties.visible = isVisible; + properties.parentID = menuPanelOverlay; + if (properties.url) { + properties.url = Script.resolvePath(properties.url); + } + itemID = Overlays.addOverlay(UI_ELEMENTS[FOOTER_ITEMS[i].type].overlay, properties); + footerOverlays[i] = itemID; + footerEnabled[i] = true; + + if (FOOTER_ITEMS[i].type === "menuButton") { + // Collision overlay. + properties = Object.clone(UI_ELEMENTS.menuButton.hoverButton.properties); + properties.parentID = itemID; + buttonID = Overlays.addOverlay(UI_ELEMENTS.menuButton.hoverButton.overlay, properties); + footerHoverOverlays[i] = buttonID; + + // Icon. + properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.icon.type].properties); + properties = Object.merge(properties, UI_ELEMENTS.menuButton.icon.properties); + properties = Object.merge(properties, FOOTER_ITEMS[i].icon.properties); + properties.url = Script.resolvePath(properties.url); + properties.visible = isVisible; + properties.parentID = buttonID; + overlayID = Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.icon.type].overlay, properties); + footerIconOverlays[i] = overlayID; + + // Label. + properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.label.type].properties); + properties = Object.merge(properties, UI_ELEMENTS.menuButton.label.properties); + properties = Object.merge(properties, FOOTER_ITEMS[i].label.properties); + properties.url = Script.resolvePath(properties.url); + properties.visible = isVisible; + properties.parentID = itemID; + overlayID = Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.label.type].overlay, properties); + footerLabelOverlays.push(overlayID); + } + } + } + function clearTool() { closeOptions(); } @@ -2860,6 +2920,21 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } } + for (i = 0, length = footerOverlays.length; i < length; i += 1) { + Overlays.editOverlay(footerOverlays[i], { visible: visible }); + } + for (i = 0, length = footerIconOverlays.length; i < length; i += 1) { + Overlays.editOverlay(footerIconOverlays[i], { visible: visible }); + } + for (i = 0, length = footerLabelOverlays.length; i < length; i += 1) { + Overlays.editOverlay(footerLabelOverlays[i], { visible: visible }); + } + if (!visible) { + for (i = 0, length = footerHoverOverlays.length; i < length; i += 1) { + Overlays.editOverlay(footerHoverOverlays[i], { visible: false }); + } + } + isVisible = visible; } @@ -2953,6 +3028,13 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { intersectionItems = optionsItems; intersectionOverlays = optionsOverlays; intersectionEnabled = optionsEnabled; + } else { + intersectedItem = footerOverlays.indexOf(intersection.overlayID); + if (intersectedItem !== NONE) { + intersectionItems = FOOTER_ITEMS; + intersectionOverlays = footerOverlays; + intersectionEnabled = footerEnabled; + } } } } @@ -2964,13 +3046,23 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Unhover old item. switch (hoveredElementType) { case "menuButton": - Overlays.editOverlay(menuHoverOverlays[hoveredItem], { - localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, - visible: false - }); - Overlays.editOverlay(menuIconOverlays[hoveredItem], { - color: UI_ELEMENTS.menuButton.icon.properties.color - }); + if (hoveredSourceOverlays === menuOverlays) { + Overlays.editOverlay(menuHoverOverlays[hoveredItem], { + localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, + visible: false + }); + Overlays.editOverlay(menuIconOverlays[hoveredItem], { + color: UI_ELEMENTS.menuButton.icon.properties.color + }); + } else { + Overlays.editOverlay(footerHoverOverlays[hoveredItem], { + localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, + visible: false + }); + Overlays.editOverlay(footerIconOverlays[hoveredItem], { + color: UI_ELEMENTS.menuButton.icon.properties.color + }); + } break; case "button": if (hoveredSourceItems[hoveredItem].enabledColor !== undefined && optionsEnabled[hoveredItem]) { @@ -3054,13 +3146,25 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { switch (hoveredElementType) { case "menuButton": Feedback.play(otherSide, Feedback.HOVER_MENU_ITEM); - Overlays.editOverlay(menuHoverOverlays[hoveredItem], { - localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, MENU_HOVER_DELTA), - visible: true - }); - Overlays.editOverlay(menuIconOverlays[hoveredItem], { - color: UI_ELEMENTS.menuButton.icon.highlightColor - }); + if (intersectionOverlays === menuOverlays) { + Overlays.editOverlay(menuHoverOverlays[hoveredItem], { + localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, + MENU_HOVER_DELTA), + visible: true + }); + Overlays.editOverlay(menuIconOverlays[hoveredItem], { + color: UI_ELEMENTS.menuButton.icon.highlightColor + }); + } else { + Overlays.editOverlay(footerHoverOverlays[hoveredItem], { + localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, + MENU_HOVER_DELTA), + visible: true + }); + Overlays.editOverlay(footerIconOverlays[hoveredItem], { + color: UI_ELEMENTS.menuButton.icon.highlightColor + }); + } break; case "button": if (intersectionEnabled[hoveredItem]) { @@ -3179,7 +3283,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }; // Button press actions. - if (intersectionOverlays === menuOverlays && intersectionItems[intersectedItem].toolOptions) { + if (intersectionOverlays === menuOverlays) { openOptions(intersectionItems[intersectedItem]); } if (intersectionItems[intersectedItem].command) { @@ -3396,8 +3500,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { staticOverlays = [menuHeaderOverlay, menuHeaderHeadingOverlay, menuHeaderBarOverlay, menuHeaderTitleOverlay, menuPanelOverlay]; - // Menu items. + // Menu and footer. openMenu(); + displayFooter(); // Initial values. optionsItems = null; @@ -3442,10 +3547,17 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { menuHoverOverlays = []; menuIconOverlays = []; menuLabelOverlays = []; + menuEnabled = []; optionsOverlays = []; optionsOverlaysLabels = []; optionsOverlaysSublabels = []; optionsExtraOverlays = []; + optionsEnabled = []; + footerOverlays = []; + footerHoverOverlays = []; + footerIconOverlays = []; + footerLabelOverlays = []; + footerEnabled = []; isDisplaying = false; } From 219d63784b7f6820a898b3b5b1852e4b54e40dba Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 22 Sep 2017 10:39:51 -0700 Subject: [PATCH 395/722] REmoving the extra variables in Linear3D and just use the _renderTRansform --- interface/src/ui/overlays/Line3DOverlay.cpp | 23 ++++++++++++++++----- interface/src/ui/overlays/Line3DOverlay.h | 4 ---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index d8a83fad49..87021cf852 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -133,8 +133,9 @@ void Line3DOverlay::render(RenderArgs* args) { auto batch = args->_batch; if (batch) { batch->setModelTransform(Transform()); - glm::vec3 start = _renderStart; - glm::vec3 end = _renderEnd; + auto& renderTransform = getRenderTransform(); + glm::vec3 start = renderTransform.getTranslation(); + glm::vec3 end = renderTransform.transform(vec3(0.0, 0.0, -1.0)); auto geometryCache = DependencyManager::get(); if (getIsDashedLine()) { @@ -269,7 +270,19 @@ Line3DOverlay* Line3DOverlay::createClone() const { } Transform Line3DOverlay::evalRenderTransform() { - _renderStart = getStart(); - _renderEnd = getEnd(); - return Parent::evalRenderTransform(); + // Capture start and endin the renderTransform: + // start is the origin + // end is at the tip of the front axis aka -Z + Transform transform; + transform.setTranslation( getStart()); + auto endPos = getEnd(); + + auto vec = endPos - transform.getTranslation(); + auto scale = glm::length(vec); + auto dir = vec / scale; + auto orientation = glm::rotation(glm::vec3(0,0,-1), dir); + transform.setRotation(orientation); + transform.setScale(scale); + + return transform; } diff --git a/interface/src/ui/overlays/Line3DOverlay.h b/interface/src/ui/overlays/Line3DOverlay.h index d1c6aa6183..bcb65b1f1e 100644 --- a/interface/src/ui/overlays/Line3DOverlay.h +++ b/interface/src/ui/overlays/Line3DOverlay.h @@ -73,10 +73,6 @@ private: float _glow { 0.0 }; float _glowWidth { 0.0 }; int _geometryCacheID; - - // Similar to the _renderTransform, we capture the start and end pos for render loop in game loop - glm::vec3 _renderStart; - glm::vec3 _renderEnd; }; #endif // hifi_Line3DOverlay_h From b424505f6560248fb99c1b358863b0ed7bf75229 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 22 Sep 2017 10:48:56 -0700 Subject: [PATCH 396/722] FIxing the case of the Circle3D --- interface/src/ui/overlays/Circle3DOverlay.cpp | 11 +---------- interface/src/ui/overlays/Circle3DOverlay.h | 2 -- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index b3e4cba5d9..4e51844d21 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -84,12 +84,7 @@ void Circle3DOverlay::render(RenderArgs* args) { batch.setPipeline(args->_shapePipeline->pipeline); } - // FIXME: THe line width of _lineWidth is not supported anymore, we ll need a workaround - // FIXME Start using the _renderTransform instead of calling for Transform from here, do the custom things needed in evalRenderTransform() - - auto transform = getTransform(); - transform.postScale(glm::vec3(getDimensions(), 1.0f)); - batch.setModelTransform(transform); + batch.setModelTransform(getRenderTransform()); // for our overlay, is solid means we draw a ring between the inner and outer radius of the circle, otherwise // we just draw a line... @@ -438,7 +433,3 @@ bool Circle3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::ve Circle3DOverlay* Circle3DOverlay::createClone() const { return new Circle3DOverlay(this); } - -Transform Circle3DOverlay::evalRenderTransform() { - return getTransform(); -} diff --git a/interface/src/ui/overlays/Circle3DOverlay.h b/interface/src/ui/overlays/Circle3DOverlay.h index e41c308d9d..11c9c9710f 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.h +++ b/interface/src/ui/overlays/Circle3DOverlay.h @@ -88,8 +88,6 @@ protected: int _minorTicksVerticesID { 0 }; bool _dirty { true }; - - Transform evalRenderTransform() override; }; From 4d530ef17354170b874d12f7e49a24f9d76b44f1 Mon Sep 17 00:00:00 2001 From: vladest Date: Fri, 22 Sep 2017 21:08:34 +0200 Subject: [PATCH 397/722] Make sure menu items vill be created even its not visible at the moment. Cleanup warnings on app close --- .../qml/hifi/tablet/TabletMenuItem.qml | 32 ++++++++++++------- .../qml/hifi/tablet/TabletMenuStack.qml | 2 -- .../qml/hifi/tablet/TabletMenuView.qml | 9 ++---- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/interface/resources/qml/hifi/tablet/TabletMenuItem.qml b/interface/resources/qml/hifi/tablet/TabletMenuItem.qml index 71e59e0d01..11d3cab35e 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuItem.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuItem.qml @@ -21,9 +21,9 @@ Item { property alias text: label.text property var source - implicitHeight: source.visible ? 2 * label.implicitHeight : 0 + implicitHeight: source !== null ? source.visible ? 2 * label.implicitHeight : 0 : 0 implicitWidth: 2 * hifi.dimensions.menuPadding.x + check.width + label.width + tail.width - visible: source.visible + visible: source !== null ? source.visible : false width: parent.width Item { @@ -42,7 +42,9 @@ Item { id: checkbox // FIXME: Should use radio buttons if source.exclusiveGroup. width: 20 - visible: source.visible && source.type === 1 && source.checkable && !source.exclusiveGroup + visible: source !== null ? + source.visible && source.type === 1 && source.checkable && !source.exclusiveGroup : + false checked: setChecked() function setChecked() { if (!source || source.type !== 1 || !source.checkable) { @@ -58,7 +60,9 @@ Item { id: radiobutton // FIXME: Should use radio buttons if source.exclusiveGroup. width: 20 - visible: source.visible && source.type === 1 && source.checkable && source.exclusiveGroup + visible: source !== null ? + source.visible && source.type === 1 && source.checkable && source.exclusiveGroup : + false checked: setChecked() function setChecked() { if (!source || source.type !== 1 || !source.checkable) { @@ -80,9 +84,13 @@ Item { anchors.left: check.right anchors.verticalCenter: parent.verticalCenter verticalAlignment: Text.AlignVCenter - color: source.enabled ? hifi.colors.baseGrayShadow : hifi.colors.baseGrayShadow50 - enabled: source.visible && (source.type !== 0 ? source.enabled : false) - visible: source.visible + color: source !== null ? + source.enabled ? hifi.colors.baseGrayShadow : + hifi.colors.baseGrayShadow50 : + "transparent" + + enabled: source !== null ? source.visible && (source.type !== 0 ? source.enabled : false) : false + visible: source !== null ? source.visible : false wrapMode: Text.WordWrap } @@ -93,7 +101,7 @@ Item { leftMargin: hifi.dimensions.menuPadding.x + check.width rightMargin: hifi.dimensions.menuPadding.x + tail.width } - visible: source.type === MenuItemType.Separator + visible: source !== null ? source.type === MenuItemType.Separator : false Rectangle { anchors { @@ -117,23 +125,23 @@ Item { RalewayLight { id: shortcut - text: source.shortcut ? source.shortcut : "" + text: source !== null ? source.shortcut ? source.shortcut : "" : "" size: hifi.fontSizes.shortcutText color: hifi.colors.baseGrayShadow anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 15 - visible: source.visible && text != "" + visible: source !== null ? source.visible && text != "" : false } HiFiGlyphs { text: hifi.glyphs.disclosureExpand - color: source.enabled ? hifi.colors.baseGrayShadow : hifi.colors.baseGrayShadow25 + color: source !== null ? source.enabled ? hifi.colors.baseGrayShadow : hifi.colors.baseGrayShadow25 : "transparent" size: 70 anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right horizontalAlignment: Text.AlignRight - visible: source.visible && (source.type === 2) + visible: source !== null ? source.visible && (source.type === 2) : false } } } diff --git a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml index e7eefbc5e7..ce4fac3bd5 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml @@ -70,7 +70,6 @@ Item { for (var i = 0; i < items.length; ++i) { var item = items[i]; - if (!item.visible) continue; switch (item.type) { case MenuItemType.Menu: result.append({"name": item.title, "item": item}) @@ -216,5 +215,4 @@ Item { function nextItem() { d.topMenu.nextItem(); } function selectCurrentItem() { d.topMenu.selectCurrentItem(); } function previousPage() { d.topMenu.previousPage(); } - } diff --git a/interface/resources/qml/hifi/tablet/TabletMenuView.qml b/interface/resources/qml/hifi/tablet/TabletMenuView.qml index 92e7f59524..ecfb653923 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuView.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuView.qml @@ -9,8 +9,6 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 import "../../styles-uit" import "." @@ -36,7 +34,6 @@ FocusScope { //color: isSubMenu ? hifi.colors.faintGray : hifi.colors.faintGray80 } - ListView { id: listView x: 0 @@ -68,8 +65,8 @@ FocusScope { delegate: TabletMenuItem { text: name source: item - onImplicitHeightChanged: listView.recalcSize() - onImplicitWidthChanged: listView.recalcSize() + onImplicitHeightChanged: listView !== null ? listView.recalcSize() : 0 + onImplicitWidthChanged: listView !== null ? listView.recalcSize() : 0 MouseArea { anchors.fill: parent @@ -124,8 +121,6 @@ FocusScope { function nextItem() { listView.currentIndex = (listView.currentIndex + listView.count + 1) % listView.count; } function selectCurrentItem() { if (listView.currentIndex != -1) root.selected(currentItem.source); } function previousPage() { root.parent.pop(); } - - } From 7724fc204bb6dd283d3e39dd0463d17422c43530 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 22 Sep 2017 14:19:54 -0700 Subject: [PATCH 398/722] working checkpoint --- libraries/entities/src/EntityItem.cpp | 87 ++++++++++++-- libraries/entities/src/EntityItem.h | 20 ++-- .../entities/src/EntityItemProperties.cpp | 111 ++++++++++++++++++ libraries/entities/src/EntityItemProperties.h | 19 +++ .../src/EntityItemPropertiesDefaults.h | 6 +- .../networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 1 + 7 files changed, 222 insertions(+), 24 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 107af837fe..506d2b9e05 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -94,7 +94,18 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_DYNAMIC; requestedProperties += PROP_LOCKED; requestedProperties += PROP_USER_DATA; + + // Certifiable properties + requestedProperties += PROP_ITEM_NAME; + requestedProperties += PROP_ITEM_DESCRIPTION; + requestedProperties += PROP_ITEM_CATEGORIES; + requestedProperties += PROP_ITEM_ARTIST; + requestedProperties += PROP_ITEM_LICENSE; + requestedProperties += PROP_LIMITED_RUN; requestedProperties += PROP_MARKETPLACE_ID; + requestedProperties += PROP_EDITION_NUMBER; + requestedProperties += PROP_CERTIFICATE_ID; + requestedProperties += PROP_NAME; requestedProperties += PROP_HREF; requestedProperties += PROP_DESCRIPTION; @@ -239,7 +250,18 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet APPEND_ENTITY_PROPERTY(PROP_DYNAMIC, getDynamic()); APPEND_ENTITY_PROPERTY(PROP_LOCKED, getLocked()); APPEND_ENTITY_PROPERTY(PROP_USER_DATA, getUserData()); + + // Certifiable Properties APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, getMarketplaceID()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_NAME, getItemName()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_DESCRIPTION, getItemDescription()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_CATEGORIES, getItemCategories()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_ARTIST, getItemArtist()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_LICENSE, getItemLicense()); + APPEND_ENTITY_PROPERTY(PROP_LIMITED_RUN, getLimitedRun()); + APPEND_ENTITY_PROPERTY(PROP_EDITION_NUMBER, getEditionNumber()); + APPEND_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, getCertificateID()); + APPEND_ENTITY_PROPERTY(PROP_NAME, getName()); APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, getCollisionSoundURL()); APPEND_ENTITY_PROPERTY(PROP_HREF, getHref()); @@ -790,6 +812,16 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef if (args.bitstreamVersion >= VERSION_ENTITIES_HAS_MARKETPLACE_ID) { READ_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, QString, setMarketplaceID); } + if (args.bitstreamVersion >= VERSION_ENTITIES_HAS_CERTIFICATE_PROPERTIES) { + READ_ENTITY_PROPERTY(PROP_ITEM_NAME, QString, setItemName); + READ_ENTITY_PROPERTY(PROP_ITEM_DESCRIPTION, QString, setItemDescription); + READ_ENTITY_PROPERTY(PROP_ITEM_CATEGORIES, QString, setItemCategories); + READ_ENTITY_PROPERTY(PROP_ITEM_ARTIST, QString, setItemArtist); + READ_ENTITY_PROPERTY(PROP_ITEM_LICENSE, QString, setItemLicense); + READ_ENTITY_PROPERTY(PROP_LIMITED_RUN, quint32, setLimitedRun); + READ_ENTITY_PROPERTY(PROP_EDITION_NUMBER, quint32, setEditionNumber); + READ_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, QString, setCertificateID); + } READ_ENTITY_PROPERTY(PROP_NAME, QString, setName); READ_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); @@ -1207,7 +1239,18 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper COPY_ENTITY_PROPERTY_TO_PROPERTIES(dynamic, getDynamic); COPY_ENTITY_PROPERTY_TO_PROPERTIES(locked, getLocked); COPY_ENTITY_PROPERTY_TO_PROPERTIES(userData, getUserData); + + // Certifiable Properties + COPY_ENTITY_PROPERTY_TO_PROPERTIES(itemName, getItemName); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(itemDescription, getItemDescription); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(itemCategories, getItemCategories); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(itemArtist, getItemArtist); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(itemLicense, getItemLicense); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(limitedRun, getLimitedRun); COPY_ENTITY_PROPERTY_TO_PROPERTIES(marketplaceID, getMarketplaceID); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(editionNumber, getEditionNumber); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(certificateID, getCertificateID); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(name, getName); COPY_ENTITY_PROPERTY_TO_PROPERTIES(href, getHref); COPY_ENTITY_PROPERTY_TO_PROPERTIES(description, getDescription); @@ -1302,7 +1345,18 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(localRenderAlpha, setLocalRenderAlpha); SET_ENTITY_PROPERTY_FROM_PROPERTIES(visible, setVisible); SET_ENTITY_PROPERTY_FROM_PROPERTIES(userData, setUserData); + + // Certifiable properties + SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemName, setItemName); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemDescription, setItemDescription); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemCategories, setItemCategories); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemArtist, setItemArtist); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemLicense, setItemLicense); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(limitedRun, setLimitedRun); SET_ENTITY_PROPERTY_FROM_PROPERTIES(marketplaceID, setMarketplaceID); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(editionNumber, setEditionNumber); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(certificateID, setCertificateID); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(name, setName); SET_ENTITY_PROPERTY_FROM_PROPERTIES(href, setHref); SET_ENTITY_PROPERTY_FROM_PROPERTIES(description, setDescription); @@ -2757,19 +2811,32 @@ void EntityItem::setUserData(const QString& value) { }); } -QString EntityItem::getMarketplaceID() const { - QString result; - withReadLock([&] { - result = _marketplaceID; - }); - return result; +// Certificate Properties +#define DEFINE_PROPERTY_GETTER(type, accessor, var) \ +type EntityItem::get##accessor() const { \ + type result; \ + withReadLock([&] { \ + result = _##var; \ + }); \ + return result; \ } -void EntityItem::setMarketplaceID(const QString& value) { - withWriteLock([&] { - _marketplaceID = value; - }); +#define DEFINE_PROPERTY_SETTER(type, accessor, var) \ +void EntityItem::set##accessor(const type##& value) { \ + withWriteLock([&] { \ + _##var = value; \ + }); \ } +#define DEFINE_PROPERTY_ACCESSOR(type, accessor, var) DEFINE_PROPERTY_GETTER(type, accessor, var) DEFINE_PROPERTY_SETTER(type, accessor, var) +DEFINE_PROPERTY_ACCESSOR(QString, ItemName, itemName) +DEFINE_PROPERTY_ACCESSOR(QString, ItemDescription, itemDescription) +DEFINE_PROPERTY_ACCESSOR(QString, ItemCategories, itemCategories) +DEFINE_PROPERTY_ACCESSOR(QString, ItemArtist, itemArtist) +DEFINE_PROPERTY_ACCESSOR(QString, ItemLicense, itemLicense) +DEFINE_PROPERTY_ACCESSOR(quint32, LimitedRun, limitedRun) +DEFINE_PROPERTY_ACCESSOR(QString, MarketplaceID, marketplaceID) +DEFINE_PROPERTY_ACCESSOR(quint32, EditionNumber, editionNumber) +DEFINE_PROPERTY_ACCESSOR(QString, CertificateID, certificateID) uint32_t EntityItem::getDirtyFlags() const { uint32_t result; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 5f3587fc43..9c20b3ba32 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -308,18 +308,18 @@ public: void setItemName(const QString& value); QString getItemDescription() const; void setItemDescription(const QString& value); - QStringList getItemCategories() const; - void setItemCategories(const QStringList& value); + QString getItemCategories() const; + void setItemCategories(const QString& value); QString getItemArtist() const; void setItemArtist(const QString& value); QString getItemLicense() const; void setItemLicense(const QString& value); - int getLimitedRun() const; - void setLimitedRun(int); + quint32 getLimitedRun() const; + void setLimitedRun(const quint32&); QString getMarketplaceID() const; void setMarketplaceID(const QString& value); - int getEditionNumber() const; - void setEditionNumber(int); + quint32 getEditionNumber() const; + void setEditionNumber(const quint32&); QString getCertificateID() const; void setCertificateID(const QString& value); QString getStaticCertificateJSON() const; @@ -555,13 +555,13 @@ protected: // Certificate Properties QString _itemName { ENTITY_ITEM_DEFAULT_ITEM_NAME }; QString _itemDescription { ENTITY_ITEM_DEFAULT_ITEM_DESCRIPTION }; - QStringList _itemCategories { ENTITY_ITEM_DEFAULT_ITEM_CATEGORIES }; + QString _itemCategories { ENTITY_ITEM_DEFAULT_ITEM_CATEGORIES }; QString _itemArtist { ENTITY_ITEM_DEFAULT_ITEM_ARTIST }; QString _itemLicense { ENTITY_ITEM_DEFAULT_ITEM_LICENSE }; - int _limitedRun { ENTITY_ITEM_DEFAULT_LIMITED_RUN }; + quint32 _limitedRun { ENTITY_ITEM_DEFAULT_LIMITED_RUN }; + QString _certificateID { ENTITY_ITEM_DEFAULT_CERTIFICATE_ID }; + quint32 _editionNumber { ENTITY_ITEM_DEFAULT_EDITION_NUMBER }; QString _marketplaceID { ENTITY_ITEM_DEFAULT_MARKETPLACE_ID }; - int _editionNumber { ENTITY_ITEM_DEFAULT_EDITION_NUMBER }; - QString _marketplaceID { ENTITY_ITEM_DEFAULT_CERTIFICATE_ID }; // NOTE: Damping is applied like this: v *= pow(1 - damping, dt) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index d6de4ec614..48eb3462c4 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -288,7 +288,18 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_RADIUS_SPREAD, radiusSpread); CHECK_PROPERTY_CHANGE(PROP_RADIUS_START, radiusStart); CHECK_PROPERTY_CHANGE(PROP_RADIUS_FINISH, radiusFinish); + + // Certifiable properties + CHECK_PROPERTY_CHANGE(PROP_ITEM_NAME, itemName); + CHECK_PROPERTY_CHANGE(PROP_ITEM_DESCRIPTION, itemDescription); + CHECK_PROPERTY_CHANGE(PROP_ITEM_CATEGORIES, itemCategories); + CHECK_PROPERTY_CHANGE(PROP_ITEM_ARTIST, itemArtist); + CHECK_PROPERTY_CHANGE(PROP_ITEM_LICENSE, itemLicense); + CHECK_PROPERTY_CHANGE(PROP_LIMITED_RUN, limitedRun); CHECK_PROPERTY_CHANGE(PROP_MARKETPLACE_ID, marketplaceID); + CHECK_PROPERTY_CHANGE(PROP_EDITION_NUMBER, editionNumber); + CHECK_PROPERTY_CHANGE(PROP_CERTIFICATE_ID, certificateID); + CHECK_PROPERTY_CHANGE(PROP_NAME, name); CHECK_PROPERTY_CHANGE(PROP_BACKGROUND_MODE, backgroundMode); CHECK_PROPERTY_CHANGE(PROP_SOURCE_URL, sourceUrl); @@ -405,7 +416,18 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ACTION_DATA, actionData); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCKED, locked); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_USER_DATA, userData); + + // Certifiable properties + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_NAME, itemName); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_DESCRIPTION, itemDescription); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_CATEGORIES, itemCategories); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_ARTIST, itemArtist); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_LICENSE, itemLicense); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LIMITED_RUN, limitedRun); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MARKETPLACE_ID, marketplaceID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EDITION_NUMBER, editionNumber); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CERTIFICATE_ID, certificateID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NAME, name); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLLISION_SOUND_URL, collisionSoundURL); @@ -671,7 +693,18 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(radiusSpread, float, setRadiusSpread); COPY_PROPERTY_FROM_QSCRIPTVALUE(radiusStart, float, setRadiusStart); COPY_PROPERTY_FROM_QSCRIPTVALUE(radiusFinish, float, setRadiusFinish); + + // Certifiable properties + COPY_PROPERTY_FROM_QSCRIPTVALUE(itemName, QString, setItemName); + COPY_PROPERTY_FROM_QSCRIPTVALUE(itemDescription, QString, setItemDescription); + COPY_PROPERTY_FROM_QSCRIPTVALUE(itemCategories, QString, setItemCategories); + COPY_PROPERTY_FROM_QSCRIPTVALUE(itemArtist, QString, setItemArtist); + COPY_PROPERTY_FROM_QSCRIPTVALUE(itemLicense, QString, setItemLicense); + COPY_PROPERTY_FROM_QSCRIPTVALUE(limitedRun, quint32, setLimitedRun); COPY_PROPERTY_FROM_QSCRIPTVALUE(marketplaceID, QString, setMarketplaceID); + COPY_PROPERTY_FROM_QSCRIPTVALUE(editionNumber, quint32, setEditionNumber); + COPY_PROPERTY_FROM_QSCRIPTVALUE(certificateID, QString, setCertificateID); + COPY_PROPERTY_FROM_QSCRIPTVALUE(name, QString, setName); COPY_PROPERTY_FROM_QSCRIPTVALUE(collisionSoundURL, QString, setCollisionSoundURL); @@ -809,7 +842,18 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(radiusSpread); COPY_PROPERTY_IF_CHANGED(radiusStart); COPY_PROPERTY_IF_CHANGED(radiusFinish); + + // Certifiable properties + COPY_PROPERTY_IF_CHANGED(itemName); + COPY_PROPERTY_IF_CHANGED(itemDescription); + COPY_PROPERTY_IF_CHANGED(itemCategories); + COPY_PROPERTY_IF_CHANGED(itemArtist); + COPY_PROPERTY_IF_CHANGED(itemLicense); + COPY_PROPERTY_IF_CHANGED(limitedRun); COPY_PROPERTY_IF_CHANGED(marketplaceID); + COPY_PROPERTY_IF_CHANGED(editionNumber); + COPY_PROPERTY_IF_CHANGED(certificateID); + COPY_PROPERTY_IF_CHANGED(name); COPY_PROPERTY_IF_CHANGED(collisionSoundURL); @@ -981,7 +1025,18 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_RADIUS_SPREAD, RadiusSpread, radiusSpread, float); ADD_PROPERTY_TO_MAP(PROP_RADIUS_START, RadiusStart, radiusStart, float); ADD_PROPERTY_TO_MAP(PROP_RADIUS_FINISH, RadiusFinish, radiusFinish, float); + + // Certifiable properties + ADD_PROPERTY_TO_MAP(PROP_ITEM_NAME, ItemName, itemName, QString); + ADD_PROPERTY_TO_MAP(PROP_ITEM_DESCRIPTION, ItemDescription, itemDescription, QString); + ADD_PROPERTY_TO_MAP(PROP_ITEM_CATEGORIES, ItemCategories, itemCategories, QString); + ADD_PROPERTY_TO_MAP(PROP_ITEM_ARTIST, ItemArtist, itemArtist, QString); + ADD_PROPERTY_TO_MAP(PROP_ITEM_LICENSE, ItemLicense, itemLicense, QString); + ADD_PROPERTY_TO_MAP(PROP_LIMITED_RUN, LimitedRun, limitedRun, quint32); ADD_PROPERTY_TO_MAP(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString); + ADD_PROPERTY_TO_MAP(PROP_EDITION_NUMBER, EditionNumber, editionNumber, quint32); + ADD_PROPERTY_TO_MAP(PROP_CERTIFICATE_ID, CertificateID, certificateID, QString); + ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_COLOR, KeyLightColor, keyLightColor, xColor); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_INTENSITY, KeyLightIntensity, keyLightIntensity, float); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_AMBIENT_INTENSITY, KeyLightAmbientIntensity, keyLightAmbientIntensity, float); @@ -1334,7 +1389,17 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem properties.getType() == EntityTypes::Sphere) { APPEND_ENTITY_PROPERTY(PROP_SHAPE, properties.getShape()); } + // certifiable properties + APPEND_ENTITY_PROPERTY(PROP_ITEM_NAME, properties.getItemName()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_DESCRIPTION, properties.getItemDescription()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_CATEGORIES, properties.getItemCategories()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_ARTIST, properties.getItemArtist()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_LICENSE, properties.getItemLicense()); + APPEND_ENTITY_PROPERTY(PROP_LIMITED_RUN, properties.getLimitedRun()); APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, properties.getMarketplaceID()); + APPEND_ENTITY_PROPERTY(PROP_EDITION_NUMBER, properties.getEditionNumber()); + APPEND_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, properties.getCertificateID()); + APPEND_ENTITY_PROPERTY(PROP_NAME, properties.getName()); APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, properties.getCollisionSoundURL()); APPEND_ENTITY_PROPERTY(PROP_ACTION_DATA, properties.getActionData()); @@ -1632,7 +1697,17 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHAPE, QString, setShape); } + // certifiable properties + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_NAME, QString, setItemName); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_DESCRIPTION, QString, setItemDescription); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_CATEGORIES, QString, setItemCategories); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_ARTIST, QString, setItemArtist); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_LICENSE, QString, setItemLicense); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LIMITED_RUN, quint32, setLimitedRun); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_MARKETPLACE_ID, QString, setMarketplaceID); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_EDITION_NUMBER, quint32, setEditionNumber); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CERTIFICATE_ID, QString, setCertificateID); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_NAME, QString, setName); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ACTION_DATA, QByteArray, setActionData); @@ -1746,7 +1821,16 @@ void EntityItemProperties::markAllChanged() { //_alphaStartChanged = true; //_alphaFinishChanged = true; + // Certifiable properties + _itemNameChanged = true; + _itemDescriptionChanged = true; + _itemCategoriesChanged = true; + _itemArtistChanged = true; + _itemLicenseChanged = true; + _limitedRunChanged = true; _marketplaceIDChanged = true; + _editionNumberChanged = true; + _certificateIDChanged = true; _keyLight.markAllChanged(); @@ -2053,9 +2137,36 @@ QList EntityItemProperties::listChangedProperties() { if (radiusFinishChanged()) { out += "radiusFinish"; } + + // Certifiable properties + if (itemNameChanged()) { + out += "itemName"; + } + if (itemDescriptionChanged()) { + out += "itemDescription"; + } + if (itemCategoriesChanged()) { + out += "itemCategories"; + } + if (itemArtistChanged()) { + out += "itemArtist"; + } + if (itemLicenseChanged()) { + out += "itemLicense"; + } + if (limitedRunChanged()) { + out += "limitedRun"; + } if (marketplaceIDChanged()) { out += "marketplaceID"; } + if (editionNumberChanged()) { + out += "editionNumber"; + } + if (certificateIDChanged()) { + out += "certificateID"; + } + if (backgroundModeChanged()) { out += "backgroundMode"; } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 212d707de0..0ca97dcadc 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -203,7 +203,15 @@ public: DEFINE_PROPERTY_REF(PROP_SHAPE, Shape, shape, QString, "Sphere"); // Certifiable Properties - related to Proof of Purchase certificates + DEFINE_PROPERTY_REF(PROP_ITEM_NAME, ItemName, itemName, QString, ENTITY_ITEM_DEFAULT_ITEM_NAME); + DEFINE_PROPERTY_REF(PROP_ITEM_DESCRIPTION, ItemDescription, itemDescription, QString, ENTITY_ITEM_DEFAULT_ITEM_DESCRIPTION); + DEFINE_PROPERTY_REF(PROP_ITEM_CATEGORIES, ItemCategories, itemCategories, QString, ENTITY_ITEM_DEFAULT_ITEM_CATEGORIES); + DEFINE_PROPERTY_REF(PROP_ITEM_ARTIST, ItemArtist, itemArtist, QString, ENTITY_ITEM_DEFAULT_ITEM_ARTIST); + DEFINE_PROPERTY_REF(PROP_ITEM_LICENSE, ItemLicense, itemLicense, QString, ENTITY_ITEM_DEFAULT_ITEM_LICENSE); + DEFINE_PROPERTY_REF(PROP_LIMITED_RUN, LimitedRun, limitedRun, quint32, ENTITY_ITEM_DEFAULT_LIMITED_RUN); DEFINE_PROPERTY_REF(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString, ENTITY_ITEM_DEFAULT_MARKETPLACE_ID); + DEFINE_PROPERTY_REF(PROP_EDITION_NUMBER, EditionNumber, editionNumber, quint32, ENTITY_ITEM_DEFAULT_EDITION_NUMBER); + DEFINE_PROPERTY_REF(PROP_CERTIFICATE_ID, CertificateID, certificateID, QString, ENTITY_ITEM_DEFAULT_CERTIFICATE_ID); // these are used when bouncing location data into and out of scripts DEFINE_PROPERTY_REF(PROP_LOCAL_POSITION, LocalPosition, localPosition, glmVec3, ENTITY_ITEM_ZERO_VEC3); @@ -428,7 +436,18 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, RadiusSpread, radiusSpread, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, RadiusStart, radiusStart, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, RadiusFinish, radiusFinish, ""); + + // Certifiable Properties + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ItemName, itemName, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ItemDescription, itemDescription, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ItemCategories, itemCategories, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ItemArtist, itemArtist, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ItemLicense, itemLicense, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, LimitedRun, limitedRun, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, MarketplaceID, marketplaceID, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, EditionNumber, editionNumber, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, CertificateID, certificateID, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, BackgroundMode, backgroundMode, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, VoxelVolumeSize, voxelVolumeSize, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, VoxelData, voxelData, ""); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index 5949c3aefb..1c52e9edd4 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -31,12 +31,12 @@ const QUuid ENTITY_ITEM_DEFAULT_SIMULATOR_ID = QUuid(); // Certificate Properties const QString ENTITY_ITEM_DEFAULT_ITEM_NAME = QString(""); const QString ENTITY_ITEM_DEFAULT_ITEM_DESCRIPTION = QString(""); -const QStringList ENTITY_ITEM_DEFAULT_ITEM_CATEGORIES = QStringList(); +const QString ENTITY_ITEM_DEFAULT_ITEM_CATEGORIES = QString(""); const QString ENTITY_ITEM_DEFAULT_ITEM_ARTIST = QString(""); const QString ENTITY_ITEM_DEFAULT_ITEM_LICENSE = QString(""); -const int ENTITY_ITEM_DEFAULT_LIMITED_RUN = -1; +const quint32 ENTITY_ITEM_DEFAULT_LIMITED_RUN = -1; const QString ENTITY_ITEM_DEFAULT_MARKETPLACE_ID = QString(""); -const int ENTITY_ITEM_DEFAULT_EDITION_NUMBER = -1; +const quint32 ENTITY_ITEM_DEFAULT_EDITION_NUMBER = -1; const QString ENTITY_ITEM_DEFAULT_CERTIFICATE_ID = QString(""); const float ENTITY_ITEM_DEFAULT_ALPHA = 1.0f; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index b95f2ff6a0..2f3f65c90b 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -30,7 +30,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return VERSION_ENTITIES_ANIMATION_ALLOW_TRANSLATION_PROPERTIES; + return VERSION_ENTITIES_HAS_CERTIFICATE_PROPERTIES; case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::JSONFilterWithFamilyTree); case PacketType::AvatarIdentity: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 4fefc6ab3a..92f9a6ffb7 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -266,6 +266,7 @@ const PacketVersion VERSION_ENTITIES_BULLET_DYNAMICS = 70; const PacketVersion VERSION_ENTITIES_HAS_SHOULD_HIGHLIGHT = 71; const PacketVersion VERSION_ENTITIES_HAS_HIGHLIGHT_SCRIPTING_INTERFACE = 72; const PacketVersion VERSION_ENTITIES_ANIMATION_ALLOW_TRANSLATION_PROPERTIES = 73; +const PacketVersion VERSION_ENTITIES_HAS_CERTIFICATE_PROPERTIES = 74; enum class EntityQueryPacketVersion: PacketVersion { JSONFilter = 18, From f25e22b463a4527d31eb98a1403e1a64d157b2b4 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 22 Sep 2017 15:06:39 -0700 Subject: [PATCH 399/722] entityInstanceNumber --- libraries/entities/src/EntityItem.cpp | 6 ++++++ libraries/entities/src/EntityItem.h | 3 +++ libraries/entities/src/EntityItemProperties.cpp | 11 +++++++++++ libraries/entities/src/EntityItemProperties.h | 2 ++ libraries/entities/src/EntityItemPropertiesDefaults.h | 3 ++- libraries/entities/src/EntityPropertyFlags.h | 1 + 6 files changed, 25 insertions(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 506d2b9e05..ff0b382a67 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -104,6 +104,7 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_LIMITED_RUN; requestedProperties += PROP_MARKETPLACE_ID; requestedProperties += PROP_EDITION_NUMBER; + requestedProperties += PROP_ENTITY_INSTANCE_NUMBER; requestedProperties += PROP_CERTIFICATE_ID; requestedProperties += PROP_NAME; @@ -260,6 +261,7 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet APPEND_ENTITY_PROPERTY(PROP_ITEM_LICENSE, getItemLicense()); APPEND_ENTITY_PROPERTY(PROP_LIMITED_RUN, getLimitedRun()); APPEND_ENTITY_PROPERTY(PROP_EDITION_NUMBER, getEditionNumber()); + APPEND_ENTITY_PROPERTY(PROP_ENTITY_INSTANCE_NUMBER, getEntityInstanceNumber()); APPEND_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, getCertificateID()); APPEND_ENTITY_PROPERTY(PROP_NAME, getName()); @@ -820,6 +822,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef READ_ENTITY_PROPERTY(PROP_ITEM_LICENSE, QString, setItemLicense); READ_ENTITY_PROPERTY(PROP_LIMITED_RUN, quint32, setLimitedRun); READ_ENTITY_PROPERTY(PROP_EDITION_NUMBER, quint32, setEditionNumber); + READ_ENTITY_PROPERTY(PROP_ENTITY_INSTANCE_NUMBER, quint32, setEntityInstanceNumber); READ_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, QString, setCertificateID); } @@ -1249,6 +1252,7 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper COPY_ENTITY_PROPERTY_TO_PROPERTIES(limitedRun, getLimitedRun); COPY_ENTITY_PROPERTY_TO_PROPERTIES(marketplaceID, getMarketplaceID); COPY_ENTITY_PROPERTY_TO_PROPERTIES(editionNumber, getEditionNumber); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(entityInstanceNumber, getEntityInstanceNumber); COPY_ENTITY_PROPERTY_TO_PROPERTIES(certificateID, getCertificateID); COPY_ENTITY_PROPERTY_TO_PROPERTIES(name, getName); @@ -1355,6 +1359,7 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(limitedRun, setLimitedRun); SET_ENTITY_PROPERTY_FROM_PROPERTIES(marketplaceID, setMarketplaceID); SET_ENTITY_PROPERTY_FROM_PROPERTIES(editionNumber, setEditionNumber); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(entityInstanceNumber, setEntityInstanceNumber); SET_ENTITY_PROPERTY_FROM_PROPERTIES(certificateID, setCertificateID); SET_ENTITY_PROPERTY_FROM_PROPERTIES(name, setName); @@ -2836,6 +2841,7 @@ DEFINE_PROPERTY_ACCESSOR(QString, ItemLicense, itemLicense) DEFINE_PROPERTY_ACCESSOR(quint32, LimitedRun, limitedRun) DEFINE_PROPERTY_ACCESSOR(QString, MarketplaceID, marketplaceID) DEFINE_PROPERTY_ACCESSOR(quint32, EditionNumber, editionNumber) +DEFINE_PROPERTY_ACCESSOR(quint32, EntityInstanceNumber, entityInstanceNumber) DEFINE_PROPERTY_ACCESSOR(QString, CertificateID, certificateID) uint32_t EntityItem::getDirtyFlags() const { diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 9c20b3ba32..244fd362bf 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -320,6 +320,8 @@ public: void setMarketplaceID(const QString& value); quint32 getEditionNumber() const; void setEditionNumber(const quint32&); + quint32 getEntityInstanceNumber() const; + void setEntityInstanceNumber(const quint32&); QString getCertificateID() const; void setCertificateID(const QString& value); QString getStaticCertificateJSON() const; @@ -561,6 +563,7 @@ protected: quint32 _limitedRun { ENTITY_ITEM_DEFAULT_LIMITED_RUN }; QString _certificateID { ENTITY_ITEM_DEFAULT_CERTIFICATE_ID }; quint32 _editionNumber { ENTITY_ITEM_DEFAULT_EDITION_NUMBER }; + quint32 _entityInstanceNumber { ENTITY_ITEM_DEFAULT_ENTITY_INSTANCE_NUMBER }; QString _marketplaceID { ENTITY_ITEM_DEFAULT_MARKETPLACE_ID }; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 48eb3462c4..e3752fc790 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -298,6 +298,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_LIMITED_RUN, limitedRun); CHECK_PROPERTY_CHANGE(PROP_MARKETPLACE_ID, marketplaceID); CHECK_PROPERTY_CHANGE(PROP_EDITION_NUMBER, editionNumber); + CHECK_PROPERTY_CHANGE(PROP_ENTITY_INSTANCE_NUMBER, entityInstanceNumber); CHECK_PROPERTY_CHANGE(PROP_CERTIFICATE_ID, certificateID); CHECK_PROPERTY_CHANGE(PROP_NAME, name); @@ -426,6 +427,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LIMITED_RUN, limitedRun); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MARKETPLACE_ID, marketplaceID); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EDITION_NUMBER, editionNumber); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ENTITY_INSTANCE_NUMBER, entityInstanceNumber); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CERTIFICATE_ID, certificateID); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NAME, name); @@ -703,6 +705,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(limitedRun, quint32, setLimitedRun); COPY_PROPERTY_FROM_QSCRIPTVALUE(marketplaceID, QString, setMarketplaceID); COPY_PROPERTY_FROM_QSCRIPTVALUE(editionNumber, quint32, setEditionNumber); + COPY_PROPERTY_FROM_QSCRIPTVALUE(entityInstanceNumber, quint32, setEntityInstanceNumber); COPY_PROPERTY_FROM_QSCRIPTVALUE(certificateID, QString, setCertificateID); COPY_PROPERTY_FROM_QSCRIPTVALUE(name, QString, setName); @@ -852,6 +855,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(limitedRun); COPY_PROPERTY_IF_CHANGED(marketplaceID); COPY_PROPERTY_IF_CHANGED(editionNumber); + COPY_PROPERTY_IF_CHANGED(entityInstanceNumber); COPY_PROPERTY_IF_CHANGED(certificateID); COPY_PROPERTY_IF_CHANGED(name); @@ -1035,6 +1039,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_LIMITED_RUN, LimitedRun, limitedRun, quint32); ADD_PROPERTY_TO_MAP(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString); ADD_PROPERTY_TO_MAP(PROP_EDITION_NUMBER, EditionNumber, editionNumber, quint32); + ADD_PROPERTY_TO_MAP(PROP_ENTITY_INSTANCE_NUMBER, EntityInstanceNumber, entityInstanceNumber, quint32); ADD_PROPERTY_TO_MAP(PROP_CERTIFICATE_ID, CertificateID, certificateID, QString); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_COLOR, KeyLightColor, keyLightColor, xColor); @@ -1398,6 +1403,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_LIMITED_RUN, properties.getLimitedRun()); APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, properties.getMarketplaceID()); APPEND_ENTITY_PROPERTY(PROP_EDITION_NUMBER, properties.getEditionNumber()); + APPEND_ENTITY_PROPERTY(PROP_ENTITY_INSTANCE_NUMBER, properties.getEntityInstanceNumber()); APPEND_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, properties.getCertificateID()); APPEND_ENTITY_PROPERTY(PROP_NAME, properties.getName()); @@ -1706,6 +1712,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LIMITED_RUN, quint32, setLimitedRun); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_MARKETPLACE_ID, QString, setMarketplaceID); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_EDITION_NUMBER, quint32, setEditionNumber); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ENTITY_INSTANCE_NUMBER, quint32, setEntityInstanceNumber); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CERTIFICATE_ID, QString, setCertificateID); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_NAME, QString, setName); @@ -1830,6 +1837,7 @@ void EntityItemProperties::markAllChanged() { _limitedRunChanged = true; _marketplaceIDChanged = true; _editionNumberChanged = true; + _entityInstanceNumberChanged = true; _certificateIDChanged = true; _keyLight.markAllChanged(); @@ -2163,6 +2171,9 @@ QList EntityItemProperties::listChangedProperties() { if (editionNumberChanged()) { out += "editionNumber"; } + if (entityInstanceNumberChanged()) { + out += "entityInstanceNumber"; + } if (certificateIDChanged()) { out += "certificateID"; } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 0ca97dcadc..1f5b4a4660 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -211,6 +211,7 @@ public: DEFINE_PROPERTY_REF(PROP_LIMITED_RUN, LimitedRun, limitedRun, quint32, ENTITY_ITEM_DEFAULT_LIMITED_RUN); DEFINE_PROPERTY_REF(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString, ENTITY_ITEM_DEFAULT_MARKETPLACE_ID); DEFINE_PROPERTY_REF(PROP_EDITION_NUMBER, EditionNumber, editionNumber, quint32, ENTITY_ITEM_DEFAULT_EDITION_NUMBER); + DEFINE_PROPERTY_REF(PROP_ENTITY_INSTANCE_NUMBER, EntityInstanceNumber, entityInstanceNumber, quint32, ENTITY_ITEM_DEFAULT_ENTITY_INSTANCE_NUMBER); DEFINE_PROPERTY_REF(PROP_CERTIFICATE_ID, CertificateID, certificateID, QString, ENTITY_ITEM_DEFAULT_CERTIFICATE_ID); // these are used when bouncing location data into and out of scripts @@ -446,6 +447,7 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, LimitedRun, limitedRun, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, MarketplaceID, marketplaceID, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, EditionNumber, editionNumber, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, EntityInstanceNumber, entityInstanceNumber, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, CertificateID, certificateID, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, BackgroundMode, backgroundMode, ""); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index 1c52e9edd4..8eae4766f0 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -36,7 +36,8 @@ const QString ENTITY_ITEM_DEFAULT_ITEM_ARTIST = QString(""); const QString ENTITY_ITEM_DEFAULT_ITEM_LICENSE = QString(""); const quint32 ENTITY_ITEM_DEFAULT_LIMITED_RUN = -1; const QString ENTITY_ITEM_DEFAULT_MARKETPLACE_ID = QString(""); -const quint32 ENTITY_ITEM_DEFAULT_EDITION_NUMBER = -1; +const quint32 ENTITY_ITEM_DEFAULT_EDITION_NUMBER = 0; +const quint32 ENTITY_ITEM_DEFAULT_ENTITY_INSTANCE_NUMBER = 0; const QString ENTITY_ITEM_DEFAULT_CERTIFICATE_ID = QString(""); const float ENTITY_ITEM_DEFAULT_ALPHA = 1.0f; diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 3aa5423505..c03630f8bf 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -197,6 +197,7 @@ enum EntityPropertyList { PROP_LIMITED_RUN, // PROP_MARKETPLACE_ID is above PROP_EDITION_NUMBER, + PROP_ENTITY_INSTANCE_NUMBER, PROP_CERTIFICATE_ID, //////////////////////////////////////////////////////////////////////////////////////////////////// From 8fdd405593efd41cca385237013e9b5026b78dc9 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 22 Sep 2017 15:10:30 -0700 Subject: [PATCH 400/722] consistent comments --- libraries/entities/src/EntityItem.cpp | 4 ++-- libraries/entities/src/EntityItem.h | 2 +- .../entities/src/EntityItemProperties.cpp | 18 +++++++++--------- .../src/EntityItemPropertiesDefaults.h | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index ff0b382a67..9084a4b878 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1350,7 +1350,7 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(visible, setVisible); SET_ENTITY_PROPERTY_FROM_PROPERTIES(userData, setUserData); - // Certifiable properties + // Certifiable Properties SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemName, setItemName); SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemDescription, setItemDescription); SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemCategories, setItemCategories); @@ -2816,7 +2816,7 @@ void EntityItem::setUserData(const QString& value) { }); } -// Certificate Properties +// Certifiable Properties #define DEFINE_PROPERTY_GETTER(type, accessor, var) \ type EntityItem::get##accessor() const { \ type result; \ diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 244fd362bf..e2221ac58a 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -554,7 +554,7 @@ protected: QString _href; //Hyperlink href QString _description; //Hyperlink description - // Certificate Properties + // Certifiable Properties QString _itemName { ENTITY_ITEM_DEFAULT_ITEM_NAME }; QString _itemDescription { ENTITY_ITEM_DEFAULT_ITEM_DESCRIPTION }; QString _itemCategories { ENTITY_ITEM_DEFAULT_ITEM_CATEGORIES }; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index e3752fc790..4e01bffe5f 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -289,7 +289,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_RADIUS_START, radiusStart); CHECK_PROPERTY_CHANGE(PROP_RADIUS_FINISH, radiusFinish); - // Certifiable properties + // Certifiable Properties CHECK_PROPERTY_CHANGE(PROP_ITEM_NAME, itemName); CHECK_PROPERTY_CHANGE(PROP_ITEM_DESCRIPTION, itemDescription); CHECK_PROPERTY_CHANGE(PROP_ITEM_CATEGORIES, itemCategories); @@ -418,7 +418,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCKED, locked); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_USER_DATA, userData); - // Certifiable properties + // Certifiable Properties COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_NAME, itemName); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_DESCRIPTION, itemDescription); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_CATEGORIES, itemCategories); @@ -696,7 +696,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(radiusStart, float, setRadiusStart); COPY_PROPERTY_FROM_QSCRIPTVALUE(radiusFinish, float, setRadiusFinish); - // Certifiable properties + // Certifiable Properties COPY_PROPERTY_FROM_QSCRIPTVALUE(itemName, QString, setItemName); COPY_PROPERTY_FROM_QSCRIPTVALUE(itemDescription, QString, setItemDescription); COPY_PROPERTY_FROM_QSCRIPTVALUE(itemCategories, QString, setItemCategories); @@ -846,7 +846,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(radiusStart); COPY_PROPERTY_IF_CHANGED(radiusFinish); - // Certifiable properties + // Certifiable Properties COPY_PROPERTY_IF_CHANGED(itemName); COPY_PROPERTY_IF_CHANGED(itemDescription); COPY_PROPERTY_IF_CHANGED(itemCategories); @@ -1030,7 +1030,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_RADIUS_START, RadiusStart, radiusStart, float); ADD_PROPERTY_TO_MAP(PROP_RADIUS_FINISH, RadiusFinish, radiusFinish, float); - // Certifiable properties + // Certifiable Properties ADD_PROPERTY_TO_MAP(PROP_ITEM_NAME, ItemName, itemName, QString); ADD_PROPERTY_TO_MAP(PROP_ITEM_DESCRIPTION, ItemDescription, itemDescription, QString); ADD_PROPERTY_TO_MAP(PROP_ITEM_CATEGORIES, ItemCategories, itemCategories, QString); @@ -1394,7 +1394,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem properties.getType() == EntityTypes::Sphere) { APPEND_ENTITY_PROPERTY(PROP_SHAPE, properties.getShape()); } - // certifiable properties + // Certifiable Properties APPEND_ENTITY_PROPERTY(PROP_ITEM_NAME, properties.getItemName()); APPEND_ENTITY_PROPERTY(PROP_ITEM_DESCRIPTION, properties.getItemDescription()); APPEND_ENTITY_PROPERTY(PROP_ITEM_CATEGORIES, properties.getItemCategories()); @@ -1703,7 +1703,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHAPE, QString, setShape); } - // certifiable properties + // Certifiable Properties READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_NAME, QString, setItemName); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_DESCRIPTION, QString, setItemDescription); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_CATEGORIES, QString, setItemCategories); @@ -1828,7 +1828,7 @@ void EntityItemProperties::markAllChanged() { //_alphaStartChanged = true; //_alphaFinishChanged = true; - // Certifiable properties + // Certifiable Properties _itemNameChanged = true; _itemDescriptionChanged = true; _itemCategoriesChanged = true; @@ -2146,7 +2146,7 @@ QList EntityItemProperties::listChangedProperties() { out += "radiusFinish"; } - // Certifiable properties + // Certifiable Properties if (itemNameChanged()) { out += "itemName"; } diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index 8eae4766f0..ab5d1d8094 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -28,7 +28,7 @@ const bool ENTITY_ITEM_DEFAULT_LOCKED = false; const QString ENTITY_ITEM_DEFAULT_USER_DATA = QString(""); const QUuid ENTITY_ITEM_DEFAULT_SIMULATOR_ID = QUuid(); -// Certificate Properties +// Certifiable Properties const QString ENTITY_ITEM_DEFAULT_ITEM_NAME = QString(""); const QString ENTITY_ITEM_DEFAULT_ITEM_DESCRIPTION = QString(""); const QString ENTITY_ITEM_DEFAULT_ITEM_CATEGORIES = QString(""); From cbe621f2b7a9a832b812d042e6af1d10f69e7734 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 22 Sep 2017 15:15:15 -0700 Subject: [PATCH 401/722] add searchRay to RayPickResult, fix precision picking caching with multiple lasers, make laser addition/removal immediate --- interface/src/raypick/LaserPointer.cpp | 7 +- interface/src/raypick/LaserPointer.h | 2 +- interface/src/raypick/LaserPointerManager.cpp | 32 ++------- interface/src/raypick/LaserPointerManager.h | 4 -- interface/src/raypick/RayPick.h | 6 ++ interface/src/raypick/RayPickManager.cpp | 68 ++++++------------- interface/src/raypick/RayPickManager.h | 5 -- libraries/shared/src/RegisteredMetaTypes.cpp | 2 + libraries/shared/src/RegisteredMetaTypes.h | 10 +-- 9 files changed, 45 insertions(+), 91 deletions(-) diff --git a/interface/src/raypick/LaserPointer.cpp b/interface/src/raypick/LaserPointer.cpp index b696afa8e5..afd2d14881 100644 --- a/interface/src/raypick/LaserPointer.cpp +++ b/interface/src/raypick/LaserPointer.cpp @@ -92,8 +92,7 @@ void LaserPointer::updateRenderStateOverlay(const OverlayID& id, const QVariant& } } -void LaserPointer::updateRenderState(const RenderState& renderState, const IntersectionType type, const float distance, const QUuid& objectID, const bool defaultState) { - PickRay pickRay = qApp->getRayPickManager().getPickRay(_rayPickUID); +void LaserPointer::updateRenderState(const RenderState& renderState, const IntersectionType type, const float distance, const QUuid& objectID, const PickRay& pickRay, const bool defaultState) { if (!renderState.getStartID().isNull()) { QVariantMap startProps; startProps.insert("position", vec3toVariant(pickRay.origin)); @@ -186,11 +185,11 @@ void LaserPointer::disableRenderState(const RenderState& renderState) { void LaserPointer::update() { RayPickResult prevRayPickResult = DependencyManager::get()->getPrevRayPickResult(_rayPickUID); if (_renderingEnabled && !_currentRenderState.empty() && _renderStates.find(_currentRenderState) != _renderStates.end() && prevRayPickResult.type != IntersectionType::NONE) { - updateRenderState(_renderStates[_currentRenderState], prevRayPickResult.type, prevRayPickResult.distance, prevRayPickResult.objectID, false); + updateRenderState(_renderStates[_currentRenderState], prevRayPickResult.type, prevRayPickResult.distance, prevRayPickResult.objectID, prevRayPickResult.searchRay, false); disableRenderState(_defaultRenderStates[_currentRenderState].second); } else if (_renderingEnabled && !_currentRenderState.empty() && _defaultRenderStates.find(_currentRenderState) != _defaultRenderStates.end()) { disableRenderState(_renderStates[_currentRenderState]); - updateRenderState(_defaultRenderStates[_currentRenderState].second, IntersectionType::NONE, _defaultRenderStates[_currentRenderState].first, QUuid(), true); + updateRenderState(_defaultRenderStates[_currentRenderState].second, IntersectionType::NONE, _defaultRenderStates[_currentRenderState].first, QUuid(), prevRayPickResult.searchRay, true); } else if (!_currentRenderState.empty()) { disableRenderState(_renderStates[_currentRenderState]); disableRenderState(_defaultRenderStates[_currentRenderState].second); diff --git a/interface/src/raypick/LaserPointer.h b/interface/src/raypick/LaserPointer.h index 23f023cf7a..5c120d8d22 100644 --- a/interface/src/raypick/LaserPointer.h +++ b/interface/src/raypick/LaserPointer.h @@ -89,7 +89,7 @@ private: QUuid _rayPickUID; void updateRenderStateOverlay(const OverlayID& id, const QVariant& props); - void updateRenderState(const RenderState& renderState, const IntersectionType type, const float distance, const QUuid& objectID, const bool defaultState); + void updateRenderState(const RenderState& renderState, const IntersectionType type, const float distance, const QUuid& objectID, const PickRay& pickRay, const bool defaultState); void disableRenderState(const RenderState& renderState); }; diff --git a/interface/src/raypick/LaserPointerManager.cpp b/interface/src/raypick/LaserPointerManager.cpp index 1fd0fe9e88..9c4d38d260 100644 --- a/interface/src/raypick/LaserPointerManager.cpp +++ b/interface/src/raypick/LaserPointerManager.cpp @@ -14,17 +14,19 @@ QUuid LaserPointerManager::createLaserPointer(const QVariant& rayProps, const La const bool faceAvatar, const bool centerEndY, const bool lockEnd, const bool enabled) { std::shared_ptr laserPointer = std::make_shared(rayProps, renderStates, defaultRenderStates, faceAvatar, centerEndY, lockEnd, enabled); if (!laserPointer->getRayUID().isNull()) { - QWriteLocker lock(&_addLock); + QWriteLocker containsLock(&_containsLock); QUuid id = QUuid::createUuid(); - _laserPointersToAdd.push(std::pair>(id, laserPointer)); + _laserPointers[id] = laserPointer; + _laserPointerLocks[id] = std::make_shared(); return id; } return QUuid(); } void LaserPointerManager::removeLaserPointer(const QUuid uid) { - QWriteLocker lock(&_removeLock); - _laserPointersToRemove.push(uid); + QWriteLocker lock(&_containsLock); + _laserPointers.remove(uid); + _laserPointerLocks.remove(uid); } void LaserPointerManager::enableLaserPointer(const QUuid uid) { @@ -69,32 +71,12 @@ const RayPickResult LaserPointerManager::getPrevRayPickResult(const QUuid uid) { } void LaserPointerManager::update() { + QReadLocker lock(&_containsLock); for (QUuid& uid : _laserPointers.keys()) { // This only needs to be a read lock because update won't change any of the properties that can be modified from scripts QReadLocker laserLock(_laserPointerLocks[uid].get()); _laserPointers[uid]->update(); } - - QWriteLocker containsLock(&_containsLock); - { - QWriteLocker lock(&_addLock); - while (!_laserPointersToAdd.empty()) { - std::pair> laserPointerToAdd = _laserPointersToAdd.front(); - _laserPointersToAdd.pop(); - _laserPointers[laserPointerToAdd.first] = laserPointerToAdd.second; - _laserPointerLocks[laserPointerToAdd.first] = std::make_shared(); - } - } - - { - QWriteLocker lock(&_removeLock); - while (!_laserPointersToRemove.empty()) { - QUuid uid = _laserPointersToRemove.front(); - _laserPointersToRemove.pop(); - _laserPointers.remove(uid); - _laserPointerLocks.remove(uid); - } - } } void LaserPointerManager::setPrecisionPicking(QUuid uid, const bool precisionPicking) { diff --git a/interface/src/raypick/LaserPointerManager.h b/interface/src/raypick/LaserPointerManager.h index b573410fe7..381119507f 100644 --- a/interface/src/raypick/LaserPointerManager.h +++ b/interface/src/raypick/LaserPointerManager.h @@ -46,10 +46,6 @@ public: private: QHash> _laserPointers; QHash> _laserPointerLocks; - QReadWriteLock _addLock; - std::queue>> _laserPointersToAdd; - QReadWriteLock _removeLock; - std::queue _laserPointersToRemove; QReadWriteLock _containsLock; }; diff --git a/interface/src/raypick/RayPick.h b/interface/src/raypick/RayPick.h index 04045b2116..0686a05718 100644 --- a/interface/src/raypick/RayPick.h +++ b/interface/src/raypick/RayPick.h @@ -70,6 +70,9 @@ public: if (doesPickNonCollidable()) { toReturn |= getBitMask(PICK_INCLUDE_NONCOLLIDABLE); } + if (doesPickCourse()) { + toReturn |= getBitMask(PICK_COURSE); + } return Flags(toReturn); } Flags getOverlayFlags() const { @@ -80,6 +83,9 @@ public: if (doesPickNonCollidable()) { toReturn |= getBitMask(PICK_INCLUDE_NONCOLLIDABLE); } + if (doesPickCourse()) { + toReturn |= getBitMask(PICK_COURSE); + } return Flags(toReturn); } Flags getAvatarFlags() const { return Flags(getBitMask(PICK_AVATARS)); } diff --git a/interface/src/raypick/RayPickManager.cpp b/interface/src/raypick/RayPickManager.cpp index 1ec8efc331..bfc6e3fcb2 100644 --- a/interface/src/raypick/RayPickManager.cpp +++ b/interface/src/raypick/RayPickManager.cpp @@ -38,11 +38,12 @@ void RayPickManager::cacheResult(const bool intersects, const RayPickResult& res res = resTemp; } } else { - cache[ray][mask] = RayPickResult(); + cache[ray][mask] = RayPickResult(res.searchRay); } } void RayPickManager::update() { + QReadLocker lock(&_containsLock); RayPickCache results; for (auto& uid : _rayPicks.keys()) { std::shared_ptr rayPick = _rayPicks[uid]; @@ -58,7 +59,7 @@ void RayPickManager::update() { } QPair rayKey = QPair(ray.origin, ray.direction); - RayPickResult res; + RayPickResult res = RayPickResult(ray); if (rayPick->getFilter().doesPickEntities()) { RayToEntityIntersectionResult entityRes; @@ -73,7 +74,7 @@ void RayPickManager::update() { } if (!fromCache) { - cacheResult(entityRes.intersects, RayPickResult(IntersectionType::ENTITY, entityRes.entityID, entityRes.distance, entityRes.intersection, entityRes.surfaceNormal), + cacheResult(entityRes.intersects, RayPickResult(IntersectionType::ENTITY, entityRes.entityID, entityRes.distance, entityRes.intersection, ray, entityRes.surfaceNormal), entityMask, res, rayKey, results); } } @@ -91,7 +92,7 @@ void RayPickManager::update() { } if (!fromCache) { - cacheResult(overlayRes.intersects, RayPickResult(IntersectionType::OVERLAY, overlayRes.overlayID, overlayRes.distance, overlayRes.intersection, overlayRes.surfaceNormal), + cacheResult(overlayRes.intersects, RayPickResult(IntersectionType::OVERLAY, overlayRes.overlayID, overlayRes.distance, overlayRes.intersection, ray, overlayRes.surfaceNormal), overlayMask, res, rayKey, results); } } @@ -100,7 +101,7 @@ void RayPickManager::update() { RayPickFilter::Flags avatarMask = rayPick->getFilter().getAvatarFlags(); if (!checkAndCompareCachedResults(rayKey, results, res, avatarMask)) { RayToAvatarIntersectionResult avatarRes = DependencyManager::get()->findRayIntersectionVector(ray, rayPick->getIncludeAvatars(), rayPick->getIgnoreAvatars()); - cacheResult(avatarRes.intersects, RayPickResult(IntersectionType::AVATAR, avatarRes.avatarID, avatarRes.distance, avatarRes.intersection), avatarMask, res, rayKey, results); + cacheResult(avatarRes.intersects, RayPickResult(IntersectionType::AVATAR, avatarRes.avatarID, avatarRes.distance, avatarRes.intersection, ray), avatarMask, res, rayKey, results); } } @@ -109,7 +110,7 @@ void RayPickManager::update() { RayPickFilter::Flags hudMask = rayPick->getFilter().getHUDFlags(); if (!checkAndCompareCachedResults(rayKey, results, res, hudMask)) { glm::vec3 hudRes = DependencyManager::get()->calculateRayUICollisionPoint(ray.origin, ray.direction); - cacheResult(true, RayPickResult(IntersectionType::HUD, 0, glm::distance(ray.origin, hudRes), hudRes), hudMask, res, rayKey, results); + cacheResult(true, RayPickResult(IntersectionType::HUD, 0, glm::distance(ray.origin, hudRes), hudRes, ray), hudMask, res, rayKey, results); } } @@ -117,56 +118,39 @@ void RayPickManager::update() { if (rayPick->getMaxDistance() == 0.0f || (rayPick->getMaxDistance() > 0.0f && res.distance < rayPick->getMaxDistance())) { rayPick->setRayPickResult(res); } else { - rayPick->setRayPickResult(RayPickResult()); - } - } - - QWriteLocker containsLock(&_containsLock); - { - QWriteLocker lock(&_addLock); - while (!_rayPicksToAdd.empty()) { - std::pair> rayPickToAdd = _rayPicksToAdd.front(); - _rayPicksToAdd.pop(); - _rayPicks[rayPickToAdd.first] = rayPickToAdd.second; - _rayPickLocks[rayPickToAdd.first] = std::make_shared(); - } - } - - { - QWriteLocker lock(&_removeLock); - while (!_rayPicksToRemove.empty()) { - QUuid uid = _rayPicksToRemove.front(); - _rayPicksToRemove.pop(); - _rayPicks.remove(uid); - _rayPickLocks.remove(uid); + rayPick->setRayPickResult(RayPickResult(ray)); } } } QUuid RayPickManager::createRayPick(const std::string& jointName, const glm::vec3& posOffset, const glm::vec3& dirOffset, const RayPickFilter& filter, const float maxDistance, const bool enabled) { - QWriteLocker lock(&_addLock); + QWriteLocker lock(&_containsLock); QUuid id = QUuid::createUuid(); - _rayPicksToAdd.push(std::pair>(id, std::make_shared(jointName, posOffset, dirOffset, filter, maxDistance, enabled))); + _rayPicks[id] = std::make_shared(jointName, posOffset, dirOffset, filter, maxDistance, enabled); + _rayPickLocks[id] = std::make_shared(); return id; } QUuid RayPickManager::createRayPick(const RayPickFilter& filter, const float maxDistance, const bool enabled) { - QWriteLocker lock(&_addLock); + QWriteLocker lock(&_containsLock); QUuid id = QUuid::createUuid(); - _rayPicksToAdd.push(std::pair>(id, std::make_shared(filter, maxDistance, enabled))); + _rayPicks[id] = std::make_shared(filter, maxDistance, enabled); + _rayPickLocks[id] = std::make_shared(); return id; } QUuid RayPickManager::createRayPick(const glm::vec3& position, const glm::vec3& direction, const RayPickFilter& filter, const float maxDistance, const bool enabled) { - QWriteLocker lock(&_addLock); + QWriteLocker lock(&_containsLock); QUuid id = QUuid::createUuid(); - _rayPicksToAdd.push(std::pair>(id, std::make_shared(position, direction, filter, maxDistance, enabled))); + _rayPicks[id] = std::make_shared(position, direction, filter, maxDistance, enabled); + _rayPickLocks[id] = std::make_shared(); return id; } void RayPickManager::removeRayPick(const QUuid uid) { - QWriteLocker lock(&_removeLock); - _rayPicksToRemove.push(uid); + QWriteLocker lock(&_containsLock); + _rayPicks.remove(uid); + _rayPickLocks.remove(uid); } void RayPickManager::enableRayPick(const QUuid uid) { @@ -185,18 +169,6 @@ void RayPickManager::disableRayPick(const QUuid uid) { } } -const PickRay RayPickManager::getPickRay(const QUuid uid) { - QReadLocker containsLock(&_containsLock); - if (_rayPicks.contains(uid)) { - bool valid; - PickRay pickRay = _rayPicks[uid]->getPickRay(valid); - if (valid) { - return pickRay; - } - } - return PickRay(); -} - const RayPickResult RayPickManager::getPrevRayPickResult(const QUuid uid) { QReadLocker containsLock(&_containsLock); if (_rayPicks.contains(uid)) { diff --git a/interface/src/raypick/RayPickManager.h b/interface/src/raypick/RayPickManager.h index f2b1ff4ae4..9717767f19 100644 --- a/interface/src/raypick/RayPickManager.h +++ b/interface/src/raypick/RayPickManager.h @@ -28,7 +28,6 @@ class RayPickManager { public: void update(); - const PickRay getPickRay(const QUuid uid); QUuid createRayPick(const std::string& jointName, const glm::vec3& posOffset, const glm::vec3& dirOffset, const RayPickFilter& filter, const float maxDistance, const bool enabled); QUuid createRayPick(const RayPickFilter& filter, const float maxDistance, const bool enabled); @@ -49,10 +48,6 @@ public: private: QHash> _rayPicks; QHash> _rayPickLocks; - QReadWriteLock _addLock; - std::queue>> _rayPicksToAdd; - QReadWriteLock _removeLock; - std::queue _rayPicksToRemove; QReadWriteLock _containsLock; typedef QHash, std::unordered_map> RayPickCache; diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 8257c883a2..7d0df3ac78 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -762,6 +762,8 @@ QScriptValue rayPickResultToScriptValue(QScriptEngine* engine, const RayPickResu QScriptValue intersection = vec3toScriptValue(engine, rayPickResult.intersection); obj.setProperty("intersection", intersection); obj.setProperty("intersects", rayPickResult.type != NONE); + QScriptValue searchRay = pickRayToScriptValue(engine, rayPickResult.searchRay); + obj.setProperty("searchRay", searchRay); QScriptValue surfaceNormal = vec3toScriptValue(engine, rayPickResult.surfaceNormal); obj.setProperty("surfaceNormal", surfaceNormal); return obj; diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index ed928a6e7b..7b7d8d8f47 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -137,7 +137,7 @@ QScriptValue pickRayToScriptValue(QScriptEngine* engine, const PickRay& pickRay) void pickRayFromScriptValue(const QScriptValue& object, PickRay& pickRay); enum IntersectionType { - NONE, + NONE = 0, ENTITY, OVERLAY, AVATAR, @@ -147,12 +147,14 @@ enum IntersectionType { class RayPickResult { public: RayPickResult() {} - RayPickResult(const IntersectionType type, const QUuid& objectID, const float distance, const glm::vec3& intersection, const glm::vec3& surfaceNormal = glm::vec3(NAN)) : - type(type), objectID(objectID), distance(distance), intersection(intersection), surfaceNormal(surfaceNormal) {} + RayPickResult(const PickRay& searchRay) : searchRay(searchRay) {} + RayPickResult(const IntersectionType type, const QUuid& objectID, const float distance, const glm::vec3& intersection, const PickRay& searchRay, const glm::vec3& surfaceNormal = glm::vec3(NAN)) : + type(type), objectID(objectID), distance(distance), intersection(intersection), searchRay(searchRay), surfaceNormal(surfaceNormal) {} IntersectionType type { NONE }; - QUuid objectID { 0 }; + QUuid objectID; float distance { FLT_MAX }; glm::vec3 intersection { NAN }; + PickRay searchRay; glm::vec3 surfaceNormal { NAN }; }; Q_DECLARE_METATYPE(RayPickResult) From e965b7fff1e1ae57ac384403e334a6a67f630ba4 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Fri, 22 Sep 2017 15:32:40 -0700 Subject: [PATCH 402/722] hand controller pointer --- .../controllerModules/farActionGrabEntity.js | 9 +- .../controllerModules/hudOverlayPointer.js | 44 ++++--- .../controllers/controllerModules/mouseHMD.js | 123 ++++++++++++++++++ .../system/controllers/controllerScripts.js | 5 +- 4 files changed, 155 insertions(+), 26 deletions(-) create mode 100644 scripts/system/controllers/controllerModules/mouseHMD.js diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index 72da3c3f70..d82048385f 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -420,16 +420,9 @@ Script.include("/~/system/libraries/controllers.js"); } }; - this.isPointingAtUI = function(controllerData) { - var hudRayPickInfo = controllerData.hudRayPicks[this.hand]; - var result = isPointingAtUI(hudRayPickInfo); - print(result); - return result; - }; - this.run = function (controllerData) { if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || - this.notPointingAtEntity(controllerData) || this.isPointingAtUI(controllerData)) { + this.notPointingAtEntity(controllerData)) { this.endNearGrabAction(); this.laserPointerOff(); return makeRunningValues(false, [], []); diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js index 0015a4a2d6..fdc5758416 100644 --- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js +++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js @@ -103,6 +103,7 @@ this.reticleMaxY; this.clicked = false; this.triggerClicked = 0; + this.movedAway = false; this.parameters = ControllerDispatcherUtils.makeDispatcherModuleParameters( 540, this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], @@ -113,8 +114,8 @@ return (this.hand === RIGHT_HAND) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; }; - this.clicked = function() { - return this.clicked; + _this.isClicked = function() { + return _this.triggerClicked; }; this.getOtherModule = function() { @@ -131,8 +132,9 @@ this.reticleMaxY = dims.y - MARGIN; }; - this.hasNotSentClick = function() { + _this.hasNotSentClick = function() { if (!_this.clicked) { + print("sending clicked"); _this.clicked = true; return true; } @@ -178,34 +180,44 @@ Reticle.setPosition(point2d); }; + this.pointingAtTablet = function(controllerData) { + var rayPick = controllerData.rayPicks[this.hand]; + return (rayPick.objectID === HMD.tabletScreenID || rayPick.objectID === HMD.homeButtonID); + }; + + this.moveMouseAwayFromTablet = function() { + if (!this.movedAway) { + var point = {x: 25, y: 25}; + // this.setReticlePosition(point); + this.movedAway = true; + } + } + this.processLaser = function(controllerData) { var controllerLocation = controllerData.controllerLocations[this.hand]; - if (controllerData.triggerValues[this.hand] < ControllerDispatcherUtils.TRIGGER_OFF_VALUE || !controllerLocation.valid) { + if ((controllerData.triggerValues[this.hand] < ControllerDispatcherUtils.TRIGGER_ON_VALUE || !controllerLocation.valid) || + this.pointingAtTablet(controllerData)) { this.exitModule(); return false; } - var hudRayPick = controllerData.hudRayPicks[this.hand]; var controllerLocation = controllerData.controllerLocations[this.hand]; var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); this.setReticlePosition(point2d); - print(Reticle.isPointOnSystemOverlay(point2d)); - if (!Reticle.isPointOnSystemOverlay(point2d)) { + if (!Reticle.isPointingAtSystemOverlay(point2d)) { this.exitModule(); - print("----> exiting <------"); return false; } - - //this.setReticlePosition(point2d); - - this.clicked = controllerData.triggerClicked[this.hand]; - + Reticle.visible = false; + this.movedAway = false; + this.triggerClicked = controllerData.triggerClicks[this.hand]; this.processControllerTriggers(controllerData); this.updateLaserPointer(controllerData); return true; }; this.exitModule = function() { + this.moveMouseAwayFromTablet(); LaserPointers.disableLaserPointer(this.laserPointer); }; @@ -240,12 +252,12 @@ } - var leftHudOverlayPointer = new HudOverlayPointer(LEFT_HAND); + var leftHudOverlayPointer = new HudOverlayPointer(LEFT_HAND); var rightHudOverlayPointer = new HudOverlayPointer(RIGHT_HAND); var clickMapping = Controller.newMapping('HudOverlayPointer-click'); - clickMapping.from(rightHudOverlayPointer.clicked()).when(rightHudOverlayPointer.hasNotSentClick()).to(Controller.Actions.ReticleClick); - clickMapping.from(leftHudOverlayPointer.clicked()).when(leftHudOverlayPointer.hasNotSentClick()).to(Controller.Actions.ReticleClick); + clickMapping.from(rightHudOverlayPointer.isClicked).to(Controller.Actions.ReticleClick); + clickMapping.from(leftHudOverlayPointer.isClicked).to(Controller.Actions.ReticleClick); clickMapping.enable(); enableDispatcherModule("LeftHudOverlayPointer", leftHudOverlayPointer); diff --git a/scripts/system/controllers/controllerModules/mouseHMD.js b/scripts/system/controllers/controllerModules/mouseHMD.js new file mode 100644 index 0000000000..746fed1246 --- /dev/null +++ b/scripts/system/controllers/controllerModules/mouseHMD.js @@ -0,0 +1,123 @@ +// +// mouseHMD.js +// +// scripts/system/controllers/controllerModules/ +// +// Created by Dante Ruiz 2017-9-22 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + var ControllerDispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js"); + + var WEIGHTING = 1 / 20; // simple moving average over last 20 samples + var ONE_MINUS_WEIGHTING = 1 - WEIGHTING; + var AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO = 20; + function TimeLock(experation) { + this.experation = experation; + this.last = 0; + this.update = function(time) { + this.last = time || Date.now(); + }; + + this.expired = function(time) { + return ((time || Date.now()) - this.last) > this.experation; + }; + } + + function MouseHMD() { + var _this = this; + this.mouseMoved = false; + this.mouseActivity = new TimeLock(5000); + this.handControllerActivity = new TimeLock(4000); + this.parameters = ControllerDispatcherUtils.makeDispatcherModuleParameters( + 10, + ["mouse"], + [], + 100); + + this.onMouseMove = function() { + _this.updateMouseActivity(); + }; + + this.onMouseClick = function() { + _this.updateMouseActivity(); + }; + + this.updateMouseActivity = function(isClick) { + if (_this.ignoreMouseActivity()) { + return; + } + + if (HMD.active) { + var now = Date.now(); + _this.mouseActivity.update(now); + } + }; + + this.ignoreMouseActivity = function() { + if (!Reticle.allowMouseCapture) { + return true; + } + + var pos = Reticle.position; + if (!pos || (pos.x == -1 && pos.y == -1)) { + return true; + } + + if (!_this.handControllerActivity.expired()) { + print("has not expired"); + return true; + } + + return false; + }; + + this.triggersPressed = function(controllerData, now) { + var onValue = ControllerDispatcherUtils.TRIGGER_ON_VALUE; + var rightHand = ControllerDispatcherUtils.RIGHT_HAND; + var leftHand = ControllerDispatcherUtils.LEFT_HAND; + var leftTriggerValue = controllerData.triggerValues[leftHand]; + var rightTriggerValue = controllerData.triggerValues[rightHand]; + + if (leftTriggerValue > onValue || rightTriggerValue > onValue) { + this.handControllerActivity.update(now); + return true; + } + + return false; + }; + + this.isReady = function(controllerData, deltaTime) { + var now = Date.now(); + this.triggersPressed(controllerData, now); + if ((HMD.active && !this.mouseActivity.expired(now)) && _this.handControllerActivity.expired()) { + Reticle.visible = true; + return ControllerDispatcherUtils.makeRunningValues(true, [], []); + } + if (HMD.active) { + Reticle.visble = false; + } + + return ControllerDispatcherUtils.makeRunningValues(false, [], []); + }; + + this.run = function(controllerData, deltaTime) { + var now = Date.now(); + if (this.mouseActivity.expired(now) || this.triggersPressed(controllerData, now)) { + Reticle.visible = false; + return ControllerDispatcherUtils.makeRunningValues(false, [], []); + } + return ControllerDispatcherUtils.makeRunningValues(true, [], []); + }; + } + + var mouseHMD = new MouseHMD(); + enableDispatcherModule("MouseHMD", mouseHMD); + + Controller.mouseMoveEvent.connect(mouseHMD.onMouseMove); + Controller.mousePressEvent.connect(mouseHMD.onMouseClick); +})(); diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index 695dea7b2c..a671651a9f 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -19,7 +19,7 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/nearParentGrabEntity.js", "controllerModules/nearParentGrabOverlay.js", "controllerModules/nearActionGrabEntity.js", - //"controllerModules/farActionGrabEntity.js", + "controllerModules/farActionGrabEntity.js", "controllerModules/tabletStylusInput.js", "controllerModules/equipEntity.js", "controllerModules/nearTrigger.js", @@ -30,7 +30,8 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/farTrigger.js", "controllerModules/teleport.js", "controllerModules/scaleAvatar.js", - "controllerModules/hudOverlayPointer.js" + "controllerModules/hudOverlayPointer.js", + "controllerModules/mouseHMD.js" ]; var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; From ee9ac3e7f9c4b9d92090b29a6ea9b8af32b7562e Mon Sep 17 00:00:00 2001 From: druiz17 Date: Fri, 22 Sep 2017 15:38:39 -0700 Subject: [PATCH 403/722] undo unnessary changes --- .../src/display-plugins/CompositorHelper.cpp | 16 ---------------- .../src/display-plugins/CompositorHelper.h | 2 -- .../libraries/controllerDispatcherUtils.js | 15 +-------------- 3 files changed, 1 insertion(+), 32 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index 294a3f6e55..2f57cc29d0 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -279,18 +279,6 @@ bool CompositorHelper::getReticleOverDesktop() const { return _isOverDesktop; } -bool CompositorHelper::isPositionOverDesktop(glm::vec2 position) const { - if (isHMD()) { - glm::vec2 maxOverlayPosition = _currentDisplayPlugin->getRecommendedUiSize(); - static const glm::vec2 minOverlayPosition; - if (glm::any(glm::lessThan(position, minOverlayPosition)) || - glm::any(glm::greaterThan(position, maxOverlayPosition))) { - return true; - } - } - return _isOverDesktop; -} - glm::vec2 CompositorHelper::getReticleMaximumPosition() const { glm::vec2 result; if (isHMD()) { @@ -480,7 +468,3 @@ void ReticleInterface::setScale(float scale) { auto& cursorManager = Cursor::Manager::instance(); cursorManager.setScale(scale); } - -bool ReticleInterface::isPointOnSystemOverlay(QVariant position) { - return !_compositor->isPositionOverDesktop(vec2FromVariant(position)); -} diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index 8534de6b9d..b1d2815f65 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -106,7 +106,6 @@ public: /// if the reticle is pointing to a system overlay (a dialog box for example) then the function returns true otherwise false bool getReticleOverDesktop() const; - bool isPositionOverDesktop(glm::vec2 position) const; void setReticleOverDesktop(bool value) { _isOverDesktop = value; } void setDisplayPlugin(const DisplayPluginPointer& displayPlugin) { _currentDisplayPlugin = displayPlugin; } @@ -196,7 +195,6 @@ public: Q_INVOKABLE void setAllowMouseCapture(bool value) { return _compositor->setAllowMouseCapture(value); } Q_INVOKABLE bool isPointingAtSystemOverlay() { return !_compositor->getReticleOverDesktop(); } - Q_INVOKABLE bool isPointOnSystemOverlay(QVariant position); Q_INVOKABLE bool getVisible() { return _compositor->getReticleVisible(); } Q_INVOKABLE void setVisible(bool visible) { _compositor->setReticleVisible(visible); } diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 652bd5765b..a05b108b31 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -40,8 +40,7 @@ entityHasActions:true, ensureDynamic:true, findGroupParent:true, - BUMPER_ON_VALUE:true, - isPointingAtUI: true + BUMPER_ON_VALUE:true */ MSECS_PER_SEC = 1000.0; @@ -311,18 +310,6 @@ findGroupParent = function (controllerData, targetProps) { return targetProps; }; -isPointingAtUI = function(intersection) { - var MARGIN = 25; - var reticleMinX = MARGIN, reticleMaxX, reticleMinY = MARGIN, reticleMaxY; - var dims = Controller.getViewportDimensions(); - reticleMaxX = dims.x - MARGIN; - reticleMaxY = dims.y - MARGIN; - var point2d = HMD.overlayFromWorldPoint(intersection.intersection); - point2d.x = Math.max(reticleMinX, Math.min(point2d.x, reticleMaxX)); - point2d.y = Math.max(reticleMinY, Math.min(point2d.y, reticleMaxY)); - return point2d; -} - if (typeof module !== 'undefined') { module.exports = { makeDispatcherModuleParameters: makeDispatcherModuleParameters, From 0e3f1c514968ae166aa3ed178b07229f58e9eab2 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 22 Sep 2017 16:11:24 -0700 Subject: [PATCH 404/722] add setLaserLength, setLockEndUUID doesn't use defaultRenderStates --- interface/src/raypick/LaserPointer.cpp | 6 ++++-- interface/src/raypick/LaserPointer.h | 2 ++ interface/src/raypick/LaserPointerManager.cpp | 8 ++++++++ interface/src/raypick/LaserPointerManager.h | 1 + interface/src/raypick/LaserPointerScriptingInterface.h | 1 + 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/interface/src/raypick/LaserPointer.cpp b/interface/src/raypick/LaserPointer.cpp index afd2d14881..55ddd01123 100644 --- a/interface/src/raypick/LaserPointer.cpp +++ b/interface/src/raypick/LaserPointer.cpp @@ -184,8 +184,10 @@ void LaserPointer::disableRenderState(const RenderState& renderState) { void LaserPointer::update() { RayPickResult prevRayPickResult = DependencyManager::get()->getPrevRayPickResult(_rayPickUID); - if (_renderingEnabled && !_currentRenderState.empty() && _renderStates.find(_currentRenderState) != _renderStates.end() && prevRayPickResult.type != IntersectionType::NONE) { - updateRenderState(_renderStates[_currentRenderState], prevRayPickResult.type, prevRayPickResult.distance, prevRayPickResult.objectID, prevRayPickResult.searchRay, false); + if (_renderingEnabled && !_currentRenderState.empty() && _renderStates.find(_currentRenderState) != _renderStates.end() && + (prevRayPickResult.type != IntersectionType::NONE || _laserLength > 0.0f || !_objectLockEnd.first.isNull())) { + float distance = _laserLength > 0.0f ? _laserLength : prevRayPickResult.distance; + updateRenderState(_renderStates[_currentRenderState], prevRayPickResult.type, distance, prevRayPickResult.objectID, prevRayPickResult.searchRay, false); disableRenderState(_defaultRenderStates[_currentRenderState].second); } else if (_renderingEnabled && !_currentRenderState.empty() && _defaultRenderStates.find(_currentRenderState) != _defaultRenderStates.end()) { disableRenderState(_renderStates[_currentRenderState]); diff --git a/interface/src/raypick/LaserPointer.h b/interface/src/raypick/LaserPointer.h index 5c120d8d22..5467a8233e 100644 --- a/interface/src/raypick/LaserPointer.h +++ b/interface/src/raypick/LaserPointer.h @@ -65,6 +65,7 @@ public: void editRenderState(const std::string& state, const QVariant& startProps, const QVariant& pathProps, const QVariant& endProps); void setPrecisionPicking(const bool precisionPicking) { DependencyManager::get()->setPrecisionPicking(_rayPickUID, precisionPicking); } + void setLaserLength(const float laserLength) { _laserLength = laserLength; } void setIgnoreEntities(const QScriptValue& ignoreEntities) { DependencyManager::get()->setIgnoreEntities(_rayPickUID, ignoreEntities); } void setIncludeEntities(const QScriptValue& includeEntities) { DependencyManager::get()->setIncludeEntities(_rayPickUID, includeEntities); } void setIgnoreOverlays(const QScriptValue& ignoreOverlays) { DependencyManager::get()->setIgnoreOverlays(_rayPickUID, ignoreOverlays); } @@ -78,6 +79,7 @@ public: private: bool _renderingEnabled; + float _laserLength { 0.0f }; std::string _currentRenderState { "" }; RenderStateMap _renderStates; DefaultRenderStateMap _defaultRenderStates; diff --git a/interface/src/raypick/LaserPointerManager.cpp b/interface/src/raypick/LaserPointerManager.cpp index 9c4d38d260..b19ecc14f0 100644 --- a/interface/src/raypick/LaserPointerManager.cpp +++ b/interface/src/raypick/LaserPointerManager.cpp @@ -87,6 +87,14 @@ void LaserPointerManager::setPrecisionPicking(QUuid uid, const bool precisionPic } } +void LaserPointerManager::setLaserLength(QUuid uid, const float laserLength) { + QReadLocker lock(&_containsLock); + if (_laserPointers.contains(uid)) { + QWriteLocker laserLock(_laserPointerLocks[uid].get()); + _laserPointers[uid]->setLaserLength(laserLength); + } +} + void LaserPointerManager::setIgnoreEntities(QUuid uid, const QScriptValue& ignoreEntities) { QReadLocker lock(&_containsLock); if (_laserPointers.contains(uid)) { diff --git a/interface/src/raypick/LaserPointerManager.h b/interface/src/raypick/LaserPointerManager.h index 381119507f..6494bb7056 100644 --- a/interface/src/raypick/LaserPointerManager.h +++ b/interface/src/raypick/LaserPointerManager.h @@ -32,6 +32,7 @@ public: const RayPickResult getPrevRayPickResult(const QUuid uid); void setPrecisionPicking(QUuid uid, const bool precisionPicking); + void setLaserLength(QUuid uid, const float laserLength); void setIgnoreEntities(QUuid uid, const QScriptValue& ignoreEntities); void setIncludeEntities(QUuid uid, const QScriptValue& includeEntities); void setIgnoreOverlays(QUuid uid, const QScriptValue& ignoreOverlays); diff --git a/interface/src/raypick/LaserPointerScriptingInterface.h b/interface/src/raypick/LaserPointerScriptingInterface.h index d65eb335b3..2f6da87b5f 100644 --- a/interface/src/raypick/LaserPointerScriptingInterface.h +++ b/interface/src/raypick/LaserPointerScriptingInterface.h @@ -31,6 +31,7 @@ public slots: Q_INVOKABLE RayPickResult getPrevRayPickResult(QUuid uid) { return qApp->getLaserPointerManager().getPrevRayPickResult(uid); } Q_INVOKABLE void setPrecisionPicking(QUuid uid, const bool precisionPicking) { qApp->getLaserPointerManager().setPrecisionPicking(uid, precisionPicking); } + Q_INVOKABLE void setLaserLength(QUuid uid, const float laserLength) { qApp->getLaserPointerManager().setLaserLength(uid, laserLength); } Q_INVOKABLE void setIgnoreEntities(QUuid uid, const QScriptValue& ignoreEntities) { qApp->getLaserPointerManager().setIgnoreEntities(uid, ignoreEntities); } Q_INVOKABLE void setIncludeEntities(QUuid uid, const QScriptValue& includeEntities) { qApp->getLaserPointerManager().setIncludeEntities(uid, includeEntities); } Q_INVOKABLE void setIgnoreOverlays(QUuid uid, const QScriptValue& ignoreOverlays) { qApp->getLaserPointerManager().setIgnoreOverlays(uid, ignoreOverlays); } From 2c7b8cdb4f9b3b12d1c893c1f4cc840a4bc67cfc Mon Sep 17 00:00:00 2001 From: druiz17 Date: Fri, 22 Sep 2017 16:26:41 -0700 Subject: [PATCH 405/722] fixing script issues --- .../controllerModules/hudOverlayPointer.js | 18 +- .../controllers/controllerModules/mouseHMD.js | 9 +- .../system/controllers/controllerScripts.js | 1 - .../controllers/handControllerPointer.js | 706 ------------------ 4 files changed, 14 insertions(+), 720 deletions(-) delete mode 100644 scripts/system/controllers/handControllerPointer.js diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js index fdc5758416..6eaf7f1cf7 100644 --- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js +++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js @@ -132,15 +132,6 @@ this.reticleMaxY = dims.y - MARGIN; }; - _this.hasNotSentClick = function() { - if (!_this.clicked) { - print("sending clicked"); - _this.clicked = true; - return true; - } - return false; - }; - this.updateLaserPointer = function(controllerData) { var RADIUS = 0.005; var dim = { x: RADIUS, y: RADIUS, z: RADIUS }; @@ -260,8 +251,13 @@ clickMapping.from(leftHudOverlayPointer.isClicked).to(Controller.Actions.ReticleClick); clickMapping.enable(); - enableDispatcherModule("LeftHudOverlayPointer", leftHudOverlayPointer); - enableDispatcherModule("RightHudOverlayPointer", rightHudOverlayPointer); + ControllerDispatcherUtils.enableDispatcherModule("LeftHudOverlayPointer", leftHudOverlayPointer); + ControllerDispatcherUtils.enableDispatcherModule("RightHudOverlayPointer", rightHudOverlayPointer); + function cleanup() { + ControllerDispatcherUtils.disableDispatcherModule("LeftHudOverlayPointer"); + ControllerDispatcherUtils.disbaleDispatcherModule("RightHudOverlayPointer"); + } + Script.scriptEnding.connect(cleanup); })(); diff --git a/scripts/system/controllers/controllerModules/mouseHMD.js b/scripts/system/controllers/controllerModules/mouseHMD.js index 746fed1246..10fe714348 100644 --- a/scripts/system/controllers/controllerModules/mouseHMD.js +++ b/scripts/system/controllers/controllerModules/mouseHMD.js @@ -69,7 +69,6 @@ } if (!_this.handControllerActivity.expired()) { - print("has not expired"); return true; } @@ -116,8 +115,14 @@ } var mouseHMD = new MouseHMD(); - enableDispatcherModule("MouseHMD", mouseHMD); + ControllerDispatcherUtils.enableDispatcherModule("MouseHMD", mouseHMD); Controller.mouseMoveEvent.connect(mouseHMD.onMouseMove); Controller.mousePressEvent.connect(mouseHMD.onMouseClick); + + function cleanup() { + ControllerDispatcherUtils.disableDispatcherModule("MouseHMD"); + } + + Script.scriptEnding.connect(cleanup); })(); diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index a671651a9f..bba305fe40 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -12,7 +12,6 @@ var CONTOLLER_SCRIPTS = [ "squeezeHands.js", "controllerDisplayManager.js", - //"handControllerPointer.js", "grab.js", "toggleAdvancedMovementForHandControllers.js", "controllerDispatcher.js", diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js deleted file mode 100644 index 1c988bfd34..0000000000 --- a/scripts/system/controllers/handControllerPointer.js +++ /dev/null @@ -1,706 +0,0 @@ -"use strict"; - -// -// handControllerPointer.js -// examples/controllers -// -// Created by Howard Stearns on 2016/04/22 -// Copyright 2016 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 -// - -(function() { // BEGIN LOCAL_SCOPE - -// Control the "mouse" using hand controller. (HMD and desktop.) -// First-person only. -// Starts right handed, but switches to whichever is free: Whichever hand was NOT most recently squeezed. -// (For now, the thumb buttons on both controllers are always on.) -// When partially squeezing over a HUD element, a laser or the reticle is shown where the active hand -// controller beam intersects the HUD. - -var activeTrigger; -function isLaserOn() { - return activeTrigger.partial(); -} -Script.include("../libraries/controllers.js"); - -// UTILITIES ------------- -// -function ignore() { } - -// Utility to make it easier to setup and disconnect cleanly. -function setupHandler(event, handler) { - event.connect(handler); - Script.scriptEnding.connect(function () { - event.disconnect(handler); - }); -} - -// If some capability is not available until expiration milliseconds after the last update. -function TimeLock(expiration) { - var last = 0; - this.update = function (optionalNow) { - last = optionalNow || Date.now(); - }; - this.expired = function (optionalNow) { - return ((optionalNow || Date.now()) - last) > expiration; - }; -} - -var handControllerLockOut = new TimeLock(2000); - -function Trigger(label) { - // This part is copied and adapted from handControllerGrab.js. Maybe we should refactor this. - var that = this; - that.label = label; - that.TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing - that.TRIGGER_OFF_VALUE = 0.10; - that.TRIGGER_ON_VALUE = that.TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab - that.rawTriggerValue = 0; - that.triggerValue = 0; // rolling average of trigger value - that.triggerClicked = false; - that.triggerClick = function (value) { that.triggerClicked = value; }; - that.triggerPress = function (value) { that.rawTriggerValue = value; }; - that.updateSmoothedTrigger = function () { // e.g., call once/update for effect - var triggerValue = that.rawTriggerValue; - // smooth out trigger value - that.triggerValue = (that.triggerValue * that.TRIGGER_SMOOTH_RATIO) + - (triggerValue * (1.0 - that.TRIGGER_SMOOTH_RATIO)); - OffscreenFlags.navigationFocusDisabled = that.triggerValue != 0.0; - }; - // Current smoothed state, without hysteresis. Answering booleans. - that.triggerSmoothedClick = function () { - return that.triggerClicked; - }; - that.triggerSmoothedSqueezed = function () { - return that.triggerValue > that.TRIGGER_ON_VALUE; - }; - that.triggerSmoothedReleased = function () { - return that.triggerValue < that.TRIGGER_OFF_VALUE; - }; - - // This part is not from handControllerGrab.js - that.state = null; // tri-state: falsey, 'partial', 'full' - that.update = function () { // update state, called from an update function - var state = that.state; - that.updateSmoothedTrigger(); - - // The first two are independent of previous state: - if (that.triggerSmoothedClick()) { - state = 'full'; - } else if (that.triggerSmoothedReleased()) { - state = null; - } else if (that.triggerSmoothedSqueezed()) { - // Another way to do this would be to have hysteresis in this branch, but that seems to make things harder to use. - // In particular, the vive has a nice detent as you release off of full, and we want that to be a transition from - // full to partial. - state = 'partial'; - } - that.state = state; - }; - // Answer a controller source function (answering either 0.0 or 1.0). - that.partial = function () { - return that.state ? 1.0 : 0.0; // either 'partial' or 'full' - }; - that.full = function () { - return (that.state === 'full') ? 1.0 : 0.0; - }; -} - -// VERTICAL FIELD OF VIEW --------- -// -// Cache the verticalFieldOfView setting and update it every so often. -var verticalFieldOfView, DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees -function updateFieldOfView() { - verticalFieldOfView = Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW; -} - -// SHIMS ---------- -// -var weMovedReticle = false; -function ignoreMouseActivity() { - // If we're paused, or if change in cursor position is from this script, not the hardware mouse. - if (!Reticle.allowMouseCapture) { - return true; - } - var pos = Reticle.position; - if (!pos || (pos.x == -1 && pos.y == -1)) { - return true; - } - // Only we know if we moved it, which is why this script has to replace depthReticle.js - if (!weMovedReticle) { - return false; - } - weMovedReticle = false; - return true; -} -var MARGIN = 25; -var reticleMinX = MARGIN, reticleMaxX, reticleMinY = MARGIN, reticleMaxY; -function updateRecommendedArea() { - var dims = Controller.getViewportDimensions(); - reticleMaxX = dims.x - MARGIN; - reticleMaxY = dims.y - MARGIN; -} -var setReticlePosition = function (point2d) { - weMovedReticle = true; - point2d.x = Math.max(reticleMinX, Math.min(point2d.x, reticleMaxX)); - point2d.y = Math.max(reticleMinY, Math.min(point2d.y, reticleMaxY)); - Reticle.setPosition(point2d); -}; - -// VISUAL AID ----------- -// Same properties as handControllerGrab search sphere -var LASER_ALPHA = 0.5; -var LASER_SEARCH_COLOR = {red: 10, green: 10, blue: 255}; -var LASER_TRIGGER_COLOR = {red: 250, green: 10, blue: 10}; -var END_DIAMETER = 0.05; -var systemLaserOn = false; - -var triggerPath = { - type: "line3d", - color: LASER_TRIGGER_COLOR, - ignoreRayIntersection: true, - visible: true, - alpha: LASER_ALPHA, - solid: true, - glow: 1.0, - drawHUDLayer: true -} -var triggerEnd = { - type: "sphere", - dimensions: {x: END_DIAMETER, y: END_DIAMETER, z: END_DIAMETER}, - color: LASER_TRIGGER_COLOR, - ignoreRayIntersection: true, - visible: true, - alpha: LASER_ALPHA, - solid: true, - drawHUDLayer: true -} - -var searchPath = { - type: "line3d", - color: LASER_SEARCH_COLOR, - ignoreRayIntersection: true, - visible: true, - alpha: LASER_ALPHA, - solid: true, - glow: 1.0, - drawHUDLayer: true -} -var searchEnd = { - type: "sphere", - dimensions: {x: END_DIAMETER, y: END_DIAMETER, z: END_DIAMETER}, - color: LASER_SEARCH_COLOR, - ignoreRayIntersection: true, - visible: true, - alpha: LASER_ALPHA, - solid: true, - drawHUDLayer: true -} - -var hudRayStates = [{name: "trigger", path: triggerPath, end: triggerEnd}, - {name: "search", path: searchPath, end: searchEnd}]; -// this offset needs to match the one in libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp:378 -var GRAB_POINT_SPHERE_OFFSET_RIGHT = { x: 0.04, y: 0.13, z: 0.039 }; -var GRAB_POINT_SPHERE_OFFSET_LEFT = { x: -0.04, y: 0.13, z: 0.039 }; -var hudRayRight = LaserPointers.createLaserPointer({ - joint: "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND", - filter: RayPick.PICK_HUD, - posOffset: GRAB_POINT_SPHERE_OFFSET_RIGHT, - renderStates: hudRayStates, - enabled: true -}); -var hudRayLeft = LaserPointers.createLaserPointer({ - joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", - filter: RayPick.PICK_HUD, - posOffset: GRAB_POINT_SPHERE_OFFSET_LEFT, - renderStates: hudRayStates, - enabled: true -}); - -// NOTE: keep this offset in sync with scripts/system/librarires/controllers.js:57 -var VERTICAL_HEAD_LASER_OFFSET = 0.1; -var hudRayHead = LaserPointers.createLaserPointer({ - joint: "Avatar", - filter: RayPick.PICK_HUD, - posOffset: {x: 0, y: VERTICAL_HEAD_LASER_OFFSET, z: 0}, - renderStates: hudRayStates, - enabled: true -}); - -var mouseRayPick = RayPick.createRayPick({ - joint: "Mouse", - filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS, - enabled: true -}); - -function isPointingAtOverlay(optionalHudPosition2d) { - return Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(optionalHudPosition2d || Reticle.position); -} - -// Generalized HUD utilities, with or without HMD: -// This "var" is for documentation. Do not change the value! -var PLANAR_PERPENDICULAR_HUD_DISTANCE = 1; -function calculateRayUICollisionPoint(position, direction, isHands) { - // Answer the 3D intersection of the HUD by the given ray, or falsey if no intersection. - if (HMD.active) { - var laserPointer; - if (isHands) { - laserPointer = activeHand == Controller.Standard.RightHand ? hudRayRight : hudRayLeft; - } else { - laserPointer = hudRayHead; - } - var result = LaserPointers.getPrevRayPickResult(laserPointer); - if (result.type != RayPick.INTERSECTED_NONE) { - return result.intersection; - } else { - return null; - } - } - // interect HUD plane, 1m in front of camera, using formula: - // scale = hudNormal dot (hudPoint - position) / hudNormal dot direction - // intersection = postion + scale*direction - var hudNormal = Quat.getForward(Camera.getOrientation()); - var hudPoint = Vec3.sum(Camera.getPosition(), hudNormal); // must also scale if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1 - var denominator = Vec3.dot(hudNormal, direction); - if (denominator === 0) { - return null; - } // parallel to plane - var numerator = Vec3.dot(hudNormal, Vec3.subtract(hudPoint, position)); - var scale = numerator / denominator; - return Vec3.sum(position, Vec3.multiply(scale, direction)); -} -var DEGREES_TO_HALF_RADIANS = Math.PI / 360; -function overlayFromWorldPoint(point) { - // Answer the 2d pixel-space location in the HUD that covers the given 3D point. - // REQUIRES: that the 3d point be on the hud surface! - // Note that this is based on the Camera, and doesn't know anything about any - // ray that may or may not have been used to compute the point. E.g., the - // overlay point is NOT the intersection of some non-camera ray with the HUD. - if (HMD.active) { - return HMD.overlayFromWorldPoint(point); - } - var cameraToPoint = Vec3.subtract(point, Camera.getPosition()); - var cameraX = Vec3.dot(cameraToPoint, Quat.getRight(Camera.getOrientation())); - var cameraY = Vec3.dot(cameraToPoint, Quat.getUp(Camera.getOrientation())); - var size = Controller.getViewportDimensions(); - var hudHeight = 2 * Math.tan(verticalFieldOfView * DEGREES_TO_HALF_RADIANS); // must adjust if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1 - var hudWidth = hudHeight * size.x / size.y; - var horizontalFraction = (cameraX / hudWidth + 0.5); - var verticalFraction = 1 - (cameraY / hudHeight + 0.5); - var horizontalPixels = size.x * horizontalFraction; - var verticalPixels = size.y * verticalFraction; - return { x: horizontalPixels, y: verticalPixels }; -} - -var gamePad = Controller.findDevice("GamePad"); -function activeHudPoint2dGamePad() { - if (!HMD.active) { - return; - } - var headPosition = MyAvatar.getHeadPosition(); - var headDirection = Quat.getUp(Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, { x: 1, y: 0, z: 0 }))); - - var hudPoint3d = calculateRayUICollisionPoint(headPosition, headDirection, false); - - if (!hudPoint3d) { - if (Menu.isOptionChecked("Overlays")) { // With our hud resetting strategy, hudPoint3d should be valid here - print('Controller is parallel to HUD'); // so let us know that our assumptions are wrong. - } - return; - } - var hudPoint2d = overlayFromWorldPoint(hudPoint3d); - - // We don't know yet if we'll want to make the cursor or laser visble, but we need to move it to see if - // it's pointing at aQML tool (aka system overlay). - setReticlePosition(hudPoint2d); - - return hudPoint2d; -} - - -function activeHudPoint2d(activeHand) { // if controller is valid, update reticle position and answer 2d point. Otherwise falsey. - var controllerPose = getControllerWorldLocation(activeHand, true); // note: this will return head pose if hand pose is invalid (third eye) - if (!controllerPose.valid) { - return; // Controller is cradled. - } - var controllerPosition = controllerPose.position; - var controllerDirection = Quat.getUp(controllerPose.rotation); - - var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection, true); - if (!hudPoint3d) { - if (Menu.isOptionChecked("Overlays")) { // With our hud resetting strategy, hudPoint3d should be valid here - print('Controller is parallel to HUD'); // so let us know that our assumptions are wrong. - } - return; - } - var hudPoint2d = overlayFromWorldPoint(hudPoint3d); - - // We don't know yet if we'll want to make the cursor or laser visble, but we need to move it to see if - // it's pointing at a QML tool (aka system overlay). - setReticlePosition(hudPoint2d); - return hudPoint2d; -} - -// MOUSE ACTIVITY -------- -// -var isSeeking = false; -var averageMouseVelocity = 0, lastIntegration = 0, lastMouse; -var WEIGHTING = 1 / 20; // simple moving average over last 20 samples -var ONE_MINUS_WEIGHTING = 1 - WEIGHTING; -var AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO = 20; -function isShakingMouse() { // True if the person is waving the mouse around trying to find it. - var now = Date.now(), mouse = Reticle.position, isShaking = false; - if (lastIntegration && (lastIntegration !== now)) { - var velocity = Vec3.length(Vec3.subtract(mouse, lastMouse)) / (now - lastIntegration); - averageMouseVelocity = (ONE_MINUS_WEIGHTING * averageMouseVelocity) + (WEIGHTING * velocity); - if (averageMouseVelocity > AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO) { - isShaking = true; - } - } - lastIntegration = now; - lastMouse = mouse; - return isShaking; -} -var NON_LINEAR_DIVISOR = 2; -var MINIMUM_SEEK_DISTANCE = 0.1; -function updateSeeking(doNotStartSeeking) { - if (!doNotStartSeeking && !isLaserOn() && (!Reticle.visible || isShakingMouse())) { - isSeeking = true; - } // e.g., if we're about to turn it on with first movement. - if (!isSeeking) { - return; - } - averageMouseVelocity = lastIntegration = 0; - var lookAt2D = HMD.getHUDLookAtPosition2D(); - if (!lookAt2D) { // If this happens, something has gone terribly wrong. - isSeeking = false; - return; // E.g., if parallel to location in HUD - } - var copy = Reticle.position; - function updateDimension(axis) { - var distanceBetween = lookAt2D[axis] - Reticle.position[axis]; - var move = distanceBetween / NON_LINEAR_DIVISOR; - if (Math.abs(move) < MINIMUM_SEEK_DISTANCE) { - return false; - } - copy[axis] += move; - return true; - } - var okX = !updateDimension('x'), okY = !updateDimension('y'); // Evaluate both. Don't short-circuit. - if (okX && okY) { - isSeeking = false; - } else { - Reticle.setPosition(copy); // Not setReticlePosition - } -} - -var mouseCursorActivity = new TimeLock(5000); -var APPARENT_MAXIMUM_DEPTH = 100.0; // this is a depth at which things all seem sufficiently distant -function updateMouseActivity(isClick) { - if (ignoreMouseActivity()) { - return; - } - var now = Date.now(); - mouseCursorActivity.update(now); - if (isClick) { - return; - } // Bug: mouse clicks should keep going. Just not hand controller clicks - handControllerLockOut.update(now); - Reticle.visible = true; -} -function expireMouseCursor(now) { - if (!isPointingAtOverlay() && mouseCursorActivity.expired(now)) { - Reticle.visible = false; - } -} -function hudReticleDistance() { // 3d distance from camera to the reticle position on hud - // (The camera is only in the center of the sphere on reset.) - var reticlePositionOnHUD = HMD.worldPointFromOverlay(Reticle.position); - return Vec3.distance(reticlePositionOnHUD, HMD.position); -} - -function maybeAdjustReticleDepth() { - if (HMD.active) { // set depth - if (isPointingAtOverlay()) { - Reticle.depth = hudReticleDistance(); - } - } -} -var ADJUST_RETICLE_DEPTH_INTERVAL = 50; // 20hz -Script.setInterval(maybeAdjustReticleDepth,ADJUST_RETICLE_DEPTH_INTERVAL); - -function onMouseMove() { - // Display cursor at correct depth (as in depthReticle.js), and updateMouseActivity. - if (ignoreMouseActivity()) { - return; - } - - if (HMD.active) { // set depth - updateSeeking(); - if (isPointingAtOverlay()) { - Reticle.depth = hudReticleDistance(); - } else { - var result = RayPick.getPrevRayPickResult(mouseRayPick); - Reticle.depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; - } - } - updateMouseActivity(); // After the above, just in case the depth movement is awkward when becoming visible. -} -function onMouseClick() { - updateMouseActivity(true); -} -setupHandler(Controller.mouseMoveEvent, onMouseMove); -setupHandler(Controller.mousePressEvent, onMouseClick); -setupHandler(Controller.mouseDoublePressEvent, onMouseClick); - -// CONTROLLER MAPPING --------- -// - -var leftTrigger = new Trigger('left'); -var rightTrigger = new Trigger('right'); -activeTrigger = rightTrigger; -var activeHand = Controller.Standard.RightHand; -var LEFT_HUD_LASER = 1; -var RIGHT_HUD_LASER = 2; -var BOTH_HUD_LASERS = LEFT_HUD_LASER + RIGHT_HUD_LASER; -var activeHudLaser = RIGHT_HUD_LASER; -function toggleHand() { // unequivocally switch which hand controls mouse position - if (activeHand === Controller.Standard.RightHand) { - activeHand = Controller.Standard.LeftHand; - activeTrigger = leftTrigger; - activeHudLaser = LEFT_HUD_LASER; - } else { - activeHand = Controller.Standard.RightHand; - activeTrigger = rightTrigger; - activeHudLaser = RIGHT_HUD_LASER; - } - clearSystemLaser(); -} -function makeToggleAction(hand) { // return a function(0|1) that makes the specified hand control mouse when 1 - return function (on) { - if (on && (activeHand !== hand)) { - toggleHand(); - } - }; -} - -var clickMapping = Controller.newMapping('handControllerPointer-click'); -Script.scriptEnding.connect(clickMapping.disable); - -// Gather the trigger data for smoothing. -clickMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); -clickMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); -clickMapping.from(Controller.Standard.RTClick).peek().to(rightTrigger.triggerClick); -clickMapping.from(Controller.Standard.LTClick).peek().to(leftTrigger.triggerClick); -// Full smoothed trigger is a click. -function isPointingAtOverlayStartedNonFullTrigger(trigger) { - // true if isPointingAtOverlay AND we were NOT full triggered when we became so. - // The idea is to not count clicks when we're full-triggering and reach the edge of a window. - var lockedIn = false; - return function () { - if (trigger !== activeTrigger) { - return lockedIn = false; - } - if (!isPointingAtOverlay()) { - return lockedIn = false; - } - if (lockedIn) { - return true; - } - lockedIn = !trigger.full(); - return lockedIn; - } -} -clickMapping.from(rightTrigger.full).when(isPointingAtOverlayStartedNonFullTrigger(rightTrigger)).to(Controller.Actions.ReticleClick); -clickMapping.from(leftTrigger.full).when(isPointingAtOverlayStartedNonFullTrigger(leftTrigger)).to(Controller.Actions.ReticleClick); -// The following is essentially like Left and Right versions of -// clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu); -// except that we first update the reticle position from the appropriate hand position, before invoking the . -var wantsMenu = 0; -clickMapping.from(function () { return wantsMenu; }).to(Controller.Actions.ContextMenu); -clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(function (clicked) { - if (clicked) { - //activeHudPoint2d(Controller.Standard.RightHand); - Messages.sendLocalMessage("toggleHand", Controller.Standard.RightHand); - } - wantsMenu = clicked; -}); -clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(function (clicked) { - if (clicked) { - //activeHudPoint2d(Controller.Standard.LeftHand); - Messages.sendLocalMessage("toggleHand", Controller.Standard.LeftHand); - } - wantsMenu = clicked; -}); -clickMapping.from(Controller.Standard.Start).peek().to(function (clicked) { - if (clicked) { - //activeHudPoint2dGamePad(); - var noHands = -1; - Messages.sendLocalMessage("toggleHand", Controller.Standard.LeftHand); - } - - wantsMenu = clicked; -}); -clickMapping.from(Controller.Hardware.Keyboard.RightMouseClicked).peek().to(function () { - // Allow the reticle depth to be set correctly: - // Wait a tick for the context menu to be displayed, and then simulate a (non-hand-controller) mouse move - // so that the system updates qml state (Reticle.pointingAtSystemOverlay) before it gives us a mouseMove. - // We don't want the system code to always do this for us, because, e.g., we do not want to get a mouseMove - // after the Left/RightSecondaryThumb gives us a context menu. Only from the mouse. - Script.setTimeout(function () { - var noHands = -1; - Messages.sendLocalMessage("toggleHand", noHands); - Reticle.setPosition(Reticle.position); - }, 0); -}); -// Partial smoothed trigger is activation. -clickMapping.from(rightTrigger.partial).to(makeToggleAction(Controller.Standard.RightHand)); -clickMapping.from(leftTrigger.partial).to(makeToggleAction(Controller.Standard.LeftHand)); -clickMapping.enable(); - -var HIFI_POINTER_DISABLE_MESSAGE_CHANNEL = "Hifi-Pointer-Disable"; -var isPointerEnabled = true; - -function clearSystemLaser() { - if (!systemLaserOn) { - return; - } - HMD.deactivateHMDHandMouse(); - LaserPointers.setRenderState(hudRayRight, ""); - LaserPointers.setRenderState(hudRayLeft, ""); - LaserPointers.setRenderState(hudRayHead, ""); - systemLaserOn = false; - weMovedReticle = true; -} -function setColoredLaser() { // answer trigger state if lasers supported, else falsey. - var mode = (activeTrigger.state === 'full') ? 'trigger' : 'search'; - - if (!systemLaserOn) { - HMD.activateHMDHandMouse(); - } - - var pose = Controller.getPoseValue(activeHand); - if (!pose.valid) { - LaserPointers.setRenderState(hudRayRight, ""); - LaserPointers.setRenderState(hudRayLeft, ""); - LaserPointers.setRenderState(hudRayHead, mode); - return true; - } - - var right = activeHand == Controller.Standard.RightHand; - LaserPointers.setRenderState(hudRayRight, right ? mode : ""); - LaserPointers.setRenderState(hudRayLeft, right ? "" : mode); - LaserPointers.setRenderState(hudRayHead, ""); - - return activeTrigger.state; -} - -// MAIN OPERATIONS ----------- -// -function update() { - var now = Date.now(); - function off() { - expireMouseCursor(); - clearSystemLaser(); - } - - updateSeeking(true); - if (!handControllerLockOut.expired(now)) { - return off(); // Let them use mouse in peace. - } - - if ((!Window.hasFocus() && !HMD.active) || !Reticle.allowMouseCapture) { - // In desktop it's pretty clear when another app is on top. In that case we bail, because - // hand controllers might be sputtering "valid" data and that will keep someone from deliberately - // using the mouse on another app. (Fogbugz case 546.) - // However, in HMD, you might not realize you're not on top, and you wouldn't be able to operate - // other apps anyway. So in that case, we DO keep going even though we're not on top. (Fogbugz 1831.) - return off(); // Don't mess with other apps or paused mouse activity - } - - leftTrigger.update(); - rightTrigger.update(); - if (!activeTrigger.state) { - return off(); // No trigger - } - - if (getGrabCommunications()) { - return off(); - } - - - var hudPoint2d = activeHudPoint2d(activeHand); - if (!hudPoint2d) { - return off(); - } - - // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. - if (isPointingAtOverlay(hudPoint2d) && isPointerEnabled) { - if (HMD.active) { - Reticle.depth = hudReticleDistance(); - - var pose = Controller.getPoseValue(activeHand); - if (!pose.valid) { - var mode = (activeTrigger.state === 'full') ? 'trigger' : 'search'; - if (!systemLaserOn) { - HMD.activateHMDHandMouse(); - } - LaserPointers.setRenderState(hudRayHead, mode); - } - } - - if (activeTrigger.state && (!systemLaserOn || (systemLaserOn !== activeTrigger.state))) { // last=>wrong color - // If the active plugin doesn't implement hand lasers, show the mouse reticle instead. - systemLaserOn = setColoredLaser(); - Reticle.visible = !systemLaserOn; - } else if ((systemLaserOn || Reticle.visible) && !activeTrigger.state) { - clearSystemLaser(); - Reticle.visible = false; - } - return; - } - // We are not pointing at a HUD element (but it could be a 3d overlay). - clearSystemLaser(); - Reticle.visible = false; -} - -// Check periodically for changes to setup. -var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // 10 seconds -function checkSettings() { - updateFieldOfView(); - updateRecommendedArea(); -} -checkSettings(); - -// Enable/disable pointer. -function handleMessages(channel, message, sender) { - if (sender === MyAvatar.sessionUUID && channel === HIFI_POINTER_DISABLE_MESSAGE_CHANNEL) { - var data = JSON.parse(message); - if (data.pointerEnabled !== undefined) { - print("pointerEnabled: " + data.pointerEnabled); - isPointerEnabled = data.pointerEnabled; - } - } -} - -Messages.subscribe(HIFI_POINTER_DISABLE_MESSAGE_CHANNEL); -Messages.messageReceived.connect(handleMessages); - -var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); -Script.update.connect(update); -Script.scriptEnding.connect(function () { - Messages.unsubscribe(HIFI_POINTER_DISABLE_MESSAGE_CHANNEL); - Messages.messageReceived.disconnect(handleMessages); - Script.clearInterval(settingsChecker); - Script.update.disconnect(update); - OffscreenFlags.navigationFocusDisabled = false; - LaserPointers.removeLaserPointer(hudRayRight); - LaserPointers.removeLaserPointer(hudRayLeft); - LaserPointers.removeLaserPointer(hudRayHead); - HMD.deactivateHMDHandMouse(); -}); - -}()); // END LOCAL_SCOPE From f53f8c14fc8ef79b12aabbf4abb283226553bb74 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 23 Sep 2017 11:34:57 +1200 Subject: [PATCH 406/722] Update wrist tool icon --- scripts/vr-edit/assets/tools/tool-icon.fbx | Bin 55292 -> 78732 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/scripts/vr-edit/assets/tools/tool-icon.fbx b/scripts/vr-edit/assets/tools/tool-icon.fbx index f407ad7feb4824077f715ede315c691f1a52eedc..b4a21c523b595d0380e7fbdbbbd6f2ca71e58fae 100644 GIT binary patch delta 36740 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 delta 14694 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 P1kwe1%xw4Npqu{(h|;)@ From bc5cd7415b5a2fd12a54138ad166092f755f9efd Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 23 Sep 2017 11:56:42 +1200 Subject: [PATCH 407/722] Use emissive FBX models for header bars --- scripts/vr-edit/assets/blue-header-bar.fbx | Bin 0 -> 20524 bytes scripts/vr-edit/assets/green-header-bar.fbx | Bin 0 -> 20492 bytes scripts/vr-edit/modules/createPalette.js | 12 ++++++++---- scripts/vr-edit/modules/toolsMenu.js | 12 ++++++++---- 4 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 scripts/vr-edit/assets/blue-header-bar.fbx create mode 100644 scripts/vr-edit/assets/green-header-bar.fbx diff --git a/scripts/vr-edit/assets/blue-header-bar.fbx b/scripts/vr-edit/assets/blue-header-bar.fbx new file mode 100644 index 0000000000000000000000000000000000000000..951a6667e72d963ab1cf09f7b3fdb0e6f1cb2640 GIT binary patch literal 20524 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 Date: Fri, 22 Sep 2017 17:13:48 -0700 Subject: [PATCH 408/722] fix scripting error --- scripts/system/libraries/controllerDispatcherUtils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index a05b108b31..6df5fa9975 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -318,7 +318,6 @@ if (typeof module !== 'undefined') { makeRunningValues: makeRunningValues, LEFT_HAND: LEFT_HAND, RIGHT_HAND: RIGHT_HAND, - isPointingAtUI: isPointingAtUI, BUMPER_ON_VALUE: BUMPER_ON_VALUE, projectOntoOverlayXYPlane: projectOntoOverlayXYPlane, projectOntoEntityXYPlane: projectOntoEntityXYPlane, From 64acc0d0a3eea8e3e5db915caff3e29743542ac2 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 23 Sep 2017 13:01:38 +1200 Subject: [PATCH 409/722] Press undo/redo and menu buttons when click them --- scripts/vr-edit/modules/toolsMenu.js | 102 ++++++++++++++++----------- 1 file changed, 60 insertions(+), 42 deletions(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index b520ccbc3e..50e001d5a1 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -2941,6 +2941,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { function update(intersection, groupsCount, entitiesCount) { var intersectedItem = NONE, intersectionItems, + menuButtonHoverOverlays, + menuButtonIconOverlays, color, localPosition, parameter, @@ -3047,22 +3049,19 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { switch (hoveredElementType) { case "menuButton": if (hoveredSourceOverlays === menuOverlays) { - Overlays.editOverlay(menuHoverOverlays[hoveredItem], { - localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, - visible: false - }); - Overlays.editOverlay(menuIconOverlays[hoveredItem], { - color: UI_ELEMENTS.menuButton.icon.properties.color - }); + menuButtonHoverOverlays = menuHoverOverlays; + menuButtonIconOverlays = menuIconOverlays; } else { - Overlays.editOverlay(footerHoverOverlays[hoveredItem], { - localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, - visible: false - }); - Overlays.editOverlay(footerIconOverlays[hoveredItem], { - color: UI_ELEMENTS.menuButton.icon.properties.color - }); + menuButtonHoverOverlays = footerHoverOverlays; + menuButtonIconOverlays = footerIconOverlays; } + Overlays.editOverlay(menuButtonHoverOverlays[hoveredItem], { + localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, + visible: false + }); + Overlays.editOverlay(menuButtonIconOverlays[hoveredItem], { + color: UI_ELEMENTS.menuButton.icon.properties.color + }); break; case "button": if (hoveredSourceItems[hoveredItem].enabledColor !== undefined && optionsEnabled[hoveredItem]) { @@ -3147,24 +3146,20 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { case "menuButton": Feedback.play(otherSide, Feedback.HOVER_MENU_ITEM); if (intersectionOverlays === menuOverlays) { - Overlays.editOverlay(menuHoverOverlays[hoveredItem], { - localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, - MENU_HOVER_DELTA), - visible: true - }); - Overlays.editOverlay(menuIconOverlays[hoveredItem], { - color: UI_ELEMENTS.menuButton.icon.highlightColor - }); + menuButtonHoverOverlays = menuHoverOverlays; + menuButtonIconOverlays = menuIconOverlays; } else { - Overlays.editOverlay(footerHoverOverlays[hoveredItem], { - localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, - MENU_HOVER_DELTA), - visible: true - }); - Overlays.editOverlay(footerIconOverlays[hoveredItem], { - color: UI_ELEMENTS.menuButton.icon.highlightColor - }); + menuButtonHoverOverlays = footerHoverOverlays; + menuButtonIconOverlays = footerIconOverlays; } + Overlays.editOverlay(menuButtonHoverOverlays[hoveredItem], { + localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, + MENU_HOVER_DELTA), + visible: true + }); + Overlays.editOverlay(menuButtonIconOverlays[hoveredItem], { + color: UI_ELEMENTS.menuButton.icon.highlightColor + }); break; case "button": if (intersectionEnabled[hoveredItem]) { @@ -3259,28 +3254,51 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { || isTriggerClicked !== (pressedItem !== null)) { if (pressedItem) { // Unpress previous button. - Overlays.editOverlay(pressedSource[pressedItem.index], { - localPosition: isHoveringButtonElement && hoveredItem === pressedItem.index - ? Vec3.sum(pressedItem.localPosition, OPTION_HOVER_DELTA) - : pressedItem.localPosition - }); - pressedItem = null; + if (pressedItem !== null) { + if (pressedItem.pressedOverlays) { + pressedSource = pressedItem.pressedOverlays; + } + Overlays.editOverlay(pressedSource[pressedItem.index], { + localPosition: isHoveringButtonElement && hoveredItem === pressedItem.index + ? Vec3.sum(pressedItem.localPosition, pressedItem.hoverDelta) + : pressedItem.localPosition + }); + pressedItem = null; + } pressedSource = null; } if (isHoveringButtonElement && (intersectionEnabled === null || intersectionEnabled[intersectedItem]) && isTriggerClicked && !wasTriggerClicked) { // Press new button. - localPosition = intersectionItems[intersectedItem].properties.localPosition; - if (hoveredElementType !== "menuButton") { + pressedSource = intersectionOverlays; + if (hoveredElementType === "menuButton") { + if (intersectionItems === MENU_ITEMS) { + menuButtonHoverOverlays = menuHoverOverlays; + } else { + menuButtonHoverOverlays = footerHoverOverlays; + } + localPosition = UI_ELEMENTS.menuButton.hoverButton.properties.localPosition; + Overlays.editOverlay(menuButtonHoverOverlays[intersectedItem], { + localPosition: Vec3.sum(Vec3.sum(localPosition, MENU_HOVER_DELTA), BUTTON_PRESS_DELTA) + }); + pressedItem = { + index: intersectedItem, + localPosition: localPosition, + hoverDelta: MENU_HOVER_DELTA, + pressedOverlays: menuButtonHoverOverlays + }; + } else { + localPosition = intersectionItems[intersectedItem].properties.localPosition; Overlays.editOverlay(intersectionOverlays[intersectedItem], { localPosition: Vec3.sum(localPosition, BUTTON_PRESS_DELTA) }); + pressedItem = { + index: intersectedItem, + localPosition: localPosition, + hoverDelta: OPTION_HOVER_DELTA + }; } pressedSource = intersectionOverlays; - pressedItem = { - index: intersectedItem, - localPosition: localPosition - }; // Button press actions. if (intersectionOverlays === menuOverlays) { From 8976befaeb93987ee0db3ac12df585f9754f616b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 23 Sep 2017 14:20:53 +1200 Subject: [PATCH 410/722] Use emissive FBX model for highlighted Tools menu header --- scripts/vr-edit/assets/gray-header.fbx | Bin 0 -> 18892 bytes scripts/vr-edit/assets/green-header.fbx | Bin 0 -> 18604 bytes scripts/vr-edit/modules/createPalette.js | 14 +++++--------- scripts/vr-edit/modules/toolsMenu.js | 21 +++++++++------------ 4 files changed, 14 insertions(+), 21 deletions(-) create mode 100644 scripts/vr-edit/assets/gray-header.fbx create mode 100644 scripts/vr-edit/assets/green-header.fbx diff --git a/scripts/vr-edit/assets/gray-header.fbx b/scripts/vr-edit/assets/gray-header.fbx new file mode 100644 index 0000000000000000000000000000000000000000..0f51d66f38a59c2e6f4ee3ed573bd8989128e645 GIT binary patch literal 18892 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 a?Kjunas7Y&>PKJt=!0qRWWIXo)c*s&RlorN literal 0 HcmV?d00001 diff --git a/scripts/vr-edit/assets/green-header.fbx b/scripts/vr-edit/assets/green-header.fbx new file mode 100644 index 0000000000000000000000000000000000000000..db77ecf0bd3314e89a8c66aa213cc91cee04c42a GIT binary patch literal 18604 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 literal 0 HcmV?d00001 diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index 9cc0f3deb8..4ba6b5f238 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -50,14 +50,14 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { }, PALETTE_HEADER_HEADING_PROPERTIES = { - dimensions: UIT.dimensions.headerHeading, + url: Script.resolvePath("../assets/gray-header.fbx"), + dimensions: UIT.dimensions.headerHeading, // Model is in rotated coordinate system but can override. localPosition: { x: 0, y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.headerHeading.y / 2, z: UIT.dimensions.headerHeading.z / 2 }, localRotation: Quat.ZERO, - color: UIT.colors.baseGray, alpha: 1.0, solid: true, ignoreRayIntersection: false, @@ -66,17 +66,13 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { PALETTE_HEADER_BAR_PROPERTIES = { url: Script.resolvePath("../assets/blue-header-bar.fbx"), - dimensions: { // FBX model is in rotated coordinate system. - x: UIT.dimensions.headerBar.z, - y: UIT.dimensions.headerBar.x, - z: UIT.dimensions.headerBar.y - }, + dimensions: UIT.dimensions.headerBar, // Model is in rotated coordinate system but can override. localPosition: { x: 0, y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.headerHeading.y - UIT.dimensions.headerBar.y / 2, z: UIT.dimensions.headerBar.z / 2 }, - localRotation: Quat.fromVec3Degrees({ x: 0, y: 90, z: 90 }), // FBX model is in rotated coordinate system. + localRotation: Quat.ZERO, alpha: 1.0, solid: true, ignoreRayIntersection: false, @@ -480,7 +476,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { // Header. properties = Object.clone(PALETTE_HEADER_HEADING_PROPERTIES); properties.parentID = paletteOriginOverlay; - paletteHeaderHeadingOverlay = Overlays.addOverlay("cube", properties); + paletteHeaderHeadingOverlay = Overlays.addOverlay("model", properties); properties = Object.clone(PALETTE_HEADER_BAR_PROPERTIES); properties.parentID = paletteOriginOverlay; paletteHeaderBarOverlay = Overlays.addOverlay("model", properties); diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index e54c81acf7..831275c1b5 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -100,10 +100,11 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, MENU_HEADER_HEADING_PROPERTIES = { - dimensions: UIT.dimensions.headerHeading, + url: Script.resolvePath("../assets/gray-header.fbx"), + highlightURL: Script.resolvePath("../assets/green-header.fbx"), + dimensions: UIT.dimensions.headerHeading, // Model is in rotated coordinate system but can override. localPosition: { x: 0, y: UIT.dimensions.headerBar.y / 2, z: -MENU_HEADER_HOVER_OFFSET.z / 2 }, localRotation: Quat.ZERO, - color: UIT.colors.baseGray, alpha: 1.0, solid: true, ignoreRayIntersection: true, @@ -112,13 +113,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { MENU_HEADER_BAR_PROPERTIES = { url: Script.resolvePath("../assets/green-header-bar.fbx"), - dimensions: { // FBX model is in rotated coordinate system. - x: UIT.dimensions.headerBar.z, - y: UIT.dimensions.headerBar.x, - z: UIT.dimensions.headerBar.y - }, + dimensions: UIT.dimensions.headerBar, // Model is in rotated coordinate system but can override. localPosition: { x: 0, y: -UIT.dimensions.headerHeading.y / 2 - UIT.dimensions.headerBar.y / 2, z: 0 }, - localRotation: Quat.fromVec3Degrees({ x: 0, y: 90, z: 90 }), // FBX model is in rotated coordinate system. + localRotation: Quat.ZERO, alpha: 1.0, solid: true, ignoreRayIntersection: true, @@ -2894,8 +2891,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (isTriggerClicked && !wasTriggerClicked) { // Lower and unhighlight heading; go back to Tools menu. Overlays.editOverlay(menuHeaderHeadingOverlay, { + url: MENU_HEADER_HEADING_PROPERTIES.url, localPosition: MENU_HEADER_HEADING_PROPERTIES.localPosition, - color: UIT.colors.baseGray, emissive: false }); isOptionsHeadingRaised = false; @@ -2904,8 +2901,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Hover heading. Feedback.play(otherSide, Feedback.HOVER_BUTTON); Overlays.editOverlay(menuHeaderHeadingOverlay, { + url: MENU_HEADER_HEADING_PROPERTIES.highlightURL, localPosition: Vec3.sum(MENU_HEADER_HEADING_PROPERTIES.localPosition, MENU_HEADER_HOVER_OFFSET), - color: UIT.colors.greenHighlight, emissive: true // TODO: This has no effect. }); Overlays.editOverlay(menuHeaderBackOverlay, { @@ -2924,8 +2921,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (isOptionsHeadingRaised) { // Unhover heading. Overlays.editOverlay(menuHeaderHeadingOverlay, { + url: MENU_HEADER_HEADING_PROPERTIES.url, localPosition: MENU_HEADER_HEADING_PROPERTIES.localPosition, - color: UIT.colors.baseGray, emissive: false }); Overlays.editOverlay(menuHeaderBackOverlay, { @@ -3372,7 +3369,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { menuHeaderOverlay = Overlays.addOverlay("cube", properties); properties = Object.clone(MENU_HEADER_HEADING_PROPERTIES); properties.parentID = menuHeaderOverlay; - menuHeaderHeadingOverlay = Overlays.addOverlay("cube", properties); + menuHeaderHeadingOverlay = Overlays.addOverlay("model", properties); properties = Object.clone(MENU_HEADER_BAR_PROPERTIES); properties.parentID = menuHeaderHeadingOverlay; menuHeaderBarOverlay = Overlays.addOverlay("model", properties); From 2afc84cf0bb85e7049863c00c5dc64129dc1788d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 24 Sep 2017 12:06:04 +1300 Subject: [PATCH 411/722] Display bounding box of currently selected groups --- scripts/vr-edit/modules/groups.js | 5 ++ scripts/vr-edit/modules/highlights.js | 35 +++++++++++- scripts/vr-edit/modules/selection.js | 7 +++ scripts/vr-edit/vr-edit.js | 82 ++++++++++++++++++++------- 4 files changed, 107 insertions(+), 22 deletions(-) diff --git a/scripts/vr-edit/modules/groups.js b/scripts/vr-edit/modules/groups.js index c3e4172ebe..9331071fe2 100644 --- a/scripts/vr-edit/modules/groups.js +++ b/scripts/vr-edit/modules/groups.js @@ -71,6 +71,10 @@ Groups = function () { return rootEntityIDs.indexOf(rootEntityID) !== -1; } + function getRootEntityIDs() { + return rootEntityIDs; + } + function groupsCount() { return selections.length; } @@ -264,6 +268,7 @@ Groups = function () { toggle: toggle, selection: selection, includes: includes, + rootEntityIDs: getRootEntityIDs, groupsCount: groupsCount, entitiesCount: entitiesCount, group: group, diff --git a/scripts/vr-edit/modules/highlights.js b/scripts/vr-edit/modules/highlights.js index 7c8cb0e99f..d893e69f10 100644 --- a/scripts/vr-edit/modules/highlights.js +++ b/scripts/vr-edit/modules/highlights.js @@ -17,11 +17,14 @@ Highlights = function (side) { var handOverlay, entityOverlays = [], + boundingBoxOverlay, + isDisplayingBoundingBox = false, HIGHLIGHT_COLOR = { red: 240, green: 240, blue: 0 }, SCALE_COLOR = { red: 0, green: 240, blue: 240 }, GROUP_COLOR = { red: 220, green: 60, blue: 220 }, HAND_HIGHLIGHT_ALPHA = 0.35, ENTITY_HIGHLIGHT_ALPHA = 0.8, + BOUNDING_BOX_ALPHA = 0.8, HAND_HIGHLIGHT_OFFSET = { x: 0.0, y: 0.11, z: 0.02 }, LEFT_HAND = 0; @@ -43,6 +46,14 @@ Highlights = function (side) { visible: false }); + boundingBoxOverlay = Overlays.addOverlay("cube", { + alpha: BOUNDING_BOX_ALPHA, + solid: false, + drawInFront: true, + ignoreRayIntersection: true, + visible: false + }); + function setHandHighlightRadius(radius) { var dimension = 2 * radius; Overlays.editOverlay(handOverlay, { @@ -75,7 +86,7 @@ Highlights = function (side) { }); } - function display(handIntersected, selection, entityIndex, overlayColor) { + function display(handIntersected, selection, entityIndex, boundingBox, overlayColor) { // Displays highlight for just entityIndex if non-null, otherwise highlights whole selection. var i, length; @@ -86,7 +97,8 @@ Highlights = function (side) { visible: handIntersected }); - if (entityIndex !== null) { + // Display entity overlays. + if (entityIndex !== null) { // Add/edit entity overlay for just entityIndex. maybeAddEntityOverlay(0); editEntityOverlay(0, selection[entityIndex], overlayColor); @@ -103,14 +115,30 @@ Highlights = function (side) { Overlays.deleteOverlay(entityOverlays[i]); entityOverlays.splice(i, 1); } + + // Update bounding box overlay. + if (boundingBox !== null) { + Overlays.editOverlay(boundingBoxOverlay, { + position: boundingBox.center, + rotation: boundingBox.orientation, + dimensions: boundingBox.dimensions, + color: overlayColor, + visible: true + }); + isDisplayingBoundingBox = true; + } else if (isDisplayingBoundingBox) { + Overlays.editOverlay(boundingBoxOverlay, { visible: false }); + isDisplayingBoundingBox = false; + } } function clear() { var i, length; - // Hide hand overlay. + // Hide hand and bounding box overlays. Overlays.editOverlay(handOverlay, { visible: false }); + Overlays.editOverlay(boundingBoxOverlay, { visible: false }); // Delete entity overlays. for (i = 0, length = entityOverlays.length; i < length; i += 1) { @@ -122,6 +150,7 @@ Highlights = function (side) { function destroy() { clear(); Overlays.deleteOverlay(handOverlay); + Overlays.deleteOverlay(boundingBoxOverlay); } return { diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index a01eef5481..b26d5e46f6 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -99,6 +99,12 @@ SelectionManager = function (side) { traverseEntityTree(rootEntityID, selection, selectionProperties); } + function append(rootEntityID) { + // Add further entities to the selection. + // Assumes that rootEntityID is not already in the selection. + traverseEntityTree(rootEntityID, selection, selectionProperties); + } + function getIntersectedEntityID() { return intersectedEntityID; } @@ -754,6 +760,7 @@ SelectionManager = function (side) { return { select: select, + append: append, selection: getSelection, count: count, intersectedEntityID: getIntersectedEntityID, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 5f7b3bb732..212f85e487 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -690,6 +690,7 @@ if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(rootEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), toolSelected === TOOL_COLOR || toolSelected === TOOL_PICK_COLOR ? selection.intersectedEntityIndex() : null, + null, toolSelected === TOOL_SCALE || otherEditor.isEditing(rootEntityID) ? highlights.SCALE_COLOR : highlights.HIGHLIGHT_COLOR); } @@ -702,6 +703,7 @@ if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(rootEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), toolSelected === TOOL_COLOR || toolSelected === TOOL_PICK_COLOR ? selection.intersectedEntityIndex() : null, + null, toolSelected === TOOL_SCALE || otherEditor.isEditing(rootEntityID) ? highlights.SCALE_COLOR : highlights.HIGHLIGHT_COLOR); if (!intersection.laserIntersected && !isUIVisible) { @@ -815,12 +817,13 @@ function enterEditorGrouping() { if (!grouping.includes(rootEntityID)) { - highlights.display(false, selection.selection(), null, highlights.GROUP_COLOR); + highlights.display(false, selection.selection(), null, null, highlights.GROUP_COLOR); } if (toolSelected === TOOL_GROUP_BOX) { if (!grouping.includes(rootEntityID)) { Feedback.play(side, Feedback.SELECT_ENTITY); - grouping.selectInBox(selection.selection()); + grouping.toggle(selection.selection()); + grouping.selectInBox(); } else { Feedback.play(side, Feedback.GENERAL_ERROR); } @@ -1281,13 +1284,14 @@ // Grouping highlights and functions. var groups, - isSelectInBox = false, highlights, exludedLeftRootEntityID = null, exludedrightRootEntityID = null, excludedRootEntityIDs = [], hasHighlights = false, - hasSelectionChanged = false; + hasSelectionChanged = false, + isSelectInBox = false, + selectInBoxSelection = null; // Selection of all groups combined in order to calculate bounding box. if (!this instanceof Grouping) { return new Grouping(); @@ -1298,6 +1302,14 @@ function toggle(selection) { groups.toggle(selection); + if (isSelectInBox) { + // When selecting in a box, toggle() is only called to add entities to the selection. + if (selectInBoxSelection.count() === 0) { + selectInBoxSelection.select(selection[0].id); + } else { + selectInBoxSelection.append(selection[0].id); + } + } if (groups.groupsCount() === 0) { hasHighlights = false; highlights.clear(); @@ -1307,31 +1319,54 @@ } } - function updateSelectInBox() { - // TODO: Calculate bounding box. + function selectInBox() { + // Add any entities or groups of entities wholly within bounding box of current selection. + // Must be wholly within otherwise selection could grow uncontrollably. + var boundingBox; - // TODO: Select further entities per bounding box. + if (selectInBoxSelection.count() > 1) { + boundingBox = selectInBoxSelection.boundingBox(); - // TODO: Update bounding box overlay. + // TODO: Select further entities per bounding box. + + hasSelectionChanged = true; + } } function startSelectInBox() { + // Start automatically selecting entities in bounding box of current selection. + var rootEntityIDs, + i, + length; + isSelectInBox = true; - // TODO: Create bounding box overlay. + // Select entities current groups combined. + selectInBoxSelection = new SelectionManager(); + rootEntityIDs = groups.rootEntityIDs(); + if (rootEntityIDs.length > 0) { + selectInBoxSelection.select(rootEntityIDs[0]); + for (i = 1, length = rootEntityIDs.length; i < length; i += 1) { + selectInBoxSelection.append(rootEntityIDs[i]); + } + } - updateSelectInBox(); - } + // Add any enclosed entities. + selectInBox(); - function selectInBox(selection) { - toggle(selection); - updateSelectInBox(); + // Show bounding box overlay plus any newly selected entities. + hasSelectionChanged = true; } function stopSelectInBox() { - isSelectInBox = false; + // Stop automatically selecting entities within bounding box of current selection. + selectInBoxSelection.destroy(); + selectInBoxSelection = null; - // TODO: Delete bounding box overlay. + // Hide bounding box overlay. + hasSelectionChanged = true; + + isSelectInBox = false; } function includes(rootEntityID) { @@ -1355,7 +1390,9 @@ } function update(leftRootEntityID, rightRootEntityID) { - var hasExludedRootEntitiesChanged; + // Update highlights displayed, excluding entities highlighted by left or right hands. + var hasExludedRootEntitiesChanged, + boundingBox; hasExludedRootEntitiesChanged = leftRootEntityID !== exludedLeftRootEntityID || rightRootEntityID !== exludedrightRootEntityID; @@ -1376,12 +1413,15 @@ exludedrightRootEntityID = rightRootEntityID; } - highlights.display(false, groups.selection(excludedRootEntityIDs), null, highlights.GROUP_COLOR); - + boundingBox = isSelectInBox && selectInBoxSelection.count() > 1 ? selectInBoxSelection.boundingBox() : null; + highlights.display(false, groups.selection(excludedRootEntityIDs), null, boundingBox, highlights.GROUP_COLOR); hasSelectionChanged = false; } function clear() { + if (isSelectInBox) { + stopSelectInBox(); + } groups.clear(); highlights.clear(); } @@ -1395,6 +1435,10 @@ highlights.destroy(); highlights = null; } + if (selectInBoxSelection) { + selectInBoxSelection.destroy(); + selectInBoxSelection = null; + } } return { From 4e0efdaa1a52007eac522200feea341612c0e32a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 24 Sep 2017 16:55:15 +1300 Subject: [PATCH 412/722] Automatically add entities in bounding box to currently selected groups --- scripts/vr-edit/modules/selection.js | 16 +++- scripts/vr-edit/vr-edit.js | 128 ++++++++++++++++++++++++--- 2 files changed, 128 insertions(+), 16 deletions(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index b26d5e46f6..4821da1429 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -16,6 +16,7 @@ SelectionManager = function (side) { "use strict"; var selection = [], // Subset of properties to provide externally. + selectionIDs = [], selectionProperties = [], // Full set of properties for history. intersectedEntityID = null, intersectedEntityIndex, @@ -42,7 +43,7 @@ SelectionManager = function (side) { return new SelectionManager(side); } - function traverseEntityTree(id, selection, selectionProperties) { + function traverseEntityTree(id, selection, selectionIDs, selectionProperties) { // Recursively traverses tree of entities and their children, gather IDs and properties. // The root entity is always the first entry. var children, @@ -65,6 +66,7 @@ SelectionManager = function (side) { collisionless: properties.collisionless, userData: properties.userData }); + selectionIDs.push(id); selectionProperties.push({ entityID: id, properties: properties }); if (id === intersectedEntityID) { @@ -74,7 +76,7 @@ SelectionManager = function (side) { children = Entities.getChildrenIDs(id); for (i = 0, length = children.length; i < length; i += 1) { if (Entities.getNestableType(children[i]) === ENTITY_TYPE) { - traverseEntityTree(children[i], selection, selectionProperties); + traverseEntityTree(children[i], selection, selectionIDs, selectionProperties); } } } @@ -95,14 +97,15 @@ SelectionManager = function (side) { // Find all children. selection = []; + selectionIDs = []; selectionProperties = []; - traverseEntityTree(rootEntityID, selection, selectionProperties); + traverseEntityTree(rootEntityID, selection, selectionIDs, selectionProperties); } function append(rootEntityID) { // Add further entities to the selection. // Assumes that rootEntityID is not already in the selection. - traverseEntityTree(rootEntityID, selection, selectionProperties); + traverseEntityTree(rootEntityID, selection, selectionIDs, selectionProperties); } function getIntersectedEntityID() { @@ -121,6 +124,10 @@ SelectionManager = function (side) { return selection; } + function contains(entityID) { + return selectionIDs.indexOf(entityID) !== -1; + } + function count() { return selection.length; } @@ -762,6 +769,7 @@ SelectionManager = function (side) { select: select, append: append, selection: getSelection, + contains: contains, count: count, intersectedEntityID: getIntersectedEntityID, intersectedEntityIndex: getIntersectedEntityIndex, diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 212f85e487..4d187572b4 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1285,13 +1285,14 @@ var groups, highlights, + selectInBoxSelection, // Selection of all entities selected. + groupSelection, // New group to add to selection. exludedLeftRootEntityID = null, exludedrightRootEntityID = null, excludedRootEntityIDs = [], hasHighlights = false, hasSelectionChanged = false, - isSelectInBox = false, - selectInBoxSelection = null; // Selection of all groups combined in order to calculate bounding box. + isSelectInBox = false; if (!this instanceof Grouping) { return new Grouping(); @@ -1299,6 +1300,74 @@ groups = new Groups(); highlights = new Highlights(); + selectInBoxSelection = new SelectionManager(); + groupSelection = new SelectionManager(); + + function getAllChildrenIDs(entityID) { + var childrenIDs = [], + ENTITY_TYPE = "entity"; + + function traverseEntityTree(id) { + var children, + i, + length; + children = Entities.getChildrenIDs(id); + for (i = 0, length = children.length; i < length; i += 1) { + if (Entities.getNestableType(children[i]) === ENTITY_TYPE) { + childrenIDs.push(children[i]); + traverseEntityTree(children[i]); + } + } + } + + traverseEntityTree(entityID); + return childrenIDs; + } + + function isInsideBoundingBox(entityID, boundingBox) { + // Are all 8 corners of entityID's bounding box inside boundingBox? + var entityProperties, + cornerPosition, + boundingBoxInverseRotation, + boundingBoxHalfDimensions, + isInside = true, + i, + CORNER_REGISTRATION_OFFSETS = [ + { x: 0, y: 0, z: 0 }, + { x: 0, y: 0, z: 1 }, + { x: 0, y: 1, z: 0 }, + { x: 0, y: 1, z: 1 }, + { x: 1, y: 0, z: 0 }, + { x: 1, y: 0, z: 1 }, + { x: 1, y: 1, z: 0 }, + { x: 1, y: 1, z: 1 } + ], + NUM_CORNERS = 8; + + entityProperties = Entities.getEntityProperties(entityID, ["position", "rotation", "dimensions", + "registrationPoint"]); + + // Convert entity coordinates into boundingBox coordinates. + boundingBoxInverseRotation = Quat.inverse(boundingBox.orientation); + entityProperties.position = Vec3.multiplyQbyV(boundingBoxInverseRotation, + Vec3.subtract(entityProperties.position, boundingBox.center)); + entityProperties.rotation = Quat.multiply(boundingBoxInverseRotation, entityProperties.rotation); + + // Check all 8 corners of entity's bounding box are inside the given bounding box. + boundingBoxHalfDimensions = Vec3.multiply(0.5, boundingBox.dimensions); + i = 0; + while (isInside && i < NUM_CORNERS) { + cornerPosition = Vec3.sum(entityProperties.position, Vec3.multiplyQbyV(entityProperties.rotation, + Vec3.multiplyVbyV(Vec3.subtract(CORNER_REGISTRATION_OFFSETS[i], entityProperties.registrationPoint), + entityProperties.dimensions))); + isInside = Math.abs(cornerPosition.x) <= boundingBoxHalfDimensions.x + && Math.abs(cornerPosition.y) <= boundingBoxHalfDimensions.y + && Math.abs(cornerPosition.z) <= boundingBoxHalfDimensions.z; + i += 1; + } + + return isInside; + } function toggle(selection) { groups.toggle(selection); @@ -1322,14 +1391,47 @@ function selectInBox() { // Add any entities or groups of entities wholly within bounding box of current selection. // Must be wholly within otherwise selection could grow uncontrollably. - var boundingBox; + var boundingBox, + entityIDs, + checkedEntityIDs = [], + entityID, + rootID, + groupIDs, + doIncludeGroup, + i, + lengthI, + j, + lengthJ; if (selectInBoxSelection.count() > 1) { boundingBox = selectInBoxSelection.boundingBox(); - - // TODO: Select further entities per bounding box. - - hasSelectionChanged = true; + entityIDs = Entities.findEntities(boundingBox.center, Vec3.length(boundingBox.dimensions) / 2); + for (i = 0, lengthI = entityIDs.length; i < lengthI; i += 1) { + entityID = entityIDs[i]; + if (checkedEntityIDs.indexOf(entityID) === -1) { + rootID = Entities.rootOf(entityID); + if (!selectInBoxSelection.contains(entityID) && Entities.hasEditableRoot(rootID)) { + groupIDs = [rootID].concat(getAllChildrenIDs(rootID)); + doIncludeGroup = true; + j = 0; + lengthJ = groupIDs.length; + while (doIncludeGroup && j < lengthJ) { + doIncludeGroup = isInsideBoundingBox(groupIDs[j], boundingBox); + j += 1; + } + checkedEntityIDs = checkedEntityIDs.concat(groupIDs); + if (doIncludeGroup) { + groupSelection.select(rootID); + groups.toggle(groupSelection.selection()); + groupSelection.clear(); + selectInBoxSelection.append(rootID); + hasSelectionChanged = true; + } + } else { + checkedEntityIDs.push(rootID); + } + } + } } } @@ -1338,11 +1440,10 @@ var rootEntityIDs, i, length; - + isSelectInBox = true; // Select entities current groups combined. - selectInBoxSelection = new SelectionManager(); rootEntityIDs = groups.rootEntityIDs(); if (rootEntityIDs.length > 0) { selectInBoxSelection.select(rootEntityIDs[0]); @@ -1360,10 +1461,9 @@ function stopSelectInBox() { // Stop automatically selecting entities within bounding box of current selection. - selectInBoxSelection.destroy(); - selectInBoxSelection = null; // Hide bounding box overlay. + selectInBoxSelection.clear(); hasSelectionChanged = true; isSelectInBox = false; @@ -1420,7 +1520,7 @@ function clear() { if (isSelectInBox) { - stopSelectInBox(); + selectInBoxSelection.clear(); } groups.clear(); highlights.clear(); @@ -1439,6 +1539,10 @@ selectInBoxSelection.destroy(); selectInBoxSelection = null; } + if (groupSelection) { + groupSelection.destroy(); + groupSelection = null; + } } return { From bbc6e5d1b592414ba6f900ae066261fd3c113274 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 24 Sep 2017 17:06:42 +1300 Subject: [PATCH 413/722] Fix bounding box isn't cleared when exit tool with header or grip click --- scripts/vr-edit/vr-edit.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 4d187572b4..8a1e131a43 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -1520,7 +1520,7 @@ function clear() { if (isSelectInBox) { - selectInBoxSelection.clear(); + stopSelectInBox(); } groups.clear(); highlights.clear(); @@ -1685,6 +1685,9 @@ Feedback.play(dominantHand, Feedback.SELECT_ENTITY); } grouping.clear(); + if (toolSelected === TOOL_GROUP_BOX) { + grouping.startSelectInBox(); + } break; case "setColor": From a73fdf439735fe44cdd386f30820e12d4f3ea55b Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 25 Sep 2017 10:19:42 -0700 Subject: [PATCH 414/722] fix pointer event properties --- libraries/shared/src/PointerEvent.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/shared/src/PointerEvent.cpp b/libraries/shared/src/PointerEvent.cpp index 7ec5e78b9f..e35832391d 100644 --- a/libraries/shared/src/PointerEvent.cpp +++ b/libraries/shared/src/PointerEvent.cpp @@ -77,13 +77,13 @@ QScriptValue PointerEvent::toScriptValue(QScriptEngine* engine, const PointerEve normal.setProperty("x", event._normal.x); normal.setProperty("y", event._normal.y); normal.setProperty("z", event._normal.z); - obj.setProperty("pos3D", normal); + obj.setProperty("normal", normal); QScriptValue direction = engine->newObject(); direction.setProperty("x", event._direction.x); direction.setProperty("y", event._direction.y); direction.setProperty("z", event._direction.z); - obj.setProperty("pos3D", direction); + obj.setProperty("direction", direction); bool isPrimaryButton = false; bool isSecondaryButton = false; From b1b31444f0859c8a864369134009791cc10ecfcc Mon Sep 17 00:00:00 2001 From: druiz17 Date: Mon, 25 Sep 2017 11:34:18 -0700 Subject: [PATCH 415/722] fix mouse visiblity --- scripts/system/controllers/controllerDispatcher.js | 9 ++++++++- .../controllers/controllerModules/mouseHMD.js | 13 ++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 22987245a4..13789a4a8e 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -236,6 +236,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); RayPick.getPrevRayPickResult(_this.leftControllerHudRayPick), RayPick.getPrevRayPickResult(_this.rightControllerHudRayPick) ]; + var mouseRayPick = RayPick.getPrevRayPickResult(_this.mouseRayPick); // if the pickray hit something very nearby, put it into the nearby entities list for (h = LEFT_HAND; h <= RIGHT_HAND; h++) { @@ -274,7 +275,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); nearbyEntityPropertiesByID: nearbyEntityPropertiesByID, nearbyOverlayIDs: nearbyOverlayIDs, rayPicks: rayPicks, - hudRayPicks: hudRayPicks + hudRayPicks: hudRayPicks, + mouseRayPick: mouseRayPick }; if (PROFILE) { Script.endProfileRange("dispatch.gather"); @@ -390,6 +392,11 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE, posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand, true) }); + this.mouseRayPick = RayPick.createRayPick({ + joint: "Mouse", + filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS, + enabled: true + }); this.handleHandMessage = function(channel, message, sender) { var data; diff --git a/scripts/system/controllers/controllerModules/mouseHMD.js b/scripts/system/controllers/controllerModules/mouseHMD.js index 10fe714348..f60136ed4e 100644 --- a/scripts/system/controllers/controllerModules/mouseHMD.js +++ b/scripts/system/controllers/controllerModules/mouseHMD.js @@ -58,6 +58,16 @@ } }; + this.adjustReticleDepth = function(controllerData) { + if (Reticle.isPointingAtSystemOverlay(Reticle.position)) { + var reticlePositionOnHUD = HMD.worldPointFromOverlay(Reticle.position); + Reticle.depth = Vec3.distance(reticlePositionOnHUD, HMD.position); + } else { + var APPARENT_MAXIMUM_DEPTH = 100.0; + var result = controllerData.mouseRayPick; + Reticle.depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; + } + } this.ignoreMouseActivity = function() { if (!Reticle.allowMouseCapture) { return true; @@ -98,7 +108,7 @@ return ControllerDispatcherUtils.makeRunningValues(true, [], []); } if (HMD.active) { - Reticle.visble = false; + Reticle.visible = false; } return ControllerDispatcherUtils.makeRunningValues(false, [], []); @@ -110,6 +120,7 @@ Reticle.visible = false; return ControllerDispatcherUtils.makeRunningValues(false, [], []); } + this.adjustReticleDepth(controllerData); return ControllerDispatcherUtils.makeRunningValues(true, [], []); }; } From 37c501b131aa6c3bab3ab2a96d9a8bbc50506367 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 25 Sep 2017 13:17:21 -0700 Subject: [PATCH 416/722] LODManager uses renderTime instead of main loop fps --- interface/src/Application.cpp | 8 +++-- interface/src/Application.h | 2 +- interface/src/LODManager.cpp | 57 +++++++++++++++++++++++++++++++---- interface/src/LODManager.h | 23 ++++++++------ 4 files changed, 71 insertions(+), 19 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e1c3af1939..4380db0e4f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4470,11 +4470,13 @@ void Application::init() { }, Qt::QueuedConnection); } -void Application::updateLOD() const { +void Application::updateLOD(float deltaTime) const { PerformanceTimer perfTimer("LOD"); // adjust it unless we were asked to disable this feature, or if we're currently in throttleRendering mode if (!isThrottleRendering()) { - DependencyManager::get()->autoAdjustLOD(_frameCounter.rate()); + float batchTime = (float)_gpuContext->getFrameTimerBatchAverage(); + float engineRunTime = (float)(_renderEngine->getConfiguration().get()->getCPURunTime()); + DependencyManager::get()->autoAdjustLOD(batchTime, engineRunTime, deltaTime); } else { DependencyManager::get()->resetLODAdjust(); } @@ -4851,7 +4853,7 @@ void Application::update(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::update()"); - updateLOD(); + updateLOD(deltaTime); if (!_physicsEnabled) { if (!domainLoadingInProgress) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 86e4f94917..be9d0e6171 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -464,7 +464,7 @@ private: void update(float deltaTime); // Various helper functions called during update() - void updateLOD() const; + void updateLOD(float deltaTime) const; void updateThreads(float deltaTime); void updateDialogs(float deltaTime) const; diff --git a/interface/src/LODManager.cpp b/interface/src/LODManager.cpp index bf756a55a5..7fd266c45d 100644 --- a/interface/src/LODManager.cpp +++ b/interface/src/LODManager.cpp @@ -39,16 +39,29 @@ float LODManager::getLODIncreaseFPS() { return getDesktopLODIncreaseFPS(); } -void LODManager::autoAdjustLOD(float currentFPS) { - +void LODManager::autoAdjustLOD(float batchTime, float engineRunTime, float deltaTimeSec) { + // NOTE: our first ~100 samples at app startup are completely all over the place, and we don't // really want to count them in our average, so we will ignore the real frame rates and stuff // our moving average with simulated good data const int IGNORE_THESE_SAMPLES = 100; if (_fpsAverageUpWindow.getSampleCount() < IGNORE_THESE_SAMPLES) { - currentFPS = ASSUMED_FPS; _lastStable = _lastUpShift = _lastDownShift = usecTimestampNow(); } + + // compute time-weighted running average renderTime + const float SYNC_AND_SWAP_TIME_BUDGET = 2.0f; // msec + float renderTime = batchTime + SYNC_AND_SWAP_TIME_BUDGET; + float maxTime = glm::max(renderTime, engineRunTime); + const float BLEND_TIMESCALE = 0.3f; // sec + float blend = BLEND_TIMESCALE / deltaTimeSec; + if (blend > 1.0f) { + blend = 1.0f; + } + _avgRenderTime = (1.0f - blend) * _avgRenderTime + blend * maxTime; // msec + + // translate into fps for legacy implementation + float currentFPS = (float)MSECS_PER_SECOND / _avgRenderTime; _fpsAverageStartWindow.updateAverage(currentFPS); _fpsAverageDownWindow.updateAverage(currentFPS); @@ -56,7 +69,6 @@ void LODManager::autoAdjustLOD(float currentFPS) { quint64 now = usecTimestampNow(); - bool changed = false; quint64 elapsedSinceDownShift = now - _lastDownShift; quint64 elapsedSinceUpShift = now - _lastUpShift; @@ -64,6 +76,7 @@ void LODManager::autoAdjustLOD(float currentFPS) { quint64 elapsedSinceStableOrUpShift = now - lastStableOrUpshift; if (_automaticLODAdjust) { + bool changed = false; // LOD Downward adjustment // If we've been downshifting, we watch a shorter downshift window so that we will quickly move toward our @@ -176,11 +189,44 @@ void LODManager::resetLODAdjust() { _isDownshifting = false; } +const float MIN_SENSIBLE_FPS = 1.0f; + +void LODManager::setDesktopLODDecreaseFPS(float fps) { + if (fps < MIN_SENSIBLE_FPS) { + // avoid divide by zero + fps = MIN_SENSIBLE_FPS; + } + _desktopMaxRenderTime = (float)MSECS_PER_SECOND / fps; +} + +float LODManager::getDesktopLODDecreaseFPS() const { + return (float)MSECS_PER_SECOND / _desktopMaxRenderTime; +} + +float LODManager::getDesktopLODIncreaseFPS() const { + return glm::max(((float)MSECS_PER_SECOND / _desktopMaxRenderTime) + INCREASE_LOD_GAP, MAX_LIKELY_DESKTOP_FPS); +} + +void LODManager::setHMDLODDecreaseFPS(float fps) { + if (fps < MIN_SENSIBLE_FPS) { + // avoid divide by zero + fps = MIN_SENSIBLE_FPS; + } + _hmdMaxRenderTime = (float)MSECS_PER_SECOND / fps; +} + +float LODManager::getHMDLODDecreaseFPS() const { + return (float)MSECS_PER_SECOND / _hmdMaxRenderTime; +} + +float LODManager::getHMDLODIncreaseFPS() const { + return glm::max(((float)MSECS_PER_SECOND / _hmdMaxRenderTime) + INCREASE_LOD_GAP, MAX_LIKELY_HMD_FPS); +} + QString LODManager::getLODFeedbackText() { // determine granularity feedback int boundaryLevelAdjust = getBoundaryLevelAdjust(); QString granularityFeedback; - switch (boundaryLevelAdjust) { case 0: { granularityFeedback = QString("."); @@ -195,7 +241,6 @@ QString LODManager::getLODFeedbackText() { granularityFeedback = QString(" at 1/%1th of standard granularity.").arg(boundaryLevelAdjust + 1); } break; } - // distance feedback float octreeSizeScale = getOctreeSizeScale(); float relativeToDefault = octreeSizeScale / DEFAULT_OCTREE_SIZE_SCALE; diff --git a/interface/src/LODManager.h b/interface/src/LODManager.h index 3d5161298d..11fd28a4e9 100644 --- a/interface/src/LODManager.h +++ b/interface/src/LODManager.h @@ -21,6 +21,8 @@ const float DEFAULT_DESKTOP_LOD_DOWN_FPS = 20.0; const float DEFAULT_HMD_LOD_DOWN_FPS = 20.0; +const float DEFAULT_DESKTOP_MAX_RENDER_TIME = (float)MSECS_PER_SECOND / DEFAULT_DESKTOP_LOD_DOWN_FPS; // msec +const float DEFAULT_HMD_MAX_RENDER_TIME = (float)MSECS_PER_SECOND / DEFAULT_HMD_LOD_DOWN_FPS; // msec const float MAX_LIKELY_DESKTOP_FPS = 59.0; // this is essentially, V-synch - 1 fps const float MAX_LIKELY_HMD_FPS = 74.0; // this is essentially, V-synch - 1 fps const float INCREASE_LOD_GAP = 15.0f; @@ -56,13 +58,13 @@ public: Q_INVOKABLE void setAutomaticLODAdjust(bool value) { _automaticLODAdjust = value; } Q_INVOKABLE bool getAutomaticLODAdjust() const { return _automaticLODAdjust; } - Q_INVOKABLE void setDesktopLODDecreaseFPS(float value) { _desktopLODDecreaseFPS = value; } - Q_INVOKABLE float getDesktopLODDecreaseFPS() const { return _desktopLODDecreaseFPS; } - Q_INVOKABLE float getDesktopLODIncreaseFPS() const { return glm::min(_desktopLODDecreaseFPS + INCREASE_LOD_GAP, MAX_LIKELY_DESKTOP_FPS); } + Q_INVOKABLE void setDesktopLODDecreaseFPS(float value); + Q_INVOKABLE float getDesktopLODDecreaseFPS() const; + Q_INVOKABLE float getDesktopLODIncreaseFPS() const; - Q_INVOKABLE void setHMDLODDecreaseFPS(float value) { _hmdLODDecreaseFPS = value; } - Q_INVOKABLE float getHMDLODDecreaseFPS() const { return _hmdLODDecreaseFPS; } - Q_INVOKABLE float getHMDLODIncreaseFPS() const { return glm::min(_hmdLODDecreaseFPS + INCREASE_LOD_GAP, MAX_LIKELY_HMD_FPS); } + Q_INVOKABLE void setHMDLODDecreaseFPS(float value); + Q_INVOKABLE float getHMDLODDecreaseFPS() const; + Q_INVOKABLE float getHMDLODIncreaseFPS() const; // User Tweakable LOD Items Q_INVOKABLE QString getLODFeedbackText(); @@ -76,7 +78,7 @@ public: Q_INVOKABLE float getLODIncreaseFPS(); static bool shouldRender(const RenderArgs* args, const AABox& bounds); - void autoAdjustLOD(float currentFPS); + void autoAdjustLOD(float batchTime, float engineRunTime, float deltaTimeSec); void loadSettings(); void saveSettings(); @@ -90,8 +92,11 @@ private: LODManager(); bool _automaticLODAdjust = true; - float _desktopLODDecreaseFPS = DEFAULT_DESKTOP_LOD_DOWN_FPS; - float _hmdLODDecreaseFPS = DEFAULT_HMD_LOD_DOWN_FPS; + //float _desktopLODDecreaseFPS = DEFAULT_DESKTOP_LOD_DOWN_FPS; + //float _hmdLODDecreaseFPS = DEFAULT_HMD_LOD_DOWN_FPS; + float _avgRenderTime { 0.0 }; + float _desktopMaxRenderTime { DEFAULT_DESKTOP_MAX_RENDER_TIME }; + float _hmdMaxRenderTime { DEFAULT_HMD_MAX_RENDER_TIME }; float _octreeSizeScale = DEFAULT_OCTREE_SIZE_SCALE; int _boundaryLevelAdjust = 0; From 19a290be7046e7ea8aef540f84f49634f299ff08 Mon Sep 17 00:00:00 2001 From: Menithal Date: Mon, 25 Sep 2017 23:22:53 +0300 Subject: [PATCH 417/722] Fixed Entity Shader emmissive Makes sure that the emissiveAmount information is used correctly for a custom shader, instead of using specular rgb information to generate the emissiveness, which was incorrect --- libraries/render-utils/src/simple.slf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/simple.slf b/libraries/render-utils/src/simple.slf index 228560f394..0dd10b8e1e 100644 --- a/libraries/render-utils/src/simple.slf +++ b/libraries/render-utils/src/simple.slf @@ -75,7 +75,7 @@ void main(void) { max(0, 1.0 - shininess / 128.0), DEFAULT_METALLIC, specular, - specular); + vec3(clamp(emissiveAmount, 0.0, 1.0))); } else { packDeferredFragment( normal, From ec40df711d8f2b9a3165bd07f43a0bcfcf78b5e1 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Mon, 25 Sep 2017 14:31:13 -0700 Subject: [PATCH 418/722] fix tablet grabbing and eslint changes --- .../controllerModules/hudOverlayPointer.js | 51 +++++++------------ .../controllers/controllerModules/mouseHMD.js | 14 +++-- .../controllerModules/tabletStylusInput.js | 9 +++- 3 files changed, 31 insertions(+), 43 deletions(-) diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js index 6eaf7f1cf7..487e491201 100644 --- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js +++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js @@ -10,10 +10,17 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Script, HMD, WebTablet, UIWebTablet, UserActivityLogger, Settings, Entities, Messages, Tablet, Overlays, - MyAvatar, Menu, AvatarInputs, Vec3 */ +/* global Script, Controller, LaserPointers, RayPick, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Camera, Quat, + getGrabPointSphereOffset, getEnabledModuleByName, makeRunningValues, Entities, NULL_UUID, + enableDispatcherModule, disableDispatcherModule, entityIsDistanceGrabbable, + makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, + PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, + AVATAR_SELF_ID, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic, + getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Reticle, Overlays, isPointingAtUI + +*/ (function() { - Script.include("/~/system/libraries/controllers.js") + Script.include("/~/system/libraries/controllers.js"); var ControllerDispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js"); var halfPath = { type: "line3d", @@ -82,18 +89,8 @@ {name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: holdPath} ]; - - // triggered when stylus presses a web overlay/entity - var HAPTIC_STYLUS_STRENGTH = 1.0; - var HAPTIC_STYLUS_DURATION = 20.0; var MARGIN = 25; - function distance2D(a, b) { - var dx = (a.x - b.x); - var dy = (a.y - b.y); - return Math.sqrt(dx * dx + dy * dy); - } - function HudOverlayPointer(hand) { var _this = this; this.hand = hand; @@ -118,15 +115,11 @@ return _this.triggerClicked; }; - this.getOtherModule = function() { - return (this.hand === RIGHT_HAND) ? leftOverlayLaserInput : rightOverlayLaserInput; - }; - this.handToController = function() { return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; }; - this.updateRecommendedArea = function() { + this.updateRecommendedArea = function() { var dims = Controller.getViewportDimensions(); this.reticleMaxX = dims.x - MARGIN; this.reticleMaxY = dims.y - MARGIN; @@ -159,7 +152,7 @@ } }; - this.calculateNewReticlePosition = function(intersection) { + this.calculateNewReticlePosition = function(intersection) { this.updateRecommendedArea(); var point2d = HMD.overlayFromWorldPoint(intersection); point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX)); @@ -176,14 +169,6 @@ return (rayPick.objectID === HMD.tabletScreenID || rayPick.objectID === HMD.homeButtonID); }; - this.moveMouseAwayFromTablet = function() { - if (!this.movedAway) { - var point = {x: 25, y: 25}; - // this.setReticlePosition(point); - this.movedAway = true; - } - } - this.processLaser = function(controllerData) { var controllerLocation = controllerData.controllerLocations[this.hand]; if ((controllerData.triggerValues[this.hand] < ControllerDispatcherUtils.TRIGGER_ON_VALUE || !controllerLocation.valid) || @@ -192,7 +177,6 @@ return false; } var hudRayPick = controllerData.hudRayPicks[this.hand]; - var controllerLocation = controllerData.controllerLocations[this.hand]; var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); this.setReticlePosition(point2d); if (!Reticle.isPointingAtSystemOverlay(point2d)) { @@ -208,10 +192,9 @@ }; this.exitModule = function() { - this.moveMouseAwayFromTablet(); LaserPointers.disableLaserPointer(this.laserPointer); }; - + this.isReady = function (controllerData) { if (this.processLaser(controllerData)) { return ControllerDispatcherUtils.makeRunningValues(true, [], []); @@ -240,12 +223,12 @@ enabled: true, defaultRenderStates: defaultRenderStates }); - } + } - - var leftHudOverlayPointer = new HudOverlayPointer(LEFT_HAND); + + var leftHudOverlayPointer = new HudOverlayPointer(LEFT_HAND); var rightHudOverlayPointer = new HudOverlayPointer(RIGHT_HAND); - + var clickMapping = Controller.newMapping('HudOverlayPointer-click'); clickMapping.from(rightHudOverlayPointer.isClicked).to(Controller.Actions.ReticleClick); clickMapping.from(leftHudOverlayPointer.isClicked).to(Controller.Actions.ReticleClick); diff --git a/scripts/system/controllers/controllerModules/mouseHMD.js b/scripts/system/controllers/controllerModules/mouseHMD.js index f60136ed4e..9ccf4912a1 100644 --- a/scripts/system/controllers/controllerModules/mouseHMD.js +++ b/scripts/system/controllers/controllerModules/mouseHMD.js @@ -13,9 +13,6 @@ (function() { var ControllerDispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js"); - var WEIGHTING = 1 / 20; // simple moving average over last 20 samples - var ONE_MINUS_WEIGHTING = 1 - WEIGHTING; - var AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO = 20; function TimeLock(experation) { this.experation = experation; this.last = 0; @@ -27,7 +24,7 @@ return ((time || Date.now()) - this.last) > this.experation; }; } - + function MouseHMD() { var _this = this; this.mouseMoved = false; @@ -67,14 +64,15 @@ var result = controllerData.mouseRayPick; Reticle.depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; } - } + }; + this.ignoreMouseActivity = function() { if (!Reticle.allowMouseCapture) { return true; } var pos = Reticle.position; - if (!pos || (pos.x == -1 && pos.y == -1)) { + if (!pos || (pos.x === -1 && pos.y === -1)) { return true; } @@ -99,7 +97,7 @@ return false; }; - + this.isReady = function(controllerData, deltaTime) { var now = Date.now(); this.triggersPressed(controllerData, now); @@ -113,7 +111,7 @@ return ControllerDispatcherUtils.makeRunningValues(false, [], []); }; - + this.run = function(controllerData, deltaTime) { var now = Date.now(); if (this.mouseActivity.expired(now) || this.triggersPressed(controllerData, now)) { diff --git a/scripts/system/controllers/controllerModules/tabletStylusInput.js b/scripts/system/controllers/controllerModules/tabletStylusInput.js index 9d01ceef65..def958b223 100644 --- a/scripts/system/controllers/controllerModules/tabletStylusInput.js +++ b/scripts/system/controllers/controllerModules/tabletStylusInput.js @@ -248,10 +248,17 @@ Script.include("/~/system/libraries/controllers.js"); } }; + this.nearGrabWantsToRun = function(controllerData) { + var moduleName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"; + var module = getEnabledModuleByName(moduleName); + var ready = module ? module.isReady(controllerData) : makeRunningValues(false, [], []); + return ready.active; + }; + this.processStylus = function(controllerData) { this.updateStylusTip(); - if (!this.stylusTip.valid || this.overlayLaserActive(controllerData)) { + if (!this.stylusTip.valid || this.overlayLaserActive(controllerData) || this.nearGrabWantsToRun(controllerData)) { this.pointFinger(false); this.hideStylus(); return false; From 5502a639e2f392e02b18794c2d3ae7b86d59185a Mon Sep 17 00:00:00 2001 From: beholder Date: Tue, 26 Sep 2017 01:07:47 +0300 Subject: [PATCH 419/722] 7877 Unexpected Behavior when pressing Enter on Input Field --- interface/resources/qml/controls-uit/Keyboard.qml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/interface/resources/qml/controls-uit/Keyboard.qml b/interface/resources/qml/controls-uit/Keyboard.qml index 4739534fcd..ab361491bd 100644 --- a/interface/resources/qml/controls-uit/Keyboard.qml +++ b/interface/resources/qml/controls-uit/Keyboard.qml @@ -116,6 +116,13 @@ Item { wrapMode: Text.WordWrap readOnly: false // we need to leave this property read-only to allow control to accept QKeyEvent selectByMouse: false + + Keys.onPressed: { + if (event.key == Qt.Key_Return) { + mirrorText.text = ""; + event.accepted = true; + } + } } MouseArea { // ... and we need this mouse area to prevent mirrorText from getting mouse events to ensure it will never get focus From d64e3aca550f27a04ccda0354d72fd66f2c29a14 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 26 Sep 2017 11:10:05 +1300 Subject: [PATCH 420/722] Polyvoxels don't have a color property --- scripts/vr-edit/modules/selection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vr-edit/modules/selection.js b/scripts/vr-edit/modules/selection.js index a01eef5481..d060e65171 100644 --- a/scripts/vr-edit/modules/selection.js +++ b/scripts/vr-edit/modules/selection.js @@ -31,7 +31,7 @@ SelectionManager = function (side) { startOrientation, isEditing = false, ENTITY_TYPE = "entity", - ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "PolyLine", "PolyVox"], + ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "PolyLine"], ENTITY_TYPES_2D = ["Text", "Web"], MIN_HISTORY_MOVE_DISTANCE = 0.005, MIN_HISTORY_ROTATE_ANGLE = 0.017453; // Radians = 1 degree. From a3ac20d9615f7f7402b6ad295382775fc11389b3 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 25 Sep 2017 15:10:14 -0700 Subject: [PATCH 421/722] minor cleanup --- interface/src/LODManager.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/interface/src/LODManager.cpp b/interface/src/LODManager.cpp index 7fd266c45d..d3c8746e16 100644 --- a/interface/src/LODManager.cpp +++ b/interface/src/LODManager.cpp @@ -50,8 +50,8 @@ void LODManager::autoAdjustLOD(float batchTime, float engineRunTime, float delta } // compute time-weighted running average renderTime - const float SYNC_AND_SWAP_TIME_BUDGET = 2.0f; // msec - float renderTime = batchTime + SYNC_AND_SWAP_TIME_BUDGET; + const float OVERLAY_AND_SWAP_TIME_BUDGET = 2.0f; // msec + float renderTime = batchTime + OVERLAY_AND_SWAP_TIME_BUDGET; float maxTime = glm::max(renderTime, engineRunTime); const float BLEND_TIMESCALE = 0.3f; // sec float blend = BLEND_TIMESCALE / deltaTimeSec; @@ -189,12 +189,12 @@ void LODManager::resetLODAdjust() { _isDownshifting = false; } -const float MIN_SENSIBLE_FPS = 1.0f; +const float MIN_DECREASE_FPS = 0.5f; void LODManager::setDesktopLODDecreaseFPS(float fps) { - if (fps < MIN_SENSIBLE_FPS) { + if (fps < MIN_DECREASE_FPS) { // avoid divide by zero - fps = MIN_SENSIBLE_FPS; + fps = MIN_DECREASE_FPS; } _desktopMaxRenderTime = (float)MSECS_PER_SECOND / fps; } @@ -208,9 +208,9 @@ float LODManager::getDesktopLODIncreaseFPS() const { } void LODManager::setHMDLODDecreaseFPS(float fps) { - if (fps < MIN_SENSIBLE_FPS) { + if (fps < MIN_DECREASE_FPS) { // avoid divide by zero - fps = MIN_SENSIBLE_FPS; + fps = MIN_DECREASE_FPS; } _hmdMaxRenderTime = (float)MSECS_PER_SECOND / fps; } From 11c55755d5010c8bcac2678927fe2fc165de518c Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 25 Sep 2017 15:10:40 -0700 Subject: [PATCH 422/722] Removing unused call --- interface/src/Application.cpp | 5 ----- interface/src/Application.h | 2 -- libraries/render-utils/src/AbstractViewStateInterface.h | 3 --- tests/render-perf/src/main.cpp | 5 ----- 4 files changed, 15 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9402cba7bc..6552141665 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5648,11 +5648,6 @@ void Application::copyDisplayViewFrustum(ViewFrustum& viewOut) const { viewOut = _displayViewFrustum; } -void Application::copyShadowViewFrustum(ViewFrustum& viewOut) const { - QMutexLocker viewLocker(&_viewMutex); - viewOut = _shadowViewFrustum; -} - // WorldBox Render Data & rendering functions class WorldBoxRenderData { diff --git a/interface/src/Application.h b/interface/src/Application.h index dc5c6a47d5..3a1f756893 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -175,7 +175,6 @@ public: // which might be different from the viewFrustum, i.e. shadowmap // passes, mirror window passes, etc void copyDisplayViewFrustum(ViewFrustum& viewOut) const; - void copyShadowViewFrustum(ViewFrustum& viewOut) const override; const OctreePacketProcessor& getOctreePacketProcessor() const { return _octreeProcessor; } QSharedPointer getEntities() const { return DependencyManager::get(); } QUndoStack* getUndoStack() { return &_undoStack; } @@ -556,7 +555,6 @@ private: ViewFrustum _viewFrustum; // current state of view frustum, perspective, orientation, etc. ViewFrustum _lastQueriedViewFrustum; /// last view frustum used to query octree servers (voxels) ViewFrustum _displayViewFrustum; - ViewFrustum _shadowViewFrustum; quint64 _lastQueriedTime; OctreeQuery _octreeQuery; // NodeData derived class for querying octee cells from octree servers diff --git a/libraries/render-utils/src/AbstractViewStateInterface.h b/libraries/render-utils/src/AbstractViewStateInterface.h index 4570ead9e1..96e9f4d222 100644 --- a/libraries/render-utils/src/AbstractViewStateInterface.h +++ b/libraries/render-utils/src/AbstractViewStateInterface.h @@ -31,9 +31,6 @@ public: /// copies the current view frustum for rendering the view state virtual void copyCurrentViewFrustum(ViewFrustum& viewOut) const = 0; - /// copies the shadow view frustum for rendering the view state - virtual void copyShadowViewFrustum(ViewFrustum& viewOut) const = 0; - virtual QThread* getMainThread() = 0; virtual PickRay computePickRay(float x, float y) const = 0; diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 58eb4d16f9..c70a74cd7f 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -443,10 +443,6 @@ protected: viewOut = _viewFrustum; } - void copyShadowViewFrustum(ViewFrustum& viewOut) const override { - viewOut = _shadowViewFrustum; - } - QThread* getMainThread() override { return QThread::currentThread(); } @@ -1118,7 +1114,6 @@ private: RenderThread _renderThread; QWindowCamera _camera; ViewFrustum _viewFrustum; // current state of view frustum, perspective, orientation, etc. - ViewFrustum _shadowViewFrustum; // current state of view frustum, perspective, orientation, etc. model::SunSkyStage _sunSkyStage; model::LightPointer _globalLight { std::make_shared() }; bool _ready { false }; From 5da5b248946d3e53d9eb619479bce5c8e4aecc7d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 25 Sep 2017 15:18:22 -0700 Subject: [PATCH 423/722] unmangle merge --- .../controllers/controllerModules/nearActionGrabEntity.js | 3 ++- .../controllers/controllerModules/nearParentGrabEntity.js | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js index 41a5202887..2484067655 100644 --- a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js @@ -217,7 +217,8 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); var targetProps = this.getTargetProps(controllerData); if (targetProps) { - if (controllerData.triggerClicks[this.hand] || controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) { + if (controllerData.triggerClicks[this.hand] || + controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) { // switch to grabbing var targetCloneable = entityIsCloneable(targetProps); if (targetCloneable) { diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js index bde173d5fb..e0bb596253 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -98,13 +98,8 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); if (this.thisHandIsParent(targetProps)) { // this should never happen, but if it does, don't set previous parent to be this hand. -<<<<<<< HEAD this.previousParentID[targetProps.id] = null; this.previousParentJointIndex[targetProps.id] = -1; -======= - // this.previousParentID[targetProps.id] = NULL; - // this.previousParentJointIndex[targetProps.id] = -1; ->>>>>>> 030da7d850d0291e8e94443ee784b39881836c17 } else { this.previousParentID[targetProps.id] = targetProps.parentID; this.previousParentJointIndex[targetProps.id] = targetProps.parentJointIndex; From cc8e618bc0379f3ec4dc8f8332e67b0531721aa7 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 25 Sep 2017 15:52:14 -0700 Subject: [PATCH 424/722] remove cruft --- interface/src/LODManager.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/interface/src/LODManager.h b/interface/src/LODManager.h index 11fd28a4e9..1b3797a0ca 100644 --- a/interface/src/LODManager.h +++ b/interface/src/LODManager.h @@ -92,8 +92,6 @@ private: LODManager(); bool _automaticLODAdjust = true; - //float _desktopLODDecreaseFPS = DEFAULT_DESKTOP_LOD_DOWN_FPS; - //float _hmdLODDecreaseFPS = DEFAULT_HMD_LOD_DOWN_FPS; float _avgRenderTime { 0.0 }; float _desktopMaxRenderTime { DEFAULT_DESKTOP_MAX_RENDER_TIME }; float _hmdMaxRenderTime { DEFAULT_HMD_MAX_RENDER_TIME }; From 6332a53ee2bc9745ff7d0e5d9a8729a5be5f8759 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Mon, 25 Sep 2017 16:36:29 -0700 Subject: [PATCH 425/722] able to grab tablet in 3rd person --- .../controllerModules/nearParentGrabOverlay.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js index 8d093afe2c..f9557f685f 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js @@ -88,13 +88,8 @@ Script.include("/~/system/libraries/utils.js"); this.startNearParentingGrabOverlay = function (controllerData) { Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); - var handJointIndex; - // if (this.ignoreIK) { - // handJointIndex = this.controllerJointIndex; - // } else { - // handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - // } - handJointIndex = this.controllerJointIndex; + this.controllerJointIndex = getControllerJointIndex(this.hand); + var handJointIndex = this.controllerJointIndex; var grabbedProperties = this.getGrabbedProperties(); From 40c42d35a5ba6560e21187bad60d5255df78d49b Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 25 Sep 2017 17:50:04 -0700 Subject: [PATCH 426/722] Fix long tablet lag on first load --- interface/src/Application.cpp | 1 + interface/src/ui/overlays/Web3DOverlay.cpp | 156 +++++++++--------- interface/src/ui/overlays/Web3DOverlay.h | 6 +- .../ui/src/ui/TabletScriptingInterface.cpp | 2 + .../ui/src/ui/TabletScriptingInterface.h | 1 + scripts/system/tablet-ui/tabletUI.js | 2 +- 6 files changed, 86 insertions(+), 82 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e1c3af1939..55b5ba4bd3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2369,6 +2369,7 @@ void Application::initializeUi() { // Pre-create a couple of Web3D overlays to speed up tablet UI auto offscreenSurfaceCache = DependencyManager::get(); + offscreenSurfaceCache->reserve(TabletScriptingInterface::QML, 1); offscreenSurfaceCache->reserve(Web3DOverlay::QML, 2); } diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 0807d1c117..c8433766b9 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -62,6 +62,7 @@ static const float OPAQUE_ALPHA_THRESHOLD = 0.99f; const QString Web3DOverlay::TYPE = "web3d"; const QString Web3DOverlay::QML = "Web3DOverlay.qml"; + Web3DOverlay::Web3DOverlay() : _dpi(DPI) { _touchDevice.setCapabilities(QTouchDevice::Position); _touchDevice.setType(QTouchDevice::TouchScreen); @@ -97,6 +98,10 @@ Web3DOverlay::~Web3DOverlay() { } } +void Web3DOverlay::rebuildWebSurface() { + destroyWebSurface(); + buildWebSurface(); +} void Web3DOverlay::destroyWebSurface() { if (!_webSurface) { @@ -136,17 +141,23 @@ void Web3DOverlay::buildWebSurface() { return; } gl::withSavedContext([&] { - _webSurface = DependencyManager::get()->acquire(pickURL()); // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces // and the current rendering load) if (_currentMaxFPS != _desiredMaxFPS) { setMaxFPS(_desiredMaxFPS); } - loadSourceURL(); - _webSurface->resume(); + + if (isWebContent()) { + _webSurface = DependencyManager::get()->acquire(QML); + _webSurface->getRootItem()->setProperty("url", _url); + _webSurface->getRootItem()->setProperty("scriptURL", _scriptURL); + } else { + _webSurface = DependencyManager::get()->acquire(_url); + setupQmlSurface(); + } + _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); _webSurface->resize(QSize(_resolution.x, _resolution.y)); - _webSurface->getRootItem()->setProperty("url", _url); - _webSurface->getRootItem()->setProperty("scriptURL", _scriptURL); + _webSurface->resume(); }); auto selfOverlayID = getOverlayID(); @@ -187,88 +198,61 @@ void Web3DOverlay::update(float deltatime) { Parent::update(deltatime); } -QString Web3DOverlay::pickURL() { +bool Web3DOverlay::isWebContent() const { QUrl sourceUrl(_url); if (sourceUrl.scheme() == "http" || sourceUrl.scheme() == "https" || _url.toLower().endsWith(".htm") || _url.toLower().endsWith(".html")) { - if (_webSurface) { - _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); - } - return "Web3DOverlay.qml"; - } else { - return QUrl::fromLocalFile(PathUtils::resourcesPath()).toString() + "/" + _url; + return true; } + return false; } -void Web3DOverlay::loadSourceURL() { - if (!_webSurface) { - return; - } +void Web3DOverlay::setupQmlSurface() { + _webSurface->getSurfaceContext()->setContextProperty("Users", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("HMD", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("UserActivityLogger", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Preferences", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Vec3", new Vec3()); + _webSurface->getSurfaceContext()->setContextProperty("Quat", new Quat()); + _webSurface->getSurfaceContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); + _webSurface->getSurfaceContext()->setContextProperty("Entities", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Snapshot", DependencyManager::get().data()); - QUrl sourceUrl(_url); - if (sourceUrl.scheme() == "http" || sourceUrl.scheme() == "https" || - _url.toLower().endsWith(".htm") || _url.toLower().endsWith(".html")) { + if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { + auto tabletScriptingInterface = DependencyManager::get(); + auto flags = tabletScriptingInterface->getFlags(); - _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); - _webSurface->load("Web3DOverlay.qml"); - _webSurface->resume(); - _webSurface->getRootItem()->setProperty("url", _url); - _webSurface->getRootItem()->setProperty("scriptURL", _scriptURL); - - } else { - _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath())); - _webSurface->load(_url, [&](QQmlContext* context, QObject* obj) {}); - _webSurface->resume(); - - _webSurface->getSurfaceContext()->setContextProperty("Users", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("offscreenFlags", flags); + _webSurface->getSurfaceContext()->setContextProperty("AddressManager", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Account", AccountScriptingInterface::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("Audio", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); _webSurface->getSurfaceContext()->setContextProperty("HMD", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("UserActivityLogger", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Preferences", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Vec3", new Vec3()); - _webSurface->getSurfaceContext()->setContextProperty("Quat", new Quat()); + _webSurface->getSurfaceContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); _webSurface->getSurfaceContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); - _webSurface->getSurfaceContext()->setContextProperty("Entities", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Snapshot", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Tablet", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Assets", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("LODManager", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("OctreeStats", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("DCModel", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("AvatarList", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("DialogsManager", DialogsManagerScriptingInterface::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("InputConfiguration", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("SoundCache", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); - if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { - auto tabletScriptingInterface = DependencyManager::get(); - auto flags = tabletScriptingInterface->getFlags(); + _webSurface->getSurfaceContext()->setContextProperty("pathToFonts", "../../"); - _webSurface->getSurfaceContext()->setContextProperty("offscreenFlags", flags); - _webSurface->getSurfaceContext()->setContextProperty("AddressManager", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Account", AccountScriptingInterface::getInstance()); - _webSurface->getSurfaceContext()->setContextProperty("Audio", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); - _webSurface->getSurfaceContext()->setContextProperty("HMD", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); - _webSurface->getSurfaceContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); - _webSurface->getSurfaceContext()->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Tablet", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Assets", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("LODManager", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("OctreeStats", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("DCModel", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); - _webSurface->getSurfaceContext()->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); - _webSurface->getSurfaceContext()->setContextProperty("AvatarList", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("DialogsManager", DialogsManagerScriptingInterface::getInstance()); - _webSurface->getSurfaceContext()->setContextProperty("InputConfiguration", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("SoundCache", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); - - _webSurface->getSurfaceContext()->setContextProperty("pathToFonts", "../../"); - - tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface.data()); - - // mark the TabletProxy object as cpp ownership. - QObject* tablet = tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"); - _webSurface->getSurfaceContext()->engine()->setObjectOwnership(tablet, QQmlEngine::CppOwnership); - - // Override min fps for tablet UI, for silky smooth scrolling - setMaxFPS(90); - } + tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface.data()); + // mark the TabletProxy object as cpp ownership. + QObject* tablet = tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"); + _webSurface->getSurfaceContext()->engine()->setObjectOwnership(tablet, QQmlEngine::CppOwnership); + // Override min fps for tablet UI, for silky smooth scrolling + setMaxFPS(90); } - _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); } void Web3DOverlay::setMaxFPS(uint8_t maxFPS) { @@ -594,11 +578,25 @@ QVariant Web3DOverlay::getProperty(const QString& property) { } void Web3DOverlay::setURL(const QString& url) { - _url = url; - if (_webSurface) { - AbstractViewStateInterface::instance()->postLambdaEvent([this, url] { - loadSourceURL(); - }); + if (url != _url) { + bool wasWebContent = isWebContent(); + _url = url; + if (_webSurface) { + if (wasWebContent && isWebContent()) { + // If we're just targeting a new web URL, then switch to that without messing around + // with the underlying QML + AbstractViewStateInterface::instance()->postLambdaEvent([this, url] { + _webSurface->getRootItem()->setProperty("url", _url); + _webSurface->getRootItem()->setProperty("scriptURL", _scriptURL); + }); + } else { + // If we're switching to or from web content, or between different QML content + // we need to destroy and rebuild the entire QML surface + AbstractViewStateInterface::instance()->postLambdaEvent([this, url] { + rebuildWebSurface(); + }); + } + } } } diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 6bd540d120..9b7be11a4a 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -31,8 +31,6 @@ public: Web3DOverlay(const Web3DOverlay* Web3DOverlay); virtual ~Web3DOverlay(); - QString pickURL(); - void loadSourceURL(); void setMaxFPS(uint8_t maxFPS); virtual void render(RenderArgs* args) override; virtual const render::ShapeKey getShapeKey() override; @@ -79,6 +77,10 @@ signals: void releaseWebSurface(); private: + void setupQmlSurface(); + void rebuildWebSurface(); + bool isWebContent() const; + InputMode _inputMode { Touch }; QSharedPointer _webSurface; gpu::TexturePointer _texture; diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 8ab03b60d0..4e625c2494 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -26,6 +26,8 @@ // FIXME move to global app properties const QString SYSTEM_TOOLBAR = "com.highfidelity.interface.toolbar.system"; const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; +const QString TabletScriptingInterface::QML = "hifi/tablet/TabletRoot.qml"; + TabletScriptingInterface::TabletScriptingInterface() { } diff --git a/libraries/ui/src/ui/TabletScriptingInterface.h b/libraries/ui/src/ui/TabletScriptingInterface.h index d3590ec62e..386bce45a8 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.h +++ b/libraries/ui/src/ui/TabletScriptingInterface.h @@ -42,6 +42,7 @@ class TabletScriptingInterface : public QObject, public Dependency { public: TabletScriptingInterface(); ~TabletScriptingInterface(); + static const QString QML; void setToolbarScriptingInterface(ToolbarScriptingInterface* toolbarScriptingInterface) { _toolbarScriptingInterface = toolbarScriptingInterface; } diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index 63c1cc51aa..9ecd0f0230 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -97,7 +97,7 @@ checkTablet() tabletScalePercentage = getTabletScalePercentageFromSettings(); - UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", + UIWebTablet = new WebTablet("hifi/tablet/TabletRoot.qml", DEFAULT_WIDTH * (tabletScalePercentage / 100), null, activeHand, true, null, false); UIWebTablet.register(); From 157b4f2e133d39173d053354aa39c113846955f4 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 25 Sep 2017 18:28:33 -0700 Subject: [PATCH 427/722] Moving some of the updates on the camera and the avatar out from render to game loop --- interface/src/Application.cpp | 55 +++++++++++++++++++++++++++-------- interface/src/Application.h | 7 +++++ 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6552141665..cf4a103ad4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2373,6 +2373,11 @@ void Application::initializeUi() { } void Application::updateCamera(RenderArgs& renderArgs) { + // load the view frustum + { + QMutexLocker viewLocker(&_viewMutex); + _myCamera.loadViewFrustum(_displayViewFrustum); + } glm::vec3 boomOffset; { @@ -2474,6 +2479,25 @@ void Application::updateCamera(RenderArgs& renderArgs) { } } } + + renderArgs._cameraMode = (int8_t)_myCamera.getMode(); // HACK + + + // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. + // Then we can move this logic into the Avatar::simulate call. + auto myAvatar = getMyAvatar(); + myAvatar->preDisplaySide(&renderArgs); + + { + QMutexLocker viewLocker(&_viewMutex); + renderArgs.setViewFrustum(_displayViewFrustum); + } +} + +void Application::editRenderArgs(RenderArgsEditor editor) { + QMutexLocker viewLocker(&_renderArgsMutex); + editor(_renderArgs); + } void Application::paintGL() { @@ -2518,6 +2542,10 @@ void Application::paintGL() { auto lodManager = DependencyManager::get(); RenderArgs renderArgs; + { + QMutexLocker viewLocker(&_renderArgsMutex); + renderArgs = _renderArgs; + } float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); { @@ -2570,7 +2598,7 @@ void Application::paintGL() { _applicationOverlay.renderOverlay(&renderArgs); } - updateCamera(renderArgs); + // updateCamera(renderArgs); /* glm::vec3 boomOffset; { @@ -5336,6 +5364,7 @@ void Application::update(float deltaTime) { avatarManager->postUpdate(deltaTime, getMain3DScene()); + { PROFILE_RANGE_EX(app, "PreRenderLambdas", 0xffff0000, (uint64_t)0); @@ -5346,6 +5375,10 @@ void Application::update(float deltaTime) { _postUpdateLambdas.clear(); } + editRenderArgs([this](RenderArgs& renderArgs) { + this->updateCamera(renderArgs); + }); + AnimDebugDraw::getInstance().update(); DependencyManager::get()->update(); @@ -5679,20 +5712,18 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. // Then we can move this logic into the Avatar::simulate call. - auto myAvatar = getMyAvatar(); - myAvatar->preDisplaySide(renderArgs); + // auto myAvatar = getMyAvatar(); + // myAvatar->preDisplaySide(renderArgs); PROFILE_RANGE(render, __FUNCTION__); PerformanceTimer perfTimer("display"); PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide()"); // load the view frustum - { - QMutexLocker viewLocker(&_viewMutex); - theCamera.loadViewFrustum(_displayViewFrustum); - } - - // TODO fix shadows and make them use the GPU library + // { + // QMutexLocker viewLocker(&_viewMutex); + // theCamera.loadViewFrustum(_displayViewFrustum); + // } // The pending changes collecting the changes here render::Transaction transaction; @@ -5740,11 +5771,11 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se { PerformanceTimer perfTimer("EngineRun"); - { + /* { QMutexLocker viewLocker(&_viewMutex); renderArgs->setViewFrustum(_displayViewFrustum); - } - renderArgs->_cameraMode = (int8_t)theCamera.getMode(); // HACK + }*/ + // renderArgs->_cameraMode = (int8_t)theCamera.getMode(); // HACK renderArgs->_scene = getMain3DScene(); _renderEngine->getRenderContext()->args = renderArgs; diff --git a/interface/src/Application.h b/interface/src/Application.h index 3a1f756893..edb356fcef 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -625,6 +625,13 @@ private: render::EnginePointer _renderEngine{ new render::Engine() }; gpu::ContextPointer _gpuContext; // initialized during window creation + mutable QMutex _renderArgsMutex{ QMutex::Recursive }; + render::Args _renderArgs; + + using RenderArgsEditor = std::function ; + void editRenderArgs(RenderArgsEditor editor); + + Overlays _overlays; ApplicationOverlay _applicationOverlay; OverlayConductor _overlayConductor; From 0b60928dd3d9c85b0163520b9251954ee25744d1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 25 Sep 2017 19:07:40 -0700 Subject: [PATCH 428/722] fix rollup-to-parent code to only roll up to entity parents --- scripts/system/libraries/controllerDispatcherUtils.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 19bc8eda8f..23735980aa 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -294,8 +294,9 @@ ensureDynamic = function (entityID) { }; findGroupParent = function (controllerData, targetProps) { - while (targetProps.parentID && targetProps.parentID !== NULL_UUID && targetProps.parentID !== AVATAR_SELF_ID) { - // XXX use controllerData.nearbyEntityPropertiesByID ? + while (targetProps.parentID && + targetProps.parentID !== NULL_UUID && + Entities.getNestableType(targetProps.parentID) == "entity") { var parentProps = Entities.getEntityProperties(targetProps.parentID, DISPATCHER_PROPERTIES); if (!parentProps) { break; From 9b7197cf3f4efedf611f34a1acc77eefbfd17a8d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 26 Sep 2017 15:36:40 +1300 Subject: [PATCH 429/722] Don't highlight entity you're inside unless you laser it --- scripts/vr-edit/vr-edit.js | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 2bcd3533cd..8a4885db6a 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -452,9 +452,8 @@ return rootEntityID; } - function isCameraOutsideEntity(entityID) { + function isCameraOutsideEntity(entityID, entityPosition) { var cameraPosition, - grabPosition, pickRay, PRECISION_PICKING = true, NO_EXCLUDE_IDS = [], @@ -462,11 +461,10 @@ intersection; cameraPosition = Camera.position; - grabPosition = side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); pickRay = { origin: cameraPosition, - direction: Vec3.normalize(Vec3.subtract(grabPosition, cameraPosition)), - length: Vec3.distance(grabPosition, cameraPosition) + direction: Vec3.normalize(Vec3.subtract(entityPosition, cameraPosition)), + length: Vec3.distance(entityPosition, cameraPosition) }; intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, [entityID], NO_EXCLUDE_IDS, VISIBLE_ONLY); @@ -894,7 +892,8 @@ function update() { - var showUI, + var isTriggerPressed, + showUI, previousState = editorState, doUpdateState, color; @@ -902,11 +901,12 @@ intersection = getIntersection(); isTriggerClicked = hand.triggerClicked(); isGripClicked = hand.gripClicked(); + isTriggerPressed = hand.triggerPressed(); // Hide UI if hand is intersecting entity and camera is outside entity, or it hand is intersecting stretch handle. if (dominantHand !== side) { - showUI = !intersection.handIntersected - || (intersection.entityID !== null && !isCameraOutsideEntity(intersection.entityID)); + showUI = !intersection.handIntersected || (intersection.entityID !== null + && !isCameraOutsideEntity(intersection.entityID, intersection.intersection)); if (showUI !== isUIVisible) { isUIVisible = !isUIVisible; ui.setVisible(isUIVisible); @@ -924,9 +924,13 @@ break; case EDITOR_SEARCHING: if (hand.valid() - && (!intersection.entityID || !(intersection.editableEntity || toolSelected === TOOL_PICK_COLOR)) && !(intersection.overlayID && !wasTriggerClicked && isTriggerClicked - && otherEditor.isHandle(intersection.overlayID))) { + && otherEditor.isHandle(intersection.overlayID)) + && !(intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) + && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab + && (isCameraOutsideEntity(intersection.entityID, intersection.intersection) || isTriggerPressed)) + && !(intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) + && (!wasTriggerClicked || isAutoGrab) && isTriggerClicked)) { // No transition. updateState(); updateTool(); @@ -940,7 +944,8 @@ rootEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) - && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab) { + && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab + && (isCameraOutsideEntity(intersection.entityID, intersection.intersection) || isTriggerPressed)) { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); @@ -991,6 +996,7 @@ case EDITOR_HIGHLIGHTING: if (hand.valid() && intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) + && (isCameraOutsideEntity(intersection.entityID, intersection.intersection) || isTriggerPressed) && !(!wasTriggerClicked && isTriggerClicked && (!otherEditor.isEditing(rootEntityID) || toolSelected !== TOOL_SCALE)) && !(!wasTriggerClicked && isTriggerClicked && intersection.overlayID @@ -1070,7 +1076,8 @@ } else { setState(EDITOR_GRABBING); } - } else if (!intersection.entityID || !intersection.editableEntity) { + } else if (!intersection.entityID || !intersection.editableEntity + || (!isCameraOutsideEntity(intersection.entityID, intersection.intersection) && !isTriggerPressed)) { setState(EDITOR_SEARCHING); } else { log(side, "ERROR: Editor: Unexpected condition B in EDITOR_HIGHLIGHTING!"); From 953614fe65579f1cb8c0652dde9bc6adae0c210a Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 25 Sep 2017 23:43:32 -0700 Subject: [PATCH 430/722] More stuff out of render to the gameloop --- interface/src/Application.cpp | 72 ++++++++++++++++++++++++++--------- interface/src/Application.h | 10 ++++- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index cf4a103ad4..10e6356a83 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2482,12 +2482,6 @@ void Application::updateCamera(RenderArgs& renderArgs) { renderArgs._cameraMode = (int8_t)_myCamera.getMode(); // HACK - - // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. - // Then we can move this logic into the Avatar::simulate call. - auto myAvatar = getMyAvatar(); - myAvatar->preDisplaySide(&renderArgs); - { QMutexLocker viewLocker(&_viewMutex); renderArgs.setViewFrustum(_displayViewFrustum); @@ -2495,8 +2489,8 @@ void Application::updateCamera(RenderArgs& renderArgs) { } void Application::editRenderArgs(RenderArgsEditor editor) { - QMutexLocker viewLocker(&_renderArgsMutex); - editor(_renderArgs); + QMutexLocker renderLocker(&_renderArgsMutex); + editor(_appRenderArgs); } @@ -2534,19 +2528,19 @@ void Application::paintGL() { } // update the avatar with a fresh HMD pose - { - PROFILE_RANGE(render, "/updateAvatar"); - getMyAvatar()->updateFromHMDSensorMatrix(getHMDSensorPose()); - } + // { + // PROFILE_RANGE(render, "/updateAvatar"); + // getMyAvatar()->updateFromHMDSensorMatrix(getHMDSensorPose()); + // } - auto lodManager = DependencyManager::get(); + // auto lodManager = DependencyManager::get(); RenderArgs renderArgs; { QMutexLocker viewLocker(&_renderArgsMutex); - renderArgs = _renderArgs; + renderArgs = _appRenderArgs._renderArgs; } - +/* float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); { PROFILE_RANGE(render, "/buildFrustrumAndArgs"); @@ -2568,7 +2562,7 @@ void Application::paintGL() { renderArgs.setViewFrustum(_viewFrustum); } } - +*/ { PROFILE_RANGE(render, "/resizeGL"); PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); @@ -2579,6 +2573,7 @@ void Application::paintGL() { { PROFILE_RANGE(render, "/gpuContextReset"); + // _gpuContext->beginFrame(getHMDSensorPose()); _gpuContext->beginFrame(getHMDSensorPose()); // Reset the gpu::Context Stages // Back to the default framebuffer; @@ -5375,8 +5370,49 @@ void Application::update(float deltaTime) { _postUpdateLambdas.clear(); } - editRenderArgs([this](RenderArgs& renderArgs) { - this->updateCamera(renderArgs); + editRenderArgs([this](AppRenderArgs& appRenderArgs) { + + appRenderArgs._eyeToWorld = getHMDSensorPose(); + + auto myAvatar = getMyAvatar(); + + + // update the avatar with a fresh HMD pose + { + PROFILE_RANGE(render, "/updateAvatar"); + myAvatar->updateFromHMDSensorMatrix(appRenderArgs._eyeToWorld); + } + + auto lodManager = DependencyManager::get(); + + float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); + { + PROFILE_RANGE(render, "/buildFrustrumAndArgs"); + { + QMutexLocker viewLocker(&_viewMutex); + // adjust near clip plane to account for sensor scaling. + auto adjustedProjection = glm::perspective(_viewFrustum.getFieldOfView(), + _viewFrustum.getAspectRatio(), + DEFAULT_NEAR_CLIP * sensorToWorldScale, + _viewFrustum.getFarClip()); + _viewFrustum.setProjection(adjustedProjection); + _viewFrustum.calculate(); + } + appRenderArgs._renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(), + lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, + RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); + { + QMutexLocker viewLocker(&_viewMutex); + appRenderArgs._renderArgs.setViewFrustum(_viewFrustum); + } + } + + this->updateCamera(appRenderArgs._renderArgs); + + // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. + // Then we can move this logic into the Avatar::simulate call. + myAvatar->preDisplaySide(&appRenderArgs._renderArgs); + }); AnimDebugDraw::getInstance().update(); diff --git a/interface/src/Application.h b/interface/src/Application.h index edb356fcef..45393cfbea 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -626,9 +626,15 @@ private: gpu::ContextPointer _gpuContext; // initialized during window creation mutable QMutex _renderArgsMutex{ QMutex::Recursive }; - render::Args _renderArgs; + struct AppRenderArgs { + render::Args _renderArgs; + glm::mat4 _eyeToWorld; + glm::mat4 _sensorToWorld; + }; + AppRenderArgs _appRenderArgs; - using RenderArgsEditor = std::function ; + + using RenderArgsEditor = std::function ; void editRenderArgs(RenderArgsEditor editor); From 67481287ff1b96edea3e58009abba7a2e26e22af Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 26 Sep 2017 09:29:43 -0700 Subject: [PATCH 431/722] comment out asserts that fail to compile in Debug --- .../entities-renderer/src/RenderableModelEntityItem.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 799a84aaee..d1e47fd906 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -318,8 +318,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { updateModelBounds(); // should never fall in here when collision model not fully loaded - // hence we assert that all geometries exist and are loaded - assert(_model && _model->isLoaded() && _compoundShapeResource && _compoundShapeResource->isLoaded()); + // TODO: assert that all geometries exist and are loaded + //assert(_model && _model->isLoaded() && _compoundShapeResource && _compoundShapeResource->isLoaded()); const FBXGeometry& collisionGeometry = _compoundShapeResource->getFBXGeometry(); ShapeInfo::PointCollection& pointCollection = shapeInfo.getPointCollection(); @@ -407,8 +407,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { } shapeInfo.setParams(type, dimensions, getCompoundShapeURL()); } else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) { - // should never fall in here when model not fully loaded - assert(_model && _model->isLoaded()); + // TODO: assert we never fall in here when model not fully loaded + //assert(_model && _model->isLoaded()); updateModelBounds(); model->updateGeometry(); From 2cc8a55151986e34fb155b6e5ff129d492bdccbf Mon Sep 17 00:00:00 2001 From: druiz17 Date: Tue, 26 Sep 2017 10:37:36 -0700 Subject: [PATCH 432/722] fix cloning equiping --- scripts/system/controllers/controllerModules/equipEntity.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index 3431f8d3c3..29db02c6de 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -499,6 +499,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var cloneID = this.cloneHotspot(grabbedProperties, controllerData); this.targetEntityID = cloneID; Entities.editEntity(this.targetEntityID, reparentProps); + controllerData.nearbyEntityPropertiesByID[this.targetEntityID] = grabbedProperties; isClone = true; } else if (!grabbedProperties.locked) { Entities.editEntity(this.targetEntityID, reparentProps); From b77ceab661715c851f4213ad2d92f48277846d76 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 26 Sep 2017 10:39:16 -0700 Subject: [PATCH 433/722] remove noisy logging added by auto-baking --- .../model-networking/src/model-networking/ModelCache.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 74c8d06736..9bfd7d9a85 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -241,7 +241,6 @@ private: }; void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { - qDebug() << "Processing geometry: " << _effectiveBaseURL; _url = _effectiveBaseURL; _textureBaseUrl = _effectiveBaseURL; QThreadPool::globalInstance()->start(new GeometryReader(_self, _effectiveBaseURL, _mapping, data, _combineParts)); @@ -255,7 +254,6 @@ void GeometryDefinitionResource::setGeometryDefinition(FBXGeometry::Pointer fbxG QHash materialIDAtlas; for (const FBXMaterial& material : _fbxGeometry->materials) { materialIDAtlas[material.materialID] = _materials.size(); - qDebug() << "setGeometryDefinition() " << _textureBaseUrl; _materials.push_back(std::make_shared(material, _textureBaseUrl)); } @@ -348,7 +346,6 @@ Geometry::Geometry(const Geometry& geometry) { _materials.reserve(geometry._materials.size()); for (const auto& material : geometry._materials) { - qDebug() << "Geometry() no base url..."; _materials.push_back(std::make_shared(*material)); } @@ -434,7 +431,6 @@ void GeometryResource::deleter() { void GeometryResource::setTextures() { if (_fbxGeometry) { for (const FBXMaterial& material : _fbxGeometry->materials) { - qDebug() << "setTextures() " << _textureBaseUrl; _materials.push_back(std::make_shared(material, _textureBaseUrl)); } } @@ -536,7 +532,6 @@ model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, image NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl) : model::Material(*material._material) { - qDebug() << "Created network material with base url: " << textureBaseUrl; _textures = Textures(MapChannel::NUM_MAP_CHANNELS); if (!material.albedoTexture.filename.isEmpty()) { auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); From 669af73a961c67b66ba86b64a36a6cbc1a9340e3 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 26 Sep 2017 10:47:24 -0700 Subject: [PATCH 434/722] Bringing as much as possible out from render to game --- interface/src/Application.cpp | 7 ++++++- interface/src/Application.h | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 10e6356a83..d4c0837989 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2536,9 +2536,13 @@ void Application::paintGL() { // auto lodManager = DependencyManager::get(); RenderArgs renderArgs; + float sensorToWorldScale; + glm::mat4 HMDSensorPose; { QMutexLocker viewLocker(&_renderArgsMutex); renderArgs = _appRenderArgs._renderArgs; + HMDSensorPose = _appRenderArgs._eyeToWorld; + sensorToWorldScale = _appRenderArgs._sensorToWorldScale; } /* float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); @@ -2574,7 +2578,7 @@ void Application::paintGL() { { PROFILE_RANGE(render, "/gpuContextReset"); // _gpuContext->beginFrame(getHMDSensorPose()); - _gpuContext->beginFrame(getHMDSensorPose()); + _gpuContext->beginFrame(HMDSensorPose); // Reset the gpu::Context Stages // Back to the default framebuffer; gpu::doInBatch(_gpuContext, [&](gpu::Batch& batch) { @@ -5386,6 +5390,7 @@ void Application::update(float deltaTime) { auto lodManager = DependencyManager::get(); float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); + appRenderArgs._sensorToWorldScale = sensorToWorldScale; { PROFILE_RANGE(render, "/buildFrustrumAndArgs"); { diff --git a/interface/src/Application.h b/interface/src/Application.h index 45393cfbea..be3fa19400 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -630,6 +630,7 @@ private: render::Args _renderArgs; glm::mat4 _eyeToWorld; glm::mat4 _sensorToWorld; + float _sensorToWorldScale { 1.0f }; }; AppRenderArgs _appRenderArgs; From 737b583745d1715dd6c19c8c3cfd4dc99a7c1d07 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 26 Sep 2017 11:08:33 -0700 Subject: [PATCH 435/722] avoid unequipping things during a HMD snap-turn --- .../system/controllers/controllerDispatcher.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 0a275e10d0..f0e9a89d9b 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -10,7 +10,7 @@ /* global Script, Entities, Overlays, Controller, Vec3, Quat, getControllerWorldLocation, RayPick, controllerDispatcherPlugins:true, controllerDispatcherPluginsNeedSort:true, LEFT_HAND, RIGHT_HAND, NEAR_GRAB_PICK_RADIUS, DEFAULT_SEARCH_SPHERE_DISTANCE, DISPATCHER_PROPERTIES, - getGrabPointSphereOffset, HMD, MyAvatar, Messages + getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities */ controllerDispatcherPlugins = {}; @@ -27,7 +27,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ; var PROFILE = false; - var DEBUG = false; + var DEBUG = true; if (typeof Test !== "undefined") { PROFILE = true; @@ -266,6 +266,20 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); }); } + // sometimes, during a HMD snap-turn, an equipped or held item wont be near + // the hand when the findEntities is done. Gather up any hand-children here. + for (h = LEFT_HAND; h <= RIGHT_HAND; h++) { + var handChildrenIDs = findHandChildEntities(h); + handChildrenIDs.forEach(function (handChildID) { + if (handChildID in nearbyEntityPropertiesByID) { + return; + } + var props = Entities.getEntityProperties(handChildID, DISPATCHER_PROPERTIES); + props.id = handChildID; + nearbyEntityPropertiesByID[handChildID] = props; + }); + } + // bundle up all the data about the current situation var controllerData = { triggerValues: [_this.leftTriggerValue, _this.rightTriggerValue], From c26d04dca159f91b8a73c4d7d949e0d1090316fd Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 26 Sep 2017 13:58:00 -0700 Subject: [PATCH 436/722] Teleport.js: Fix message bugs, and made eslint clean * 'Hifi-Teleport-Ignore-Add' and 'Hifi-Teleport-Ignore-Remove' messages should now should work * 'Hifi-Teleport-Disabler' message should now work --- .../controllers/controllerModules/teleport.js | 692 ++++++++---------- 1 file changed, 319 insertions(+), 373 deletions(-) diff --git a/scripts/system/controllers/controllerModules/teleport.js b/scripts/system/controllers/controllerModules/teleport.js index 548179761c..31c5a42a2c 100644 --- a/scripts/system/controllers/controllerModules/teleport.js +++ b/scripts/system/controllers/controllerModules/teleport.js @@ -14,7 +14,6 @@ enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters, makeRunningValues, Vec3, LaserPointers, RayPick, HMD, Uuid, AvatarList */ -/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ Script.include("/~/system/libraries/Xform.js"); Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -22,388 +21,354 @@ Script.include("/~/system/libraries/controllers.js"); (function() { // BEGIN LOCAL_SCOPE -var inTeleportMode = false; + var TARGET_MODEL_URL = Script.resolvePath("../../assets/models/teleport-destination.fbx"); + var TOO_CLOSE_MODEL_URL = Script.resolvePath("../../assets/models/teleport-cancel.fbx"); + var SEAT_MODEL_URL = Script.resolvePath("../../assets/models/teleport-seat.fbx"); -var SMOOTH_ARRIVAL_SPACING = 33; -var NUMBER_OF_STEPS = 6; - -var TARGET_MODEL_URL = Script.resolvePath("../../assets/models/teleport-destination.fbx"); -var TOO_CLOSE_MODEL_URL = Script.resolvePath("../../assets/models/teleport-cancel.fbx"); -var SEAT_MODEL_URL = Script.resolvePath("../../assets/models/teleport-seat.fbx"); - -var TARGET_MODEL_DIMENSIONS = { - x: 1.15, - y: 0.5, - z: 1.15 -}; - -var COLORS_TELEPORT_SEAT = { - red: 255, - green: 0, - blue: 170 -}; - -var COLORS_TELEPORT_CAN_TELEPORT = { - red: 97, - green: 247, - blue: 255 -}; - -var COLORS_TELEPORT_CANCEL = { - red: 255, - green: 184, - blue: 73 -}; - -var TELEPORT_CANCEL_RANGE = 1; -var COOL_IN_DURATION = 500; - -var handInfo = { - right: { - controllerInput: Controller.Standard.RightHand - }, - left: { - controllerInput: Controller.Standard.LeftHand - } -}; - -var cancelPath = { - type: "line3d", - color: COLORS_TELEPORT_CANCEL, - ignoreRayIntersection: true, - alpha: 1, - solid: true, - drawInFront: true, - glow: 1.0 -}; -var teleportPath = { - type: "line3d", - color: COLORS_TELEPORT_CAN_TELEPORT, - ignoreRayIntersection: true, - alpha: 1, - solid: true, - drawInFront: true, - glow: 1.0 -}; -var seatPath = { - type: "line3d", - color: COLORS_TELEPORT_SEAT, - ignoreRayIntersection: true, - alpha: 1, - solid: true, - drawInFront: true, - glow: 1.0 -}; -var cancelEnd = { - type: "model", - url: TOO_CLOSE_MODEL_URL, - dimensions: TARGET_MODEL_DIMENSIONS, - ignoreRayIntersection: true -}; -var teleportEnd = { - type: "model", - url: TARGET_MODEL_URL, - dimensions: TARGET_MODEL_DIMENSIONS, - ignoreRayIntersection: true -}; -var seatEnd = { - type: "model", - url: SEAT_MODEL_URL, - dimensions: TARGET_MODEL_DIMENSIONS, - ignoreRayIntersection: true -}; - -var teleportRenderStates = [{name: "cancel", path: cancelPath, end: cancelEnd}, - {name: "teleport", path: teleportPath, end: teleportEnd}, - {name: "seat", path: seatPath, end: seatEnd}]; - -var DEFAULT_DISTANCE = 50; -var teleportDefaultRenderStates = [{name: "cancel", distance: DEFAULT_DISTANCE, path: cancelPath}]; - -function ThumbPad(hand) { - this.hand = hand; - var _thisPad = this; - - this.buttonPress = function(value) { - _thisPad.buttonValue = value; - }; -} - -function Trigger(hand) { - this.hand = hand; - var _this = this; - - this.buttonPress = function(value) { - _this.buttonValue = value; + var TARGET_MODEL_DIMENSIONS = { + x: 1.15, + y: 0.5, + z: 1.15 }; - this.down = function() { - var down = _this.buttonValue === 1 ? 1.0 : 0.0; - return down; - }; -} - -var coolInTimeout = null; -var ignoredEntities = []; - -var TELEPORTER_STATES = { - IDLE: 'idle', - COOL_IN: 'cool_in', - TARGETTING: 'targetting', - TARGETTING_INVALID: 'targetting_invalid', -}; - -var TARGET = { - NONE: 'none', // Not currently targetting anything - INVISIBLE: 'invisible', // The current target is an invvsible surface - INVALID: 'invalid', // The current target is invalid (wall, ceiling, etc.) - SURFACE: 'surface', // The current target is a valid surface - SEAT: 'seat', // The current target is a seat -}; - -function Teleporter(hand) { - var _this = this; - this.hand = hand; - this.buttonValue = 0; - this.active = false; - this.state = TELEPORTER_STATES.IDLE; - this.currentTarget = TARGET.INVALID; - this.currentResult = null; - - this.getOtherModule = function() { - var otherModule = this.hand === RIGHT_HAND ? leftTeleporter : rightTeleporter; - return otherModule; + var COLORS_TELEPORT_SEAT = { + red: 255, + green: 0, + blue: 170 }; - this.teleportRayHandVisible = LaserPointers.createLaserPointer({ - joint: (_this.hand === RIGHT_HAND) ? "RightHand" : "LeftHand", - filter: RayPick.PICK_ENTITIES, - faceAvatar: true, - centerEndY: false, - renderStates: teleportRenderStates, - defaultRenderStates: teleportDefaultRenderStates - }); - this.teleportRayHandInvisible = LaserPointers.createLaserPointer({ - joint: (_this.hand === RIGHT_HAND) ? "RightHand" : "LeftHand", - filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE, - faceAvatar: true, - centerEndY: false, - renderStates: teleportRenderStates - }); - this.teleportRayHeadVisible = LaserPointers.createLaserPointer({ - joint: "Avatar", - filter: RayPick.PICK_ENTITIES, - faceAvatar: true, - centerEndY: false, - renderStates: teleportRenderStates, - defaultRenderStates: teleportDefaultRenderStates - }); - this.teleportRayHeadInvisible = LaserPointers.createLaserPointer({ - joint: "Avatar", - filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE, - faceAvatar: true, - centerEndY: false, - renderStates: teleportRenderStates - }); - - this.teleporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); - this.teleportMappingInternal = Controller.newMapping(this.teleporterMappingInternalName); - - this.enableMappings = function() { - Controller.enableMapping(this.teleporterMappingInternalName); + var COLORS_TELEPORT_CAN_TELEPORT = { + red: 97, + green: 247, + blue: 255 }; - this.disableMappings = function() { - Controller.disableMapping(teleporter.teleporterMappingInternalName); + var COLORS_TELEPORT_CANCEL = { + red: 255, + green: 184, + blue: 73 }; - this.cleanup = function() { - this.disableMappings(); + var TELEPORT_CANCEL_RANGE = 1; + var COOL_IN_DURATION = 500; - LaserPointers.removeLaserPointer(this.teleportRayHandVisible); - LaserPointers.removeLaserPointer(this.teleportRayHandInvisible); - LaserPointers.removeLaserPointer(this.teleportRayHeadVisible); - LaserPointers.removeLaserPointer(this.teleportRayHeadInvisible); - }; - - this.buttonPress = function(value) { - _this.buttonValue = value; - }; - - this.parameters = makeDispatcherModuleParameters( - 80, - this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], - [], - 100); - - this.enterTeleport = function() { - if (coolInTimeout !== null) { - Script.clearTimeout(coolInTimeout); + var handInfo = { + right: { + controllerInput: Controller.Standard.RightHand + }, + left: { + controllerInput: Controller.Standard.LeftHand } + }; - this.state = TELEPORTER_STATES.COOL_IN; - coolInTimeout = Script.setTimeout(function() { - if (_this.state === TELEPORTER_STATES.COOL_IN) { - _this.state = TELEPORTER_STATES.TARGETTING; + var cancelPath = { + type: "line3d", + color: COLORS_TELEPORT_CANCEL, + ignoreRayIntersection: true, + alpha: 1, + solid: true, + drawInFront: true, + glow: 1.0 + }; + var teleportPath = { + type: "line3d", + color: COLORS_TELEPORT_CAN_TELEPORT, + ignoreRayIntersection: true, + alpha: 1, + solid: true, + drawInFront: true, + glow: 1.0 + }; + var seatPath = { + type: "line3d", + color: COLORS_TELEPORT_SEAT, + ignoreRayIntersection: true, + alpha: 1, + solid: true, + drawInFront: true, + glow: 1.0 + }; + var cancelEnd = { + type: "model", + url: TOO_CLOSE_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + ignoreRayIntersection: true + }; + var teleportEnd = { + type: "model", + url: TARGET_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + ignoreRayIntersection: true + }; + var seatEnd = { + type: "model", + url: SEAT_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + ignoreRayIntersection: true + }; + + var teleportRenderStates = [{name: "cancel", path: cancelPath, end: cancelEnd}, + {name: "teleport", path: teleportPath, end: teleportEnd}, + {name: "seat", path: seatPath, end: seatEnd}]; + + var DEFAULT_DISTANCE = 50; + var teleportDefaultRenderStates = [{name: "cancel", distance: DEFAULT_DISTANCE, path: cancelPath}]; + + var coolInTimeout = null; + var ignoredEntities = []; + + var TELEPORTER_STATES = { + IDLE: 'idle', + COOL_IN: 'cool_in', + TARGETTING: 'targetting', + TARGETTING_INVALID: 'targetting_invalid' + }; + + var TARGET = { + NONE: 'none', // Not currently targetting anything + INVISIBLE: 'invisible', // The current target is an invvsible surface + INVALID: 'invalid', // The current target is invalid (wall, ceiling, etc.) + SURFACE: 'surface', // The current target is a valid surface + SEAT: 'seat' // The current target is a seat + }; + + function Teleporter(hand) { + var _this = this; + this.hand = hand; + this.buttonValue = 0; + this.disabled = false; // used by the 'Hifi-Teleport-Disabler' message handler + this.active = false; + this.state = TELEPORTER_STATES.IDLE; + this.currentTarget = TARGET.INVALID; + this.currentResult = null; + + this.getOtherModule = function() { + var otherModule = this.hand === RIGHT_HAND ? leftTeleporter : rightTeleporter; + return otherModule; + }; + + this.teleportRayHandVisible = LaserPointers.createLaserPointer({ + joint: (_this.hand === RIGHT_HAND) ? "RightHand" : "LeftHand", + filter: RayPick.PICK_ENTITIES, + faceAvatar: true, + centerEndY: false, + renderStates: teleportRenderStates, + defaultRenderStates: teleportDefaultRenderStates + }); + this.teleportRayHandInvisible = LaserPointers.createLaserPointer({ + joint: (_this.hand === RIGHT_HAND) ? "RightHand" : "LeftHand", + filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + centerEndY: false, + renderStates: teleportRenderStates + }); + this.teleportRayHeadVisible = LaserPointers.createLaserPointer({ + joint: "Avatar", + filter: RayPick.PICK_ENTITIES, + faceAvatar: true, + centerEndY: false, + renderStates: teleportRenderStates, + defaultRenderStates: teleportDefaultRenderStates + }); + this.teleportRayHeadInvisible = LaserPointers.createLaserPointer({ + joint: "Avatar", + filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + centerEndY: false, + renderStates: teleportRenderStates + }); + + this.cleanup = function() { + LaserPointers.removeLaserPointer(this.teleportRayHandVisible); + LaserPointers.removeLaserPointer(this.teleportRayHandInvisible); + LaserPointers.removeLaserPointer(this.teleportRayHeadVisible); + LaserPointers.removeLaserPointer(this.teleportRayHeadInvisible); + }; + + this.buttonPress = function(value) { + _this.buttonValue = value; + }; + + this.parameters = makeDispatcherModuleParameters( + 80, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + this.enterTeleport = function() { + if (coolInTimeout !== null) { + Script.clearTimeout(coolInTimeout); } - }, COOL_IN_DURATION); - // pad scale with avatar size - var AVATAR_PROPORTIONAL_TARGET_MODEL_DIMENSIONS = Vec3.multiply(MyAvatar.sensorToWorldScale, TARGET_MODEL_DIMENSIONS); + this.state = TELEPORTER_STATES.COOL_IN; + coolInTimeout = Script.setTimeout(function() { + if (_this.state === TELEPORTER_STATES.COOL_IN) { + _this.state = TELEPORTER_STATES.TARGETTING; + } + }, COOL_IN_DURATION); - if (!Vec3.equal(AVATAR_PROPORTIONAL_TARGET_MODEL_DIMENSIONS, cancelEnd.dimensions)) { - cancelEnd.dimensions = AVATAR_PROPORTIONAL_TARGET_MODEL_DIMENSIONS; - teleportEnd.dimensions = AVATAR_PROPORTIONAL_TARGET_MODEL_DIMENSIONS; - seatEnd.dimensions = AVATAR_PROPORTIONAL_TARGET_MODEL_DIMENSIONS; + // pad scale with avatar size + var AVATAR_PROPORTIONAL_TARGET_MODEL_DIMENSIONS = Vec3.multiply(MyAvatar.sensorToWorldScale, TARGET_MODEL_DIMENSIONS); - teleportRenderStates = [{name: "cancel", path: cancelPath, end: cancelEnd}, - {name: "teleport", path: teleportPath, end: teleportEnd}, - {name: "seat", path: seatPath, end: seatEnd}]; + if (!Vec3.equal(AVATAR_PROPORTIONAL_TARGET_MODEL_DIMENSIONS, cancelEnd.dimensions)) { + cancelEnd.dimensions = AVATAR_PROPORTIONAL_TARGET_MODEL_DIMENSIONS; + teleportEnd.dimensions = AVATAR_PROPORTIONAL_TARGET_MODEL_DIMENSIONS; + seatEnd.dimensions = AVATAR_PROPORTIONAL_TARGET_MODEL_DIMENSIONS; - LaserPointers.editRenderState(this.teleportRayHandVisible, "cancel", teleportRenderStates[0]); - LaserPointers.editRenderState(this.teleportRayHandInvisible, "cancel", teleportRenderStates[0]); - LaserPointers.editRenderState(this.teleportRayHeadVisible, "cancel", teleportRenderStates[0]); - LaserPointers.editRenderState(this.teleportRayHeadInvisible, "cancel", teleportRenderStates[0]); + teleportRenderStates = [{name: "cancel", path: cancelPath, end: cancelEnd}, + {name: "teleport", path: teleportPath, end: teleportEnd}, + {name: "seat", path: seatPath, end: seatEnd}]; - LaserPointers.editRenderState(this.teleportRayHandVisible, "teleport", teleportRenderStates[1]); - LaserPointers.editRenderState(this.teleportRayHandInvisible, "teleport", teleportRenderStates[1]); - LaserPointers.editRenderState(this.teleportRayHeadVisible, "teleport", teleportRenderStates[1]); - LaserPointers.editRenderState(this.teleportRayHeadInvisible, "teleport", teleportRenderStates[1]); + LaserPointers.editRenderState(this.teleportRayHandVisible, "cancel", teleportRenderStates[0]); + LaserPointers.editRenderState(this.teleportRayHandInvisible, "cancel", teleportRenderStates[0]); + LaserPointers.editRenderState(this.teleportRayHeadVisible, "cancel", teleportRenderStates[0]); + LaserPointers.editRenderState(this.teleportRayHeadInvisible, "cancel", teleportRenderStates[0]); - LaserPointers.editRenderState(this.teleportRayHandVisible, "seat", teleportRenderStates[2]); - LaserPointers.editRenderState(this.teleportRayHandInvisible, "seat", teleportRenderStates[2]); - LaserPointers.editRenderState(this.teleportRayHeadVisible, "seat", teleportRenderStates[2]); - LaserPointers.editRenderState(this.teleportRayHeadInvisible, "seat", teleportRenderStates[2]); - } - }; + LaserPointers.editRenderState(this.teleportRayHandVisible, "teleport", teleportRenderStates[1]); + LaserPointers.editRenderState(this.teleportRayHandInvisible, "teleport", teleportRenderStates[1]); + LaserPointers.editRenderState(this.teleportRayHeadVisible, "teleport", teleportRenderStates[1]); + LaserPointers.editRenderState(this.teleportRayHeadInvisible, "teleport", teleportRenderStates[1]); - this.isReady = function(controllerData, deltaTime) { - var otherModule = this.getOtherModule(); - if (_this.buttonValue !== 0 && !otherModule.active) { - this.active = true; - this.enterTeleport(); - return makeRunningValues(true, [], []); - } - return makeRunningValues(false, [], []); - }; + LaserPointers.editRenderState(this.teleportRayHandVisible, "seat", teleportRenderStates[2]); + LaserPointers.editRenderState(this.teleportRayHandInvisible, "seat", teleportRenderStates[2]); + LaserPointers.editRenderState(this.teleportRayHeadVisible, "seat", teleportRenderStates[2]); + LaserPointers.editRenderState(this.teleportRayHeadInvisible, "seat", teleportRenderStates[2]); + } + }; - this.run = function(controllerData, deltaTime) { - //_this.state = TELEPORTER_STATES.TARGETTING; + this.isReady = function(controllerData, deltaTime) { + var otherModule = this.getOtherModule(); + if (!this.disabled && this.buttonValue !== 0 && !otherModule.active) { + this.active = true; + this.enterTeleport(); + return makeRunningValues(true, [], []); + } + return makeRunningValues(false, [], []); + }; - // Get current hand pose information to see if the pose is valid - var pose = Controller.getPoseValue(handInfo[(_this.hand === RIGHT_HAND) ? 'right' : 'left'].controllerInput); - var mode = pose.valid ? _this.hand : 'head'; - if (!pose.valid) { + this.run = function(controllerData, deltaTime) { + + // Get current hand pose information to see if the pose is valid + var pose = Controller.getPoseValue(handInfo[(_this.hand === RIGHT_HAND) ? 'right' : 'left'].controllerInput); + var mode = pose.valid ? _this.hand : 'head'; + if (!pose.valid) { + LaserPointers.disableLaserPointer(_this.teleportRayHandVisible); + LaserPointers.disableLaserPointer(_this.teleportRayHandInvisible); + LaserPointers.enableLaserPointer(_this.teleportRayHeadVisible); + LaserPointers.enableLaserPointer(_this.teleportRayHeadInvisible); + } else { + LaserPointers.enableLaserPointer(_this.teleportRayHandVisible); + LaserPointers.enableLaserPointer(_this.teleportRayHandInvisible); + LaserPointers.disableLaserPointer(_this.teleportRayHeadVisible); + LaserPointers.disableLaserPointer(_this.teleportRayHeadInvisible); + } + + // We do up to 2 ray picks to find a teleport location. + // There are 2 types of teleport locations we are interested in: + // 1. A visible floor. This can be any entity surface that points within some degree of "up" + // 2. A seat. The seat can be visible or invisible. + // + // * In the first pass we pick against visible and invisible entities so that we can find invisible seats. + // We might hit an invisible entity that is not a seat, so we need to do a second pass. + // * In the second pass we pick against visible entities only. + // + var result; + if (mode === 'head') { + result = LaserPointers.getPrevRayPickResult(_this.teleportRayHeadInvisible); + } else { + result = LaserPointers.getPrevRayPickResult(_this.teleportRayHandInvisible); + } + + var teleportLocationType = getTeleportTargetType(result); + if (teleportLocationType === TARGET.INVISIBLE) { + if (mode === 'head') { + result = LaserPointers.getPrevRayPickResult(_this.teleportRayHeadVisible); + } else { + result = LaserPointers.getPrevRayPickResult(_this.teleportRayHandVisible); + } + teleportLocationType = getTeleportTargetType(result); + } + + if (teleportLocationType === TARGET.NONE) { + // Use the cancel default state + this.setTeleportState(mode, "cancel", ""); + } else if (teleportLocationType === TARGET.INVALID || teleportLocationType === TARGET.INVISIBLE) { + this.setTeleportState(mode, "", "cancel"); + } else if (teleportLocationType === TARGET.SURFACE) { + if (this.state === TELEPORTER_STATES.COOL_IN) { + this.setTeleportState(mode, "cancel", ""); + } else { + this.setTeleportState(mode, "teleport", ""); + } + } else if (teleportLocationType === TARGET.SEAT) { + this.setTeleportState(mode, "", "seat"); + } + return this.teleport(result, teleportLocationType); + }; + + this.teleport = function(newResult, target) { + var result = newResult; + if (_this.buttonValue !== 0) { + return makeRunningValues(true, [], []); + } + + if (target === TARGET.NONE || target === TARGET.INVALID || this.state === TELEPORTER_STATES.COOL_IN) { + // Do nothing + } else if (target === TARGET.SEAT) { + Entities.callEntityMethod(result.objectID, 'sit'); + } else if (target === TARGET.SURFACE) { + var offset = getAvatarFootOffset(); + result.intersection.y += offset; + MyAvatar.goToLocation(result.intersection, false, {x: 0, y: 0, z: 0, w: 1}, false); + HMD.centerUI(); + MyAvatar.centerBody(); + } + + this.disableLasers(); + this.active = false; + return makeRunningValues(false, [], []); + }; + + this.disableLasers = function() { LaserPointers.disableLaserPointer(_this.teleportRayHandVisible); LaserPointers.disableLaserPointer(_this.teleportRayHandInvisible); - LaserPointers.enableLaserPointer(_this.teleportRayHeadVisible); - LaserPointers.enableLaserPointer(_this.teleportRayHeadInvisible); - } else { - LaserPointers.enableLaserPointer(_this.teleportRayHandVisible); - LaserPointers.enableLaserPointer(_this.teleportRayHandInvisible); LaserPointers.disableLaserPointer(_this.teleportRayHeadVisible); LaserPointers.disableLaserPointer(_this.teleportRayHeadInvisible); - } + }; - // We do up to 2 ray picks to find a teleport location. - // There are 2 types of teleport locations we are interested in: - // 1. A visible floor. This can be any entity surface that points within some degree of "up" - // 2. A seat. The seat can be visible or invisible. - // - // * In the first pass we pick against visible and invisible entities so that we can find invisible seats. - // We might hit an invisible entity that is not a seat, so we need to do a second pass. - // * In the second pass we pick against visible entities only. - // - var result; - if (mode === 'head') { - result = LaserPointers.getPrevRayPickResult(_this.teleportRayHeadInvisible); - } else { - result = LaserPointers.getPrevRayPickResult(_this.teleportRayHandInvisible); - } - - var teleportLocationType = getTeleportTargetType(result); - if (teleportLocationType === TARGET.INVISIBLE) { + this.setTeleportState = function(mode, visibleState, invisibleState) { if (mode === 'head') { - result = LaserPointers.getPrevRayPickResult(_this.teleportRayHeadVisible); + LaserPointers.setRenderState(_this.teleportRayHeadVisible, visibleState); + LaserPointers.setRenderState(_this.teleportRayHeadInvisible, invisibleState); } else { - result = LaserPointers.getPrevRayPickResult(_this.teleportRayHandVisible); + LaserPointers.setRenderState(_this.teleportRayHandVisible, visibleState); + LaserPointers.setRenderState(_this.teleportRayHandInvisible, invisibleState); } - teleportLocationType = getTeleportTargetType(result); - } + }; - if (teleportLocationType === TARGET.NONE) { - // Use the cancel default state - this.setTeleportState(mode, "cancel", ""); - } else if (teleportLocationType === TARGET.INVALID || teleportLocationType === TARGET.INVISIBLE) { - this.setTeleportState(mode, "", "cancel"); - } else if (teleportLocationType === TARGET.SURFACE) { - if (this.state === TELEPORTER_STATES.COOL_IN) { - this.setTeleportState(mode, "cancel", ""); - } else { - this.setTeleportState(mode, "teleport", ""); - } - } else if (teleportLocationType === TARGET.SEAT) { - this.setTeleportState(mode, "", "seat"); - } - return this.teleport(result, teleportLocationType); - }; - - this.teleport = function(newResult, target) { - var result = newResult; - if (_this.buttonValue !== 0) { - return makeRunningValues(true, [], []); - } - - if (target === TARGET.NONE || target === TARGET.INVALID || this.state === TELEPORTER_STATES.COOL_IN) { - // Do nothing - } else if (target === TARGET.SEAT) { - Entities.callEntityMethod(result.objectID, 'sit'); - } else if (target === TARGET.SURFACE) { - var offset = getAvatarFootOffset(); - result.intersection.y += offset; - MyAvatar.goToLocation(result.intersection, false, {x: 0, y: 0, z: 0, w: 1}, false); - HMD.centerUI(); - MyAvatar.centerBody(); - } - - this.disableLasers(); - this.active = false; - return makeRunningValues(false, [], []); - }; - - this.disableLasers = function() { - LaserPointers.disableLaserPointer(_this.teleportRayHandVisible); - LaserPointers.disableLaserPointer(_this.teleportRayHandInvisible); - LaserPointers.disableLaserPointer(_this.teleportRayHeadVisible); - LaserPointers.disableLaserPointer(_this.teleportRayHeadInvisible); - }; - - this.setTeleportState = function(mode, visibleState, invisibleState) { - if (mode === 'head') { - LaserPointers.setRenderState(_this.teleportRayHeadVisible, visibleState); - LaserPointers.setRenderState(_this.teleportRayHeadInvisible, invisibleState); - } else { - LaserPointers.setRenderState(_this.teleportRayHandVisible, visibleState); - LaserPointers.setRenderState(_this.teleportRayHandInvisible, invisibleState); - } - }; -} + this.setIgnoreEntities = function(entitiesToIgnore) { + LaserPointers.setIgnoreEntities(this.teleportRayHandVisible, entitiesToIgnore); + LaserPointers.setIgnoreEntities(this.teleportRayHandInvisible, entitiesToIgnore); + LaserPointers.setIgnoreEntities(this.teleportRayHeadVisible, entitiesToIgnore); + LaserPointers.setIgnoreEntities(this.teleportRayHeadInvisible, entitiesToIgnore); + }; + } // related to repositioning the avatar after you teleport var FOOT_JOINT_NAMES = ["RightToe_End", "RightToeBase", "RightFoot"]; var DEFAULT_ROOT_TO_FOOT_OFFSET = 0.5; function getAvatarFootOffset() { - + // find a valid foot jointIndex var footJointIndex = -1; var i, l = FOOT_JOINT_NAMES.length; for (i = 0; i < l; i++) { footJointIndex = MyAvatar.getJointIndex(FOOT_JOINT_NAMES[i]); - if (footJointIndex != -1) { + if (footJointIndex !== -1) { break; } } - if (footJointIndex != -1) { + if (footJointIndex !== -1) { // default vertical offset from foot to avatar root. var footPos = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(footJointIndex); if (footPos.x === 0 && footPos.y === 0 && footPos.z === 0.0) { @@ -417,23 +382,8 @@ function Teleporter(hand) { } } - var leftPad = new ThumbPad('left'); - var rightPad = new ThumbPad('right'); - var mappingName, teleportMapping; - var TELEPORT_DELAY = 0; - - function isMoving() { - var LY = Controller.getValue(Controller.Standard.LY); - var LX = Controller.getValue(Controller.Standard.LX); - if (LY !== 0 || LX !== 0) { - return true; - } else { - return false; - } - } - function parseJSON(json) { try { return JSON.parse(json); @@ -447,10 +397,10 @@ function Teleporter(hand) { // you can't teleport there. var MAX_ANGLE_FROM_UP_TO_TELEPORT = 70; function getTeleportTargetType(result) { - if (result.type == RayPick.INTERSECTED_NONE) { + if (result.type === RayPick.INTERSECTED_NONE) { return TARGET.NONE; } - + var props = Entities.getEntityProperties(result.objectID, ['userData', 'visible']); var data = parseJSON(props.userData); if (data !== undefined && data.seat !== undefined) { @@ -482,7 +432,7 @@ function Teleporter(hand) { function registerMappings() { mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); teleportMapping = Controller.newMapping(mappingName); - + teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightTeleporter.buttonPress); teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftTeleporter.buttonPress); } @@ -502,41 +452,37 @@ function Teleporter(hand) { } Script.scriptEnding.connect(cleanup); - var setIgnoreEntities = function() { - LaserPointers.setIgnoreEntities(teleporter.teleportRayRightVisible, ignoredEntities); - LaserPointers.setIgnoreEntities(teleporter.teleportRayRightInvisible, ignoredEntities); - LaserPointers.setIgnoreEntities(teleporter.teleportRayLeftVisible, ignoredEntities); - LaserPointers.setIgnoreEntities(teleporter.teleportRayLeftInvisible, ignoredEntities); - LaserPointers.setIgnoreEntities(teleporter.teleportRayHeadVisible, ignoredEntities); - LaserPointers.setIgnoreEntities(teleporter.teleportRayHeadInvisible, ignoredEntities); - }; - - var isDisabled = false; var handleTeleportMessages = function(channel, message, sender) { if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Teleport-Disabler') { if (message === 'both') { - isDisabled = 'both'; + leftTeleporter.disabled = true; + rightTeleporter.disabled = true; } if (message === 'left') { - isDisabled = 'left'; + leftTeleporter.disabled = true; + rightTeleporter.disabled = false; } if (message === 'right') { - isDisabled = 'right'; + leftTeleporter.disabled = false; + rightTeleporter.disabled = true; } if (message === 'none') { - isDisabled = false; + leftTeleporter.disabled = false; + rightTeleporter.disabled = false; } } else if (channel === 'Hifi-Teleport-Ignore-Add' && !Uuid.isNull(message) && ignoredEntities.indexOf(message) === -1) { ignoredEntities.push(message); - setIgnoreEntities(); + leftTeleporter.setIgnoreEntities(ignoredEntities); + rightTeleporter.setIgnoreEntities(ignoredEntities); } else if (channel === 'Hifi-Teleport-Ignore-Remove' && !Uuid.isNull(message)) { var removeIndex = ignoredEntities.indexOf(message); if (removeIndex > -1) { ignoredEntities.splice(removeIndex, 1); - setIgnoreEntities(); + leftTeleporter.setIgnoreEntities(ignoredEntities); + rightTeleporter.setIgnoreEntities(ignoredEntities); } } } From 4403c27b5275e4f26d110671c0dbb0c46350b62f Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 26 Sep 2017 14:14:37 -0700 Subject: [PATCH 437/722] working checkpoint. still needs verification/hashing, etc. --- libraries/entities/src/EntityItem.cpp | 25 ++++++++++++-- libraries/entities/src/EntityItem.h | 4 --- .../entities/src/EntityItemProperties.cpp | 20 +++++------ libraries/entities/src/ModelEntityItem.cpp | 34 +++++++++++-------- .../entities/src/ParticleEffectEntityItem.cpp | 10 +++--- libraries/entities/src/ZoneEntityItem.h | 2 +- 6 files changed, 59 insertions(+), 36 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 9084a4b878..5a205742ae 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1568,6 +1568,27 @@ float EntityItem::getRadius() const { return 0.5f * glm::length(getDimensions()); } +// Checking Certifiable Properties +QString EntityItem::getStaticCertificateJSON() const { + // Produce a compact json of every non-default static certificate property, with the property names in alphabetical order. + // The static certificate properties include all an only those properties that cannot be changed without altering the identity + // of the entity as reviewed during the certification submission. + return "FIXME"; +} +QString EntityItem::getStaticCertificateHash() const { + // The base64 encoded, sha224 hash of static certificate json. + return "FIXME"; +} +bool EntityItem::verifyStaticCertificateProperties() const { + // True IIF a non-empty certificateID matches the static certificate json. + // I.e., if we can verify that the certificateID was produced by High Fidelity signing the static certificate hash. + if (_certificateID.isEmpty()) { + return false; + } + return false; // fixme +} + + void EntityItem::adjustShapeInfoByRegistration(ShapeInfo& info) const { if (_registrationPoint != ENTITY_ITEM_DEFAULT_REGISTRATION_POINT) { glm::mat4 scale = glm::scale(getDimensions()); @@ -2829,8 +2850,8 @@ type EntityItem::get##accessor() const { \ #define DEFINE_PROPERTY_SETTER(type, accessor, var) \ void EntityItem::set##accessor(const type##& value) { \ withWriteLock([&] { \ - _##var = value; \ - }); \ + _##var = value; \ + }); \ } #define DEFINE_PROPERTY_ACCESSOR(type, accessor, var) DEFINE_PROPERTY_GETTER(type, accessor, var) DEFINE_PROPERTY_SETTER(type, accessor, var) DEFINE_PROPERTY_ACCESSOR(QString, ItemName, itemName) diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index e2221ac58a..a6153c0234 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -327,10 +327,6 @@ public: QString getStaticCertificateJSON() const; QString getStaticCertificateHash() const; bool verifyStaticCertificateProperties() const; - QString getVerifiedCertificateId(); - - bool getShouldHighlight() const; - void setShouldHighlight(const bool value); // TODO: get rid of users of getRadius()... float getRadius() const; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 4e01bffe5f..007c5dcc6c 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -1394,6 +1394,11 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem properties.getType() == EntityTypes::Sphere) { APPEND_ENTITY_PROPERTY(PROP_SHAPE, properties.getShape()); } + APPEND_ENTITY_PROPERTY(PROP_NAME, properties.getName()); + APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, properties.getCollisionSoundURL()); + APPEND_ENTITY_PROPERTY(PROP_ACTION_DATA, properties.getActionData()); + APPEND_ENTITY_PROPERTY(PROP_ALPHA, properties.getAlpha()); + // Certifiable Properties APPEND_ENTITY_PROPERTY(PROP_ITEM_NAME, properties.getItemName()); APPEND_ENTITY_PROPERTY(PROP_ITEM_DESCRIPTION, properties.getItemDescription()); @@ -1405,11 +1410,6 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_EDITION_NUMBER, properties.getEditionNumber()); APPEND_ENTITY_PROPERTY(PROP_ENTITY_INSTANCE_NUMBER, properties.getEntityInstanceNumber()); APPEND_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, properties.getCertificateID()); - - APPEND_ENTITY_PROPERTY(PROP_NAME, properties.getName()); - APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, properties.getCollisionSoundURL()); - APPEND_ENTITY_PROPERTY(PROP_ACTION_DATA, properties.getActionData()); - APPEND_ENTITY_PROPERTY(PROP_ALPHA, properties.getAlpha()); } if (propertyCount > 0) { @@ -1703,6 +1703,11 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHAPE, QString, setShape); } + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_NAME, QString, setName); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ACTION_DATA, QByteArray, setActionData); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ALPHA, float, setAlpha); + // Certifiable Properties READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_NAME, QString, setItemName); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_DESCRIPTION, QString, setItemDescription); @@ -1715,11 +1720,6 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ENTITY_INSTANCE_NUMBER, quint32, setEntityInstanceNumber); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CERTIFICATE_ID, QString, setCertificateID); - READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_NAME, QString, setName); - READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); - READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ACTION_DATA, QByteArray, setActionData); - READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ALPHA, float, setAlpha); - return valid; } diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index b02cf04651..b50ed008a7 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -215,16 +215,18 @@ void ModelEntityItem::debugDump() const { } void ModelEntityItem::setShapeType(ShapeType type) { - if (type != _shapeType) { - if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { - // dynamic and STATIC_MESH are incompatible - // since the shape is being set here we clear the dynamic bit - _dynamic = false; - _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + withWriteLock([&] { + if (type != _shapeType) { + if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { + // dynamic and STATIC_MESH are incompatible + // since the shape is being set here we clear the dynamic bit + _dynamic = false; + _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + } + _shapeType = type; + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; } - _shapeType = type; - _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; - } + }); } ShapeType ModelEntityItem::getShapeType() const { @@ -257,13 +259,15 @@ void ModelEntityItem::setModelURL(const QString& url) { } void ModelEntityItem::setCompoundShapeURL(const QString& url) { - if (_compoundShapeURL != url) { - ShapeType oldType = computeTrueShapeType(); - _compoundShapeURL = url; - if (oldType != computeTrueShapeType()) { - _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + withWriteLock([&] { + if (_compoundShapeURL != url) { + ShapeType oldType = computeTrueShapeType(); + _compoundShapeURL = url; + if (oldType != computeTrueShapeType()) { + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + } } - } + }); } void ModelEntityItem::setAnimationURL(const QString& url) { diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index 9bbf4323da..c6616f8cd3 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -633,10 +633,12 @@ void ParticleEffectEntityItem::debugDump() const { } void ParticleEffectEntityItem::setShapeType(ShapeType type) { - if (type != _shapeType) { - _shapeType = type; - _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; - } + withWriteLock([&] { + if (type != _shapeType) { + _shapeType = type; + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + } + }); } void ParticleEffectEntityItem::setMaxParticles(quint32 maxParticles) { diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 14e7cd2f40..c3be53599c 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -56,7 +56,7 @@ public: static void setDrawZoneBoundaries(bool value) { _drawZoneBoundaries = value; } virtual bool isReadyToComputeShape() const override { return false; } - void setShapeType(ShapeType type) override { _shapeType = type; } + void setShapeType(ShapeType type) override { withWriteLock([&] { _shapeType = type; }); } virtual ShapeType getShapeType() const override; virtual bool hasCompoundShapeURL() const; From f081c36f25d1715898b42743b683c12b9c8e2829 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 26 Sep 2017 14:19:31 -0700 Subject: [PATCH 438/722] ViveControllerManager: Code review feedback on PR #11422 --- plugins/openvr/src/ViveControllerManager.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 81173722fb..430dc193a3 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -708,8 +708,6 @@ controller::Pose ViveControllerManager::InputDevice::addOffsetToPuckPose(const c puckPoseIter++; } - //auto puckPoseIter = _poseStateMap.find(puckIndex); - if (puckPoseIter != _validTrackedObjects.end()) { glm::mat4 postMat; // identity From a7cfb5d6351ea55ef50c1bbf6da9fc6c659130ba Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 26 Sep 2017 14:21:25 -0700 Subject: [PATCH 439/722] teleport.js: fix for TELEPORT_CANCEL_RANGE with large/small avatar scale --- scripts/system/controllers/controllerModules/teleport.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/controllerModules/teleport.js b/scripts/system/controllers/controllerModules/teleport.js index 31c5a42a2c..d2717a1348 100644 --- a/scripts/system/controllers/controllerModules/teleport.js +++ b/scripts/system/controllers/controllerModules/teleport.js @@ -50,7 +50,7 @@ Script.include("/~/system/libraries/controllers.js"); }; var TELEPORT_CANCEL_RANGE = 1; - var COOL_IN_DURATION = 500; + var COOL_IN_DURATION = 300; var handInfo = { right: { @@ -422,7 +422,7 @@ Script.include("/~/system/libraries/controllers.js"); if (angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) || angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) || - Vec3.distance(MyAvatar.position, result.intersection) <= TELEPORT_CANCEL_RANGE) { + Vec3.distance(MyAvatar.position, result.intersection) <= TELEPORT_CANCEL_RANGE * MyAvatar.sensorToWorldScale) { return TARGET.INVALID; } else { return TARGET.SURFACE; From 76c1fe688c1b7330b99c9d78c94fa3f0cde9c175 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Tue, 26 Sep 2017 14:52:01 -0700 Subject: [PATCH 440/722] fix auto dropping --- .../controllerModules/nearParentGrabEntity.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js index e0bb596253..837e63c341 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -148,9 +148,12 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); if (now - this.lastUnequipCheckTime > MSECS_PER_SEC * TEAR_AWAY_CHECK_TIME) { this.lastUnequipCheckTime = now; if (props.parentID == AVATAR_SELF_ID) { + var sensorScaleFactor = MyAvatar.sensorToWorldScale; var handPosition = controllerData.controllerLocations[this.hand].position; var dist = distanceBetweenPointAndEntityBoundingBox(handPosition, props); - if (dist > TEAR_AWAY_DISTANCE) { + var distance = Vec3.distance(props.position, handPosition); + if ((dist > TEAR_AWAY_DISTANCE) || + (distance > NEAR_GRAB_RADIUS * sensorScaleFactor)) { this.autoUnequipCounter++; } else { this.autoUnequipCounter = 0; @@ -214,8 +217,10 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); for (var i = 0; i < nearbyEntityProperties.length; i++) { var props = nearbyEntityProperties[i]; var handPosition = controllerData.controllerLocations[this.hand].position; - var distance = Vec3.distance(props.position, handPosition); - if (distance > NEAR_GRAB_RADIUS * sensorScaleFactor) { + var dist = distanceBetweenPointAndEntityBoundingBox(handPosition, props); + var distance = Vec3.distance(handPosition, props.position); + if ((dist > TEAR_AWAY_DISTANCE) || + (distance > NEAR_GRAB_RADIUS * sensorScaleFactor)) { continue; } if (entityIsGrabbable(props)) { From 5a1242a1acde2e344ba3ef3f8753ea447f7688d2 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Tue, 26 Sep 2017 15:06:31 -0700 Subject: [PATCH 441/722] fix spelling error --- .../controllers/controllerModules/nearParentGrabEntity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js index 837e63c341..9323f651a2 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -187,7 +187,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); var previousParentID = _this.previousParentID[childID]; var previousParentJointIndex = _this.previousParentJointIndex[childID]; - // The main flaw with keeping track of previous parantage in individual scripts is: + // The main flaw with keeping track of previous parentage in individual scripts is: // (1) A grabs something (2) B takes it from A (3) A takes it from B (4) A releases it // now A and B will take turns passing it back to the other. Detect this and stop the loop here... var UNHOOK_LOOP_DETECT_MS = 200; From 644532dd5949439f46346383784b3ea7b21808ad Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 26 Sep 2017 15:17:23 -0700 Subject: [PATCH 442/722] only send startNearTrigger once --- .../controllers/controllerModules/nearTrigger.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/system/controllers/controllerModules/nearTrigger.js b/scripts/system/controllers/controllerModules/nearTrigger.js index edea1f993d..cb913bc735 100644 --- a/scripts/system/controllers/controllerModules/nearTrigger.js +++ b/scripts/system/controllers/controllerModules/nearTrigger.js @@ -26,6 +26,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.previousParentID = {}; this.previousParentJointIndex = {}; this.previouslyUnhooked = {}; + this.startSent = false; this.parameters = makeDispatcherModuleParameters( 520, @@ -76,7 +77,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); var targetProps = this.getTargetProps(controllerData); if (targetProps) { this.targetEntityID = targetProps.id; - this.startNearTrigger(controllerData); return makeRunningValues(true, [this.targetEntityID], []); } else { return makeRunningValues(false, [], []); @@ -84,12 +84,16 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); }; this.run = function (controllerData) { - if (controllerData.triggerClicks[this.hand] === 0) { + if (!this.startSent) { + this.startNearTrigger(controllerData); + this.startSent = true; + } else if (controllerData.triggerClicks[this.hand] === 0) { this.endNearTrigger(controllerData); + this.startSent = false; return makeRunningValues(false, [], []); + } else { + this.continueNearTrigger(controllerData); } - - this.continueNearTrigger(controllerData); return makeRunningValues(true, [this.targetEntityID], []); }; From badb45c921999a96e3303a4bda8d3b646142c28b Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 26 Sep 2017 15:20:45 -0700 Subject: [PATCH 443/722] fixing the web surface now getting scaled when the table t is grabbed is fixed --- interface/src/ui/overlays/Web3DOverlay.cpp | 15 +++++++++++++-- interface/src/ui/overlays/Web3DOverlay.h | 3 +++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 0807d1c117..a3f470aa62 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -302,7 +302,6 @@ void Web3DOverlay::render(RenderArgs* args) { emit resizeWebSurface(); } - vec2 halfSize = getSize() / 2.0f; vec4 color(toGlm(getColor()), getAlpha()); if (!_texture) { @@ -318,8 +317,11 @@ void Web3DOverlay::render(RenderArgs* args) { Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; batch.setResourceTexture(0, _texture); + auto renderTransform = getRenderTransform(); - batch.setModelTransform(getRenderTransform()); + auto size = renderTransform.getScale(); + renderTransform.setScale(1.0f); + batch.setModelTransform(renderTransform); auto geometryCache = DependencyManager::get(); if (color.a < OPAQUE_ALPHA_THRESHOLD) { @@ -327,10 +329,19 @@ void Web3DOverlay::render(RenderArgs* args) { } else { geometryCache->bindWebBrowserProgram(batch); } + + vec2 halfSize = vec2(size.x, size.y) / 2.0f; geometryCache->renderQuad(batch, halfSize * -1.0f, halfSize, vec2(0), vec2(1), color, _geometryId); batch.setResourceTexture(0, nullptr); // restore default white color after me } +Transform Web3DOverlay::evalRenderTransform() { + Transform transform = Parent::evalRenderTransform(); + transform.setScale(1.0f); + transform.postScale(glm::vec3(getSize(), 1.0f)); + return transform; +} + const render::ShapeKey Web3DOverlay::getShapeKey() { auto builder = render::ShapeKey::Builder().withoutCullFace().withDepthBias().withOwnPipeline(); if (isTransparent()) { diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 6bd540d120..6afccaf3fc 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -78,6 +78,9 @@ signals: void requestWebSurface(); void releaseWebSurface(); +protected: + Transform evalRenderTransform() override; + private: InputMode _inputMode { Touch }; QSharedPointer _webSurface; From 76889d1b6708b1c669f14c4a98ab3c7e9e8edf88 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 27 Sep 2017 12:04:05 +1300 Subject: [PATCH 444/722] Improve handle scaling of entity with near-zero dimension --- scripts/vr-edit/vr-edit.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index a2c4ef7997..89eae8da52 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -391,6 +391,7 @@ initialHandleDistance, laserOffset, MIN_SCALE = 0.001, + MIN_SCALE_HANDLE_DISTANCE = 0.0001, getIntersection, // Function. intersection, @@ -548,6 +549,7 @@ + Vec3.dot(Vec3.subtract(otherTargetPosition, Overlays.getProperty(overlayID, "position")), scaleAxis); initialHandleDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, initialTargetPosition), scaleAxis)); initialHandleDistance -= handleTargetOffset; + initialHandleDistance = Math.max(initialHandleDistance, MIN_SCALE_HANDLE_DISTANCE); selection.startHandleScaling(initialTargetPosition); handles.startScaling(); @@ -629,6 +631,7 @@ scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis); handleDistance = Vec3.dot(Vec3.subtract(otherTargetPosition, targetPosition), scaleAxis); handleDistance -= handleTargetOffset; + handleDistance = Math.max(handleDistance, MIN_SCALE_HANDLE_DISTANCE); // Scale selection relative to initial dimensions. scale = handleDistance / initialHandleDistance; From 49512c95d753aaf94209c551354c48b548d76490 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 26 Sep 2017 16:17:59 -0700 Subject: [PATCH 445/722] possibly fix threading crashes, mirror and fade --- interface/src/SecondaryCamera.cpp | 39 +++++++++---------- interface/src/SecondaryCamera.h | 19 +++++---- .../src/model-networking/TextureCache.cpp | 16 +++++--- .../src/model-networking/TextureCache.h | 9 +++-- libraries/render-utils/src/FadeEffect.cpp | 9 +++-- libraries/render-utils/src/FadeEffect.h | 2 +- 6 files changed, 51 insertions(+), 43 deletions(-) diff --git a/interface/src/SecondaryCamera.cpp b/interface/src/SecondaryCamera.cpp index 56b8b3ef85..9d19b8fb0f 100644 --- a/interface/src/SecondaryCamera.cpp +++ b/interface/src/SecondaryCamera.cpp @@ -18,7 +18,6 @@ using RenderArgsPointer = std::shared_ptr; void MainRenderTask::build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor, bool isDeferred) { - task.addJob("RenderShadowTask", cullFunctor); const auto items = task.addJob("FetchCullSort", cullFunctor); assert(items.canCast()); @@ -30,14 +29,6 @@ void MainRenderTask::build(JobModel& task, const render::Varying& inputs, render } class SecondaryCameraJob { // Changes renderContext for our framebuffer and view. - QUuid _attachedEntityId{}; - glm::vec3 _position{}; - glm::quat _orientation{}; - float _vFoV{}; - float _nearClipPlaneDistance{}; - float _farClipPlaneDistance{}; - EntityPropertyFlags _attachedEntityPropertyFlags; - QSharedPointer _entityScriptingInterface; public: using Config = SecondaryCameraJobConfig; using JobModel = render::Job::ModelO; @@ -55,13 +46,15 @@ public: _vFoV = config.vFoV; _nearClipPlaneDistance = config.nearClipPlaneDistance; _farClipPlaneDistance = config.farClipPlaneDistance; + _textureWidth = config.textureWidth; + _textureHeight = config.textureHeight; } void run(const render::RenderContextPointer& renderContext, RenderArgsPointer& cachedArgs) { auto args = renderContext->args; auto textureCache = DependencyManager::get(); gpu::FramebufferPointer destFramebuffer; - destFramebuffer = textureCache->getSpectatorCameraFramebuffer(); // FIXME: Change the destination based on some unimplemented config var + destFramebuffer = textureCache->getSpectatorCameraFramebuffer(_textureWidth, _textureHeight); // FIXME: Change the destination based on some unimplemented config var if (destFramebuffer) { _cachedArgsPointer->_blitFramebuffer = args->_blitFramebuffer; _cachedArgsPointer->_viewport = args->_viewport; @@ -98,6 +91,18 @@ public: protected: RenderArgs _cachedArgs; RenderArgsPointer _cachedArgsPointer; + +private: + QUuid _attachedEntityId; + glm::vec3 _position; + glm::quat _orientation; + float _vFoV; + float _nearClipPlaneDistance; + float _farClipPlaneDistance; + int _textureWidth; + int _textureHeight; + EntityPropertyFlags _attachedEntityPropertyFlags; + QSharedPointer _entityScriptingInterface; }; void SecondaryCameraJobConfig::setPosition(glm::vec3 pos) { @@ -123,16 +128,10 @@ void SecondaryCameraJobConfig::enableSecondaryCameraRenderConfigs(bool enabled) setEnabled(enabled); } -void SecondaryCameraJobConfig::resetSizeSpectatorCamera(int width, int height) { // Carefully adjust the framebuffer / texture. - qApp->getRenderEngine()->getConfiguration()->getConfig()->resetSize(width, height); -} - -void SecondaryCameraRenderTaskConfig::resetSize(int width, int height) { // FIXME: Add an arg here for "destinationFramebuffer" - bool wasEnabled = isEnabled(); - setEnabled(false); - auto textureCache = DependencyManager::get(); - textureCache->resetSpectatorCameraFramebuffer(width, height); // FIXME: Call the correct reset function based on the "destinationFramebuffer" arg - setEnabled(wasEnabled); +void SecondaryCameraJobConfig::resetSizeSpectatorCamera(int width, int height) { + textureWidth = width; + textureHeight = height; + emit dirty(); } class EndSecondaryCameraFrame { // Restores renderContext. diff --git a/interface/src/SecondaryCamera.h b/interface/src/SecondaryCamera.h index 0941959c0a..a9b438ec6f 100644 --- a/interface/src/SecondaryCamera.h +++ b/interface/src/SecondaryCamera.h @@ -18,7 +18,6 @@ #include #include - class MainRenderTask { public: using JobModel = render::Task::Model; @@ -37,12 +36,15 @@ class SecondaryCameraJobConfig : public render::Task::Config { // Exposes second Q_PROPERTY(float nearClipPlaneDistance MEMBER nearClipPlaneDistance NOTIFY dirty) // Secondary camera's near clip plane distance. In meters. Q_PROPERTY(float farClipPlaneDistance MEMBER farClipPlaneDistance NOTIFY dirty) // Secondary camera's far clip plane distance. In meters. public: - QUuid attachedEntityId{}; - glm::vec3 position{}; - glm::quat orientation{}; - float vFoV{ DEFAULT_FIELD_OF_VIEW_DEGREES }; - float nearClipPlaneDistance{ DEFAULT_NEAR_CLIP }; - float farClipPlaneDistance{ DEFAULT_FAR_CLIP }; + QUuid attachedEntityId; + glm::vec3 position; + glm::quat orientation; + float vFoV { DEFAULT_FIELD_OF_VIEW_DEGREES }; + float nearClipPlaneDistance { DEFAULT_NEAR_CLIP }; + float farClipPlaneDistance { DEFAULT_FAR_CLIP }; + int textureWidth { TextureCache::DEFAULT_SPECTATOR_CAM_WIDTH }; + int textureHeight { TextureCache::DEFAULT_SPECTATOR_CAM_HEIGHT }; + SecondaryCameraJobConfig() : render::Task::Config(false) {} signals: void dirty(); @@ -59,9 +61,6 @@ class SecondaryCameraRenderTaskConfig : public render::Task::Config { Q_OBJECT public: SecondaryCameraRenderTaskConfig() : render::Task::Config(false) {} - void resetSize(int width, int height); -signals: - void dirty(); }; class SecondaryCameraRenderTask { diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index a4ce892521..5d8f840c85 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -1003,6 +1003,7 @@ NetworkTexturePointer TextureCache::getResourceTexture(QUrl resourceTextureUrl) if (_spectatorCameraFramebuffer) { texture = _spectatorCameraFramebuffer->getRenderBuffer(0); if (texture) { + texture->setSource(SPECTATOR_CAMERA_FRAME_URL.toString().toStdString()); _spectatorCameraNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); return _spectatorCameraNetworkTexture; } @@ -1016,6 +1017,7 @@ NetworkTexturePointer TextureCache::getResourceTexture(QUrl resourceTextureUrl) if (_hmdPreviewFramebuffer) { texture = _hmdPreviewFramebuffer->getRenderBuffer(0); if (texture) { + texture->setSource(HMD_PREVIEW_FRAME_URL.toString().toStdString()); _hmdPreviewNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); return _hmdPreviewNetworkTexture; } @@ -1033,14 +1035,18 @@ const gpu::FramebufferPointer& TextureCache::getHmdPreviewFramebuffer(int width, } const gpu::FramebufferPointer& TextureCache::getSpectatorCameraFramebuffer() { + // If we're taking a screenshot and the spectator cam buffer hasn't been created yet, reset to the default size if (!_spectatorCameraFramebuffer) { - resetSpectatorCameraFramebuffer(2048, 1024); + return getSpectatorCameraFramebuffer(DEFAULT_SPECTATOR_CAM_WIDTH, DEFAULT_SPECTATOR_CAM_HEIGHT); } return _spectatorCameraFramebuffer; } -void TextureCache::resetSpectatorCameraFramebuffer(int width, int height) { - _spectatorCameraFramebuffer.reset(gpu::Framebuffer::create("spectatorCamera", gpu::Element::COLOR_SRGBA_32, width, height)); - _spectatorCameraNetworkTexture.reset(); - emit spectatorCameraFramebufferReset(); +const gpu::FramebufferPointer& TextureCache::getSpectatorCameraFramebuffer(int width, int height) { + // If we aren't taking a screenshot, we might need to resize or create the camera buffer + if (!_spectatorCameraFramebuffer || _spectatorCameraFramebuffer->getWidth() != width || _spectatorCameraFramebuffer->getHeight() != height) { + _spectatorCameraFramebuffer.reset(gpu::Framebuffer::create("spectatorCamera", gpu::Element::COLOR_SRGBA_32, width, height)); + emit spectatorCameraFramebufferReset(); + } + return _spectatorCameraFramebuffer; } diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 5bc5aa7d96..1102694f86 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -167,12 +167,13 @@ public: gpu::TexturePointer getTextureByHash(const std::string& hash); gpu::TexturePointer cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture); - - /// SpectatorCamera rendering targets. NetworkTexturePointer getResourceTexture(QUrl resourceTextureUrl); - const gpu::FramebufferPointer& getSpectatorCameraFramebuffer(); - void resetSpectatorCameraFramebuffer(int width, int height); const gpu::FramebufferPointer& getHmdPreviewFramebuffer(int width, int height); + const gpu::FramebufferPointer& getSpectatorCameraFramebuffer(); + const gpu::FramebufferPointer& getSpectatorCameraFramebuffer(int width, int height); + + static const int DEFAULT_SPECTATOR_CAM_WIDTH { 2048 }; + static const int DEFAULT_SPECTATOR_CAM_HEIGHT { 1024 }; signals: void spectatorCameraFramebufferReset(); diff --git a/libraries/render-utils/src/FadeEffect.cpp b/libraries/render-utils/src/FadeEffect.cpp index c94fe717f1..418d02a4e7 100644 --- a/libraries/render-utils/src/FadeEffect.cpp +++ b/libraries/render-utils/src/FadeEffect.cpp @@ -16,8 +16,7 @@ #include FadeEffect::FadeEffect() { - auto texturePath = PathUtils::resourcesPath() + "images/fadeMask.png"; - _maskMap = DependencyManager::get()->getImageTexture(texturePath, image::TextureUsage::STRICT_TEXTURE); + } void FadeEffect::build(render::Task::TaskConcept& task, const task::Varying& editableItems) { @@ -29,11 +28,15 @@ void FadeEffect::build(render::Task::TaskConcept& task, const task::Varying& edi task.addJob("FadeEdit", fadeEditInput); } -render::ShapePipeline::BatchSetter FadeEffect::getBatchSetter() const { +render::ShapePipeline::BatchSetter FadeEffect::getBatchSetter() { return [this](const render::ShapePipeline& shapePipeline, gpu::Batch& batch, render::Args*) { auto program = shapePipeline.pipeline->getProgram(); auto maskMapLocation = program->getTextures().findLocation("fadeMaskMap"); auto bufferLocation = program->getUniformBuffers().findLocation("fadeParametersBuffer"); + if (!_maskMap) { + auto texturePath = PathUtils::resourcesPath() + "images/fadeMask.png"; + _maskMap = DependencyManager::get()->getImageTexture(texturePath, image::TextureUsage::STRICT_TEXTURE); + } batch.setResourceTexture(maskMapLocation, _maskMap); batch.setUniformBuffer(bufferLocation, _configurations); }; diff --git a/libraries/render-utils/src/FadeEffect.h b/libraries/render-utils/src/FadeEffect.h index 4b4e401332..9827f67a7f 100644 --- a/libraries/render-utils/src/FadeEffect.h +++ b/libraries/render-utils/src/FadeEffect.h @@ -21,7 +21,7 @@ public: void build(render::Task::TaskConcept& task, const task::Varying& editableItems); - render::ShapePipeline::BatchSetter getBatchSetter() const; + render::ShapePipeline::BatchSetter getBatchSetter(); render::ShapePipeline::ItemSetter getItemUniformSetter() const; render::ShapePipeline::ItemSetter getItemStoredSetter(); From 8180acbffcac123e05e21a5ab5c90618f53addf5 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 26 Sep 2017 16:29:22 -0700 Subject: [PATCH 446/722] use same trigger test as in isReady --- scripts/system/controllers/controllerModules/nearTrigger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/nearTrigger.js b/scripts/system/controllers/controllerModules/nearTrigger.js index cb913bc735..09ba5b9430 100644 --- a/scripts/system/controllers/controllerModules/nearTrigger.js +++ b/scripts/system/controllers/controllerModules/nearTrigger.js @@ -87,7 +87,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); if (!this.startSent) { this.startNearTrigger(controllerData); this.startSent = true; - } else if (controllerData.triggerClicks[this.hand] === 0) { + } else if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE) { this.endNearTrigger(controllerData); this.startSent = false; return makeRunningValues(false, [], []); From acb0bf8d20d49233edac518eede808ec61f3ae03 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 25 Sep 2017 20:22:44 -0700 Subject: [PATCH 447/722] Asset Browser tweaks --- .../resources/qml/{ => hifi}/AssetServer.qml | 189 +++++++++++++++--- interface/src/Application.cpp | 2 +- .../AssetMappingsScriptingInterface.cpp | 9 + .../AssetMappingsScriptingInterface.h | 5 + 4 files changed, 180 insertions(+), 25 deletions(-) rename interface/resources/qml/{ => hifi}/AssetServer.qml (80%) diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml similarity index 80% rename from interface/resources/qml/AssetServer.qml rename to interface/resources/qml/hifi/AssetServer.qml index 649ea49153..e54d4048ec 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -14,10 +14,10 @@ import QtQuick.Controls.Styles 1.4 import QtQuick.Dialogs 1.2 as OriginalDialogs import Qt.labs.settings 1.0 -import "styles-uit" -import "controls-uit" as HifiControls -import "windows" -import "dialogs" +import "../styles-uit" +import "../controls-uit" as HifiControls +import "../windows" +import "../dialogs" ScrollingWindow { id: root @@ -58,6 +58,14 @@ ScrollingWindow { assetMappingsModel.autoRefreshEnabled = false; } + function letterbox(headerGlyph, headerText, message) { + letterboxMessage.headerGlyph = headerGlyph; + letterboxMessage.headerText = headerText; + letterboxMessage.text = message; + letterboxMessage.visible = true; + letterboxMessage.popupRadius = 0; + } + function doDeleteFile(path) { console.log("Deleting " + path); @@ -154,10 +162,7 @@ ScrollingWindow { } function handleGetMappingsError(errorString) { - errorMessageBox( - "There was a problem retreiving the list of assets from your Asset Server.\n" - + errorString - ); + errorMessageBox("There was a problem retreiving the list of assets from your Asset Server.\n" + errorString); } function addToWorld() { @@ -457,10 +462,16 @@ ScrollingWindow { text: message }); } - + Item { width: pane.contentWidth height: pane.height + + // The letterbox used for popup messages + LetterboxMessage { + id: letterboxMessage; + z: 999; // Force the popup on top of everything else + } HifiControls.ContentSection { id: assetDirectory @@ -476,7 +487,7 @@ ScrollingWindow { HifiControls.Button { text: "Add To World" - color: hifi.buttons.black + color: hifi.buttons.blue colorScheme: root.colorScheme width: 120 @@ -513,6 +524,7 @@ ScrollingWindow { id: treeView anchors.top: assetDirectory.bottom anchors.bottom: infoRow.top + anchors.bottomMargin: 2 * hifi.dimensions.contentSpacing.y anchors.margins: hifi.dimensions.contentMargin.x + 2 // Extra for border anchors.left: parent.left anchors.right: parent.right @@ -584,8 +596,24 @@ ScrollingWindow { ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) - elide: Text.ElideRight horizontalAlignment: styleData.column === 1 ? TextInput.AlignHCenter : TextInput.AlignLeft + + elide: Text.ElideMiddle + + MouseArea { + id: mouseArea + anchors.fill: parent + + acceptedButtons: Qt.NoButton + hoverEnabled: true + + onEntered: { + if (parent.truncated) { + treeLabelToolTip.show(parent); + } + } + onExited: treeLabelToolTip.hide(); + } } } Component { @@ -668,6 +696,42 @@ ScrollingWindow { } } + Rectangle { + id: treeLabelToolTip + visible: false + z: 100 // Render on top + + width: toolTipText.width + 2 * hifi.dimensions.textPadding + height: hifi.dimensions.tableRowHeight + color: colorScheme == hifi.colorSchemes.light ? hifi.colors.tableRowLightOdd : hifi.colors.tableRowDarkOdd + border.color: colorScheme == hifi.colorSchemes.light ? hifi.colors.black : hifi.colors.lightGrayText + + FiraSansSemiBold { + id: toolTipText + anchors.centerIn: parent + + size: hifi.fontSizes.tableText + color: colorScheme == hifi.colorSchemes.light ? hifi.colors.black : hifi.colors.lightGrayText + } + + Timer { + id: showTimer + interval: 1000 + onTriggered: { treeLabelToolTip.visible = true; } + } + function show(item) { + var coord = item.mapToItem(parent, item.x, item.y); + + toolTipText.text = item.text; + treeLabelToolTip.x = coord.x - hifi.dimensions.textPadding; + treeLabelToolTip.y = coord.y; + showTimer.start(); + } + function hide() { + showTimer.stop(); + treeLabelToolTip.visible = false; + } + } MouseArea { propagateComposedEvents: true @@ -712,31 +776,43 @@ ScrollingWindow { } } - Row { + Item { id: infoRow anchors.left: treeView.left anchors.right: treeView.right anchors.bottom: uploadSection.top - anchors.bottomMargin: hifi.dimensions.contentSpacing.y - spacing: hifi.dimensions.contentSpacing.x + anchors.topMargin: 2 * hifi.dimensions.contentSpacing.y + anchors.bottomMargin: 2 * hifi.dimensions.contentSpacing.y RalewayRegular { + id: treeInfo + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + function makeText() { + var pendingBakes = assetMappingsModel.bakesPendingCount; + if (selectedItems > 1 || pendingBakes === 0) { + return selectedItems + " items selected"; + } else { + return pendingBakes + " bakes pending" + } + } + size: hifi.fontSizes.sectionName font.capitalization: Font.AllUppercase - text: selectedItems + " items selected" + text: makeText() color: hifi.colors.lightGrayText } HifiControls.CheckBox { - function isChecked() { - var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); - var bakingDisabled = (status === "Not Baked" || status === "--"); - return selectedItems === 1 && !bakingDisabled; - } + id: bakingCheckbox + anchors.left: treeInfo.right + anchors.leftMargin: 2 * hifi.dimensions.contentSpacing.x + anchors.verticalCenter: parent.verticalCenter - text: "Use baked (optimized) versions" + text: " Use baked version" colorScheme: root.colorScheme - enabled: selectedItems === 1 && assetProxyModel.data(treeView.selection.currentIndex, 0x105) !== "--" + enabled: isEnabled() checked: isChecked() onClicked: { var mappings = []; @@ -752,7 +828,72 @@ ScrollingWindow { checked = Qt.binding(isChecked); } + + function isEnabled() { + if (!treeView.selection.hasSelection) { + return false; + } + + var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); + if (status === "--") { + return false; + } + var bakingEnabled = status !== "Not Baked"; + + for (var i in treeView.selection.selectedIndexes) { + var thisStatus = assetProxyModel.data(treeView.selection.selectedIndexes[i], 0x105); + if (thisStatus === "--") { + return false; + } + var thisBakingEnalbed = (thisStatus !== "Not Baked"); + + if (bakingEnabled !== thisBakingEnalbed) { + return false; + } + } + + return true; + } + function isChecked() { + if (!treeView.selection.hasSelection) { + return false; + } + + var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); + return isEnabled() && status !== "Not Baked"; + } } + + Item { + anchors.left: bakingCheckbox.right + anchors.leftMargin: hifi.dimensions.contentSpacing.x + anchors.verticalCenter: parent.verticalCenter + width: infoGlyph.size; + height: infoGlyph.size; + + HiFiGlyphs { + id: infoGlyph; + anchors.fill: parent; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + text: hifi.glyphs.question; + size: 35; + color: hifi.colors.lightGrayText; + } + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: infoGlyph.color = hifi.colors.blueHighlight; + onExited: infoGlyph.color = hifi.colors.lightGrayText; + onClicked: letterbox(hifi.glyphs.question, + "What is baking?", + "Baking is a process we use to compress geometric meshes and textures.
    " + + "We do this for efficient storage and transmission of models.
    " + + "In some cases, we have been able to achieve 60% compression of original models.

    " + + "We highly recommend you leave baking on to enable faster transmission decode of models" + + "in interface resulting in better experience for users visiting your domain."); + } + } } HifiControls.ContentSection { @@ -790,7 +931,7 @@ ScrollingWindow { id: image width: 24 height: 24 - source: "../images/Loading-Outer-Ring.png" + source: "../../images/Loading-Outer-Ring.png" RotationAnimation on rotation { loops: Animation.Infinite from: 0 @@ -801,7 +942,7 @@ ScrollingWindow { Image { width: 24 height: 24 - source: "../images/Loading-Inner-H.png" + source: "../../images/Loading-Inner-H.png" } HifiControls.Label { id: uploadProgressLabel diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e1c3af1939..d538472450 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6398,7 +6398,7 @@ void Application::showAssetServerWidget(QString filePath) { if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { return; } - static const QUrl url { "AssetServer.qml" }; + static const QUrl url { "hifi/AssetServer.qml" }; auto startUpload = [=](QQmlContext* context, QObject* newObject){ if (!filePath.isEmpty()) { diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.cpp b/interface/src/scripting/AssetMappingsScriptingInterface.cpp index 5031016c3f..6c6f6dc244 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.cpp +++ b/interface/src/scripting/AssetMappingsScriptingInterface.cpp @@ -239,6 +239,7 @@ void AssetMappingModel::refresh() { connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) mutable { if (request->getError() == MappingRequest::NoError) { + int bakesPendingCount = 0; auto mappings = request->getMappings(); auto existingPaths = _pathToItemMap.keys(); for (auto& mapping : mappings) { @@ -287,6 +288,9 @@ void AssetMappingModel::refresh() { auto statusString = isFolder ? "--" : bakingStatusToString(mapping.second.status); lastItem->setData(statusString, Qt::UserRole + 5); lastItem->setData(mapping.second.bakingErrors, Qt::UserRole + 6); + if (mapping.second.status == Pending) { + ++bakesPendingCount; + } } Q_ASSERT(fullPath == path); @@ -334,6 +338,11 @@ void AssetMappingModel::refresh() { item = nextItem; } } + + if (bakesPendingCount != _bakesPendingCount) { + _bakesPendingCount = bakesPendingCount; + emit bakesPendingCountChanged(_bakesPendingCount); + } } else { emit errorGettingMappings(request->getErrorString()); } diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.h b/interface/src/scripting/AssetMappingsScriptingInterface.h index 04ab488838..49d92ec070 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.h +++ b/interface/src/scripting/AssetMappingsScriptingInterface.h @@ -26,6 +26,7 @@ class AssetMappingModel : public QStandardItemModel { Q_OBJECT Q_PROPERTY(bool autoRefreshEnabled READ isAutoRefreshEnabled WRITE setAutoRefreshEnabled) + Q_PROPERTY(int bakesPendingCount READ getBakesPendingCount NOTIFY bakesPendingCountChanged) public: AssetMappingModel(); @@ -38,10 +39,13 @@ public: bool isKnownMapping(QString path) const { return _pathToItemMap.contains(path); } bool isKnownFolder(QString path) const; + int getBakesPendingCount() const { return _bakesPendingCount; } + public slots: void clear(); signals: + void bakesPendingCountChanged(int newCount); void errorGettingMappings(QString errorString); void updated(); @@ -50,6 +54,7 @@ private: QHash _pathToItemMap; QTimer _autoRefreshTimer; + int _bakesPendingCount{ 0 }; }; Q_DECLARE_METATYPE(AssetMappingModel*) From 306cf883feb5851a17c124356cc7c3c37edd8eb5 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 26 Sep 2017 18:18:41 -0700 Subject: [PATCH 448/722] don't reload animGraph if url didn't change --- libraries/animation/src/Rig.cpp | 38 +++++++++++++++++---------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index fb255fd66d..86a1e629b4 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1619,28 +1619,30 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo } void Rig::initAnimGraph(const QUrl& url) { - _animGraphURL = url; + if (_animGraphURL != url) { + _animGraphURL = url; - _animNode.reset(); + _animNode.reset(); - // load the anim graph - _animLoader.reset(new AnimNodeLoader(url)); - connect(_animLoader.get(), &AnimNodeLoader::success, [this](AnimNode::Pointer nodeIn) { - _animNode = nodeIn; - _animNode->setSkeleton(_animSkeleton); + // load the anim graph + _animLoader.reset(new AnimNodeLoader(url)); + connect(_animLoader.get(), &AnimNodeLoader::success, [this](AnimNode::Pointer nodeIn) { + _animNode = nodeIn; + _animNode->setSkeleton(_animSkeleton); - if (_userAnimState.clipNodeEnum != UserAnimState::None) { - // restore the user animation we had before reset. - UserAnimState origState = _userAnimState; - _userAnimState = { UserAnimState::None, "", 30.0f, false, 0.0f, 0.0f }; - overrideAnimation(origState.url, origState.fps, origState.loop, origState.firstFrame, origState.lastFrame); - } + if (_userAnimState.clipNodeEnum != UserAnimState::None) { + // restore the user animation we had before reset. + UserAnimState origState = _userAnimState; + _userAnimState = { UserAnimState::None, "", 30.0f, false, 0.0f, 0.0f }; + overrideAnimation(origState.url, origState.fps, origState.loop, origState.firstFrame, origState.lastFrame); + } - emit onLoadComplete(); - }); - connect(_animLoader.get(), &AnimNodeLoader::error, [url](int error, QString str) { - qCCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str; - }); + emit onLoadComplete(); + }); + connect(_animLoader.get(), &AnimNodeLoader::error, [url](int error, QString str) { + qCCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str; + }); + } } bool Rig::getModelRegistrationPoint(glm::vec3& modelRegistrationPointOut) const { From dda715e17139ae2aa4ee77aac5bd7fe7992787d6 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 26 Sep 2017 18:01:25 -0700 Subject: [PATCH 449/722] Update TabletAssetServer --- interface/resources/qml/hifi/AssetServer.qml | 15 +- .../qml/hifi/dialogs/TabletAssetServer.qml | 165 ++++++++++++++++-- 2 files changed, 156 insertions(+), 24 deletions(-) diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index e54d4048ec..86b5229474 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -38,7 +38,7 @@ ScrollingWindow { property var assetMappingsModel: Assets.mappingModel; property var currentDirectory; property var selectedItems: treeView.selection.selectedIndexes.length; - + Settings { category: "Overlay.AssetServer" property alias x: root.x @@ -337,7 +337,7 @@ ScrollingWindow { if (!path) { return; } - + var modalMessage = ""; var items = selectedItems.toString(); var isFolder = assetProxyModel.data(treeView.selection.currentIndex, 0x101); @@ -490,18 +490,18 @@ ScrollingWindow { color: hifi.buttons.blue colorScheme: root.colorScheme width: 120 - + enabled: canAddToWorld(assetProxyModel.data(treeView.selection.currentIndex, 0x100)) - + onClicked: root.addToWorld() } - + HifiControls.Button { text: "Rename" color: hifi.buttons.black colorScheme: root.colorScheme width: 80 - + onClicked: root.renameFile() enabled: canRename() } @@ -952,10 +952,9 @@ ScrollingWindow { text: "In progress..." colorScheme: root.colorScheme } - } } - } } } + diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 3f1fcf6bda..8d8fb75199 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -17,6 +17,7 @@ import Qt.labs.settings 1.0 import "../../styles-uit" import "../../controls-uit" as HifiControls import "../../windows" +import ".." Rectangle { id: root @@ -57,6 +58,14 @@ Rectangle { Component.onDestruction: { assetMappingsModel.autoRefreshEnabled = false; } + + function letterbox(headerGlyph, headerText, message) { + letterboxMessage.headerGlyph = headerGlyph; + letterboxMessage.headerText = headerText; + letterboxMessage.text = message; + letterboxMessage.visible = true; + letterboxMessage.popupRadius = 0; + } function doDeleteFile(path) { console.log("Deleting " + path); @@ -154,10 +163,7 @@ Rectangle { } function handleGetMappingsError(errorString) { - errorMessageBox( - "There was a problem retreiving the list of assets from your Asset Server.\n" - + errorString - ); + errorMessageBox("There was a problem retreiving the list of assets from your Asset Server.\n" + errorString); } function addToWorld() { @@ -477,7 +483,7 @@ Rectangle { HifiControls.Button { text: "Add To World" - color: hifi.buttons.black + color: hifi.buttons.blue colorScheme: root.colorScheme width: 120 @@ -583,8 +589,24 @@ Rectangle { ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) - elide: Text.ElideRight horizontalAlignment: styleData.column === 1 ? TextInput.AlignHCenter : TextInput.AlignLeft + + elide: Text.ElideMiddle + + MouseArea { + id: mouseArea + anchors.fill: parent + + acceptedButtons: Qt.NoButton + hoverEnabled: true + + onEntered: { + if (parent.truncated) { + treeLabelToolTip.show(parent); + } + } + onExited: treeLabelToolTip.hide(); + } } } Component { @@ -667,6 +689,42 @@ Rectangle { } } + Rectangle { + id: treeLabelToolTip + visible: false + z: 100 // Render on top + + width: toolTipText.width + 2 * hifi.dimensions.textPadding + height: hifi.dimensions.tableRowHeight + color: colorScheme == hifi.colorSchemes.light ? hifi.colors.tableRowLightOdd : hifi.colors.tableRowDarkOdd + border.color: colorScheme == hifi.colorSchemes.light ? hifi.colors.black : hifi.colors.lightGrayText + + FiraSansSemiBold { + id: toolTipText + anchors.centerIn: parent + + size: hifi.fontSizes.tableText + color: colorScheme == hifi.colorSchemes.light ? hifi.colors.black : hifi.colors.lightGrayText + } + + Timer { + id: showTimer + interval: 1000 + onTriggered: { treeLabelToolTip.visible = true; } + } + function show(item) { + var coord = item.mapToItem(parent, item.x, item.y); + + toolTipText.text = item.text; + treeLabelToolTip.x = coord.x - hifi.dimensions.textPadding; + treeLabelToolTip.y = coord.y; + showTimer.start(); + } + function hide() { + showTimer.stop(); + treeLabelToolTip.visible = false; + } + } MouseArea { propagateComposedEvents: true @@ -715,26 +773,36 @@ Rectangle { id: infoRow anchors.left: treeView.left anchors.right: treeView.right - anchors.bottomMargin: hifi.dimensions.contentSpacing.y - spacing: hifi.dimensions.contentSpacing.x + anchors.topMargin: 2 * hifi.dimensions.contentSpacing.y + anchors.bottomMargin: 2 * hifi.dimensions.contentSpacing.y RalewayRegular { + anchors.verticalCenter: parent.verticalCenter + + function makeText() { + var pendingBakes = assetMappingsModel.bakesPendingCount; + if (selectedItems > 1 || pendingBakes === 0) { + return selectedItems + " items selected"; + } else { + return pendingBakes + " bakes pending" + } + } + size: hifi.fontSizes.sectionName font.capitalization: Font.AllUppercase - text: selectedItems + " items selected" + text: makeText() color: hifi.colors.lightGrayText } HifiControls.CheckBox { - function isChecked() { - var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); - var bakingDisabled = (status === "Not Baked" || status === "--"); - return selectedItems === 1 && !bakingDisabled; - } + id: bakingCheckbox + anchors.left: treeInfo.right + anchors.leftMargin: 2 * hifi.dimensions.contentSpacing.x + anchors.verticalCenter: parent.verticalCenter - text: "Use baked (optimized) versions" + text: " Use baked version" colorScheme: root.colorScheme - enabled: selectedItems === 1 && assetProxyModel.data(treeView.selection.currentIndex, 0x105) !== "--" + enabled: isEnabled() checked: isChecked() onClicked: { var mappings = []; @@ -750,7 +818,72 @@ Rectangle { checked = Qt.binding(isChecked); } + + function isEnabled() { + if (!treeView.selection.hasSelection) { + return false; + } + + var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); + if (status === "--") { + return false; + } + var bakingEnabled = status !== "Not Baked"; + + for (var i in treeView.selection.selectedIndexes) { + var thisStatus = assetProxyModel.data(treeView.selection.selectedIndexes[i], 0x105); + if (thisStatus === "--") { + return false; + } + var thisBakingEnalbed = (thisStatus !== "Not Baked"); + + if (bakingEnabled !== thisBakingEnalbed) { + return false; + } + } + + return true; + } + function isChecked() { + if (!treeView.selection.hasSelection) { + return false; + } + + var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); + return isEnabled() && status !== "Not Baked"; + } } + + Item { + anchors.left: bakingCheckbox.right + anchors.leftMargin: hifi.dimensions.contentSpacing.x + anchors.verticalCenter: parent.verticalCenter + width: infoGlyph.size; + height: infoGlyph.size; + + HiFiGlyphs { + id: infoGlyph; + anchors.fill: parent; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + text: hifi.glyphs.question; + size: 35; + color: hifi.colors.lightGrayText; + } + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: infoGlyph.color = hifi.colors.blueHighlight; + onExited: infoGlyph.color = hifi.colors.lightGrayText; + onClicked: letterbox(hifi.glyphs.question, + "What is baking?", + "Baking is a process we use to compress geometric meshes and textures.
    " + + "We do this for efficient storage and transmission of models.
    " + + "In some cases, we have been able to achieve 60% compression of original models.

    " + + "We highly recommend you leave baking on to enable faster transmission decode of models" + + "in interface resulting in better experience for users visiting your domain."); + } + } } HifiControls.TabletContentSection { From 03bd887f9ab7b6a06bd4745e6c0c4d92c01a5d76 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 26 Sep 2017 18:42:50 -0700 Subject: [PATCH 450/722] Re-sync files --- interface/resources/qml/hifi/AssetServer.qml | 11 +++-------- .../resources/qml/hifi/dialogs/TabletAssetServer.qml | 8 +++----- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index 86b5229474..d2866d8262 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -776,17 +776,13 @@ ScrollingWindow { } } - Item { + Row { id: infoRow anchors.left: treeView.left anchors.right: treeView.right anchors.bottom: uploadSection.top - anchors.topMargin: 2 * hifi.dimensions.contentSpacing.y - anchors.bottomMargin: 2 * hifi.dimensions.contentSpacing.y RalewayRegular { - id: treeInfo - anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter function makeText() { @@ -804,9 +800,10 @@ ScrollingWindow { color: hifi.colors.lightGrayText } + HifiControls.HorizontalSpacer { } + HifiControls.CheckBox { id: bakingCheckbox - anchors.left: treeInfo.right anchors.leftMargin: 2 * hifi.dimensions.contentSpacing.x anchors.verticalCenter: parent.verticalCenter @@ -865,8 +862,6 @@ ScrollingWindow { } Item { - anchors.left: bakingCheckbox.right - anchors.leftMargin: hifi.dimensions.contentSpacing.x anchors.verticalCenter: parent.verticalCenter width: infoGlyph.size; height: infoGlyph.size; diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 8d8fb75199..ee38c278a5 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -773,8 +773,7 @@ Rectangle { id: infoRow anchors.left: treeView.left anchors.right: treeView.right - anchors.topMargin: 2 * hifi.dimensions.contentSpacing.y - anchors.bottomMargin: 2 * hifi.dimensions.contentSpacing.y + anchors.bottom: uploadSection.top RalewayRegular { anchors.verticalCenter: parent.verticalCenter @@ -794,9 +793,10 @@ Rectangle { color: hifi.colors.lightGrayText } + HifiControls.HorizontalSpacer { } + HifiControls.CheckBox { id: bakingCheckbox - anchors.left: treeInfo.right anchors.leftMargin: 2 * hifi.dimensions.contentSpacing.x anchors.verticalCenter: parent.verticalCenter @@ -855,8 +855,6 @@ Rectangle { } Item { - anchors.left: bakingCheckbox.right - anchors.leftMargin: hifi.dimensions.contentSpacing.x anchors.verticalCenter: parent.verticalCenter width: infoGlyph.size; height: infoGlyph.size; From ce3e5eb1a3a5420dd2a6b18b74d6fbdc021000d8 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 26 Sep 2017 18:56:27 -0700 Subject: [PATCH 451/722] set grabbable and other checkboxes correctly if userData is blank or malformed. near-trigger blocks near-grab --- .../controllerModules/nearTrigger.js | 2 +- scripts/system/html/js/entityProperties.js | 25 ++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/scripts/system/controllers/controllerModules/nearTrigger.js b/scripts/system/controllers/controllerModules/nearTrigger.js index 09ba5b9430..56a4e93495 100644 --- a/scripts/system/controllers/controllerModules/nearTrigger.js +++ b/scripts/system/controllers/controllerModules/nearTrigger.js @@ -29,7 +29,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.startSent = false; this.parameters = makeDispatcherModuleParameters( - 520, + 480, this.hand === RIGHT_HAND ? ["rightHandTrigger", "rightHand"] : ["leftHandTrigger", "leftHand"], [], 100); diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 58173a794c..aad26cc7ba 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -840,40 +840,59 @@ function loaded() { elCloneableLimit.value = 0; elCloneableLifetime.value = 300; + var grabbablesSet = false; var parsedUserData = {} try { parsedUserData = JSON.parse(properties.userData); if ("grabbableKey" in parsedUserData) { + grabbablesSet = true; var grabbableData = parsedUserData["grabbableKey"]; if ("grabbable" in grabbableData) { elGrabbable.checked = grabbableData.grabbable; + } else { + elGrabbable.checked = true; } if ("wantsTrigger" in grabbableData) { elWantsTrigger.checked = grabbableData.wantsTrigger; + } else { + elWantsTrigger.checked = false; } if ("ignoreIK" in grabbableData) { elIgnoreIK.checked = grabbableData.ignoreIK; + } else { + elIgnoreIK.checked = true; } if ("cloneable" in grabbableData) { elCloneable.checked = grabbableData.cloneable; elCloneableGroup.style.display = elCloneable.checked ? "block": "none"; - elCloneableDynamic.checked = grabbableData.cloneDynamic ? grabbableData.cloneDynamic : properties.dynamic; + elCloneableDynamic.checked = + grabbableData.cloneDynamic ? grabbableData.cloneDynamic : properties.dynamic; if (elCloneable.checked) { if ("cloneLifetime" in grabbableData) { - elCloneableLifetime.value = grabbableData.cloneLifetime ? grabbableData.cloneLifetime : 300; + elCloneableLifetime.value = + grabbableData.cloneLifetime ? grabbableData.cloneLifetime : 300; } if ("cloneLimit" in grabbableData) { elCloneableLimit.value = grabbableData.cloneLimit ? grabbableData.cloneLimit : 0; } if ("cloneAvatarEntity" in grabbableData) { - elCloneableAvatarEntity.checked = grabbableData.cloneAvatarEntity ? grabbableData.cloneAvatarEntity : false; + elCloneableAvatarEntity.checked = + grabbableData.cloneAvatarEntity ? grabbableData.cloneAvatarEntity : false; } } + } else { + elCloneable.checked = false; } } } catch (e) { } + if (!grabbablesSet) { + elGrabbable.checked = true; + elWantsTrigger.checked = false; + elIgnoreIK.checked = true; + elCloneable.checked = false; + } elCollisionSoundURL.value = properties.collisionSoundURL; elLifetime.value = properties.lifetime; From bf984e8d1ae01f8bf8c219fff77faa87017561c9 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 26 Sep 2017 19:05:36 -0700 Subject: [PATCH 452/722] Fix tablet asset browser --- interface/resources/qml/hifi/AssetServer.qml | 18 +++++++------- .../qml/hifi/dialogs/TabletAssetServer.qml | 24 ++++++++++++------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index d2866d8262..276c93d8dd 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -66,6 +66,15 @@ ScrollingWindow { letterboxMessage.popupRadius = 0; } + function errorMessageBox(message) { + return desktop.messageBox({ + icon: hifi.icons.warning, + defaultButton: OriginalDialogs.StandardButton.Ok, + title: "Error", + text: message + }); + } + function doDeleteFile(path) { console.log("Deleting " + path); @@ -453,15 +462,6 @@ ScrollingWindow { }); } } - - function errorMessageBox(message) { - return desktop.messageBox({ - icon: hifi.icons.warning, - defaultButton: OriginalDialogs.StandardButton.Ok, - title: "Error", - text: message - }); - } Item { width: pane.contentWidth diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index ee38c278a5..24267f3a96 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -66,6 +66,15 @@ Rectangle { letterboxMessage.visible = true; letterboxMessage.popupRadius = 0; } + + function errorMessageBox(message) { + return tabletRoot.messageBox({ + icon: hifi.icons.warning, + defaultButton: OriginalDialogs.StandardButton.Ok, + title: "Error", + text: message + }); + } function doDeleteFile(path) { console.log("Deleting " + path); @@ -454,14 +463,11 @@ Rectangle { }); } } - - function errorMessageBox(message) { - return tabletRoot.messageBox({ - icon: hifi.icons.warning, - defaultButton: OriginalDialogs.StandardButton.Ok, - title: "Error", - text: message - }); + + // The letterbox used for popup messages + LetterboxMessage { + id: letterboxMessage; + z: 999; // Force the popup on top of everything else } Column { @@ -773,7 +779,7 @@ Rectangle { id: infoRow anchors.left: treeView.left anchors.right: treeView.right - anchors.bottom: uploadSection.top + anchors.bottomMargin: hifi.dimensions.contentSpacing.y RalewayRegular { anchors.verticalCenter: parent.verticalCenter From 895ffaf1f9a2e582a17082293c377120393b2277 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 27 Sep 2017 15:09:10 +1300 Subject: [PATCH 453/722] Fix group clear button messing up select-in-box state --- scripts/vr-edit/modules/toolsMenu.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 5640a29937..35ac7347ba 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -2776,7 +2776,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { break; case "clearGroupSelection": - optionsToggles.groupSelectionBoxButton = false; index = clearGroupingButtonIndex; Overlays.editOverlay(optionsOverlays[index], { color: optionsItems[index].properties.color From 73483eef945e4d1a419d6b05977cda28661a3e91 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 26 Sep 2017 19:19:34 -0700 Subject: [PATCH 454/722] Update information sentence --- interface/resources/qml/hifi/AssetServer.qml | 6 +----- interface/resources/qml/hifi/dialogs/TabletAssetServer.qml | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index 276c93d8dd..9ec61e0057 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -882,11 +882,7 @@ ScrollingWindow { onExited: infoGlyph.color = hifi.colors.lightGrayText; onClicked: letterbox(hifi.glyphs.question, "What is baking?", - "Baking is a process we use to compress geometric meshes and textures.
    " + - "We do this for efficient storage and transmission of models.
    " + - "In some cases, we have been able to achieve 60% compression of original models.

    " + - "We highly recommend you leave baking on to enable faster transmission decode of models" + - "in interface resulting in better experience for users visiting your domain."); + "Baking compresses and optimizes files for faster network transfer and display. We recommend you bake your content to reduce initial load times for your visitors."); } } } diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 24267f3a96..2018433be6 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -881,11 +881,7 @@ Rectangle { onExited: infoGlyph.color = hifi.colors.lightGrayText; onClicked: letterbox(hifi.glyphs.question, "What is baking?", - "Baking is a process we use to compress geometric meshes and textures.
    " + - "We do this for efficient storage and transmission of models.
    " + - "In some cases, we have been able to achieve 60% compression of original models.

    " + - "We highly recommend you leave baking on to enable faster transmission decode of models" + - "in interface resulting in better experience for users visiting your domain."); + "Baking compresses and optimizes files for faster network transfer and display. We recommend you bake your content to reduce initial load times for your visitors."); } } } From d4fc26f3b728e792010d191d7ed0abb6d5a7ae80 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 27 Sep 2017 15:22:33 +1300 Subject: [PATCH 455/722] Fix header highlighting if close and reeopen app with tool selected --- scripts/vr-edit/modules/toolsMenu.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 35ac7347ba..4a3b8c94d9 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -2473,9 +2473,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { pressedItem = null; isOptionsOpen = false; - - // Display menu items. - openMenu(); } function displayFooter() { @@ -2531,6 +2528,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { function clearTool() { closeOptions(); + openMenu(); } function setPresetsLabelToCustom() { @@ -2924,6 +2922,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { case "closeOptions": closeOptions(); + openMenu(); break; case "clearTool": @@ -3659,17 +3658,17 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (!isDisplaying) { return; } + + if (isOptionsOpen) { + closeOptions(); + } + Overlays.deleteOverlay(menuOriginOverlay); // Automatically deletes all other overlays because they're children. menuOverlays = []; menuHoverOverlays = []; menuIconOverlays = []; menuLabelOverlays = []; menuEnabled = []; - optionsOverlays = []; - optionsOverlaysLabels = []; - optionsOverlaysSublabels = []; - optionsExtraOverlays = []; - optionsEnabled = []; footerOverlays = []; footerHoverOverlays = []; footerIconOverlays = []; From e623b1e0a531660191ca1f308ca6d5fd7817acdf Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 27 Sep 2017 19:21:07 +1300 Subject: [PATCH 456/722] Alternative, more robust handle scaling method --- scripts/vr-edit/vr-edit.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/vr-edit.js b/scripts/vr-edit/vr-edit.js index 89eae8da52..81db666727 100644 --- a/scripts/vr-edit/vr-edit.js +++ b/scripts/vr-edit/vr-edit.js @@ -388,6 +388,7 @@ handleUnitScaleAxis, handleScaleDirections, handleTargetOffset, + initialHandToTargetOffset, initialHandleDistance, laserOffset, MIN_SCALE = 0.001, @@ -539,8 +540,12 @@ isScalingWithHand = intersection.handIntersected; + // Grab center of selection. otherTargetPosition = targetPosition; - initialTargetPosition = getScaleTargetPosition(); + initialTargetPosition = selection.boundingBox().center; + initialHandToTargetOffset = Vec3.subtract(initialTargetPosition, hand.position()); + + // Initial handle offset from center of selection. selectionPositionAndOrientation = selection.getPositionAndOrientation(); handleUnitScaleAxis = handles.scalingAxis(overlayID); // Unit vector in direction of scaling. handleScaleDirections = handles.scalingDirections(overlayID); // Which axes to scale the selection on. @@ -551,6 +556,7 @@ initialHandleDistance -= handleTargetOffset; initialHandleDistance = Math.max(initialHandleDistance, MIN_SCALE_HANDLE_DISTANCE); + // Start scaling. selection.startHandleScaling(initialTargetPosition); handles.startScaling(); isHandleScaling = true; @@ -626,8 +632,10 @@ deltaHandOrientation = Quat.multiply(hand.orientation(), initialHandOrientationInverse); selectionOrientation = Quat.multiply(deltaHandOrientation, initialSelectionOrientation); + // Position selection per grabbing hand. + targetPosition = Vec3.sum(hand.position(), Vec3.multiplyQbyV(deltaHandOrientation, initialHandToTargetOffset)); + // Desired distance of handle from other hand - targetPosition = getScaleTargetPosition(); scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis); handleDistance = Vec3.dot(Vec3.subtract(otherTargetPosition, targetPosition), scaleAxis); handleDistance -= handleTargetOffset; From c2471e340a1f9438f9e6e3555e5bcaa739f1a14b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 27 Sep 2017 21:49:02 +1300 Subject: [PATCH 457/722] Reduce number of calls to Script.resolvePath --- scripts/vr-edit/modules/createPalette.js | 28 ++- scripts/vr-edit/modules/toolIcon.js | 9 +- scripts/vr-edit/modules/toolsMenu.js | 215 +++++++++++------------ 3 files changed, 117 insertions(+), 135 deletions(-) diff --git a/scripts/vr-edit/modules/createPalette.js b/scripts/vr-edit/modules/createPalette.js index da7740d38a..968be03ed4 100644 --- a/scripts/vr-edit/modules/createPalette.js +++ b/scripts/vr-edit/modules/createPalette.js @@ -80,7 +80,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { }, PALETTE_TITLE_PROPERTIES = { - url: "../assets/create/create-heading.svg", + url: Script.resolvePath("../assets/create/create-heading.svg"), scale: 0.0363, localPosition: { x: 0, @@ -155,7 +155,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { { icon: { properties: { - url: "../assets/create/cube.fbx" + url: Script.resolvePath("../assets/create/cube.fbx") } }, entity: { @@ -167,7 +167,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { { icon: { properties: { - url: "../assets/create/sphere.fbx" + url: Script.resolvePath("../assets/create/sphere.fbx") } }, entity: { @@ -179,7 +179,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { { icon: { properties: { - url: "../assets/create/tetrahedron.fbx" + url: Script.resolvePath("../assets/create/tetrahedron.fbx") } }, entity: { @@ -192,7 +192,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { { icon: { properties: { - url: "../assets/create/octahedron.fbx" + url: Script.resolvePath("../assets/create/octahedron.fbx") } }, entity: { @@ -205,7 +205,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { { icon: { properties: { - url: "../assets/create/icosahedron.fbx" + url: Script.resolvePath("../assets/create/icosahedron.fbx") } }, entity: { @@ -218,7 +218,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { { icon: { properties: { - url: "../assets/create/dodecahedron.fbx" + url: Script.resolvePath("../assets/create/dodecahedron.fbx") } }, entity: { @@ -231,7 +231,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { { icon: { properties: { - url: "../assets/create/hexagon.fbx", + url: Script.resolvePath("../assets/create/hexagon.fbx"), dimensions: { x: 0.02078, y: 0.024, z: 0.024 }, localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) } @@ -246,7 +246,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { { icon: { properties: { - url: "../assets/create/prism.fbx", + url: Script.resolvePath("../assets/create/prism.fbx"), localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) } }, @@ -260,7 +260,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { { icon: { properties: { - url: "../assets/create/octagon.fbx", + url: Script.resolvePath("../assets/create/octagon.fbx"), dimensions: { x: 0.023805, y: 0.024, z: 0.024 }, localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) } @@ -275,7 +275,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { { icon: { properties: { - url: "../assets/create/cylinder.fbx", + url: Script.resolvePath("../assets/create/cylinder.fbx"), localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) } }, @@ -289,7 +289,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { { icon: { properties: { - url: "../assets/create/cone.fbx", + url: Script.resolvePath("../assets/create/cone.fbx"), localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) } }, @@ -303,7 +303,7 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { { icon: { properties: { - url: "../assets/create/circle.fbx", + url: Script.resolvePath("../assets/create/circle.fbx"), dimensions: { x: 0.024, y: 0.0005, z: 0.024 }, localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 }) } @@ -478,7 +478,6 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { paletteHeaderBarOverlay = Overlays.addOverlay("model", properties); properties = Object.clone(PALETTE_TITLE_PROPERTIES); properties.parentID = paletteHeaderHeadingOverlay; - properties.url = Script.resolvePath(properties.url); paletteTitleOverlay = Overlays.addOverlay("image3d", properties); // Palette background. @@ -504,7 +503,6 @@ CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.clone(PALETTE_ITEM.icon.properties); properties = Object.merge(properties, PALETTE_ITEMS[i].icon.properties); properties.parentID = paletteItemHoverOverlays[i]; - properties.url = Script.resolvePath(properties.url); iconOverlays[i] = Overlays.addOverlay(PALETTE_ITEM.icon.overlay, properties); } diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index 6ea6965a36..cac31ed83f 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -26,7 +26,7 @@ ToolIcon = function (side) { MODEL_TYPE = "model", MODEL_PROPERTIES = { - url: "../assets/tools/tool-icon.fbx", + url: Script.resolvePath("../assets/tools/tool-icon.fbx"), dimensions: Vec3.multiply(MODEL_SCALE, MODEL_DIMENSIONS), solid: true, alpha: 1.0, @@ -114,7 +114,6 @@ ToolIcon = function (side) { // Model. properties = Object.clone(MODEL_PROPERTIES); - properties.url = Script.resolvePath(properties.url); properties.parentJointIndex = handJointIndex; properties.localPosition = localPosition; properties.localRotation = localRotation; @@ -124,7 +123,7 @@ ToolIcon = function (side) { properties = Object.clone(IMAGE_PROPERTIES); properties = Object.merge(properties, ICON_PROPERTIES); properties.parentID = modelOverlay; - properties.url = Script.resolvePath(iconInfo.icon.properties.url); + properties.url = iconInfo.icon.properties.url; properties.dimensions = { x: ICON_SCALE_FACTOR * iconInfo.icon.properties.dimensions.x, y: ICON_SCALE_FACTOR * iconInfo.icon.properties.dimensions.y @@ -136,7 +135,7 @@ ToolIcon = function (side) { properties = Object.clone(IMAGE_PROPERTIES); properties = Object.merge(properties, LABEL_PROPERTIES); properties.parentID = modelOverlay; - properties.url = Script.resolvePath(iconInfo.label.properties.url); + properties.url = iconInfo.label.properties.url; properties.scale = LABEL_SCALE_FACTOR * iconInfo.label.properties.scale; Overlays.addOverlay(IMAGE_TYPE, properties); @@ -144,7 +143,7 @@ ToolIcon = function (side) { properties = Object.clone(IMAGE_PROPERTIES); properties = Object.merge(properties, SUBLABEL_PROPERTIES); properties.parentID = modelOverlay; - properties.url = Script.resolvePath(iconInfo.sublabel.properties.url); + properties.url = iconInfo.sublabel.properties.url; properties.scale = LABEL_SCALE_FACTOR * iconInfo.sublabel.properties.scale; Overlays.addOverlay(IMAGE_TYPE, properties); } diff --git a/scripts/vr-edit/modules/toolsMenu.js b/scripts/vr-edit/modules/toolsMenu.js index 4a3b8c94d9..182b53179f 100644 --- a/scripts/vr-edit/modules/toolsMenu.js +++ b/scripts/vr-edit/modules/toolsMenu.js @@ -129,7 +129,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, MENU_HEADER_BACK_PROPERTIES = { - url: "../assets/tools/back-icon.svg", + url: Script.resolvePath("../assets/tools/back-icon.svg"), dimensions: { x: 0.0069, y: 0.0107 }, localPosition: { x: -MENU_HEADER_HEADING_PROPERTIES.dimensions.x / 2 + 0.0118 + 0.0069 / 2, @@ -146,7 +146,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, MENU_HEADER_TITLE_PROPERTIES = { - url: "../assets/tools/tools-heading.svg", + url: Script.resolvePath("../assets/tools/tools-heading.svg"), scale: 0.0327, localPosition: { x: 0, @@ -162,7 +162,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { visible: true }, - MENU_HEADER_TITLE_BACK_URL = "../assets/tools/back-heading.svg", + MENU_HEADER_TITLE_BACK_URL = Script.resolvePath("../assets/tools/back-heading.svg"), MENU_HEADER_TITLE_BACK_SCALE = 0.0256, MENU_HEADER_ICON_OFFSET = { @@ -173,7 +173,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, MENU_HEADER_ICON_PROPERTIES = { - url: "../assets/tools/color-icon.svg", // Initial value so that the overlay is initialized OK. + url: Script.resolvePath("../assets/tools/color-icon.svg"), // Initial value so that the overlay is initialized OK. dimensions: { x: 0.01, y: 0.01 }, // "" localPosition: Vec3.ZERO, // "" localRotation: Quat.ZERO, @@ -390,7 +390,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { "horizontalRule": { overlay: "image3d", properties: { - url: "../assets/horizontal-rule.svg", + url: Script.resolvePath("../assets/horizontal-rule.svg"), dimensions: { x: UIT.dimensions.panel.x - 2 * UIT.dimensions.leftMargin, y: 0.001 }, localRotation: Quat.ZERO, color: UIT.colors.baseGrayShadow, @@ -432,7 +432,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { zeroIndicator: { overlay: "image3d", properties: { - url: "../assets/horizontal-rule.svg", + url: Script.resolvePath("../assets/horizontal-rule.svg"), dimensions: { x: 0.02, y: 0.001 }, localRotation: Quat.ZERO, color: UIT.colors.lightGrayText, @@ -561,6 +561,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, + PICKLIST_UP_ARROW = Script.resolvePath("../assets/tools/common/up-arrow.svg"), + PICKLIST_DOWN_ARROW = Script.resolvePath("../assets/tools/common/down-arrow.svg"), + BUTTON_UI_ELEMENTS = ["button", "menuButton", "toggleButton", "swatch"], MENU_HOVER_DELTA = { x: 0, y: 0, z: 0.006 }, @@ -592,7 +595,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "image", properties: { color: UIT.colors.white, - url: "../assets/tools/color/swatches-label.svg", + url: Script.resolvePath("../assets/tools/color/swatches-label.svg"), scale: 0.0345, localPosition: { x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0345 / 2, @@ -761,8 +764,8 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2 } }, - imageURL: "../assets/tools/color/color-circle.png", - imageOverlayURL: "../assets/tools/color/color-circle-black.png", + imageURL: Script.resolvePath("../assets/tools/color/color-circle.png"), + imageOverlayURL: Script.resolvePath("../assets/tools/color/color-circle-black.png"), command: { method: "setColorPerCircle" } @@ -779,9 +782,9 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }) }, useBaseColor: true, - imageURL: "../assets/tools/color/slider-white.png", + imageURL: Script.resolvePath("../assets/tools/color/slider-white.png"), // Alpha PNG created by overlaying two black-to-transparent gradients in order to achieve visual effect. - imageOverlayURL: "../assets/tools/color/slider-alpha.png", + imageOverlayURL: Script.resolvePath("../assets/tools/color/slider-alpha.png"), command: { method: "setColorPerSlider" } @@ -798,7 +801,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, label: { - url: "../assets/tools/color/pick-color-label.svg", + url: Script.resolvePath("../assets/tools/color/pick-color-label.svg"), scale: 0.0120 }, command: { @@ -830,7 +833,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "image", properties: { color: UIT.colors.white, - url: "../assets/tools/common/actions-label.svg", + url: Script.resolvePath("../assets/tools/common/actions-label.svg"), scale: 0.0276, localPosition: { x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0276 / 2, @@ -861,7 +864,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, label: { - url: "../assets/tools/common/finish-label.svg", + url: Script.resolvePath("../assets/tools/common/finish-label.svg"), scale: 0.0318 }, command: { @@ -883,7 +886,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "stretchInfoIcon", type: "image", properties: { - url: "../assets/tools/common/info-icon.svg", + url: Script.resolvePath("../assets/tools/common/info-icon.svg"), dimensions: { x: 0.0321, y: 0.0320 }, localPosition: { x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0321 / 2, @@ -897,7 +900,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "stretchInfo", type: "image", properties: { - url: "../assets/tools/stretch/info-text.svg", + url: Script.resolvePath("../assets/tools/stretch/info-text.svg"), scale: 0.1340, localPosition: { // Vertically center on info icon. x: -UIT.dimensions.panel.x / 2 + 0.0679 + 0.1340 / 2, @@ -914,7 +917,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "image", properties: { color: UIT.colors.white, - url: "../assets/tools/common/actions-label.svg", + url: Script.resolvePath("../assets/tools/common/actions-label.svg"), scale: 0.0276, localPosition: { x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0276 / 2, @@ -945,7 +948,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, label: { - url: "../assets/tools/common/finish-label.svg", + url: Script.resolvePath("../assets/tools/common/finish-label.svg"), scale: 0.0318 }, command: { @@ -959,7 +962,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "image", properties: { color: UIT.colors.white, - url: "../assets/tools/common/actions-label.svg", + url: Script.resolvePath("../assets/tools/common/actions-label.svg"), scale: 0.0276, localPosition: { x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0276 / 2, @@ -998,7 +1001,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { enabledColor: UIT.colors.greenHighlight, highlightColor: UIT.colors.greenShadow, label: { - url: "../assets/tools/group/group-label.svg", + url: Script.resolvePath("../assets/tools/group/group-label.svg"), scale: 0.0351, color: UIT.colors.baseGray }, @@ -1022,7 +1025,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { enabledColor: UIT.colors.redHighlight, highlightColor: UIT.colors.redAccent, label: { - url: "../assets/tools/group/ungroup-label.svg", + url: Script.resolvePath("../assets/tools/group/ungroup-label.svg"), scale: 0.0496, color: UIT.colors.baseGray }, @@ -1054,7 +1057,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, label: { - url: "../assets/tools/group/selection-box-label.svg", + url: Script.resolvePath("../assets/tools/group/selection-box-label.svg"), scale: 0.0161 }, command: { @@ -1076,7 +1079,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { enabledColor: UIT.colors.greenHighlight, highlightColor: UIT.colors.greenShadow, label: { - url: "../assets/tools/group/clear-label.svg", + url: Script.resolvePath("../assets/tools/group/clear-label.svg"), scale: 0.0314, color: UIT.colors.baseGray }, @@ -1093,7 +1096,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "image", properties: { color: UIT.colors.white, - url: "../assets/tools/physics/properties-label.svg", + url: Script.resolvePath("../assets/tools/physics/properties-label.svg"), scale: 0.0376, localPosition: { x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0376 / 2, @@ -1131,7 +1134,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: 0.0034, z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, - url: "../assets/tools/physics/buttons/gravity-label.svg", + url: Script.resolvePath("../assets/tools/physics/buttons/gravity-label.svg"), scale: 0.0240, color: UIT.colors.white }, @@ -1141,16 +1144,16 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: -0.0034, z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, - url: "../assets/tools/physics/buttons/off-label.svg", + url: Script.resolvePath("../assets/tools/physics/buttons/off-label.svg"), scale: 0.0104, color: UIT.colors.white // SVG has gray color. }, onSublabel: { - url: "../assets/tools/physics/buttons/on-label.svg", + url: Script.resolvePath("../assets/tools/physics/buttons/on-label.svg"), scale: 0.0081 }, offSublabel: { - url: "../assets/tools/physics/buttons/off-label.svg", + url: Script.resolvePath("../assets/tools/physics/buttons/off-label.svg"), scale: 0.0104 }, setting: { @@ -1179,7 +1182,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: 0.0034, z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, - url: "../assets/tools/physics/buttons/collisions-label.svg", + url: Script.resolvePath("../assets/tools/physics/buttons/collisions-label.svg"), scale: 0.0338, color: UIT.colors.white }, @@ -1189,16 +1192,16 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: -0.0034, z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, - url: "../assets/tools/physics/buttons/off-label.svg", + url: Script.resolvePath("../assets/tools/physics/buttons/off-label.svg"), scale: 0.0104, color: UIT.colors.white // SVG has gray color. }, onSublabel: { - url: "../assets/tools/physics/buttons/on-label.svg", + url: Script.resolvePath("../assets/tools/physics/buttons/on-label.svg"), scale: 0.0081 }, offSublabel: { - url: "../assets/tools/physics/buttons/off-label.svg", + url: Script.resolvePath("../assets/tools/physics/buttons/off-label.svg"), scale: 0.0104 }, setting: { @@ -1227,7 +1230,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: 0.0034, z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, - url: "../assets/tools/physics/buttons/grabbable-label.svg", + url: Script.resolvePath("../assets/tools/physics/buttons/grabbable-label.svg"), scale: 0.0334, color: UIT.colors.white }, @@ -1237,16 +1240,16 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: -0.0034, z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, - url: "../assets/tools/physics/buttons/off-label.svg", + url: Script.resolvePath("../assets/tools/physics/buttons/off-label.svg"), scale: 0.0104, color: UIT.colors.white // SVG has gray color. }, onSublabel: { - url: "../assets/tools/physics/buttons/on-label.svg", + url: Script.resolvePath("../assets/tools/physics/buttons/on-label.svg"), scale: 0.0081 }, offSublabel: { - url: "../assets/tools/physics/buttons/off-label.svg", + url: Script.resolvePath("../assets/tools/physics/buttons/off-label.svg"), scale: 0.0104 }, setting: { @@ -1264,7 +1267,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "image", properties: { color: UIT.colors.white, - url: "../assets/tools/physics/presets-label.svg", + url: Script.resolvePath("../assets/tools/physics/presets-label.svg"), scale: 0.0270, localPosition: { x: UIT.dimensions.panel.x / 2 - UIT.dimensions.rightMargin - 0.1416 + 0.0270 / 2, @@ -1289,7 +1292,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "presetDefault", type: "picklistItem", label: { - url: "../assets/tools/physics/presets/default-label.svg", + url: Script.resolvePath("../assets/tools/physics/presets/default-label.svg"), scale: 0.0436, localPosition: { x: -0.1416 / 2 + 0.017 + 0.0436 / 2, @@ -1306,7 +1309,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "presetLead", type: "picklistItem", label: { - url: "../assets/tools/physics/presets/lead-label.svg", + url: Script.resolvePath("../assets/tools/physics/presets/lead-label.svg"), scale: 0.0243, localPosition: { x: -0.1416 / 2 + 0.017 + 0.0243 / 2, @@ -1320,7 +1323,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "presetWood", type: "picklistItem", label: { - url: "../assets/tools/physics/presets/wood-label.svg", + url: Script.resolvePath("../assets/tools/physics/presets/wood-label.svg"), scale: 0.0316, localPosition: { x: -0.1416 / 2 + 0.017 + 0.0316 / 2, @@ -1334,7 +1337,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "presetIce", type: "picklistItem", label: { - url: "../assets/tools/physics/presets/ice-label.svg", + url: Script.resolvePath("../assets/tools/physics/presets/ice-label.svg"), scale: 0.0144, localPosition: { x: -0.1416 / 2 + 0.017 + 0.0144 / 2, @@ -1348,7 +1351,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "presetRubber", type: "picklistItem", label: { - url: "../assets/tools/physics/presets/rubber-label.svg", + url: Script.resolvePath("../assets/tools/physics/presets/rubber-label.svg"), scale: 0.0400, localPosition: { x: -0.1416 / 2 + 0.017 + 0.0400 / 2, @@ -1362,7 +1365,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "presetCotton", type: "picklistItem", label: { - url: "../assets/tools/physics/presets/cotton-label.svg", + url: Script.resolvePath("../assets/tools/physics/presets/cotton-label.svg"), scale: 0.0393, localPosition: { x: -0.1416 / 2 + 0.017 + 0.0393 / 2, @@ -1376,7 +1379,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "presetTumbleweed", type: "picklistItem", label: { - url: "../assets/tools/physics/presets/tumbleweed-label.svg", + url: Script.resolvePath("../assets/tools/physics/presets/tumbleweed-label.svg"), scale: 0.0687, localPosition: { x: -0.1416 / 2 + 0.017 + 0.0687 / 2, @@ -1390,7 +1393,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "presetZeroG", type: "picklistItem", label: { - url: "../assets/tools/physics/presets/zero-g-label.svg", + url: Script.resolvePath("../assets/tools/physics/presets/zero-g-label.svg"), scale: 0.0375, localPosition: { x: -0.1416 / 2 + 0.017 + 0.0375 / 2, @@ -1404,7 +1407,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "presetBalloon", type: "picklistItem", label: { - url: "../assets/tools/physics/presets/balloon-label.svg", + url: Script.resolvePath("../assets/tools/physics/presets/balloon-label.svg"), scale: 0.0459, localPosition: { x: -0.1416 / 2 + 0.017 + 0.0459 / 2, @@ -1426,7 +1429,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, label: { - url: "../assets/tools/physics/presets/default-label.svg", + url: Script.resolvePath("../assets/tools/physics/presets/default-label.svg"), scale: 0.0436, localPosition: { x: -0.1416 / 2 + 0.017 + 0.0436 / 2, @@ -1435,7 +1438,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, sublabel: { - url: "../assets/tools/common/down-arrow.svg", + url: Script.resolvePath("../assets/tools/common/down-arrow.svg"), scale: 0.0080, localPosition: { x: 0.1416 / 2 - 0.0108 - 0.0080 / 2, @@ -1445,7 +1448,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: UIT.colors.white // SVG is colored baseGrayHighlight }, customLabel: { - url: "../assets/tools/physics/presets/custom-label.svg", + url: Script.resolvePath("../assets/tools/physics/presets/custom-label.svg"), scale: 0.0522, localPosition: { x: -0.1416 / 2 + 0.017 + 0.0522 / 2, @@ -1490,7 +1493,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: -0.04375, z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, - url: "../assets/tools/physics/sliders/gravity-label.svg", + url: Script.resolvePath("../assets/tools/physics/sliders/gravity-label.svg"), scale: 0.0240 }, setting: { @@ -1519,7 +1522,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: -0.04375, z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, - url: "../assets/tools/physics/sliders/bounce-label.svg", + url: Script.resolvePath("../assets/tools/physics/sliders/bounce-label.svg"), scale: 0.0233 }, setting: { @@ -1548,7 +1551,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: -0.04375, z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, - url: "../assets/tools/physics/sliders/friction-label.svg", + url: Script.resolvePath("../assets/tools/physics/sliders/friction-label.svg"), scale: 0.0258 }, setting: { @@ -1577,7 +1580,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { y: -0.04375, z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset }, - url: "../assets/tools/physics/sliders/density-label.svg", + url: Script.resolvePath("../assets/tools/physics/sliders/density-label.svg"), scale: 0.0241 }, setting: { @@ -1596,7 +1599,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { type: "image", properties: { color: UIT.colors.white, - url: "../assets/tools/common/actions-label.svg", + url: Script.resolvePath("../assets/tools/common/actions-label.svg"), scale: 0.0276, localPosition: { x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0276 / 2, @@ -1627,7 +1630,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { } }, label: { - url: "../assets/tools/common/finish-label.svg", + url: Script.resolvePath("../assets/tools/common/finish-label.svg"), scale: 0.0318 }, command: { @@ -1649,7 +1652,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "deleteInfoIcon", type: "image", properties: { - url: "../assets/tools/common/info-icon.svg", + url: Script.resolvePath("../assets/tools/common/info-icon.svg"), dimensions: { x: 0.0321, y: 0.0320 }, localPosition: { x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0321 / 2, @@ -1663,7 +1666,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { id: "deleteInfo", type: "image", properties: { - url: "../assets/tools/delete/info-text.svg", + url: Script.resolvePath("../assets/tools/delete/info-text.svg"), scale: 0.1416, localPosition: { // Vertically off-center w.r.t. info icon. x: -UIT.dimensions.panel.x / 2 + 0.0679 + 0.1340 / 2, @@ -1692,25 +1695,25 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, icon: { properties: { - url: "../assets/tools/color-icon.svg", + url: Script.resolvePath("../assets/tools/color-icon.svg"), dimensions: { x: 0.0165, y: 0.0187 } }, headerOffset: { x: -0.00825, y: 0.0020, z: 0 } }, label: { properties: { - url: "../assets/tools/color-label.svg", + url: Script.resolvePath("../assets/tools/color-label.svg"), scale: 0.0241 } }, sublabel: { properties: { - url: "../assets/tools/tool-label.svg", + url: Script.resolvePath("../assets/tools/tool-label.svg"), scale: 0.0152 } }, title: { - url: "../assets/tools/color-tool-heading.svg", + url: Script.resolvePath("../assets/tools/color-tool-heading.svg"), scale: 0.0631 }, toolOptions: "colorOptions", @@ -1731,25 +1734,25 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, icon: { properties: { - url: "../assets/tools/stretch-icon.svg", + url: Script.resolvePath("../assets/tools/stretch-icon.svg"), dimensions: { x: 0.0167, y: 0.0167 } }, headerOffset: { x: -0.00835, y: 0, z: 0 } }, label: { properties: { - url: "../assets/tools/stretch-label.svg", + url: Script.resolvePath("../assets/tools/stretch-label.svg"), scale: 0.0311 } }, sublabel: { properties: { - url: "../assets/tools/tool-label.svg", + url: Script.resolvePath("../assets/tools/tool-label.svg"), scale: 0.0152 } }, title: { - url: "../assets/tools/stretch-tool-heading.svg", + url: Script.resolvePath("../assets/tools/stretch-tool-heading.svg"), scale: 0.0737 }, toolOptions: "scaleOptions", @@ -1769,25 +1772,25 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, icon: { properties: { - url: "../assets/tools/clone-icon.svg", + url: Script.resolvePath("../assets/tools/clone-icon.svg"), dimensions: { x: 0.0154, y: 0.0155 } }, headerOffset: { x: -0.0077, y: 0, z: 0 } }, label: { properties: { - url: "../assets/tools/clone-label.svg", + url: Script.resolvePath("../assets/tools/clone-label.svg"), scale: 0.0231 } }, sublabel: { properties: { - url: "../assets/tools/tool-label.svg", + url: Script.resolvePath("../assets/tools/tool-label.svg"), scale: 0.0152 } }, title: { - url: "../assets/tools/clone-tool-heading.svg", + url: Script.resolvePath("../assets/tools/clone-tool-heading.svg"), scale: 0.0621 }, toolOptions: "cloneOptions", @@ -1807,25 +1810,25 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, icon: { properties: { - url: "../assets/tools/group-icon.svg", + url: Script.resolvePath("../assets/tools/group-icon.svg"), dimensions: { x: 0.0161, y: 0.0114 } }, headerOffset: { x: -0.00805, y: 0, z: 0 } }, label: { properties: { - url: "../assets/tools/group-label.svg", + url: Script.resolvePath("../assets/tools/group-label.svg"), scale: 0.0250 } }, sublabel: { properties: { - url: "../assets/tools/tool-label.svg", + url: Script.resolvePath("../assets/tools/tool-label.svg"), scale: 0.0152 } }, title: { - url: "../assets/tools/group-tool-heading.svg", + url: Script.resolvePath("../assets/tools/group-tool-heading.svg"), scale: 0.0647 }, toolOptions: "groupOptions", @@ -1845,25 +1848,25 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, icon: { properties: { - url: "../assets/tools/physics-icon.svg", + url: Script.resolvePath("../assets/tools/physics-icon.svg"), dimensions: { x: 0.0180, y: 0.0198 } }, headerOffset: { x: -0.009, y: 0, z: 0 } }, label: { properties: { - url: "../assets/tools/physics-label.svg", + url: Script.resolvePath("../assets/tools/physics-label.svg"), scale: 0.0297 } }, sublabel: { properties: { - url: "../assets/tools/tool-label.svg", + url: Script.resolvePath("../assets/tools/tool-label.svg"), scale: 0.0152 } }, title: { - url: "../assets/tools/physics-tool-heading.svg", + url: Script.resolvePath("../assets/tools/physics-tool-heading.svg"), scale: 0.0712 }, toolOptions: "physicsOptions", @@ -1883,25 +1886,25 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, icon: { properties: { - url: "../assets/tools/delete-icon.svg", + url: Script.resolvePath("../assets/tools/delete-icon.svg"), dimensions: { x: 0.0161, y: 0.0161 } }, headerOffset: { x: -0.00805, y: 0, z: 0 } }, label: { properties: { - url: "../assets/tools/delete-label.svg", + url: Script.resolvePath("../assets/tools/delete-label.svg"), scale: 0.0254 } }, sublabel: { properties: { - url: "../assets/tools/tool-label.svg", + url: Script.resolvePath("../assets/tools/tool-label.svg"), scale: 0.0152 } }, title: { - url: "../assets/tools/delete-tool-heading.svg", + url: Script.resolvePath("../assets/tools/delete-tool-heading.svg"), scale: 0.0653 }, toolOptions: "deleteOptions", @@ -1941,13 +1944,13 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, icon: { properties: { - url: "../assets/tools/undo-icon.svg", + url: Script.resolvePath("../assets/tools/undo-icon.svg"), dimensions: { x: 0.0180, y: 0.0186 } } }, label: { properties: { - url: "../assets/tools/undo-label.svg", + url: Script.resolvePath("../assets/tools/undo-label.svg"), scale: 0.0205 } }, @@ -1967,13 +1970,13 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { }, icon: { properties: { - url: "../assets/tools/redo-icon.svg", + url: Script.resolvePath("../assets/tools/redo-icon.svg"), dimensions: { x: 0.0180, y: 0.0186 } } }, label: { properties: { - url: "../assets/tools/redo-label.svg", + url: Script.resolvePath("../assets/tools/redo-label.svg"), scale: 0.0192 } }, @@ -2086,7 +2089,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Update header. Overlays.editOverlay(menuHeaderBackOverlay, { visible: false }); Overlays.editOverlay(menuHeaderTitleOverlay, { - url: Script.resolvePath(MENU_HEADER_TITLE_PROPERTIES.url), + url: MENU_HEADER_TITLE_PROPERTIES.url, scale: MENU_HEADER_TITLE_PROPERTIES.scale }); Overlays.editOverlay(menuHeaderIconOverlay, { visible: false }); @@ -2111,7 +2114,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.icon.type].properties); properties = Object.merge(properties, UI_ELEMENTS.menuButton.icon.properties); properties = Object.merge(properties, MENU_ITEMS[i].icon.properties); - properties.url = Script.resolvePath(properties.url); properties.visible = isVisible; properties.parentID = buttonID; overlayID = Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.icon.type].overlay, properties); @@ -2121,7 +2123,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.label.type].properties); properties = Object.merge(properties, UI_ELEMENTS.menuButton.label.properties); properties = Object.merge(properties, MENU_ITEMS[i].label.properties); - properties.url = Script.resolvePath(properties.url); properties.visible = isVisible; properties.parentID = itemID; overlayID = Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.label.type].overlay, properties); @@ -2132,7 +2133,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.sublabel.type].properties); properties = Object.merge(properties, UI_ELEMENTS.menuButton.sublabel.properties); properties = Object.merge(properties, MENU_ITEMS[i].sublabel.properties); - properties.url = Script.resolvePath(properties.url); properties.visible = isVisible; properties.parentID = itemID; overlayID = Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.sublabel.type].overlay, properties); @@ -2176,7 +2176,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { closeMenu(); // Update header. - optionsHeadingURL = Script.resolvePath(menuItem.title.url); + optionsHeadingURL = menuItem.title.url; optionsHeadingScale = menuItem.title.scale; Overlays.editOverlay(menuHeaderBackOverlay, { visible: isVisible }); Overlays.editOverlay(menuHeaderTitleOverlay, { @@ -2184,7 +2184,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { scale: optionsHeadingScale }); Overlays.editOverlay(menuHeaderIconOverlay, { - url: Script.resolvePath(menuItem.icon.properties.url), + url: menuItem.icon.properties.url, dimensions: menuItem.icon.properties.dimensions, localPosition: Vec3.sum(MENU_HEADER_ICON_OFFSET, menuItem.icon.headerOffset), visible: isVisible @@ -2199,9 +2199,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.merge(properties, optionsItems[i].properties); } properties.parentID = parentID; - if (properties.url) { - properties.url = Script.resolvePath(properties.url); - } sublabelModifier = null; if (optionsItems[i].setting) { optionsSettings[optionsItems[i].id] = { key: optionsItems[i].setting.key }; @@ -2244,7 +2241,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { childProperties = Object.clone(UI_ELEMENTS.image.properties); childProperties = Object.merge(childProperties, UI_ELEMENTS[optionsItems[i].type].label); childProperties = Object.merge(childProperties, optionsItems[i].label); - childProperties.url = Script.resolvePath(childProperties.url); childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; id = Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); optionsOverlaysLabels[i] = id; @@ -2256,7 +2252,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { if (sublabelModifier) { childProperties = Object.merge(childProperties, sublabelModifier); } - childProperties.url = Script.resolvePath(childProperties.url); childProperties.parentID = optionsOverlays[optionsOverlays.length - 1]; id = Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties); optionsOverlaysSublabels[i] = id; @@ -2290,7 +2285,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Zero indicator. childProperties = Object.clone(UI_ELEMENTS.barSlider.zeroIndicator.properties); - childProperties.url = Script.resolvePath(childProperties.url); childProperties.dimensions = { x: properties.dimensions.x, y: UI_ELEMENTS.barSlider.zeroIndicator.properties.dimensions.y @@ -2311,7 +2305,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Primary image. if (optionsItems[i].imageURL) { childProperties = Object.clone(UI_ELEMENTS.image.properties); - childProperties.url = Script.resolvePath(optionsItems[i].imageURL); + childProperties.url = optionsItems[i].imageURL; childProperties.dimensions = { x: properties.dimensions.x, y: properties.dimensions.y }; imageOffset += 2 * IMAGE_OFFSET; // Double offset to prevent clipping against background. if (optionsItems[i].useBaseColor) { @@ -2328,7 +2322,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Overlay image. if (optionsItems[i].imageOverlayURL) { childProperties = Object.clone(UI_ELEMENTS.image.properties); - childProperties.url = Script.resolvePath(optionsItems[i].imageOverlayURL); + childProperties.url = optionsItems[i].imageOverlayURL; childProperties.dimensions = { x: properties.dimensions.x, y: properties.dimensions.y }; childProperties.emissive = false; imageOffset += IMAGE_OFFSET; @@ -2362,7 +2356,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Primary image. if (optionsItems[i].imageURL) { childProperties = Object.clone(UI_ELEMENTS.image.properties); - childProperties.url = Script.resolvePath(optionsItems[i].imageURL); + childProperties.url = optionsItems[i].imageURL; childProperties.scale = properties.dimensions.x; imageOffset += 2 * IMAGE_OFFSET; // Double offset to prevent clipping against background. childProperties.localPosition = { x: 0, y: properties.dimensions.y / 2 + imageOffset, z: 0 }; @@ -2375,7 +2369,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Overlay image. if (optionsItems[i].imageOverlayURL) { childProperties = Object.clone(UI_ELEMENTS.image.properties); - childProperties.url = Script.resolvePath(optionsItems[i].imageOverlayURL); + childProperties.url = optionsItems[i].imageOverlayURL; childProperties.scale = properties.dimensions.x; imageOffset += IMAGE_OFFSET; childProperties.emissive = false; @@ -2489,9 +2483,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.merge(properties, FOOTER_ITEMS[i].properties); properties.visible = isVisible; properties.parentID = menuPanelOverlay; - if (properties.url) { - properties.url = Script.resolvePath(properties.url); - } itemID = Overlays.addOverlay(UI_ELEMENTS[FOOTER_ITEMS[i].type].overlay, properties); footerOverlays[i] = itemID; footerEnabled[i] = true; @@ -2507,7 +2498,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.icon.type].properties); properties = Object.merge(properties, UI_ELEMENTS.menuButton.icon.properties); properties = Object.merge(properties, FOOTER_ITEMS[i].icon.properties); - properties.url = Script.resolvePath(properties.url); properties.visible = isVisible; properties.parentID = buttonID; overlayID = Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.icon.type].overlay, properties); @@ -2517,7 +2507,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.label.type].properties); properties = Object.merge(properties, UI_ELEMENTS.menuButton.label.properties); properties = Object.merge(properties, FOOTER_ITEMS[i].label.properties); - properties.url = Script.resolvePath(properties.url); properties.visible = isVisible; properties.parentID = itemID; overlayID = Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.label.type].overlay, properties); @@ -2537,7 +2526,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { optionsSettings.physicsPresets.value = "custom"; label = optionsItems[optionsOverlaysIDs.indexOf("physicsPresets")].customLabel; Overlays.editOverlay(optionsOverlaysLabels[optionsOverlaysIDs.indexOf("physicsPresets")], { - url: Script.resolvePath(label.url), + url: label.url, scale: label.scale, localPosition: label.localPosition }); @@ -2799,7 +2788,6 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { : UI_ELEMENTS[optionsItems[index].type].offHoverColor }); properties = Object.clone(value ? optionsItems[index].onSublabel : optionsItems[index].offSublabel); - properties.url = Script.resolvePath(properties.url); Overlays.editOverlay(optionsOverlaysSublabels[index], properties); uiCommandCallback(command, value); break; @@ -2820,7 +2808,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { : UI_ELEMENTS.picklist.properties.color }); Overlays.editOverlay(optionsOverlaysSublabels[index], { - url: Script.resolvePath("../assets/tools/common/down-arrow.svg") + url: PICKLIST_DOWN_ARROW }); // Hide options. @@ -2849,7 +2837,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { localPosition: Vec3.sum(optionsItems[index].properties.localPosition, PICKLIST_HOVER_DELTA) }); Overlays.editOverlay(optionsOverlaysSublabels[index], { - url: Script.resolvePath("../assets/tools/common/up-arrow.svg") + url: PICKLIST_UP_ARROW }); // Show options. @@ -2875,7 +2863,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Update picklist label. label = optionsItems[optionsOverlaysIDs.indexOf(parameter)].label; Overlays.editOverlay(optionsOverlaysLabels[optionsOverlaysIDs.indexOf("physicsPresets")], { - url: Script.resolvePath(label.url), + url: label.url, scale: label.scale, localPosition: label.localPosition }); @@ -3062,7 +3050,7 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { color: UIT.colors.white }); Overlays.editOverlay(menuHeaderTitleOverlay, { - url: Script.resolvePath(MENU_HEADER_TITLE_BACK_URL), + url: MENU_HEADER_TITLE_BACK_URL, scale: MENU_HEADER_TITLE_BACK_SCALE }); Overlays.editOverlay(menuHeaderIconOverlay, { @@ -3592,15 +3580,12 @@ ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) { // Heading content. properties = Object.clone(MENU_HEADER_BACK_PROPERTIES); properties.parentID = menuHeaderHeadingOverlay; - properties.url = Script.resolvePath(properties.url); menuHeaderBackOverlay = Overlays.addOverlay("image3d", properties); properties = Object.clone(MENU_HEADER_TITLE_PROPERTIES); properties.parentID = menuHeaderHeadingOverlay; - properties.url = Script.resolvePath(properties.url); menuHeaderTitleOverlay = Overlays.addOverlay("image3d", properties); properties = Object.clone(MENU_HEADER_ICON_PROPERTIES); properties.parentID = menuHeaderHeadingOverlay; - properties.url = Script.resolvePath(properties.url); menuHeaderIconOverlay = Overlays.addOverlay("image3d", properties); // Panel background. From ad838c6b3c38b71a30bcdbc663539b911bccf6fc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 27 Sep 2017 21:53:26 +1300 Subject: [PATCH 458/722] Adjust position of wrist icon --- scripts/vr-edit/modules/toolIcon.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/vr-edit/modules/toolIcon.js b/scripts/vr-edit/modules/toolIcon.js index cac31ed83f..a71dfdc156 100644 --- a/scripts/vr-edit/modules/toolIcon.js +++ b/scripts/vr-edit/modules/toolIcon.js @@ -19,8 +19,8 @@ ToolIcon = function (side) { MODEL_DIMENSIONS = { x: 0.1944, y: 0.1928, z: 0.1928 }, // Raw FBX dimensions. MODEL_SCALE = 0.7, // Adjust icon dimensions so that the green bar matches that of the Tools header. - MODEL_POSITION_LEFT_HAND = { x: -0.03, y: 0.035, z: 0 }, // x raises in thumb direction; y moves in fingers direction. - MODEL_POSITION_RIGHT_HAND = { x: 0.03, y: 0.035, z: 0 }, // "" + MODEL_POSITION_LEFT_HAND = { x: -0.025, y: 0.03, z: 0 }, // x raises in thumb direction; y moves in fingers direction. + MODEL_POSITION_RIGHT_HAND = { x: 0.025, y: 0.03, z: 0 }, // "" MODEL_ROTATION_LEFT_HAND = Quat.fromVec3Degrees({ x: 0, y: 0, z: 100 }), MODEL_ROTATION_RIGHT_HAND = Quat.fromVec3Degrees({ x: 0, y: 180, z: -100 }), From 20b1ab00876d5d82459c63bf54be303b352cb998 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 27 Sep 2017 22:06:28 +1300 Subject: [PATCH 459/722] Tidying --- scripts/vr-edit/modules/feedback.js | 8 ++++---- scripts/vr-edit/modules/hand.js | 6 +++--- scripts/vr-edit/utilities/utilities.js | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/scripts/vr-edit/modules/feedback.js b/scripts/vr-edit/modules/feedback.js index f1456df5e6..bf9b00ebc1 100644 --- a/scripts/vr-edit/modules/feedback.js +++ b/scripts/vr-edit/modules/feedback.js @@ -23,8 +23,8 @@ Feedback = (function () { CREATE_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/create.wav")), EQUIP_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/equip.wav")), ERROR_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/error.wav")), - UNDO_SOUND = DROP_SOUND, // TODO - REDO_SOUND = DROP_SOUND, // TODO + UNDO_SOUND = DROP_SOUND, + REDO_SOUND = DROP_SOUND, FEEDBACK_PARAMETERS = { DROP_TOOL: { sound: DROP_SOUND, volume: 0.3, haptic: 0.75 }, @@ -37,8 +37,8 @@ Feedback = (function () { EQUIP_TOOL: { sound: EQUIP_SOUND, volume: 0.3, haptic: 0.6 }, APPLY_PROPERTY: { sound: null, volume: 0, haptic: 0.3 }, APPLY_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 }, - UNDO_ACTION: { sound: UNDO_SOUND, volume: 0.1, haptic: 0.2 }, // TODO - REDO_ACTION: { sound: REDO_SOUND, volume: 0.1, haptic: 0.2 }, // TODO + UNDO_ACTION: { sound: UNDO_SOUND, volume: 0.1, haptic: 0.2 }, + REDO_ACTION: { sound: REDO_SOUND, volume: 0.1, haptic: 0.2 }, GENERAL_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 } }, diff --git a/scripts/vr-edit/modules/hand.js b/scripts/vr-edit/modules/hand.js index 174068a3d1..bd899c4724 100644 --- a/scripts/vr-edit/modules/hand.js +++ b/scripts/vr-edit/modules/hand.js @@ -27,11 +27,11 @@ Hand = function (side) { isTriggerPressed, isTriggerClicked, - TRIGGER_ON_VALUE = 0.15, // Per handControllerGrab.js. - TRIGGER_OFF_VALUE = 0.1, // Per handControllerGrab.js. + TRIGGER_ON_VALUE = 0.15, // Per controllerDispatcherUtils.js. + TRIGGER_OFF_VALUE = 0.1, // Per controllerDispatcherUtils.js. TRIGGER_CLICKED_VALUE = 1.0, - NEAR_GRAB_RADIUS = 0.05, // Different from handControllerGrab.js's value of 0.1. + NEAR_GRAB_RADIUS = 0.05, // Different from controllerDispatcherUtils.js. NEAR_HOVER_RADIUS = 0.025, LEFT_HAND = 0, diff --git a/scripts/vr-edit/utilities/utilities.js b/scripts/vr-edit/utilities/utilities.js index 07ee894731..198e45c256 100644 --- a/scripts/vr-edit/utilities/utilities.js +++ b/scripts/vr-edit/utilities/utilities.js @@ -115,7 +115,8 @@ if (typeof Object.merge !== "function") { b = JSON.stringify(objectB); if (a === "{}") { return JSON.parse(b); // Always return a new object. - } else if (b === "{}") { + } + if (b === "{}") { return JSON.parse(a); // "" } return JSON.parse(a.slice(0, -1) + "," + b.slice(1)); From 38096b48d97d0e73fd2b62e157c94fda15771350 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Wed, 27 Sep 2017 10:52:56 -0700 Subject: [PATCH 460/722] fixing entitySelectionTool --- scripts/system/edit.js | 1 + .../system/libraries/entitySelectionTool.js | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 467fa95dd7..5d29d8103b 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1245,6 +1245,7 @@ var lastPosition = null; Script.update.connect(function (deltaTime) { progressDialog.move(); selectionDisplay.checkMove(); + selectionDisplay.checkControllerMove(); var dOrientation = Math.abs(Quat.dot(Camera.orientation, lastOrientation) - 1); var dPosition = Vec3.distance(Camera.position, lastPosition); if (dOrientation > 0.001 || dPosition > 0.001) { diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 44f3c9e041..9c84660949 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -276,6 +276,10 @@ SelectionDisplay = (function() { var overlayNames = []; var lastCameraPosition = Camera.getPosition(); var lastCameraOrientation = Camera.getOrientation(); + var lastControllerPoses = [ + getControllerWorldLocation(Controller.Standard.LeftHand, true), + getControllerWorldLocation(Controller.Standard.RightHand, true) + ]; var handleHoverColor = { red: 224, @@ -4065,6 +4069,22 @@ SelectionDisplay = (function() { } }; + that.checkControllerMove = function() { + if (SelectionManager.hasSelection()) { + var controllerPose = getControllerWorldLocation(activeHand, true); + var hand = (activeHand === Controller.Standard.LeftHand) ? 0 : 1; + print(hand); + if (controllerPose.valid && lastControllerPoses[hand].valid) { + if (!Vec3.equal(controllerPose.position, lastControllerPoses[hand].position) || + !Vec3.equal(controllerPose.rotation, lastControllerPoses[hand].rotation)) { + print("setting controller pose"); + that.mouseMoveEvent({}); + } + } + lastControllerPoses[hand] = controllerPose; + } + }; + // FUNCTION: MOUSE PRESS EVENT that.mousePressEvent = function(event) { var wantDebug = false; From 3677718e48ca2e50b4bee8401e0514766257cb24 Mon Sep 17 00:00:00 2001 From: beholder Date: Wed, 27 Sep 2017 21:33:18 +0300 Subject: [PATCH 461/722] 7926 Create Mode: the keyboard tray is not styled correctly --- interface/resources/qml/controls-uit/Keyboard.qml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/controls-uit/Keyboard.qml b/interface/resources/qml/controls-uit/Keyboard.qml index ab361491bd..81579d9f71 100644 --- a/interface/resources/qml/controls-uit/Keyboard.qml +++ b/interface/resources/qml/controls-uit/Keyboard.qml @@ -11,9 +11,14 @@ import QtQuick 2.0 import "." -Item { +Rectangle { id: keyboardBase + anchors.left: parent.left + anchors.right: parent.right + + color: "#252525" + property bool raised: false property bool numeric: false @@ -105,6 +110,7 @@ Item { height: showMirrorText ? mirrorTextHeight : 0 width: keyboardWidth color: "#252525" + anchors.horizontalCenter: parent.horizontalCenter TextEdit { id: mirrorText From cea3c002dd57167d726171ddd93ee370c677c686 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Wed, 27 Sep 2017 11:47:26 -0700 Subject: [PATCH 462/722] improving far rotate --- .../controllers/controllerModules/farActionGrabEntity.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index 03e2c0baee..5c31c859e9 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -369,11 +369,6 @@ Script.include("/~/system/libraries/controllers.js"); otherFarGrabModule.currentObjectRotation = Quat.multiply(controllerRotationDelta, otherFarGrabModule.currentObjectRotation); - // Rotate about the translation controller's target position. - this.offsetPosition = Vec3.multiplyQbyV(controllerRotationDelta, this.offsetPosition); - otherFarGrabModule.offsetPosition = Vec3.multiplyQbyV(controllerRotationDelta, - otherFarGrabModule.offsetPosition); - this.previousWorldControllerRotation = worldControllerRotation; }; @@ -495,6 +490,7 @@ Script.include("/~/system/libraries/controllers.js"); } if (otherFarGrabModule.grabbedThingID === this.grabbedThingID && otherFarGrabModule.distanceHolding) { + this.prepareDistanceRotatingData(controllerData); this.distanceRotate(otherFarGrabModule); } else { this.distanceHolding = true; From 553829f7ab94acbd08c5dc97d255b0bede8cd77d Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 27 Sep 2017 11:57:20 -0700 Subject: [PATCH 463/722] make sure FadeEffect dependency is created early --- interface/src/Application.cpp | 4 ++++ libraries/render-utils/src/FadeEffect.cpp | 9 +++------ libraries/render-utils/src/FadeEffect.h | 2 +- libraries/render-utils/src/RenderDeferredTask.cpp | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e1c3af1939..369e4848d1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -198,6 +198,8 @@ #include #include +#include + #include "commerce/Ledger.h" #include "commerce/Wallet.h" #include "commerce/QmlCommerce.h" @@ -682,6 +684,8 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); diff --git a/libraries/render-utils/src/FadeEffect.cpp b/libraries/render-utils/src/FadeEffect.cpp index 418d02a4e7..c94fe717f1 100644 --- a/libraries/render-utils/src/FadeEffect.cpp +++ b/libraries/render-utils/src/FadeEffect.cpp @@ -16,7 +16,8 @@ #include FadeEffect::FadeEffect() { - + auto texturePath = PathUtils::resourcesPath() + "images/fadeMask.png"; + _maskMap = DependencyManager::get()->getImageTexture(texturePath, image::TextureUsage::STRICT_TEXTURE); } void FadeEffect::build(render::Task::TaskConcept& task, const task::Varying& editableItems) { @@ -28,15 +29,11 @@ void FadeEffect::build(render::Task::TaskConcept& task, const task::Varying& edi task.addJob("FadeEdit", fadeEditInput); } -render::ShapePipeline::BatchSetter FadeEffect::getBatchSetter() { +render::ShapePipeline::BatchSetter FadeEffect::getBatchSetter() const { return [this](const render::ShapePipeline& shapePipeline, gpu::Batch& batch, render::Args*) { auto program = shapePipeline.pipeline->getProgram(); auto maskMapLocation = program->getTextures().findLocation("fadeMaskMap"); auto bufferLocation = program->getUniformBuffers().findLocation("fadeParametersBuffer"); - if (!_maskMap) { - auto texturePath = PathUtils::resourcesPath() + "images/fadeMask.png"; - _maskMap = DependencyManager::get()->getImageTexture(texturePath, image::TextureUsage::STRICT_TEXTURE); - } batch.setResourceTexture(maskMapLocation, _maskMap); batch.setUniformBuffer(bufferLocation, _configurations); }; diff --git a/libraries/render-utils/src/FadeEffect.h b/libraries/render-utils/src/FadeEffect.h index 9827f67a7f..4b4e401332 100644 --- a/libraries/render-utils/src/FadeEffect.h +++ b/libraries/render-utils/src/FadeEffect.h @@ -21,7 +21,7 @@ public: void build(render::Task::TaskConcept& task, const task::Varying& editableItems); - render::ShapePipeline::BatchSetter getBatchSetter(); + render::ShapePipeline::BatchSetter getBatchSetter() const; render::ShapePipeline::ItemSetter getItemUniformSetter() const; render::ShapePipeline::ItemSetter getItemStoredSetter(); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index c67a1c7875..85df1ee8de 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -49,8 +49,8 @@ using namespace render; extern void initOverlay3DPipelines(render::ShapePlumber& plumber, bool depthTest = false); extern void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter); -RenderDeferredTask::RenderDeferredTask() { - DependencyManager::set(); +RenderDeferredTask::RenderDeferredTask() +{ } void RenderDeferredTask::configure(const Config& config) From 58a00a89d788321115acf197f3c60460be189080 Mon Sep 17 00:00:00 2001 From: druiz17 Date: Wed, 27 Sep 2017 12:58:11 -0700 Subject: [PATCH 464/722] remove debug print --- scripts/system/libraries/entitySelectionTool.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 9c84660949..4df25c41b7 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -4073,7 +4073,6 @@ SelectionDisplay = (function() { if (SelectionManager.hasSelection()) { var controllerPose = getControllerWorldLocation(activeHand, true); var hand = (activeHand === Controller.Standard.LeftHand) ? 0 : 1; - print(hand); if (controllerPose.valid && lastControllerPoses[hand].valid) { if (!Vec3.equal(controllerPose.position, lastControllerPoses[hand].position) || !Vec3.equal(controllerPose.rotation, lastControllerPoses[hand].rotation)) { From 00c3dcee2fc2059320d7e89d526c1d809b6c7f15 Mon Sep 17 00:00:00 2001 From: beholder Date: Thu, 28 Sep 2017 00:28:54 +0300 Subject: [PATCH 465/722] 7925 Create Mode: keyboard focused entry field is not visible note: changed the way of calculation scroll value after showing virtual keyboard. Now it doesn't require KEYBOARD_HEGHT and will always try to position focused element at vertical center of the client area --- .../resources/html/raiseAndLowerKeyboard.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/interface/resources/html/raiseAndLowerKeyboard.js b/interface/resources/html/raiseAndLowerKeyboard.js index 27ead23124..f87312b838 100644 --- a/interface/resources/html/raiseAndLowerKeyboard.js +++ b/interface/resources/html/raiseAndLowerKeyboard.js @@ -14,7 +14,6 @@ var isWindowFocused = true; var isKeyboardRaised = false; var isNumericKeyboard = false; - var KEYBOARD_HEIGHT = 200; function shouldRaiseKeyboard() { var nodeName = document.activeElement.nodeName; @@ -56,13 +55,15 @@ } if (!isKeyboardRaised) { - var delta = document.activeElement.getBoundingClientRect().bottom + 10 - - (document.body.clientHeight - KEYBOARD_HEIGHT); - if (delta > 0) { - setTimeout(function () { - document.body.scrollTop += delta; - }, 500); // Allow time for keyboard to be raised in QML. - } + var timeout = setTimeout(function () { + clearTimeout(timeout); + + var elementRect = document.activeElement.getBoundingClientRect(); + var absoluteElementTop = elementRect.top + window.scrollY; + var middle = absoluteElementTop - (window.innerHeight / 2); + + window.scrollTo(0, middle); + }, 500); // Allow time for keyboard to be raised in QML. } isKeyboardRaised = keyboardRaised; From b602c1847524425fa94395139c5aa6a65a2fb615 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 27 Sep 2017 12:07:42 -0700 Subject: [PATCH 466/722] prevent zero dimensions for Volume3DOverlay --- interface/src/ui/overlays/Volume3DOverlay.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/interface/src/ui/overlays/Volume3DOverlay.cpp b/interface/src/ui/overlays/Volume3DOverlay.cpp index 5be3247cec..8aa8490937 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.cpp +++ b/interface/src/ui/overlays/Volume3DOverlay.cpp @@ -45,7 +45,19 @@ void Volume3DOverlay::setProperties(const QVariantMap& properties) { } if (dimensions.isValid()) { - setDimensions(vec3FromVariant(dimensions)); + glm::vec3 scale = vec3FromVariant(dimensions); + // don't allow a zero or negative dimension component to reach the renderTransform + const float MIN_DIMENSION = 0.0001f; + if (scale.x < MIN_DIMENSION) { + scale.x = MIN_DIMENSION; + } + if (scale.y < MIN_DIMENSION) { + scale.y = MIN_DIMENSION; + } + if (scale.z < MIN_DIMENSION) { + scale.z = MIN_DIMENSION; + } + setDimensions(scale); } } From 26ea7034d84b8afa81c4abdbea31a0fc8b7caaa9 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 27 Sep 2017 14:30:26 -0700 Subject: [PATCH 467/722] prevent zero scale for Line3DOverlay --- interface/src/ui/overlays/Line3DOverlay.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index 87021cf852..69f52c5f2e 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -278,9 +278,10 @@ Transform Line3DOverlay::evalRenderTransform() { auto endPos = getEnd(); auto vec = endPos - transform.getTranslation(); - auto scale = glm::length(vec); + const float MIN_LINE_LENGTH = 0.0001f; + auto scale = glm::max(glm::length(vec), MIN_LINE_LENGTH); auto dir = vec / scale; - auto orientation = glm::rotation(glm::vec3(0,0,-1), dir); + auto orientation = glm::rotation(glm::vec3(0.0f, 0.0f, -1.0f), dir); transform.setRotation(orientation); transform.setScale(scale); From 7ad3a5a1e38b9dc5ee5db12c6e95da95936d0a08 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 27 Sep 2017 14:43:51 -0700 Subject: [PATCH 468/722] Commerce: Tons of Interface changes (#11463) * canRez(Tmp)Certified() * CertifiedItem beginnings * Skeleton of verifyOwnerChallenge() * Controlled failure; updateLocation() skeletion * Controlled failure on checkout page with ctrl+f * Skeleton Purchases first-use tutorial * Initial progress on new setup * Security pic tip * Skeleton Certificate page * Updates to Certificate * General progress; setup is nearly complete * Better buttons; last step almost done * Initial progress on wallet home * Completed recent transactions * Security page * Scrollbar * Fix auth error text * PassphraseSelection * Change security pic * Minor layout changes; beginnings of emulated header * Various layout changes; wallet nav bar * Help screen * Quick onaccepted change * First pass at new purchases * Small style updates * Some error progress * Lightbox in purchases * Collapse other help answers when clicking on another * REZZED notif * Commerce Lightbox * Lots of new interactions in Purchases * Hook up 'view certificate' * Fix errors, fix close button on cert * Purchases timer; much faster filter * Add debugCheckout * Purchase updates * GlyphButton; separator; Checkout Success; Ledger fix; debug modes * Lock glyph below security pic should be white * Various fixes, round 1 * Circular mask * Passphrase change button fix; TextField error edge highlighting * Recent Activity fixes * Various changes * Standard Security Pic location * Color changes * Filter bar changes * Styling for multiple owned items * Minor language change * Header dropdown (harder than expected) * Small fixes * View backup instructions * marketplaces.js onCommerceScreen * Beginnign of new injection * Marketplace injection changes * Purchase button style changes * More button styling * MY PURCHASES button * marketplace onUsernameChanged * New help QA * Help text changes etc * Downscale security image, reducing filesize * Lots of bugfixes * Cleanup before PR * Only open cert during inspection if commerce switch is on * Help text changes * Purchase status incl. change to confirmed; Help text; Open Explorer to hifikey * Quick glyph change * New 'wallet not set up' flow for when entering Purchases or Checkout without set-up wallet --- .../src/scripts/EntityScriptServer.cpp | 4 +- domain-server/src/DomainGatekeeper.cpp | 4 + domain-server/src/DomainServer.cpp | 5 +- .../src/DomainServerSettingsManager.cpp | 8 + interface/resources/fonts/hifi-glyphs.ttf | Bin 29268 -> 30784 bytes .../resources/qml/controls-uit/Button.qml | 47 +- .../resources/qml/controls-uit/Label.qml | 17 +- .../resources/qml/controls-uit/Separator.qml | 14 +- .../resources/qml/controls-uit/TextField.qml | 80 +- .../qml/hifi/commerce/checkout/Checkout.qml | 985 ++++++++---------- .../hifi/commerce/common/CommerceLightbox.qml | 153 +++ .../common/EmulatedMarketplaceHeader.qml | 333 ++++++ .../common/images/marketplaceHeaderImage.png | Bin 0 -> 5805 bytes .../InspectionCertificate.qml | 321 ++++++ .../inspectionCertificate/images/cert-bg.jpg | Bin 0 -> 64886 bytes .../commerce/purchases/FirstUseTutorial.qml | 196 ++++ .../hifi/commerce/purchases/PurchasedItem.qml | 471 +++++++-- .../qml/hifi/commerce/purchases/Purchases.qml | 514 +++++---- .../qml/hifi/commerce/wallet/Help.qml | 237 ++++- .../qml/hifi/commerce/wallet/NeedsLogIn.qml | 6 + .../qml/hifi/commerce/wallet/NotSetUp.qml | 127 --- ...ctionLightbox.qml => PassphraseChange.qml} | 110 +- .../hifi/commerce/wallet/PassphraseModal.qml | 257 +++-- .../commerce/wallet/PassphraseSelection.qml | 199 ++-- .../qml/hifi/commerce/wallet/Security.qml | 354 +++---- ...onLightbox.qml => SecurityImageChange.qml} | 180 ++-- .../wallet/SecurityImageSelection.qml | 6 +- .../qml/hifi/commerce/wallet/Wallet.qml | 472 +++++---- .../qml/hifi/commerce/wallet/WalletHome.qml | 305 +++--- .../qml/hifi/commerce/wallet/WalletSetup.qml | 746 +++++++++++++ .../commerce/wallet/WalletSetupLightbox.qml | 502 --------- .../qml/hifi/commerce/wallet/images/01.jpg | Bin 64005 -> 36764 bytes .../qml/hifi/commerce/wallet/images/02.jpg | Bin 36813 -> 19981 bytes .../qml/hifi/commerce/wallet/images/03.jpg | Bin 115650 -> 78081 bytes .../qml/hifi/commerce/wallet/images/04.jpg | Bin 51611 -> 14601 bytes .../qml/hifi/commerce/wallet/images/05.jpg | Bin 46063 -> 29209 bytes .../qml/hifi/commerce/wallet/images/06.jpg | Bin 76195 -> 43356 bytes .../commerce/wallet/images/lockOverlay.png | Bin 10087 -> 0 bytes .../hifi/commerce/wallet/images/wallet-bg.jpg | Bin 0 -> 9195 bytes .../commerce/wallet/images/wallet-tip-bg.png | Bin 0 -> 61761 bytes .../qml/styles-uit/HifiConstants.qml | 28 +- .../qml/styles-uit/RalewayRegular.qml | 2 +- interface/src/commerce/Ledger.cpp | 57 +- interface/src/commerce/Ledger.h | 8 +- interface/src/commerce/QmlCommerce.cpp | 4 +- interface/src/commerce/QmlCommerce.h | 2 +- interface/src/commerce/Wallet.cpp | 34 + interface/src/commerce/Wallet.h | 9 +- .../ui/overlays/ContextOverlayInterface.cpp | 20 +- .../src/ui/overlays/ContextOverlayInterface.h | 6 + libraries/entities/src/EntityItem.h | 3 - .../entities/src/EntityScriptingInterface.cpp | 12 + .../entities/src/EntityScriptingInterface.h | 14 + libraries/entities/src/EntityTree.cpp | 13 +- libraries/networking/src/LimitedNodeList.cpp | 8 + libraries/networking/src/LimitedNodeList.h | 4 + libraries/networking/src/Node.h | 2 + libraries/networking/src/NodePermissions.cpp | 10 + libraries/networking/src/NodePermissions.h | 4 +- libraries/networking/src/udt/PacketHeaders.h | 1 + scripts/system/commerce/wallet.js | 8 + scripts/system/html/js/marketplacesInject.js | 158 ++- scripts/system/marketplaces/marketplaces.js | 142 ++- 63 files changed, 4767 insertions(+), 2435 deletions(-) create mode 100644 interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml create mode 100644 interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml create mode 100644 interface/resources/qml/hifi/commerce/common/images/marketplaceHeaderImage.png create mode 100644 interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml create mode 100644 interface/resources/qml/hifi/commerce/inspectionCertificate/images/cert-bg.jpg create mode 100644 interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml delete mode 100644 interface/resources/qml/hifi/commerce/wallet/NotSetUp.qml rename interface/resources/qml/hifi/commerce/wallet/{PassphraseSelectionLightbox.qml => PassphraseChange.qml} (62%) rename interface/resources/qml/hifi/commerce/wallet/{SecurityImageSelectionLightbox.qml => SecurityImageChange.qml} (57%) create mode 100644 interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml delete mode 100644 interface/resources/qml/hifi/commerce/wallet/WalletSetupLightbox.qml delete mode 100644 interface/resources/qml/hifi/commerce/wallet/images/lockOverlay.png create mode 100644 interface/resources/qml/hifi/commerce/wallet/images/wallet-bg.jpg create mode 100644 interface/resources/qml/hifi/commerce/wallet/images/wallet-tip-bg.png diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index fd53ab2391..2bfcdcdf8c 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -102,7 +102,7 @@ static const QString ENTITY_SCRIPT_SERVER_LOGGING_NAME = "entity-script-server"; void EntityScriptServer::handleReloadEntityServerScriptPacket(QSharedPointer message, SharedNodePointer senderNode) { // These are temporary checks until we can ensure that nodes eventually disconnect if the Domain Server stops telling them // about each other. - if (senderNode->getCanRez() || senderNode->getCanRezTmp()) { + if (senderNode->getCanRez() || senderNode->getCanRezTmp() || senderNode->getCanRezCertified() || senderNode->getCanRezTmpCertified()) { auto entityID = QUuid::fromRfc4122(message->read(NUM_BYTES_RFC4122_UUID)); if (_entityViewer.getTree() && !_shuttingDown) { @@ -116,7 +116,7 @@ void EntityScriptServer::handleReloadEntityServerScriptPacket(QSharedPointer message, SharedNodePointer senderNode) { // These are temporary checks until we can ensure that nodes eventually disconnect if the Domain Server stops telling them // about each other. - if (senderNode->getCanRez() || senderNode->getCanRezTmp()) { + if (senderNode->getCanRez() || senderNode->getCanRezTmp() || senderNode->getCanRezCertified() || senderNode->getCanRezTmpCertified()) { MessageID messageID; message->readPrimitive(&messageID); auto entityID = QUuid::fromRfc4122(message->read(NUM_BYTES_RFC4122_UUID)); diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index dbbcc004ca..09bebf806a 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -269,6 +269,8 @@ void DomainGatekeeper::updateNodePermissions() { userPerms.permissions |= NodePermissions::Permission::canAdjustLocks; userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; + userPerms.permissions |= NodePermissions::Permission::canRezPermanentCertifiedEntities; + userPerms.permissions |= NodePermissions::Permission::canRezTemporaryCertifiedEntities; userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; } else { @@ -358,6 +360,8 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo userPerms.permissions |= NodePermissions::Permission::canAdjustLocks; userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; + userPerms.permissions |= NodePermissions::Permission::canRezPermanentCertifiedEntities; + userPerms.permissions |= NodePermissions::Permission::canRezTemporaryCertifiedEntities; userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; newNode->setPermissions(userPerms); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index b7336e1505..436f49c7ca 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -959,7 +959,8 @@ bool DomainServer::isInInterestSet(const SharedNodePointer& nodeA, const SharedN bool isAgentWithoutRights = nodeA->getType() == NodeType::Agent && nodeB->getType() == NodeType::EntityScriptServer - && !nodeA->getCanRez() && !nodeA->getCanRezTmp(); + && !nodeA->getCanRez() && !nodeA->getCanRezTmp() + && !nodeA->getCanRezCertified() && !nodeA->getCanRezTmpCertified(); if (isAgentWithoutRights) { return false; @@ -968,7 +969,7 @@ bool DomainServer::isInInterestSet(const SharedNodePointer& nodeA, const SharedN bool isScriptServerForIneffectiveAgent = (nodeA->getType() == NodeType::EntityScriptServer && nodeB->getType() == NodeType::Agent) && ((nodeBData && !nodeBData->getNodeInterestSet().contains(NodeType::EntityScriptServer)) - || (!nodeB->getCanRez() && !nodeB->getCanRezTmp())); + || (!nodeB->getCanRez() && !nodeB->getCanRezTmp() && !nodeB->getCanRezCertified() && !nodeB->getCanRezTmpCertified())); return !isScriptServerForIneffectiveAgent; } else { diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index d93126f2c7..c801a4a71a 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -302,6 +302,14 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList _standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canReplaceDomainContent); packPermissions(); } + + if (oldVersion < 1.9) { + unpackPermissions(); + // This was prior to addition of canRez(Tmp)Certified; add those to localhost permissions by default + _standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canRezPermanentCertifiedEntities); + _standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canRezTemporaryCertifiedEntities); + packPermissions(); + } } unpackPermissions(); diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index 558562cec52168a9025db4b7c4dd20280a5a5d74..3db48602b1eb845238497d2f3750fa7d281ccfbc 100644 GIT binary patch delta 1938 zcmZ`)U2I%O75>iLxik0P-M#yB{}S7~j`!MkW1A%P{@m;OZ)5DFL4>9yC6qSB+1lGU ziS1Zxn>ZlYkhDpfD3!q|4=sH`kQY)~r$RN z(wsBroH<9|nb9}1eUEUVU}@Wq|!2V9%bett>xy00p@49{}3vg^SO9kiI+(h@-$+ zd%j+qi~se}F9CQLKsY~NZ`Sxj^d3MqfZqAV)%EV5dVdG#4S;eB=VxmjGf^B+%K+_K ztgSEOK7AFS;TE8!+G72~=yxZ8D{EuUXm{q^_OUj-UApuXR3j(}4eU*4EWj{OTU zhSvDg2jcFIX5#L5X`F6uPS7*dzx93Up{X^D;TEpqBqp(h5xmyUQGoXBY8M5agwkGh zfz!cOx6rUt{af$#Z~cvRQu;Ek;CZ}&8@P&VxQ-XM1W(eYr9Q|Me^N3^W2KGK{qm9W zjq*O9{rK$UV-j>2x{N+Crd7-xf&55}n+jgl)zspw|OmV>M% zMIk5zRSG>fOEgLf=WIgC0ozbW{h3rW{w7-%Z%MB6HwEWvcaM-&m2-O1O-Mx{MiW9d z2OLc&-Xj#5)rc;(m6}%-QhK_DsHzGHAA~XV(mQm&1+h=;Kq~}M7=@ANdWEp$g;AyG zmZE&%SE7pa^0FX3A%*99ZhKo81qDHkOi#ucc{oaEa#pvRrc^USi9X7t+)+iU2i-fr zA+L+_G@b8r-Kj%Iy6l1%6K;&ZqjIhI=hVe_vPs%|24Je^L)UOnG{mJwwlW z)#=Y233?qP-Z5b9=1Ll1xP@!9O5a2hIl#&nf-3V|DGpGQgjWumtphDpTBcKaMH9N2 zePYoxbn~gn96y#wB|g9B;B?Ppl$Hs@8a<{bOk;Atr97&~6WZ8UcJ1HqoCL#FEYVH+ z4hkp%RuD#^>*YwARFG~rxk(ZS$Rz3Qtb%BQqH-9O13%vs(ssRK_zx%1)lv0a)v~90 z#+jPoT<40(bj4H@gR=q7Q&VYv#qZedQ*TSiomd z6%tvp-BD^e!=QCuex)20r0YeVEIB?cZktx8*jClEXA_onCpVDG#Pu0H&K`GD(H}p1 zJT51u8;u6twh~V#kE-gn=(NmjGxLX(lWXRj6weg&L_#lQ9!kYisrckjQ5t>4L`R!{ zlQ>R4qSui{KaKzt6av`>Y*GuB=d__kl?hfUtN-sIuZls+kJHDk3{opJLOJrBVvZb} zHdG@X|BKmlOxrdc^PZDtTI{2@Vkcs;6EVta;+|ejIZmpo-(yTwh;)NC(o!Sa9AS<* zV>+y2+TFxE4+qCD&%|Ogu}4x`#ZAs6-4TrmmG(^ceG!Bu%+ibW4FmwSNiBDnTbZCL zh0Q<;Y0?gtD=j{SQApQR#njA_Ftwbi`+8CsdT&OxIAhsijg0hDL#N`jE{p-Ef0JoO zayZLlgGNR-M9$u-SzTu-;aMBqga2xZP~VM@7>#ymi;OA-2XNJgx64`HTofT zx9-_5L}B1d(c-DwO|7S-RIE}+Yl=1$386Q1C1eVdLC6iRsOsySCN*tbq=?DulB-O; z9#fh6j*6|{o=p-BZ~pn*0UF+nE!?J?TR&Lfl%XbFqHoh>x3v`vPZQWU%;g8GN z`TFd+eM4G(eRjUKbh@t0KQ}Ap&o9>Z4aJrlXTDZjt?xTmzZhSs&#pDjtX|x=d}dZ$ rs4XniSNTePX--?Nt*l%)-Oz!_c*i?>bZhZh$_+(APD!NA5vdfsm0~DR=aLJOOd+%jVNs;-L2k+&&Ob zPAi+-$qa~dFu0`U^v5sfDOk7!vCVvOW&5z}0kRcvZ`a`jyni}lvn>P9coiI7-PWq_ zsnn`F5|d49P^Kkl@5v{L;1JXus1G0sT=f<}m2QWO2Huy}?t|Z5ezC)9vsf|rs{(e? zbDFU=vq>as<(DH8wf2{HWz+5}sA4CJv#Pf-ZRqL6a5UWW+4@`RkPWN7?DzgZRjd93 D=?G^_ diff --git a/interface/resources/qml/controls-uit/Button.qml b/interface/resources/qml/controls-uit/Button.qml index 59f8a63238..5acc31dfd0 100644 --- a/interface/resources/qml/controls-uit/Button.qml +++ b/interface/resources/qml/controls-uit/Button.qml @@ -15,8 +15,11 @@ import QtQuick.Controls.Styles 1.4 import "../styles-uit" Original.Button { + id: root; + property int color: 0 property int colorScheme: hifi.colorSchemes.light + property string buttonGlyph: ""; width: 120 height: hifi.dimensions.controlLineHeight @@ -28,6 +31,13 @@ Original.Button { background: Rectangle { radius: hifi.buttons.radius + border.width: (control.color === hifi.buttons.none || + (control.color === hifi.buttons.noneBorderless && control.hovered) || + (control.color === hifi.buttons.noneBorderlessWhite && control.hovered) || + (control.color === hifi.buttons.noneBorderlessGray && control.hovered)) ? 1 : 0; + border.color: control.color === hifi.buttons.noneBorderless ? hifi.colors.blueHighlight : + (control.color === hifi.buttons.noneBorderlessGray ? hifi.colors.baseGray : hifi.colors.white); + gradient: Gradient { GradientStop { position: 0.2 @@ -60,14 +70,35 @@ Original.Button { } } - label: RalewayBold { - font.capitalization: Font.AllUppercase - color: enabled ? hifi.buttons.textColor[control.color] - : hifi.buttons.disabledTextColor[control.colorScheme] - size: hifi.fontSizes.buttonLabel - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - text: control.text + label: Item { + HiFiGlyphs { + id: buttonGlyph; + visible: root.buttonGlyph !== ""; + text: root.buttonGlyph === "" ? hifi.glyphs.question : root.buttonGlyph; + // Size + size: 34; + // Anchors + anchors.right: buttonText.left; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + // Style + color: enabled ? hifi.buttons.textColor[control.color] + : hifi.buttons.disabledTextColor[control.colorScheme]; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + RalewayBold { + id: buttonText; + anchors.centerIn: parent; + font.capitalization: Font.AllUppercase + color: enabled ? hifi.buttons.textColor[control.color] + : hifi.buttons.disabledTextColor[control.colorScheme] + size: hifi.fontSizes.buttonLabel + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + text: control.text + } } } } diff --git a/interface/resources/qml/controls-uit/Label.qml b/interface/resources/qml/controls-uit/Label.qml index 330d74fa14..1dc3aa0dd4 100644 --- a/interface/resources/qml/controls-uit/Label.qml +++ b/interface/resources/qml/controls-uit/Label.qml @@ -17,6 +17,19 @@ RalewaySemiBold { property int colorScheme: hifi.colorSchemes.light size: hifi.fontSizes.inputLabel - color: enabled ? (colorScheme == hifi.colorSchemes.light ? hifi.colors.lightGray : hifi.colors.lightGrayText) - : (colorScheme == hifi.colorSchemes.light ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight); + color: { + if (colorScheme === hifi.colorSchemes.dark) { + if (enabled) { + hifi.colors.lightGrayText + } else { + hifi.colors.baseGrayHighlight + } + } else { + if (enabled) { + hifi.colors.lightGray + } else { + hifi.colors.lightGrayText + } + } + } } diff --git a/interface/resources/qml/controls-uit/Separator.qml b/interface/resources/qml/controls-uit/Separator.qml index 5a775221f6..5e2d278454 100644 --- a/interface/resources/qml/controls-uit/Separator.qml +++ b/interface/resources/qml/controls-uit/Separator.qml @@ -12,8 +12,13 @@ import QtQuick 2.5 import "../styles-uit" Item { + property int colorScheme: 0; + + readonly property var topColor: [ hifi.colors.baseGrayShadow, hifi.colors.faintGray ]; + readonly property var bottomColor: [ hifi.colors.baseGrayHighlight, hifi.colors.faintGray ]; + // Size - height: 2; + height: colorScheme === 0 ? 2 : 1; Rectangle { // Size width: parent.width; @@ -21,18 +26,19 @@ Item { // Anchors anchors.left: parent.left; anchors.bottom: parent.bottom; - anchors.bottomMargin: height; // Style - color: hifi.colors.baseGrayShadow; + color: topColor[colorScheme]; } Rectangle { + visible: colorScheme === 0; // Size width: parent.width; height: 1; // Anchors anchors.left: parent.left; anchors.bottom: parent.bottom; + anchors.bottomMargin: -height; // Style - color: hifi.colors.baseGrayHighlight; + color: bottomColor[colorScheme]; } } diff --git a/interface/resources/qml/controls-uit/TextField.qml b/interface/resources/qml/controls-uit/TextField.qml index 65fab00700..a8b2ed45a5 100644 --- a/interface/resources/qml/controls-uit/TextField.qml +++ b/interface/resources/qml/controls-uit/TextField.qml @@ -20,9 +20,13 @@ TextField { property int colorScheme: hifi.colorSchemes.light readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light + readonly property bool isFaintGrayColorScheme: colorScheme == hifi.colorSchemes.faintGray property bool isSearchField: false property string label: "" property real controlHeight: height + (textFieldLabel.visible ? textFieldLabel.height + 1 : 0) + property bool hasRoundedBorder: false + property bool error: false; + property bool hasClearButton: false; placeholderText: textField.placeholderText @@ -35,16 +39,53 @@ TextField { y: textFieldLabel.visible ? textFieldLabel.height + textFieldLabel.anchors.bottomMargin : 0 style: TextFieldStyle { - textColor: isLightColorScheme - ? (textField.activeFocus ? hifi.colors.black : hifi.colors.lightGray) - : (textField.activeFocus ? hifi.colors.white : hifi.colors.lightGrayText) + textColor: { + if (isLightColorScheme) { + if (textField.activeFocus) { + hifi.colors.black + } else { + hifi.colors.lightGray + } + } else if (isFaintGrayColorScheme) { + if (textField.activeFocus) { + hifi.colors.black + } else { + hifi.colors.lightGray + } + } else { + if (textField.activeFocus) { + hifi.colors.white + } else { + hifi.colors.lightGrayText + } + } + } background: Rectangle { - color: isLightColorScheme - ? (textField.activeFocus ? hifi.colors.white : hifi.colors.textFieldLightBackground) - : (textField.activeFocus ? hifi.colors.black : hifi.colors.baseGrayShadow) - border.color: hifi.colors.primaryHighlight - border.width: textField.activeFocus ? 1 : 0 - radius: isSearchField ? textField.height / 2 : 0 + color: { + if (isLightColorScheme) { + if (textField.activeFocus) { + hifi.colors.white + } else { + hifi.colors.textFieldLightBackground + } + } else if (isFaintGrayColorScheme) { + if (textField.activeFocus) { + hifi.colors.white + } else { + hifi.colors.faintGray50 + } + } else { + if (textField.activeFocus) { + hifi.colors.black + } else { + hifi.colors.baseGrayShadow + } + } + } + border.color: textField.error ? hifi.colors.redHighlight : + (textField.activeFocus ? hifi.colors.primaryHighlight : (isFaintGrayColorScheme ? hifi.colors.lightGrayText : hifi.colors.lightGray)) + border.width: textField.activeFocus || hasRoundedBorder || textField.error ? 1 : 0 + radius: isSearchField ? textField.height / 2 : (hasRoundedBorder ? 4 : 0) HiFiGlyphs { text: hifi.glyphs.search @@ -55,12 +96,29 @@ TextField { anchors.leftMargin: hifi.dimensions.textPadding - 2 visible: isSearchField } + + HiFiGlyphs { + text: hifi.glyphs.error + color: textColor + size: 40 + anchors.right: parent.right + anchors.rightMargin: hifi.dimensions.textPadding - 2 + anchors.verticalCenter: parent.verticalCenter + visible: hasClearButton && textField.text !== ""; + + MouseArea { + anchors.fill: parent; + onClicked: { + textField.text = ""; + } + } + } } - placeholderTextColor: hifi.colors.lightGray + placeholderTextColor: isFaintGrayColorScheme ? hifi.colors.lightGrayText : hifi.colors.lightGray selectedTextColor: hifi.colors.black selectionColor: hifi.colors.primaryHighlight padding.left: (isSearchField ? textField.height - 2 : 0) + hifi.dimensions.textPadding - padding.right: hifi.dimensions.textPadding + padding.right: (hasClearButton ? textField.height - 2 : 0) + hifi.dimensions.textPadding } HifiControls.Label { diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 14a84311aa..182a8df055 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -18,6 +18,7 @@ import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls import "../wallet" as HifiWallet +import "../common" as HifiCommerceCommon // references XXX from root context @@ -29,25 +30,21 @@ Rectangle { property bool purchasesReceived: false; property bool balanceReceived: false; property bool securityImageResultReceived: false; - property string itemId: ""; - property string itemHref: ""; - property double balanceAfterPurchase: 0; + property string itemId; + property string itemPreviewImageUrl; + property string itemHref; + property double balanceAfterPurchase; property bool alreadyOwned: false; - property int itemPriceFull: 0; + property int itemPrice: 0; property bool itemIsJson: true; + property bool shouldBuyWithControlledFailure: false; + property bool debugCheckoutSuccess: false; + property bool canRezCertifiedItems: false; // Style - color: hifi.colors.baseGray; + color: hifi.colors.white; Hifi.QmlCommerce { id: commerce; - onAccountResult: { - if (result.status === "success") { - commerce.getKeyFilePathIfExists(); - } else { - // unsure how to handle a failure here. We definitely cannot proceed. - } - } - onLoginStatusResult: { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; @@ -57,9 +54,18 @@ Rectangle { } } + onAccountResult: { + if (result.status === "success") { + commerce.getKeyFilePathIfExists(); + } else { + // unsure how to handle a failure here. We definitely cannot proceed. + } + } + onKeyFilePathIfExistsResult: { if (path === "" && root.activeView !== "notSetUp") { root.activeView = "notSetUp"; + notSetUpTimer.start(); } else if (path !== "" && root.activeView === "initialize") { commerce.getSecurityImage(); } @@ -69,26 +75,17 @@ Rectangle { securityImageResultReceived = true; if (!exists && root.activeView !== "notSetUp") { // "If security image is not set up" root.activeView = "notSetUp"; + notSetUpTimer.start(); } else if (exists && root.activeView === "initialize") { commerce.getWalletAuthenticatedStatus(); - } else if (exists) { - // just set the source again (to be sure the change was noticed) - securityImage.source = ""; - securityImage.source = "image://security/securityImage"; } } onWalletAuthenticatedStatusResult: { - if (!isAuthenticated && !passphraseModal.visible) { - passphraseModal.visible = true; + if (!isAuthenticated && root.activeView !== "passphraseModal") { + root.activeView = "passphraseModal"; } else if (isAuthenticated) { - root.activeView = "checkoutMain"; - if (!balanceReceived) { - commerce.balance(); - } - if (!purchasesReceived) { - commerce.inventory(); - } + authSuccessStep(); } } @@ -106,8 +103,7 @@ Rectangle { console.log("Failed to get balance", result.data.message); } else { root.balanceReceived = true; - hfcBalanceText.text = result.data.balance + " HFC"; - balanceAfterPurchase = result.data.balance - root.itemPriceFull; + root.balanceAfterPurchase = result.data.balance - root.itemPrice; root.setBuyText(); } } @@ -127,68 +123,57 @@ Rectangle { } } + Timer { + id: notSetUpTimer; + interval: 200; + onTriggered: { + sendToScript({method: 'checkout_walletNotSetUp'}); + } + } + + HifiCommerceCommon.CommerceLightbox { + id: lightboxPopup; + visible: false; + anchors.fill: parent; + + Connections { + onSendToParent: { + sendToScript(msg); + } + } + } + // // TITLE BAR START // - Item { + HifiCommerceCommon.EmulatedMarketplaceHeader { id: titleBarContainer; + z: 998; visible: !needsLogIn.visible; // Size width: parent.width; - height: 50; + height: 70; // Anchors anchors.left: parent.left; anchors.top: parent.top; - // Title Bar text - RalewaySemiBold { - id: titleBarText; - text: "MARKETPLACE"; - // Text size - size: hifi.fontSizes.overlayTitle; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.bottom: parent.bottom; - width: paintedWidth; - // Style - color: hifi.colors.faintGray; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - // Security Image (TEMPORARY!) - Image { - id: securityImage; - // Anchors - anchors.top: parent.top; - anchors.right: parent.right; - anchors.verticalCenter: parent.verticalCenter; - height: parent.height - 10; - width: height; - fillMode: Image.PreserveAspectFit; - mipmap: true; - cache: false; - source: "image://security/securityImage"; - } - Image { - id: securityImageOverlay; - source: "../wallet/images/lockOverlay.png"; - width: securityImage.width * 0.45; - height: securityImage.height * 0.45; - anchors.bottom: securityImage.bottom; - anchors.right: securityImage.right; - mipmap: true; - opacity: 0.9; - } - - // Separator - HifiControlsUit.Separator { - anchors.left: parent.left; - anchors.right: parent.right; - anchors.bottom: parent.bottom; + Connections { + onSendToParent: { + if (msg.method === 'needsLogIn' && root.activeView !== "needsLogIn") { + root.activeView = "needsLogIn"; + } else if (msg.method === 'showSecurityPicLightbox') { + lightboxPopup.titleText = "Your Security Pic"; + lightboxPopup.bodyImageSource = msg.securityImageSource; + lightboxPopup.bodyText = lightboxPopup.securityPicBodyText; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.button2text = "GO TO WALLET"; + lightboxPopup.button2method = "sendToParent({method: 'checkout_openWallet'});"; + lightboxPopup.visible = true; + } else { + sendToScript(msg); + } + } } } // @@ -202,7 +187,7 @@ Rectangle { anchors.bottom: parent.top; anchors.left: parent.left; anchors.right: parent.right; - color: hifi.colors.baseGray; + color: hifi.colors.white; Component.onCompleted: { securityImageResultReceived = false; @@ -235,101 +220,21 @@ Rectangle { HifiWallet.PassphraseModal { id: passphraseModal; - visible: false; - anchors.top: titleBarContainer.bottom; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.right: parent.right; + visible: root.activeView === "passphraseModal"; + anchors.fill: parent; + titleBarText: "Checkout"; + titleBarIcon: hifi.glyphs.wallet; Connections { onSendSignalToParent: { - sendToScript(msg); - } - } - } - - // - // "WALLET NOT SET UP" START - // - Item { - id: notSetUp; - visible: root.activeView === "notSetUp"; - anchors.top: titleBarContainer.bottom; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.right: parent.right; - - RalewayRegular { - id: notSetUpText; - text: "Your wallet isn't set up.

    Set up your Wallet (no credit card necessary) to claim your free HFC " + - "and get items from the Marketplace."; - // Text size - size: 24; - // Anchors - anchors.top: parent.top; - anchors.bottom: notSetUpActionButtonsContainer.top; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - // Style - color: hifi.colors.faintGray; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - } - - Item { - id: notSetUpActionButtonsContainer; - // Size - width: root.width; - height: 70; - // Anchors - anchors.left: parent.left; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 24; - - // "Cancel" button - HifiControlsUit.Button { - id: cancelButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.left: parent.left; - anchors.leftMargin: 20; - width: parent.width/2 - anchors.leftMargin*2; - text: "Cancel" - onClicked: { - sendToScript({method: 'checkout_cancelClicked', params: itemId}); - } - } - - // "Set Up" button - HifiControlsUit.Button { - id: setUpButton; - color: hifi.buttons.blue; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: parent.width/2 - anchors.rightMargin*2; - text: "Set Up Wallet" - onClicked: { - sendToScript({method: 'checkout_setUpClicked'}); + if (msg.method === "authSuccess") { + authSuccessStep(); + } else { + sendToScript(msg); } } } } - // - // "WALLET NOT SET UP" END - // // // CHECKOUT CONTENTS START @@ -342,263 +247,117 @@ Rectangle { anchors.left: parent.left; anchors.right: parent.right; - // - // ITEM DESCRIPTION START - // - Item { - id: itemDescriptionContainer; - // Anchors - anchors.left: parent.left; - anchors.leftMargin: 32; - anchors.right: parent.right; - anchors.rightMargin: 32; + RalewayRegular { + id: confirmPurchaseText; anchors.top: parent.top; - anchors.bottom: checkoutActionButtonsContainer.top; + anchors.topMargin: 30; + anchors.left: parent.left; + anchors.leftMargin: 16; + width: paintedWidth; + height: paintedHeight; + text: "Confirm Purchase:"; + color: hifi.colors.baseGray; + size: 28; + } + + HifiControlsUit.Separator { + id: separator; + colorScheme: 1; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: confirmPurchaseText.bottom; + anchors.topMargin: 16; + } - // HFC Balance text - Item { - id: hfcBalanceContainer; + Item { + id: itemContainer; + anchors.top: separator.bottom; + anchors.topMargin: 24; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 120; + + Image { + id: itemPreviewImage; + source: root.itemPreviewImageUrl; + anchors.left: parent.left; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + width: height; + fillMode: Image.PreserveAspectCrop; + } + + RalewaySemiBold { + id: itemNameText; + // Text size + size: 26; // Anchors anchors.top: parent.top; - anchors.topMargin: 30; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: childrenRect.height; - - RalewaySemiBold { - id: hfcBalanceTextLabel; - text: "Balance:"; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - width: paintedWidth; - height: paintedHeight; - // Text size - size: 30; - // Style - color: hifi.colors.lightGrayText; - // Alignment - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - } - RalewayRegular { - id: hfcBalanceText; - text: "-- HFC"; - // Text size - size: hfcBalanceTextLabel.size; - // Anchors - anchors.top: parent.top; - anchors.left: hfcBalanceTextLabel.right; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: paintedHeight; - // Style - color: hifi.colors.lightGrayText; - // Alignment - horizontalAlignment: Text.AlignRight; - verticalAlignment: Text.AlignVCenter; - } - } - - // Item Name text - Item { - id: itemNameContainer; - // Anchors - anchors.top: hfcBalanceContainer.bottom; - anchors.topMargin: 32; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: childrenRect.height; - - RalewaySemiBold { - id: itemNameTextLabel; - text: "Item:"; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - width: paintedWidth; - height: paintedHeight; - // Text size - size: 20; - // Style - color: hifi.colors.lightGrayText; - // Alignment - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - } - RalewayRegular { - id: itemNameText; - // Text size - size: itemNameTextLabel.size; - // Anchors - anchors.top: parent.top; - anchors.left: itemNameTextLabel.right; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: paintedHeight; - // Style - color: hifi.colors.lightGrayText; - elide: Text.ElideRight; - // Alignment - horizontalAlignment: Text.AlignRight; - verticalAlignment: Text.AlignVCenter; - } - } - - - // Item Author text - Item { - id: itemAuthorContainer; - // Anchors - anchors.top: itemNameContainer.bottom; - anchors.topMargin: 4; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: childrenRect.height; - - RalewaySemiBold { - id: itemAuthorTextLabel; - text: "Author:"; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - width: paintedWidth; - height: paintedHeight; - // Text size - size: 20; - // Style - color: hifi.colors.lightGrayText; - // Alignment - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - } - RalewayRegular { - id: itemAuthorText; - // Text size - size: itemAuthorTextLabel.size; - // Anchors - anchors.top: parent.top; - anchors.left: itemAuthorTextLabel.right; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: paintedHeight; - // Style - color: hifi.colors.lightGrayText; - elide: Text.ElideRight; - // Alignment - horizontalAlignment: Text.AlignRight; - verticalAlignment: Text.AlignVCenter; - } + anchors.left: itemPreviewImage.right; + anchors.leftMargin: 12; + anchors.right: itemPriceContainer.left; + anchors.rightMargin: 8; + height: 30; + // Style + color: hifi.colors.blueAccent; + elide: Text.ElideRight; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; } // "Item Price" container Item { id: itemPriceContainer; // Anchors - anchors.top: itemAuthorContainer.bottom; - anchors.topMargin: 32; - anchors.left: parent.left; - anchors.leftMargin: 16; + anchors.top: parent.top; anchors.right: parent.right; - anchors.rightMargin: 16; - height: childrenRect.height; + height: 30; + width: childrenRect.width; - RalewaySemiBold { + // "HFC" balance label + HiFiGlyphs { id: itemPriceTextLabel; - text: "Item Price:"; + text: hifi.glyphs.hfc; + // Size + size: 36; // Anchors + anchors.right: itemPriceText.left; + anchors.rightMargin: 4; anchors.top: parent.top; - anchors.left: parent.left; + anchors.topMargin: -4; width: paintedWidth; height: paintedHeight; - // Text size - size: 30; // Style - color: hifi.colors.lightGrayText; - // Alignment - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; + color: hifi.colors.blueAccent; } - RalewayRegular { + FiraSansSemiBold { id: itemPriceText; - text: "-- HFC"; + text: "--"; // Text size - size: itemPriceTextLabel.size; + size: 26; // Anchors anchors.top: parent.top; - anchors.left: itemPriceTextLabel.right; - anchors.leftMargin: 16; anchors.right: parent.right; anchors.rightMargin: 16; - height: paintedHeight; - // Style - color: (balanceAfterPurchase >= 0) ? hifi.colors.lightGrayText : hifi.colors.redHighlight; - // Alignment - horizontalAlignment: Text.AlignRight; - verticalAlignment: Text.AlignVCenter; - } - } - - // "Balance After Purchase" container - Item { - id: balanceAfterPurchaseContainer; - // Anchors - anchors.top: itemPriceContainer.bottom; - anchors.topMargin: 16; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: childrenRect.height; - - RalewaySemiBold { - id: balanceAfterPurchaseTextLabel; - text: "Balance After Purchase:"; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; width: paintedWidth; height: paintedHeight; - // Text size - size: 20; // Style - color: hifi.colors.lightGrayText; - // Alignment - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - } - RalewayRegular { - id: balanceAfterPurchaseText; - text: balanceAfterPurchase + " HFC"; - // Text size - size: balanceAfterPurchaseTextLabel.size; - // Anchors - anchors.top: parent.top; - anchors.left: balanceAfterPurchaseTextLabel.right; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - height: paintedHeight; - // Style - color: (balanceAfterPurchase >= 0) ? hifi.colors.lightGrayText : hifi.colors.redHighlight; - // Alignment - horizontalAlignment: Text.AlignRight; - verticalAlignment: Text.AlignVCenter; + color: hifi.colors.blueAccent; } } } - // - // ITEM DESCRIPTION END - // + + HifiControlsUit.Separator { + id: separator2; + colorScheme: 1; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: itemContainer.bottom; + anchors.topMargin: itemContainer.anchors.topMargin; + } // @@ -608,46 +367,88 @@ Rectangle { id: checkoutActionButtonsContainer; // Size width: root.width; - height: 200; // Anchors + anchors.top: separator2.bottom; + anchors.topMargin: 16; anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; anchors.bottom: parent.bottom; anchors.bottomMargin: 8; - // "Cancel" button - HifiControlsUit.Button { - id: cancelPurchaseButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; + Rectangle { + id: buyTextContainer; + visible: buyText.text !== ""; anchors.top: parent.top; - anchors.topMargin: 3; - height: 40; anchors.left: parent.left; - anchors.leftMargin: 20; - width: parent.width/2 - anchors.leftMargin*2; - text: "Cancel" - onClicked: { - sendToScript({method: 'checkout_cancelClicked', params: itemId}); + anchors.right: parent.right; + height: buyText.height + 30; + radius: 4; + border.width: 2; + + HiFiGlyphs { + id: buyGlyph; + // Size + size: 46; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 4; + anchors.top: parent.top; + anchors.topMargin: 8; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.baseGray; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignTop; + } + + RalewaySemiBold { + id: buyText; + // Text size + size: 18; + // Anchors + anchors.left: buyGlyph.right; + anchors.leftMargin: 8; + anchors.right: parent.right; + anchors.rightMargin: 12; + anchors.verticalCenter: parent.verticalCenter; + height: paintedHeight; + // Style + color: hifi.colors.black; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + + onLinkActivated: { + sendToScript({method: 'checkout_goToPurchases', filterText: itemNameText.text}); + } } } // "Buy" button HifiControlsUit.Button { id: buyButton; - enabled: (balanceAfterPurchase >= 0 && purchasesReceived && balanceReceived) || !itemIsJson; + enabled: (root.balanceAfterPurchase >= 0 && purchasesReceived && balanceReceived) || !itemIsJson; color: hifi.buttons.blue; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; + colorScheme: hifi.colorSchemes.light; + anchors.top: buyTextContainer.visible ? buyTextContainer.bottom : checkoutActionButtonsContainer.top; + anchors.topMargin: buyTextContainer.visible ? 12 : 16; height: 40; + anchors.left: parent.left; anchors.right: parent.right; - anchors.rightMargin: 20; - width: parent.width/2 - anchors.rightMargin*2; - text: (itemIsJson ? ((purchasesReceived && balanceReceived) ? (root.alreadyOwned ? "Buy Another" : "Buy"): "--") : "Get Item"); + text: (itemIsJson ? ((purchasesReceived && balanceReceived) ? "Confirm Purchase" : "--") : "Get Item"); onClicked: { if (itemIsJson) { buyButton.enabled = false; - commerce.buy(itemId, itemPriceFull); + if (!root.shouldBuyWithControlledFailure) { + commerce.buy(itemId, itemPrice); + } else { + commerce.buy(itemId, itemPrice, true); + } } else { if (urlHandler.canHandleUrl(itemHref)) { urlHandler.handleUrl(itemHref); @@ -656,42 +457,20 @@ Rectangle { } } - // "Purchases" button + // "Cancel" button HifiControlsUit.Button { - id: goToPurchasesButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; + id: cancelPurchaseButton; + color: hifi.buttons.noneBorderlessGray; + colorScheme: hifi.colorSchemes.light; anchors.top: buyButton.bottom; - anchors.topMargin: 20; - anchors.bottomMargin: 7; + anchors.topMargin: 16; height: 40; anchors.left: parent.left; - anchors.leftMargin: 20; - width: parent.width - anchors.leftMargin*2; - text: "View Purchases" - onClicked: { - sendToScript({method: 'checkout_goToPurchases'}); - } - } - - RalewayRegular { - id: buyText; - // Text size - size: 20; - // Anchors - anchors.bottom: parent.bottom; - anchors.bottomMargin: 10; - height: paintedHeight; - anchors.left: parent.left; - anchors.leftMargin: 10; anchors.right: parent.right; - anchors.rightMargin: 10; - // Style - color: hifi.colors.faintGray; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + text: "Cancel" + onClicked: { + sendToScript({method: 'checkout_cancelClicked', params: itemId}); + } } } // @@ -711,102 +490,209 @@ Rectangle { anchors.top: titleBarContainer.bottom; anchors.bottom: root.bottom; anchors.left: parent.left; + anchors.leftMargin: 16; anchors.right: parent.right; + anchors.rightMargin: 16; RalewayRegular { id: completeText; - text: "Purchase Complete!

    You bought " + (itemNameText.text) + " by " + (itemAuthorText.text) + ""; - // Text size - size: 24; - // Anchors anchors.top: parent.top; + anchors.topMargin: 30; + anchors.left: parent.left; + width: paintedWidth; + height: paintedHeight; + text: "Thank you for your order!"; + color: hifi.colors.baseGray; + size: 28; + } + + RalewaySemiBold { + id: completeText2; + text: "The item " + '
    ' + itemNameText.text + '' + + " has been added to your Purchases and a receipt will appear in your Wallet's transaction history."; + // Text size + size: 20; + // Anchors + anchors.top: completeText.bottom; + anchors.topMargin: 10; + height: paintedHeight; + anchors.left: parent.left; + anchors.right: parent.right; + // Style + color: hifi.colors.black; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + onLinkActivated: { + sendToScript({method: 'checkout_itemLinkClicked', itemId: itemId}); + } + } + + Rectangle { + id: rezzedNotifContainer; + z: 997; + visible: false; + color: hifi.colors.blueHighlight; + anchors.fill: rezNowButton; + radius: 5; + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + } + + RalewayBold { + anchors.fill: parent; + text: "REZZED"; + size: 18; + color: hifi.colors.white; + verticalAlignment: Text.AlignVCenter; + horizontalAlignment: Text.AlignHCenter; + } + + Timer { + id: rezzedNotifContainerTimer; + interval: 2000; + onTriggered: rezzedNotifContainer.visible = false + } + } + // "Rez" button + HifiControlsUit.Button { + id: rezNowButton; + enabled: root.canRezCertifiedItems; + buttonGlyph: hifi.glyphs.lightning; + color: hifi.buttons.red; + colorScheme: hifi.colorSchemes.light; + anchors.top: completeText2.bottom; + anchors.topMargin: 30; + height: 50; + anchors.left: parent.left; + anchors.right: parent.right; + text: "Rez It" + onClicked: { + if (urlHandler.canHandleUrl(itemHref)) { + urlHandler.handleUrl(itemHref); + } + rezzedNotifContainer.visible = true; + rezzedNotifContainerTimer.start(); + } + } + RalewaySemiBold { + id: noPermissionText; + visible: !root.canRezCertifiedItems; + text: 'You do not have Certified Rez permissions in this domain.' + // Text size + size: 16; + // Anchors + anchors.top: rezNowButton.bottom; + anchors.topMargin: 4; + height: paintedHeight; + anchors.left: parent.left; + anchors.right: parent.right; + // Style + color: hifi.colors.redAccent; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + onLinkActivated: { + lightboxPopup.titleText = "Rez Permission Required"; + lightboxPopup.bodyText = "You don't have permission to rez certified items in this domain.

    " + + "Use the GOTO app to visit another domain or go to your own sandbox."; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.button2text = "OPEN GOTO"; + lightboxPopup.button2method = "sendToParent({method: 'purchases_openGoTo'});"; + lightboxPopup.visible = true; + } + } + + RalewaySemiBold { + id: myPurchasesLink; + text: 'View this item in My Purchases'; + // Text size + size: 20; + // Anchors + anchors.top: noPermissionText.visible ? noPermissionText.bottom : rezNowButton.bottom; anchors.topMargin: 40; height: paintedHeight; anchors.left: parent.left; anchors.right: parent.right; // Style - color: hifi.colors.faintGray; + color: hifi.colors.black; wrapMode: Text.WordWrap; // Alignment - horizontalAlignment: Text.AlignHCenter; + horizontalAlignment: Text.AlignLeft; verticalAlignment: Text.AlignVCenter; + onLinkActivated: { + sendToScript({method: 'checkout_goToPurchases'}); + } } - Item { - id: checkoutSuccessActionButtonsContainer; - // Size - width: root.width; - height: 70; + RalewaySemiBold { + id: walletLink; + text: 'View receipt in Wallet'; + // Text size + size: 20; // Anchors - anchors.top: completeText.bottom; - anchors.topMargin: 30; + anchors.top: myPurchasesLink.bottom; + anchors.topMargin: 20; + height: paintedHeight; anchors.left: parent.left; anchors.right: parent.right; - - // "Purchases" button - HifiControlsUit.Button { - id: purchasesButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.left: parent.left; - anchors.leftMargin: 20; - width: parent.width/2 - anchors.leftMargin*2; - text: "View Purchases"; - onClicked: { - sendToScript({method: 'checkout_goToPurchases'}); - } - } - - // "Rez Now!" button - HifiControlsUit.Button { - id: rezNowButton; - color: hifi.buttons.blue; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: parent.width/2 - anchors.rightMargin*2; - text: "Rez Now!" - onClicked: { - if (urlHandler.canHandleUrl(itemHref)) { - urlHandler.handleUrl(itemHref); - } - } + // Style + color: hifi.colors.black; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + onLinkActivated: { + sendToScript({method: 'purchases_openWallet'}); } } - Item { - id: continueShoppingButtonContainer; - // Size - width: root.width; - height: 70; + RalewayRegular { + id: pendingText; + text: 'Your item is marked "pending" while your purchase is being confirmed. ' + + 'Learn More'; + // Text size + size: 20; // Anchors + anchors.top: walletLink.bottom; + anchors.topMargin: 60; + height: paintedHeight; anchors.left: parent.left; + anchors.right: parent.right; + // Style + color: hifi.colors.black; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + onLinkActivated: { + lightboxPopup.titleText = "Purchase Confirmations"; + lightboxPopup.bodyText = 'Your item is marked "pending" while your purchase is being confirmed.

    ' + + 'Confirmations usually take about 90 seconds.'; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.visible = true; + } + } + + // "Continue Shopping" button + HifiControlsUit.Button { + id: continueShoppingButton; + color: hifi.buttons.noneBorderlessGray; + colorScheme: hifi.colorSchemes.light; anchors.bottom: parent.bottom; - anchors.bottomMargin: 8; - // "Continue Shopping" button - HifiControlsUit.Button { - id: continueShoppingButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.left: parent.left; - anchors.leftMargin: 20; - width: parent.width/2 - anchors.rightMargin*2; - text: "Continue Shopping"; - onClicked: { - sendToScript({method: 'checkout_continueShopping', itemId: itemId}); - } + anchors.bottomMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 14; + width: parent.width/2 - anchors.rightMargin; + height: 60; + text: "Continue Shopping"; + onClicked: { + sendToScript({method: 'checkout_continueShopping', itemId: itemId}); } } } @@ -837,7 +723,7 @@ Rectangle { anchors.left: parent.left; anchors.right: parent.right; // Style - color: hifi.colors.faintGray; + color: hifi.colors.black; wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; @@ -855,7 +741,7 @@ Rectangle { anchors.left: parent.left; anchors.right: parent.right; // Style - color: hifi.colors.faintGray; + color: hifi.colors.black; wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; @@ -875,7 +761,7 @@ Rectangle { HifiControlsUit.Button { id: backToMarketplaceButton; color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; + colorScheme: hifi.colorSchemes.light; anchors.top: parent.top; anchors.topMargin: 3; anchors.bottom: parent.bottom; @@ -894,6 +780,20 @@ Rectangle { // CHECKOUT FAILURE END // + Keys.onPressed: { + if ((event.key == Qt.Key_F) && (event.modifiers & Qt.ControlModifier)) { + if (!root.shouldBuyWithControlledFailure) { + buyButton.text += " DEBUG FAIL ON" + buyButton.color = hifi.buttons.red; + root.shouldBuyWithControlledFailure = true; + } else { + buyButton.text = (itemIsJson ? ((purchasesReceived && balanceReceived) ? (root.alreadyOwned ? "Buy Another" : "Buy"): "--") : "Get Item"); + buyButton.color = hifi.buttons.blue; + root.shouldBuyWithControlledFailure = false; + } + } + } + // // FUNCTION DEFINITIONS START // @@ -915,13 +815,14 @@ Rectangle { case 'updateCheckoutQML': itemId = message.params.itemId; itemNameText.text = message.params.itemName; - itemAuthorText.text = message.params.itemAuthor; - root.itemPriceFull = message.params.itemPrice; - itemPriceText.text = root.itemPriceFull === 0 ? "Free" : "" + root.itemPriceFull + " HFC"; + root.itemPrice = message.params.itemPrice; + itemPriceText.text = root.itemPrice === 0 ? "Free" : root.itemPrice; itemHref = message.params.itemHref; + itemPreviewImageUrl = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg"; if (itemHref.indexOf('.json') === -1) { root.itemIsJson = false; } + root.canRezCertifiedItems = message.canRezCertifiedItems; setBuyText(); break; default: @@ -944,15 +845,25 @@ Rectangle { if (root.purchasesReceived && root.balanceReceived) { if (root.balanceAfterPurchase < 0) { if (root.alreadyOwned) { - buyText.text = "You do not have enough HFC to purchase this item again. Go to your Purchases to view the copy you own."; + buyText.text = "Your Wallet does not have sufficient funds to purchase this item again.
    " + + 'View the copy you own in My Purchases'; } else { - buyText.text = "You do not have enough HFC to purchase this item."; + buyText.text = "Your Wallet does not have sufficient funds to purchase this item."; } + buyTextContainer.color = "#FFC3CD"; + buyTextContainer.border.color = "#F3808F"; + buyGlyph.text = hifi.glyphs.error; + buyGlyph.size = 54; } else { if (root.alreadyOwned) { - buyText.text = "You already own this item. If you buy it again, you'll be able to use multiple copies of it at once."; + buyText.text = 'You already own this item.
    Purchasing it will buy another copy.
    View this item in My Purchases'; + buyTextContainer.color = "#FFD6AD"; + buyTextContainer.border.color = "#FAC07D"; + buyGlyph.text = hifi.glyphs.alert; + buyGlyph.size = 46; } else { - buyText.text = "This item will be added to your Purchases, which can be accessed from Marketplace."; + buyText.text = ""; } } } else { @@ -960,6 +871,24 @@ Rectangle { } } else { buyText.text = "This Marketplace item isn't an entity. It will not be added to your Purchases."; + buyTextContainer.color = "#FFD6AD"; + buyTextContainer.border.color = "#FAC07D"; + buyGlyph.text = hifi.glyphs.alert; + buyGlyph.size = 46; + } + } + + function authSuccessStep() { + if (!root.debugCheckoutSuccess) { + root.activeView = "checkoutMain"; + } else { + root.activeView = "checkoutSuccess"; + } + if (!balanceReceived) { + commerce.balance(); + } + if (!purchasesReceived) { + commerce.inventory(); } } diff --git a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml new file mode 100644 index 0000000000..0b236d4566 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml @@ -0,0 +1,153 @@ +// +// CommerceLightbox.qml +// qml/hifi/commerce/common +// +// CommerceLightbox +// +// Created by Zach Fox on 2017-09-19 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtGraphicalEffects 1.0 +import QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Rectangle { + property string titleText; + property string bodyImageSource; + property string bodyText; + property string button1text; + property string button1method; + property string button2text; + property string button2method; + + readonly property string securityPicBodyText: "When you see your Security Pic, your actions and data are securely making use of your " + + "Wallet's private keys.

    You can change your Security Pic in your Wallet."; + + id: root; + visible: false; + anchors.fill: parent; + color: Qt.rgba(0, 0, 0, 0.5); + z: 999; + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + } + + Rectangle { + anchors.centerIn: parent; + width: parent.width - 100; + height: childrenRect.height + 30; + color: "white"; + + RalewaySemiBold { + id: titleText; + text: root.titleText; + anchors.top: parent.top; + anchors.topMargin: 30; + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + height: paintedHeight; + color: hifi.colors.baseGray; + size: 24; + verticalAlignment: Text.AlignTop; + wrapMode: Text.WordWrap; + } + + Image { + id: bodyImage; + visible: root.bodyImageSource; + source: root.bodyImageSource ? root.bodyImageSource : ""; + anchors.top: root.titleText ? titleText.bottom : parent.top; + anchors.topMargin: root.titleText ? 20 : 30; + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + height: 140; + fillMode: Image.PreserveAspectFit; + mipmap: true; + } + + RalewayRegular { + id: bodyText; + text: root.bodyText; + anchors.top: root.bodyImageSource ? bodyImage.bottom : (root.titleText ? titleText.bottom : parent.top); + anchors.topMargin: root.bodyImageSource ? 20 : (root.titleText ? 20 : 30); + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + height: paintedHeight; + color: hifi.colors.baseGray; + size: 20; + verticalAlignment: Text.AlignTop; + wrapMode: Text.WordWrap; + } + + Item { + id: buttons; + anchors.top: bodyText.bottom; + anchors.topMargin: 30; + anchors.left: parent.left; + anchors.right: parent.right; + height: 70; + + // Button 1 + HifiControlsUit.Button { + color: hifi.buttons.noneBorderlessGray; + colorScheme: hifi.colorSchemes.light; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 20; + anchors.left: parent.left; + anchors.leftMargin: 10; + width: root.button2text ? parent.width/2 - anchors.leftMargin*2 : parent.width - anchors.leftMargin * 2; + text: root.button1text; + onClicked: { + eval(button1method); + } + } + + // Button 2 + HifiControlsUit.Button { + visible: root.button2text; + color: hifi.buttons.noneBorderless; + colorScheme: hifi.colorSchemes.light; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 10; + width: parent.width/2 - anchors.rightMargin*2; + text: root.button2text; + onClicked: { + eval(button2method); + } + } + } + } + + // + // FUNCTION DEFINITIONS START + // + signal sendToParent(var msg); + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml new file mode 100644 index 0000000000..5786350721 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -0,0 +1,333 @@ +// +// EmulatedMarketplaceHeader.qml +// qml/hifi/commerce/common +// +// EmulatedMarketplaceHeader +// +// Created by Zach Fox on 2017-09-18 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.7 +import QtGraphicalEffects 1.0 +import QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Item { + HifiConstants { id: hifi; } + + id: root; + property string referrerURL: "https://metaverse.highfidelity.com/marketplace?"; + readonly property int additionalDropdownHeight: usernameDropdown.height - myUsernameButton.anchors.bottomMargin; + + height: mainContainer.height + additionalDropdownHeight; + + Hifi.QmlCommerce { + id: commerce; + + onLoginStatusResult: { + if (!isLoggedIn) { + sendToParent({method: "needsLogIn"}); + } + } + + onAccountResult: { + if (result.status === "success") { + commerce.getKeyFilePathIfExists(); + } else { + // unsure how to handle a failure here. We definitely cannot proceed. + } + } + + onSecurityImageResult: { + if (exists) { + securityImage.source = ""; + securityImage.source = "image://security/securityImage"; + } + } + } + + Component.onCompleted: { + commerce.getLoginStatus(); + commerce.getSecurityImage(); + } + + Connections { + target: GlobalServices + onMyUsernameChanged: { + commerce.getLoginStatus(); + } + } + + Rectangle { + id: mainContainer; + color: hifi.colors.white; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + height: 70; + + Image { + id: marketplaceHeaderImage; + source: "images/marketplaceHeaderImage.png"; + anchors.top: parent.top; + anchors.topMargin: 2; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 10; + anchors.left: parent.left; + anchors.leftMargin: 8; + width: 140; + fillMode: Image.PreserveAspectFit; + + MouseArea { + anchors.fill: parent; + onClicked: { + sendToParent({method: "header_marketplaceImageClicked", referrerURL: root.referrerURL}); + } + } + } + + Item { + id: buttonAndUsernameContainer; + anchors.left: marketplaceHeaderImage.right; + anchors.leftMargin: 8; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 10; + anchors.right: securityImage.left; + anchors.rightMargin: 6; + + Rectangle { + id: myPurchasesLink; + anchors.right: myUsernameButton.left; + anchors.rightMargin: 8; + anchors.verticalCenter: parent.verticalCenter; + height: 40; + width: myPurchasesText.paintedWidth + 10; + + RalewaySemiBold { + id: myPurchasesText; + text: "My Purchases"; + // Text size + size: 18; + // Style + color: hifi.colors.blueAccent; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + // Anchors + anchors.centerIn: parent; + } + + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + sendToParent({method: 'header_goToPurchases'}); + } + onEntered: myPurchasesText.color = hifi.colors.blueHighlight; + onExited: myPurchasesText.color = hifi.colors.blueAccent; + } + } + + FontLoader { id: ralewayRegular; source: "../../../../fonts/Raleway-Regular.ttf"; } + TextMetrics { + id: textMetrics; + font.family: ralewayRegular.name + text: usernameText.text; + } + + Rectangle { + id: myUsernameButton; + anchors.right: parent.right; + anchors.verticalCenter: parent.verticalCenter; + height: 40; + width: usernameText.width + 25; + color: "white"; + radius: 4; + border.width: 1; + border.color: hifi.colors.lightGray; + + // Username Text + RalewayRegular { + id: usernameText; + text: Account.username; + // Text size + size: 18; + // Style + color: hifi.colors.baseGray; + elide: Text.ElideRight; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + width: Math.min(textMetrics.width + 25, 110); + // Anchors + anchors.centerIn: parent; + rightPadding: 10; + } + + HiFiGlyphs { + id: dropdownIcon; + text: hifi.glyphs.caratDn; + // Size + size: 50; + // Anchors + anchors.right: parent.right; + anchors.rightMargin: -14; + anchors.verticalCenter: parent.verticalCenter; + horizontalAlignment: Text.AlignHCenter; + // Style + color: hifi.colors.baseGray; + } + + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + usernameDropdown.visible = !usernameDropdown.visible; + } + onEntered: usernameText.color = hifi.colors.baseGrayShadow; + onExited: usernameText.color = hifi.colors.baseGray; + } + } + } + + Image { + id: securityImage; + source: ""; + visible: securityImage.source !== ""; + anchors.right: parent.right; + anchors.rightMargin: 6; + anchors.top: parent.top; + anchors.topMargin: 6; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 16; + width: height; + mipmap: true; + + MouseArea { + enabled: securityImage.visible; + anchors.fill: parent; + onClicked: { + sendToParent({method: "showSecurityPicLightbox", securityImageSource: securityImage.source}); + } + } + } + + LinearGradient { + z: 996; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: 10; + start: Qt.point(0, 0); + end: Qt.point(0, height); + gradient: Gradient { + GradientStop { position: 0.0; color: hifi.colors.lightGrayText } + GradientStop { position: 1.0; color: hifi.colors.white } + } + } + + Item { + id: usernameDropdown; + z: 998; + visible: false; + anchors.top: buttonAndUsernameContainer.bottom; + anchors.topMargin: -buttonAndUsernameContainer.anchors.bottomMargin; + anchors.right: buttonAndUsernameContainer.right; + height: childrenRect.height; + width: 100; + + Rectangle { + id: myItemsButton; + color: hifi.colors.white; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: 50; + + RalewaySemiBold { + anchors.fill: parent; + text: "My Items" + color: hifi.colors.baseGray; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + size: 18; + } + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: { + myItemsButton.color = hifi.colors.blueHighlight; + } + onExited: { + myItemsButton.color = hifi.colors.white; + } + onClicked: { + sendToParent({method: "header_myItemsClicked"}); + } + } + } + + Rectangle { + id: logOutButton; + color: hifi.colors.white; + anchors.top: myItemsButton.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: 50; + + RalewaySemiBold { + anchors.fill: parent; + text: "Log Out" + color: hifi.colors.baseGray; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + size: 18; + } + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: { + logOutButton.color = hifi.colors.blueHighlight; + } + onExited: { + logOutButton.color = hifi.colors.white; + } + onClicked: { + Account.logOut(); + } + } + } + } + + DropShadow { + z: 997; + visible: usernameDropdown.visible; + anchors.fill: usernameDropdown; + horizontalOffset: 3; + verticalOffset: 3; + radius: 8.0; + samples: 17; + color: "#80000000"; + source: usernameDropdown; + } + } + + + // + // FUNCTION DEFINITIONS START + // + signal sendToParent(var msg); + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/common/images/marketplaceHeaderImage.png b/interface/resources/qml/hifi/commerce/common/images/marketplaceHeaderImage.png new file mode 100644 index 0000000000000000000000000000000000000000..f49504c539638df3f97be80e9128692c15264135 GIT binary patch literal 5805 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 D(fgCI literal 0 HcmV?d00001 diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml new file mode 100644 index 0000000000..65bfcfc4b3 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml @@ -0,0 +1,321 @@ +// +// InspectionCertificate.qml +// qml/hifi/commerce/inspectionCertificate +// +// InspectionCertificate +// +// Created by Zach Fox on 2017-09-14 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls +import "../wallet" as HifiWallet + +// references XXX from root context + +Rectangle { + HifiConstants { id: hifi; } + + id: root; + property string marketplaceId: ""; + property string itemName: "--"; + property string itemOwner: "--"; + property string itemEdition: "--"; + property string dateOfPurchase: ""; + property bool closeGoesToPurchases: false; + // Style + color: hifi.colors.faintGray; + Hifi.QmlCommerce { + id: commerce; + } + + Image { + anchors.fill: parent; + source: "images/cert-bg.jpg"; + } + + // Title text + RalewayLight { + id: titleBarText; + text: "Certificate"; + // Text size + size: 40; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 40; + anchors.left: parent.left; + anchors.leftMargin: 45; + anchors.right: parent.right; + height: paintedHeight; + // Style + color: hifi.colors.darkGray; + } + // Title text + RalewayRegular { + id: popText; + text: "PROOF OF PURCHASE"; + // Text size + size: 16; + // Anchors + anchors.top: titleBarText.bottom; + anchors.topMargin: 4; + anchors.left: titleBarText.left; + anchors.right: titleBarText.right; + height: paintedHeight; + // Style + color: hifi.colors.baseGray; + } + + // + // "CERTIFICATE" START + // + Item { + id: certificateContainer; + anchors.top: popText.bottom; + anchors.topMargin: 30; + anchors.bottom: buttonsContainer.top; + anchors.left: parent.left; + anchors.right: parent.right; + + RalewayRegular { + id: itemNameHeader; + text: "ITEM NAME"; + // Text size + size: 16; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 45; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: paintedHeight; + // Style + color: hifi.colors.baseGray; + } + RalewaySemiBold { + id: itemName; + text: root.itemName; + // Text size + size: 28; + // Anchors + anchors.top: itemNameHeader.bottom; + anchors.topMargin: 4; + anchors.left: itemNameHeader.left; + anchors.right: itemNameHeader.right; + height: paintedHeight; + // Style + color: hifi.colors.blueAccent; + elide: Text.ElideRight; + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + sendToScript({method: 'inspectionCertificate_showInMarketplaceClicked', itemId: root.marketplaceId}); + } + onEntered: itemName.color = hifi.colors.blueHighlight; + onExited: itemName.color = hifi.colors.blueAccent; + } + } + + RalewayRegular { + id: ownedByHeader; + text: "OWNER"; + // Text size + size: 16; + // Anchors + anchors.top: itemName.bottom; + anchors.topMargin: 20; + anchors.left: parent.left; + anchors.leftMargin: 45; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: paintedHeight; + // Style + color: hifi.colors.baseGray; + } + RalewayRegular { + id: ownedBy; + text: root.itemOwner; + // Text size + size: 22; + // Anchors + anchors.top: ownedByHeader.bottom; + anchors.topMargin: 4; + anchors.left: ownedByHeader.left; + anchors.right: ownedByHeader.right; + height: paintedHeight; + // Style + color: hifi.colors.darkGray; + elide: Text.ElideRight; + } + + RalewayRegular { + id: editionHeader; + text: "EDITION"; + // Text size + size: 16; + // Anchors + anchors.top: ownedBy.bottom; + anchors.topMargin: 20; + anchors.left: parent.left; + anchors.leftMargin: 45; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: paintedHeight; + // Style + color: hifi.colors.baseGray; + } + AnonymousProRegular { + id: edition; + text: root.itemEdition; + // Text size + size: 22; + // Anchors + anchors.top: editionHeader.bottom; + anchors.topMargin: 4; + anchors.left: editionHeader.left; + anchors.right: editionHeader.right; + height: paintedHeight; + // Style + color: hifi.colors.darkGray; + } + + RalewayRegular { + id: dateOfPurchaseHeader; + text: "DATE OF PURCHASE"; + visible: root.dateOfPurchase !== ""; + // Text size + size: 16; + // Anchors + anchors.top: ownedBy.bottom; + anchors.topMargin: 20; + anchors.left: parent.left; + anchors.leftMargin: 45; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: paintedHeight; + // Style + color: hifi.colors.baseGray; + } + AnonymousProRegular { + id: dateOfPurchase; + text: root.dateOfPurchase; + visible: root.dateOfPurchase !== ""; + // Text size + size: 22; + // Anchors + anchors.top: editionHeader.bottom; + anchors.topMargin: 4; + anchors.left: editionHeader.left; + anchors.right: editionHeader.right; + height: paintedHeight; + // Style + color: hifi.colors.darkGray; + } + + RalewayRegular { + id: errorText; + text: "Here we will display some text if there's an error with the certificate " + + "(DMCA takedown, invalid cert, location of item updated)"; + // Text size + size: 20; + // Anchors + anchors.top: root.dateOfPurchase !== "" ? dateOfPurchase.bottom : edition.bottom; + anchors.topMargin: 40; + anchors.left: root.dateOfPurchase !== "" ? dateOfPurchase.left : edition.left; + anchors.right: root.dateOfPurchase !== "" ? dateOfPurchase.right : edition.right; + anchors.bottom: parent.bottom; + // Style + wrapMode: Text.WordWrap; + color: hifi.colors.redHighlight; + verticalAlignment: Text.AlignTop; + } + } + // + // "CERTIFICATE" END + // + + Item { + id: buttonsContainer; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 50; + anchors.left: parent.left; + anchors.right: parent.right; + height: 50; + + // "Cancel" button + HifiControlsUit.Button { + color: hifi.buttons.noneBorderless; + colorScheme: hifi.colorSchemes.light; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 30; + width: parent.width/2 - 50; + height: 50; + text: "close"; + onClicked: { + sendToScript({method: 'inspectionCertificate_closeClicked', closeGoesToPurchases: root.closeGoesToPurchases}); + } + } + + // "Show In Marketplace" button + HifiControlsUit.Button { + id: showInMarketplaceButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.light; + anchors.top: parent.top; + anchors.right: parent.right; + anchors.rightMargin: 30; + width: parent.width/2 - 50; + height: 50; + text: "View In Market" + onClicked: { + sendToScript({method: 'inspectionCertificate_showInMarketplaceClicked', itemId: root.marketplaceId}); + } + } + } + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the JavaScript, in this case the Marketplaces JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from a script. + // + function fromScript(message) { + switch (message.method) { + case 'inspectionCertificate_setMarketplaceId': + root.marketplaceId = message.marketplaceId; + root.closeGoesToPurchases = message.closeGoesToPurchases; + break; + case 'inspectionCertificate_setItemInfo': + root.itemName = message.itemName; + root.itemOwner = message.itemOwner; + root.itemEdition = message.itemEdition; + break; + default: + console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); + } + } + signal sendToScript(var message); + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/images/cert-bg.jpg b/interface/resources/qml/hifi/commerce/inspectionCertificate/images/cert-bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9cecc79869f0fd8e4a856d47ef0793b66c5bd775 GIT binary patch literal 64886 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%0ALDH9;7k>;jA5WiP#)y1LdYG*LxpkQs&Rb^< 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