From 40279c8fc5ac1483c466f4bae7b11a46bdc39a2b Mon Sep 17 00:00:00 2001 From: EdgarPironti Date: Thu, 14 Jan 2016 18:20:40 -0800 Subject: [PATCH] Item, Review, Shop and Vendor scripts. --- examples/vrShop/item/shopItemEntityScript.js | 389 ++++++++ examples/vrShop/item/shopItemGrab.js | 862 ++++++++++++++++++ .../vrShop/review/shopReviewEntityScript.js | 477 ++++++++++ examples/vrShop/review/shopReviewerAC.js | 96 ++ .../shop/shopGrabSwapperEntityScript.js | 55 ++ examples/vrShop/vendor/shopVendorAddItem.js | 101 ++ 6 files changed, 1980 insertions(+) create mode 100644 examples/vrShop/item/shopItemEntityScript.js create mode 100644 examples/vrShop/item/shopItemGrab.js create mode 100644 examples/vrShop/review/shopReviewEntityScript.js create mode 100644 examples/vrShop/review/shopReviewerAC.js create mode 100644 examples/vrShop/shop/shopGrabSwapperEntityScript.js create mode 100644 examples/vrShop/vendor/shopVendorAddItem.js diff --git a/examples/vrShop/item/shopItemEntityScript.js b/examples/vrShop/item/shopItemEntityScript.js new file mode 100644 index 0000000000..0b54df0f90 --- /dev/null +++ b/examples/vrShop/item/shopItemEntityScript.js @@ -0,0 +1,389 @@ +// shopItemEntityScript.js +// +// This script makes the shop items react properly to the grab (see shopItemGrab.js) and gives it the capability to interact with the shop environment (inspection entity and cart) +// The first time the item is grabbed a copy of itself is created on the shelf. +// If the item isn't held by the user it can be inInspect or inCart, otherwise it's destroyed +// The start and release grab methods handle the creation of the item copy, UI, inspection entity +// If the item is released into a valid zone, the doSomething() method of that zone is called and the item sends its ID + +// Created by Alessandro Signa and Edgar Pironti on 01/13/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 +// + +(function() { + var utilitiesScript = Script.resolvePath("../../libraries/utils.js"); + var overlayManagerScript = Script.resolvePath("../../libraries/overlayManager.js"); + var inspectEntityScript = Script.resolvePath("../inspect/shopInspectEntityScript.js"); + + Script.include(utilitiesScript); + Script.include(overlayManagerScript); + + + var RED_IMAGE_URL = "https://dl.dropboxusercontent.com/u/14127429/FBX/VRshop/inspRED.png"; + var GREEN_IMAGE_URL = "https://dl.dropboxusercontent.com/u/14127429/FBX/VRshop/inspGREEN.png"; + var MIN_DIMENSION_THRESHOLD = null; + var MAX_DIMENSION_THRESHOLD = null; + var PENETRATION_THRESHOLD = 0.2; + var MAPPING_NAME = "controllerMapping_Inspection"; + var SHOPPING_CART_NAME = "Shopping cart"; + + var _this; + var onShelf = true; + var inspecting = false; + var inCart = false; + var overlayInspectRed = true; + var zoneID = null; + var newPosition; + var originalDimensions = null; + var deltaLX = 0; + var deltaLY = 0; + var deltaRX = 0; + var deltaRY = 0; + var inspectingEntity = null; + var inspectPanel = null; + var background = null; + var textCart = null; + var cartID = null; + + // this is the "constructor" for the entity as a JS object we don't do much here, but we do want to remember + // our this object, so we can access it in cases where we're called without a this (like in the case of various global signals) + ItemEntity = function() { + _this = this; + }; + + function update(deltaTime) { + if(inspecting){ + _this.orientationPositionUpdate(); + } + }; + + + ItemEntity.prototype = { + + // preload() will be called when the entity has become visible (or known) to the interface + // it gives us a chance to set our local JavaScript object up. In this case it means: + // * remembering our entityID, so we can access it in cases where we're called without an entityID + // * connecting to the update signal so we can check our grabbed state + preload: function(entityID) { + this.entityID = entityID; + print("PRELOAD: " + Entities.getEntityProperties(this.entityID).name + " " + entityID + " at:"); + Vec3.print(Entities.getEntityProperties(this.entityID).name, Entities.getEntityProperties(this.entityID).position); + + Script.update.connect(update); + + MIN_DIMENSION_THRESHOLD = Vec3.length(Entities.getEntityProperties(this.entityID).dimensions)/2; + MAX_DIMENSION_THRESHOLD = Vec3.length(Entities.getEntityProperties(this.entityID).dimensions)*2; + + }, + + //create a rectangle red or green do guide the user in putting the item in the inspection area + createInspectOverlay: function (entityBindID) { + inspectPanel = new OverlayPanel({ + anchorPositionBinding: { entity: entityBindID }, + anchorRotationBinding: { entity: entityBindID }, + isFacingAvatar: false + }); + + background = new Image3DOverlay({ + url: RED_IMAGE_URL, + dimensions: { + x: 0.6, + y: 0.6, + }, + isFacingAvatar: false, + alpha: 1, + ignoreRayIntersection: false, + offsetPosition: { + x: 0, + y: 0, + z: 0 + }, + emissive: true, + }); + + inspectPanel.addChild(background); + }, + + //add an overlay on the item grabbed by the customer to give him the feedback if the item is inside the bounding box of the cart + createCartOverlay: function (entityBindID) { + cartPanel = new OverlayPanel({ + anchorPositionBinding: { entity: entityBindID }, + offsetPosition: { x: 0, y: 0.2, z: 0.1 }, + isFacingAvatar: true, + + }); + var entityProperties = Entities.getEntityProperties(entityBindID); + var userDataObj = JSON.parse(entityProperties.userData); + var availabilityNumber = userDataObj.infoKey.availability; + + textCart = new Text3DOverlay({ + text: availabilityNumber > 0 ? "Store the item!" : "Not available!", + isFacingAvatar: false, + alpha: 1.0, + ignoreRayIntersection: true, + dimensions: { x: 0, y: 0 }, + backgroundColor: { red: 255, green: 255, blue: 255 }, + color: { red: 0, green: 0, blue: 0 }, + topMargin: 0.00625, + leftMargin: 0.00625, + bottomMargin: 0.1, + rightMargin: 0.00625, + lineHeight: 0.02, + alpha: 1, + backgroundAlpha: 0.3, + visible: false + }); + + cartPanel.addChild(textCart); + + }, + + + + setCartOverlayVisible: function () { + textCart.visible = true; + }, + + setCartOverlayNotVisible: function () { + textCart.visible = false; + }, + + changeOverlayColor: function () { + if (overlayInspectRed) { + //print ("Change color of overlay to green"); + overlayInspectRed = false; + background.url = GREEN_IMAGE_URL; + } else { + //print ("Change color of overlay to red"); + overlayInspectRed = true; + background.url = RED_IMAGE_URL; + } + }, + + //What is done by this methos changes acoordingly to the previous state of the item - onShelf , inCart, inInspect + startNearGrab: function () { + + //we have to distinguish if the user who is grabbing has a cart or not + //if he does it is a buyer, otherwise he probably want to do a review + var thisItemPosition = Entities.getEntityProperties(_this.entityID).position; + + // if onShelf is true, this is the first grab + if (onShelf === true) { + + var foundEntities = Entities.findEntities(thisItemPosition, 5); + foundEntities.forEach( function (foundEntityID) { + var entityName = Entities.getEntityProperties(foundEntityID).name; + if (entityName == SHOPPING_CART_NAME) { + var cartOwnerID = getEntityCustomData('ownerKey', foundEntityID, null).ownerID; + if (cartOwnerID == MyAvatar.sessionUUID) { + cartID = foundEntityID; + } + } + }); + + // Create a copy of this entity if it is the first grab + print("creating a copy of the grabbed dentity"); + var entityProperties = Entities.getEntityProperties(_this.entityID); + + var entityOnShelf = Entities.addEntity({ + type: entityProperties.type, + name: entityProperties.name, + position: thisItemPosition, + dimensions: entityProperties.dimensions, + rotation: entityProperties.rotation, + collisionsWillMove: false, + ignoreForCollisions: true, + modelURL: entityProperties.modelURL, + shapeType: entityProperties.shapeType, + originalTextures: entityProperties.originalTextures, + script: entityProperties.script, + userData: entityProperties.userData + }); + + var tempUserDataObj = JSON.parse(entityProperties.userData); + var availabilityNumber = tempUserDataObj.infoKey.availability; + + if (availabilityNumber > 0 && cartID) { + tempUserDataObj.infoKey.availability = tempUserDataObj.infoKey.availability - 1; + setEntityCustomData('infoKey', entityOnShelf, tempUserDataObj.infoKey); + } + + setEntityCustomData('statusKey', _this.entityID, { + status: "inHand" + }); + + onShelf = false; + setEntityCustomData('ownerKey', _this.entityID, { + ownerID: MyAvatar.sessionUUID + }); + originalDimensions = entityProperties.dimensions; + + } + + //if cartID is defined the user is a buyer + if (cartID) { + // Everytime we grab, we create the inspectEntity and the inspectAreaOverlay in front of the avatar + if(!inspecting) { + inspectingEntity = Entities.addEntity({ + type: "Box", + name: "inspectionEntity", + dimensions: {x: 0.5, y: 0.5, z: 0.5}, + collisionsWillMove: false, + ignoreForCollisions: false, + visible: false, + script: inspectEntityScript, + userData: JSON.stringify({ + ownerKey: { + ownerID: MyAvatar.sessionUUID + }, + itemKey: { + itemID: _this.entityID + }, + grabbableKey: { + grabbable: false + } + }) + }); + } + + Entities.editEntity(_this.entityID, { ignoreForCollisions: false }); + _this.createInspectOverlay(inspectingEntity); + _this.createCartOverlay(_this.entityID); + + if (inspecting === true) { + inspecting = false; + Entities.editEntity(_this.entityID, { dimensions: originalDimensions }); + Controller.disableMapping(MAPPING_NAME); + setEntityCustomData('statusKey', _this.entityID, { + status: "inHand" + }); + } else if (inCart === true) { + inCart = false; + Entities.editEntity(_this.entityID, { dimensions: originalDimensions }); + setEntityCustomData('statusKey', _this.entityID, { + status: "inHand" + }); + var dataJSON = { + id: _this.entityID + }; + var dataArray = [JSON.stringify(dataJSON)]; + Entities.callEntityMethod(zoneID, 'refreshCartContent', dataArray); + } + } + }, + + continueNearGrab: function () { + }, + + //Every time the item is released I have to check what's the role of the user and where the release happens + releaseGrab: function () { + + //if cart ID is not defined destroy the item whatever the zone is because the user is a reviewer + if (!cartID) { + Entities.deleteEntity(this.entityID); + return; + } + + Entities.editEntity(_this.entityID, { ignoreForCollisions: true }); + // Destroy overlay + inspectPanel.destroy(); + cartPanel.destroy(); + inspectPanel = cartPanel = null; + + if (zoneID !== null) { + var dataJSON = { + id: _this.entityID + }; + var dataArray = [JSON.stringify(dataJSON)]; + Entities.callEntityMethod(zoneID, 'doSomething', dataArray); + + var statusObj = getEntityCustomData('statusKey', _this.entityID, null); + + //There are two known zones where the relase can happen: inspecting area and cart + if (statusObj.status == "inInspect") { + inspecting = true; + + var mapping = Controller.newMapping(MAPPING_NAME); + mapping.from(Controller.Standard.LX).to(function (value) { + deltaLX = value; + }); + mapping.from(Controller.Standard.LY).to(function (value) { + deltaLY = value; + }); + mapping.from(Controller.Standard.RX).to(function (value) { + deltaRX = value; + }); + mapping.from(Controller.Standard.RY).to(function (value) { + deltaRY = value; + }); + Controller.enableMapping(MAPPING_NAME); + } else if (statusObj.status == "inCart") { + Entities.deleteEntity(inspectingEntity); + inspectingEntity = null; + + var entityProperties = Entities.getEntityProperties(this.entityID); + var userDataObj = JSON.parse(entityProperties.userData); + var availabilityNumber = userDataObj.infoKey.availability; + //if the item is no more available, destroy it + if (availabilityNumber == 0) { + Entities.deleteEntity(this.entityID); + } + print("inCart is TRUE"); + inCart = true; + } else { // any other zone + //print("------------zoneID is something"); + Entities.deleteEntity(inspectingEntity); + inspectingEntity = null; + } + + } else { // ZoneID is null, released somewhere that is not a zone. + //print("------------zoneID is null"); + Entities.deleteEntity(inspectingEntity); + inspectingEntity = null; + Entities.deleteEntity(this.entityID); + } + + }, + + //Analysing the collisions we can define the value of zoneID + collisionWithEntity: function(myID, otherID, collisionInfo) { + var penetrationValue = Vec3.length(collisionInfo.penetration); + if (penetrationValue > PENETRATION_THRESHOLD && zoneID === null) { + zoneID = otherID; + print("Item IN: " + Entities.getEntityProperties(zoneID).name); + } else if (penetrationValue < PENETRATION_THRESHOLD && zoneID !== null && otherID == zoneID) { + print("Item OUT: " + Entities.getEntityProperties(zoneID).name); + zoneID = null; + } + }, + + //this method handles the rotation and scale of the item while in inspect and also guarantees to keep the item in the proper position. It's done every update + orientationPositionUpdate: function() { + //position + var inspectingEntityPosition = Entities.getEntityProperties(inspectingEntity).position; //at this time inspectingEntity is a valid entity + var inspectingEntityRotation = Entities.getEntityProperties(inspectingEntity).rotation; + newPosition = Vec3.sum(inspectingEntityPosition, Vec3.multiply(Quat.getFront(inspectingEntityRotation), -0.2)); //put the item near to the face of the user + Entities.editEntity(_this.entityID, { position: newPosition }); + //orientation + var newRotation = Quat.multiply(Entities.getEntityProperties(_this.entityID).rotation, Quat.fromPitchYawRollDegrees(deltaRY*10, deltaRX*10, 0)) + Entities.editEntity(_this.entityID, { rotation: newRotation }); + //zoom + var oldDimension = Entities.getEntityProperties(_this.entityID).dimensions; + var scaleFactor = (deltaLY * 0.1) + 1; + if (!((Vec3.length(oldDimension) < MIN_DIMENSION_THRESHOLD && scaleFactor < 1) || (Vec3.length(oldDimension) > MAX_DIMENSION_THRESHOLD && scaleFactor > 1))) { + var newDimension = Vec3.multiply(oldDimension, scaleFactor); + Entities.editEntity(_this.entityID, { dimensions: newDimension }); + } + }, + + unload: function (entityID) { + Script.update.disconnect(update); + } + }; + + // entity scripts always need to return a newly constructed object of our type + return new ItemEntity(); +}) \ No newline at end of file diff --git a/examples/vrShop/item/shopItemGrab.js b/examples/vrShop/item/shopItemGrab.js new file mode 100644 index 0000000000..a7226675eb --- /dev/null +++ b/examples/vrShop/item/shopItemGrab.js @@ -0,0 +1,862 @@ +// shopItemGrab.js +// +// Semplified and coarse version of handControllerGrab.js with the addition of the ownerID concept. +// This grab is the only one which should run in the vrShop. It allows only near grab and add the feature of checking the ownerID. (See shopGrapSwapperEntityScript.js) +// + +Script.include("../../libraries/utils.js"); + +var SHOP_GRAB_CHANNEL = "Hifi-vrShop-Grab"; +Messages.sendMessage('Hifi-Hand-Disabler', "both"); //disable both the hands from handControlledGrab + +// +// add lines where the hand ray picking is happening +// +var WANT_DEBUG = false; + +// +// these tune time-averaging and "on" value for analog trigger +// + +var TRIGGER_SMOOTH_RATIO = 0.1; // 0.0 disables smoothing of trigger value +var TRIGGER_ON_VALUE = 0.4; +var TRIGGER_OFF_VALUE = 0.15; +// +// distant manipulation +// + +var DISTANCE_HOLDING_RADIUS_FACTOR = 5; // multiplied by distance between hand and object +var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position +var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did + +var NO_INTERSECT_COLOR = { + red: 10, + green: 10, + blue: 255 +}; // line color when pick misses +var INTERSECT_COLOR = { + red: 250, + green: 10, + blue: 10 +}; // line color when pick hits +var LINE_ENTITY_DIMENSIONS = { + x: 1000, + y: 1000, + z: 1000 +}; +var LINE_LENGTH = 500; +var PICK_MAX_DISTANCE = 500; // max length of pick-ray + +// +// near grabbing +// + +var GRAB_RADIUS = 0.03; // if the ray misses but an object is this close, it will still be selected +var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position +var NEAR_GRABBING_VELOCITY_SMOOTH_RATIO = 1.0; // adjust time-averaging of held object's velocity. 1.0 to disable. +var NEAR_PICK_MAX_DISTANCE = 0.3; // max length of pick-ray for close grabbing to be selected +var RELEASE_VELOCITY_MULTIPLIER = 1.5; // affects throwing things +var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object +var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed + +// +// equip +// + +var EQUIP_SPRING_SHUTOFF_DISTANCE = 0.05; +var EQUIP_SPRING_TIMEFRAME = 0.4; // how quickly objects move to their new position + +// +// other constants +// + +var RIGHT_HAND = 1; +var LEFT_HAND = 0; + +var ZERO_VEC = { + x: 0, + y: 0, + z: 0 +}; + +var NULL_ACTION_ID = "{00000000-0000-0000-000000000000}"; +var MSEC_PER_SEC = 1000.0; + +// these control how long an abandoned pointer line or action will hang around +var LIFETIME = 10; +var ACTION_TTL = 15; // seconds +var ACTION_TTL_REFRESH = 5; +var PICKS_PER_SECOND_PER_HAND = 5; +var MSECS_PER_SEC = 1000.0; +var GRABBABLE_PROPERTIES = [ + "position", + "rotation", + "gravity", + "ignoreForCollisions", + "collisionsWillMove", + "locked", + "name" +]; + +var GRABBABLE_DATA_KEY = "grabbableKey"; // shared with grab.js +var GRAB_USER_DATA_KEY = "grabKey"; // shared with grab.js + +var DEFAULT_GRABBABLE_DATA = { + grabbable: true, + invertSolidWhileHeld: false +}; + + +// states for the state machine +var STATE_OFF = 0; +var STATE_SEARCHING = 1; +var STATE_NEAR_GRABBING = 4; +var STATE_CONTINUE_NEAR_GRABBING = 5; +var STATE_NEAR_TRIGGER = 6; +var STATE_CONTINUE_NEAR_TRIGGER = 7; +var STATE_FAR_TRIGGER = 8; +var STATE_CONTINUE_FAR_TRIGGER = 9; +var STATE_RELEASE = 10; +var STATE_EQUIP_SEARCHING = 11; +var STATE_EQUIP = 12 +var STATE_CONTINUE_EQUIP_BD = 13; // equip while bumper is still held down +var STATE_CONTINUE_EQUIP = 14; +var STATE_EQUIP_SPRING = 16; + + +function stateToName(state) { + switch (state) { + case STATE_OFF: + return "off"; + case STATE_SEARCHING: + return "searching"; + case STATE_NEAR_GRABBING: + return "near_grabbing"; + case STATE_CONTINUE_NEAR_GRABBING: + return "continue_near_grabbing"; + case STATE_NEAR_TRIGGER: + return "near_trigger"; + case STATE_CONTINUE_NEAR_TRIGGER: + return "continue_near_trigger"; + case STATE_FAR_TRIGGER: + return "far_trigger"; + case STATE_CONTINUE_FAR_TRIGGER: + return "continue_far_trigger"; + case STATE_RELEASE: + return "release"; + case STATE_EQUIP_SEARCHING: + return "equip_searching"; + case STATE_EQUIP: + return "equip"; + case STATE_CONTINUE_EQUIP_BD: + return "continue_equip_bd"; + case STATE_CONTINUE_EQUIP: + return "continue_equip"; + case STATE_EQUIP_SPRING: + return "state_equip_spring"; + } + + return "unknown"; +} + + + +function MyController(hand) { + this.hand = hand; + if (this.hand === RIGHT_HAND) { + this.getHandPosition = MyAvatar.getRightPalmPosition; + this.getHandRotation = MyAvatar.getRightPalmRotation; + } else { + this.getHandPosition = MyAvatar.getLeftPalmPosition; + this.getHandRotation = MyAvatar.getLeftPalmRotation; + } + + var SPATIAL_CONTROLLERS_PER_PALM = 2; + var TIP_CONTROLLER_OFFSET = 1; + this.palm = SPATIAL_CONTROLLERS_PER_PALM * hand; + this.tip = SPATIAL_CONTROLLERS_PER_PALM * hand + TIP_CONTROLLER_OFFSET; + + this.actionID = null; // action this script created... + this.grabbedEntity = null; // on this entity. + this.state = STATE_OFF; + this.pointer = null; // entity-id of line object + this.triggerValue = 0; // rolling average of trigger value + this.rawTriggerValue = 0; + + this.offsetPosition = { + x: 0.0, + y: 0.0, + z: 0.0 + }; + this.offsetRotation = { + x: 0.0, + y: 0.0, + z: 0.0, + w: 1.0 + }; + + var _this = this; + + this.update = function() { + + this.updateSmoothedTrigger(); + + switch (this.state) { + case STATE_OFF: + this.off(); + this.touchTest(); + break; + case STATE_SEARCHING: + this.search(); + break; + case STATE_EQUIP_SEARCHING: + this.search(); + break; + case STATE_NEAR_GRABBING: + case STATE_EQUIP: + this.nearGrabbing(); + break; + case STATE_EQUIP_SPRING: + this.pullTowardEquipPosition() + break; + case STATE_CONTINUE_NEAR_GRABBING: + case STATE_CONTINUE_EQUIP_BD: + case STATE_CONTINUE_EQUIP: + this.continueNearGrabbing(); + break; + case STATE_NEAR_TRIGGER: + this.nearTrigger(); + break; + case STATE_CONTINUE_NEAR_TRIGGER: + this.continueNearTrigger(); + break; + case STATE_FAR_TRIGGER: + this.farTrigger(); + break; + case STATE_CONTINUE_FAR_TRIGGER: + this.continueFarTrigger(); + break; + case STATE_RELEASE: + this.release(); + break; + } + }; + + this.setState = function(newState) { + if (WANT_DEBUG) { + print("STATE: " + stateToName(this.state) + " --> " + stateToName(newState) + ", hand: " + this.hand); + } + this.state = newState; + } + + this.debugLine = function(closePoint, farPoint, color) { + Entities.addEntity({ + type: "Line", + name: "Grab Debug Entity", + dimensions: LINE_ENTITY_DIMENSIONS, + visible: true, + position: closePoint, + linePoints: [ZERO_VEC, farPoint], + color: color, + lifetime: 0.1, + collisionsWillMove: false, + ignoreForCollisions: true, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }) + }); + } + + this.lineOn = function(closePoint, farPoint, color) { + // draw a line + if (this.pointer === null) { + this.pointer = Entities.addEntity({ + type: "Line", + name: "grab pointer", + dimensions: LINE_ENTITY_DIMENSIONS, + visible: false, + position: closePoint, + linePoints: [ZERO_VEC, farPoint], + color: color, + lifetime: LIFETIME, + collisionsWillMove: false, + ignoreForCollisions: true, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }) + }); + } else { + var age = Entities.getEntityProperties(this.pointer, "age").age; + this.pointer = Entities.editEntity(this.pointer, { + position: closePoint, + linePoints: [ZERO_VEC, farPoint], + color: color, + lifetime: age + LIFETIME + }); + } + }; + + this.lineOff = function() { + if (this.pointer !== null) { + Entities.deleteEntity(this.pointer); + } + this.pointer = null; + }; + + this.triggerPress = function(value) { + _this.rawTriggerValue = value; + }; + + + this.updateSmoothedTrigger = function() { + var triggerValue = this.rawTriggerValue; + // smooth out trigger value + this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) + + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); + }; + + this.triggerSmoothedSqueezed = function() { + return this.triggerValue > TRIGGER_ON_VALUE; + }; + + this.triggerSmoothedReleased = function() { + return this.triggerValue < TRIGGER_OFF_VALUE; + }; + + this.triggerSqueezed = function() { + var triggerValue = this.rawTriggerValue; + return triggerValue > TRIGGER_ON_VALUE; + }; + + this.off = function() { + if (this.triggerSmoothedSqueezed()) { + this.lastPickTime = 0; + this.setState(STATE_SEARCHING); + return; + } + } + + this.search = function() { + this.grabbedEntity = null; + + if (this.state == STATE_SEARCHING && this.triggerSmoothedReleased()) { + this.setState(STATE_RELEASE); + return; + } + + // the trigger is being pressed, do a ray test + var handPosition = this.getHandPosition(); + var distantPickRay = { + origin: handPosition, + direction: Quat.getUp(this.getHandRotation()), + length: PICK_MAX_DISTANCE + }; + + // don't pick 60x per second. + var pickRays = []; + var now = Date.now(); + if (now - this.lastPickTime > MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { + pickRays = [distantPickRay]; + this.lastPickTime = now; + } + + for (var index = 0; index < pickRays.length; ++index) { + var pickRay = pickRays[index]; + var directionNormalized = Vec3.normalize(pickRay.direction); + var directionBacked = Vec3.multiply(directionNormalized, PICK_BACKOFF_DISTANCE); + var pickRayBacked = { + origin: Vec3.subtract(pickRay.origin, directionBacked), + direction: pickRay.direction + }; + + + var intersection = Entities.findRayIntersection(pickRayBacked, true); + + if (intersection.intersects) { + // the ray is intersecting something we can move. + var intersectionDistance = Vec3.distance(pickRay.origin, intersection.intersection); + + var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, intersection.entityID, DEFAULT_GRABBABLE_DATA); + + if (intersection.properties.name == "Grab Debug Entity") { + continue; + } + + if (typeof grabbableData.grabbable !== 'undefined' && !grabbableData.grabbable) { + continue; + } + if (intersectionDistance > pickRay.length) { + // too far away for this ray. + continue; + } + if (intersectionDistance <= NEAR_PICK_MAX_DISTANCE) { + // the hand is very close to the intersected object. go into close-grabbing mode. + if (grabbableData.wantsTrigger) { + this.grabbedEntity = intersection.entityID; + this.setState(STATE_NEAR_TRIGGER); + return; + } else if (!intersection.properties.locked) { + var ownerObj = getEntityCustomData('ownerKey', intersection.entityID, null); + + if (ownerObj == null || ownerObj.ownerID === MyAvatar.sessionUUID) { //I can only grab new or already mine items + this.grabbedEntity = intersection.entityID; + if (this.state == STATE_SEARCHING) { + this.setState(STATE_NEAR_GRABBING); + } else { // equipping + if (typeof grabbableData.spatialKey !== 'undefined') { + this.setState(STATE_EQUIP_SPRING); + } else { + this.setState(STATE_EQUIP); + } + } + return; + } + } + } + } + } + + this.lineOn(distantPickRay.origin, Vec3.multiply(distantPickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + }; + + this.nearGrabbing = function() { + var now = Date.now(); + var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); + + if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) { + this.setState(STATE_RELEASE); + //Entities.callEntityMethod(this.grabbedEntity, "releaseGrab"); + return; + } + + this.lineOff(); + + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); + if (grabbedProperties.collisionsWillMove && NEAR_GRABBING_KINEMATIC) { + Entities.editEntity(this.grabbedEntity, { + collisionsWillMove: false + }); + } + + var handRotation = this.getHandRotation(); + var handPosition = this.getHandPosition(); + + var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); + + if (this.state != STATE_NEAR_GRABBING && grabbableData.spatialKey) { + // if an object is "equipped" and has a spatialKey, use it. + if (grabbableData.spatialKey.relativePosition) { + this.offsetPosition = grabbableData.spatialKey.relativePosition; + } + if (grabbableData.spatialKey.relativeRotation) { + this.offsetRotation = grabbableData.spatialKey.relativeRotation; + } + } else { + var objectRotation = grabbedProperties.rotation; + this.offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); + + var currentObjectPosition = grabbedProperties.position; + var offset = Vec3.subtract(currentObjectPosition, handPosition); + this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset); + } + + this.actionID = NULL_ACTION_ID; + this.actionID = Entities.addAction("hold", this.grabbedEntity, { + hand: this.hand === RIGHT_HAND ? "right" : "left", + timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, + relativePosition: this.offsetPosition, + relativeRotation: this.offsetRotation, + ttl: ACTION_TTL, + kinematic: NEAR_GRABBING_KINEMATIC, + kinematicSetVelocity: true + }); + if (this.actionID === NULL_ACTION_ID) { + this.actionID = null; + } else { + this.actionTimeout = now + (ACTION_TTL * MSEC_PER_SEC); + if (this.state == STATE_NEAR_GRABBING) { + this.setState(STATE_CONTINUE_NEAR_GRABBING); + } else { + // equipping + Entities.callEntityMethod(this.grabbedEntity, "startEquip", [JSON.stringify(this.hand)]); + this.startHandGrasp(); + this.setState(STATE_CONTINUE_EQUIP_BD); + } + + if (this.hand === RIGHT_HAND) { + Entities.callEntityMethod(this.grabbedEntity, "setRightHand"); + } else { + Entities.callEntityMethod(this.grabbedEntity, "setLeftHand"); + } + + Entities.callEntityMethod(this.grabbedEntity, "setHand", [this.hand]); + + Entities.callEntityMethod(this.grabbedEntity, "startNearGrab"); + + } + + this.currentHandControllerTipPosition = + (this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition; + + this.currentObjectTime = Date.now(); + }; + + this.continueNearGrabbing = function() { + if (this.state == STATE_CONTINUE_NEAR_GRABBING && this.triggerSmoothedReleased()) { + this.setState(STATE_RELEASE); + //Entities.callEntityMethod(this.grabbedEntity, "releaseGrab"); + return; + } + + // Keep track of the fingertip velocity to impart when we release the object. + // Note that the idea of using a constant 'tip' velocity regardless of the + // object's actual held offset is an idea intended to make it easier to throw things: + // Because we might catch something or transfer it between hands without a good idea + // of it's actual offset, let's try imparting a velocity which is at a fixed radius + // from the palm. + + var handControllerPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition; + var now = Date.now(); + + var deltaPosition = Vec3.subtract(handControllerPosition, this.currentHandControllerTipPosition); // meters + var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds + + this.currentHandControllerTipPosition = handControllerPosition; + this.currentObjectTime = now; + Entities.callEntityMethod(this.grabbedEntity, "continueNearGrab"); + + if (this.state === STATE_CONTINUE_EQUIP_BD) { + Entities.callEntityMethod(this.grabbedEntity, "continueEquip"); + } + + if (this.actionTimeout - now < ACTION_TTL_REFRESH * MSEC_PER_SEC) { + // if less than a 5 seconds left, refresh the actions ttl + Entities.updateAction(this.grabbedEntity, this.actionID, { + hand: this.hand === RIGHT_HAND ? "right" : "left", + timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, + relativePosition: this.offsetPosition, + relativeRotation: this.offsetRotation, + ttl: ACTION_TTL, + kinematic: NEAR_GRABBING_KINEMATIC, + kinematicSetVelocity: true + }); + this.actionTimeout = now + (ACTION_TTL * MSEC_PER_SEC); + } + }; + + + this.pullTowardEquipPosition = function() { + this.lineOff(); + + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); + var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); + + // use a spring to pull the object to where it will be when equipped + var relativeRotation = { + x: 0.0, + y: 0.0, + z: 0.0, + w: 1.0 + }; + var relativePosition = { + x: 0.0, + y: 0.0, + z: 0.0 + }; + if (grabbableData.spatialKey.relativePosition) { + relativePosition = grabbableData.spatialKey.relativePosition; + } + if (grabbableData.spatialKey.relativeRotation) { + relativeRotation = grabbableData.spatialKey.relativeRotation; + } + var handRotation = this.getHandRotation(); + var handPosition = this.getHandPosition(); + var targetRotation = Quat.multiply(handRotation, relativeRotation); + var offset = Vec3.multiplyQbyV(targetRotation, relativePosition); + var targetPosition = Vec3.sum(handPosition, offset); + + if (typeof this.equipSpringID === 'undefined' || + this.equipSpringID === null || + this.equipSpringID === NULL_ACTION_ID) { + this.equipSpringID = Entities.addAction("spring", this.grabbedEntity, { + targetPosition: targetPosition, + linearTimeScale: EQUIP_SPRING_TIMEFRAME, + targetRotation: targetRotation, + angularTimeScale: EQUIP_SPRING_TIMEFRAME, + ttl: ACTION_TTL + }); + if (this.equipSpringID === NULL_ACTION_ID) { + this.equipSpringID = null; + this.setState(STATE_OFF); + return; + } + } else { + Entities.updateAction(this.grabbedEntity, this.equipSpringID, { + targetPosition: targetPosition, + linearTimeScale: EQUIP_SPRING_TIMEFRAME, + targetRotation: targetRotation, + angularTimeScale: EQUIP_SPRING_TIMEFRAME, + ttl: ACTION_TTL + }); + } + + if (Vec3.distance(grabbedProperties.position, targetPosition) < EQUIP_SPRING_SHUTOFF_DISTANCE) { + Entities.deleteAction(this.grabbedEntity, this.equipSpringID); + this.equipSpringID = null; + this.setState(STATE_EQUIP); + } + }; + + this.nearTrigger = function() { + if (this.triggerSmoothedReleased()) { + this.setState(STATE_RELEASE); + Entities.callEntityMethod(this.grabbedEntity, "stopNearTrigger"); + return; + } + if (this.hand === RIGHT_HAND) { + Entities.callEntityMethod(this.grabbedEntity, "setRightHand"); + } else { + Entities.callEntityMethod(this.grabbedEntity, "setLeftHand"); + } + + Entities.callEntityMethod(this.grabbedEntity, "setHand", [this.hand]); + + Entities.callEntityMethod(this.grabbedEntity, "startNearTrigger"); + this.setState(STATE_CONTINUE_NEAR_TRIGGER); + }; + + this.farTrigger = function() { + if (this.triggerSmoothedReleased()) { + this.setState(STATE_RELEASE); + Entities.callEntityMethod(this.grabbedEntity, "stopFarTrigger"); + return; + } + + if (this.hand === RIGHT_HAND) { + Entities.callEntityMethod(this.grabbedEntity, "setRightHand"); + } else { + Entities.callEntityMethod(this.grabbedEntity, "setLeftHand"); + } + Entities.callEntityMethod(this.grabbedEntity, "setHand", [this.hand]); + Entities.callEntityMethod(this.grabbedEntity, "startFarTrigger"); + this.setState(STATE_CONTINUE_FAR_TRIGGER); + }; + + this.continueNearTrigger = function() { + if (this.triggerSmoothedReleased()) { + this.setState(STATE_RELEASE); + Entities.callEntityMethod(this.grabbedEntity, "stopNearTrigger"); + return; + } + + Entities.callEntityMethod(this.grabbedEntity, "continueNearTrigger"); + }; + + this.continueFarTrigger = function() { + if (this.triggerSmoothedReleased()) { + this.setState(STATE_RELEASE); + Entities.callEntityMethod(this.grabbedEntity, "stopNearTrigger"); + return; + } + + var handPosition = this.getHandPosition(); + var pickRay = { + origin: handPosition, + direction: Quat.getUp(this.getHandRotation()) + }; + + var now = Date.now(); + if (now - this.lastPickTime > MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { + var intersection = Entities.findRayIntersection(pickRay, true); + this.lastPickTime = now; + if (intersection.entityID != this.grabbedEntity) { + this.setState(STATE_RELEASE); + Entities.callEntityMethod(this.grabbedEntity, "stopFarTrigger"); + return; + } + } + + this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + Entities.callEntityMethod(this.grabbedEntity, "continueFarTrigger"); + }; + + _this.allTouchedIDs = {}; + this.touchTest = function() { + var maxDistance = 0.05; + var leftHandPosition = MyAvatar.getLeftPalmPosition(); + var rightHandPosition = MyAvatar.getRightPalmPosition(); + var leftEntities = Entities.findEntities(leftHandPosition, maxDistance); + var rightEntities = Entities.findEntities(rightHandPosition, maxDistance); + var ids = []; + + if (leftEntities.length !== 0) { + leftEntities.forEach(function(entity) { + ids.push(entity); + }); + + } + + if (rightEntities.length !== 0) { + rightEntities.forEach(function(entity) { + ids.push(entity); + }); + } + + ids.forEach(function(id) { + + var props = Entities.getEntityProperties(id, ["boundingBox", "name"]); + if (props.name === 'pointer') { + return; + } else { + var entityMinPoint = props.boundingBox.brn; + var entityMaxPoint = props.boundingBox.tfl; + var leftIsTouching = pointInExtents(leftHandPosition, entityMinPoint, entityMaxPoint); + var rightIsTouching = pointInExtents(rightHandPosition, entityMinPoint, entityMaxPoint); + + if ((leftIsTouching || rightIsTouching) && _this.allTouchedIDs[id] === undefined) { + // we haven't been touched before, but either right or left is touching us now + _this.allTouchedIDs[id] = true; + _this.startTouch(id); + } else if ((leftIsTouching || rightIsTouching) && _this.allTouchedIDs[id]) { + // we have been touched before and are still being touched + // continue touch + _this.continueTouch(id); + } else if (_this.allTouchedIDs[id]) { + delete _this.allTouchedIDs[id]; + _this.stopTouch(id); + + } else { + //we are in another state + return; + } + } + + }); + + }; + + this.startTouch = function(entityID) { + Entities.callEntityMethod(entityID, "startTouch"); + }; + + this.continueTouch = function(entityID) { + Entities.callEntityMethod(entityID, "continueTouch"); + }; + + this.stopTouch = function(entityID) { + Entities.callEntityMethod(entityID, "stopTouch"); + }; + + this.release = function() { + + this.lineOff(); + + if (this.grabbedEntity !== null) { + if (this.actionID !== null) { + Entities.deleteAction(this.grabbedEntity, this.actionID); + Entities.callEntityMethod(this.grabbedEntity, "releaseGrab"); + print("CALLING releaseGrab"); + } + } + + + this.grabbedEntity = null; + this.actionID = null; + this.setState(STATE_OFF); + + }; + + this.cleanup = function() { + this.release(); + this.endHandGrasp(); + }; + + + + + //this is our handler, where we do the actual work of changing animation settings + this.graspHand = function(animationProperties) { + var result = {}; + //full alpha on overlay for this hand + //set grab to true + //set idle to false + //full alpha on the blend btw open and grab + if (_this.hand === RIGHT_HAND) { + result['rightHandOverlayAlpha'] = 1.0; + result['isRightHandGrab'] = true; + result['isRightHandIdle'] = false; + result['rightHandGrabBlend'] = 1.0; + } else if (_this.hand === LEFT_HAND) { + result['leftHandOverlayAlpha'] = 1.0; + result['isLeftHandGrab'] = true; + result['isLeftHandIdle'] = false; + result['leftHandGrabBlend'] = 1.0; + } + //return an object with our updated settings + return result; + } + + this.graspHandler = null + this.startHandGrasp = function() { + if (this.hand === RIGHT_HAND) { + this.graspHandler = MyAvatar.addAnimationStateHandler(this.graspHand, ['isRightHandGrab']); + } else if (this.hand === LEFT_HAND) { + this.graspHandler = MyAvatar.addAnimationStateHandler(this.graspHand, ['isLeftHandGrab']); + } + } + + this.endHandGrasp = function() { + // Tell the animation system we don't need any more callbacks. + MyAvatar.removeAnimationStateHandler(this.graspHandler); + } + +} + +var rightController = new MyController(RIGHT_HAND); +var leftController = new MyController(LEFT_HAND); + +var MAPPING_NAME = "com.highfidelity.handControllerGrab"; + +var mapping = Controller.newMapping(MAPPING_NAME); +mapping.from([Controller.Standard.RT]).peek().to(rightController.triggerPress); +mapping.from([Controller.Standard.LT]).peek().to(leftController.triggerPress); + +Controller.enableMapping(MAPPING_NAME); + +var handToDisable = 'none'; + +function update() { + if (handToDisable !== LEFT_HAND) { + leftController.update(); + } + if (handToDisable !== RIGHT_HAND) { + rightController.update(); + } +} + +Messages.subscribe(SHOP_GRAB_CHANNEL); + +stopScriptMessage = function(channel, message, sender) { + if (channel == SHOP_GRAB_CHANNEL && sender === MyAvatar.sessionUUID) { + //stop this script and enable the handControllerGrab + Messages.sendMessage('Hifi-Hand-Disabler', "none"); + Messages.unsubscribe(SHOP_GRAB_CHANNEL); + Messages.messageReceived.disconnect(stopScriptMessage); + Script.stop(); + } +} + +Messages.messageReceived.connect(stopScriptMessage); + +function cleanup() { + rightController.cleanup(); + leftController.cleanup(); + Controller.disableMapping(MAPPING_NAME); +} + +Script.scriptEnding.connect(cleanup); +Script.update.connect(update); \ No newline at end of file diff --git a/examples/vrShop/review/shopReviewEntityScript.js b/examples/vrShop/review/shopReviewEntityScript.js new file mode 100644 index 0000000000..49f0a49dea --- /dev/null +++ b/examples/vrShop/review/shopReviewEntityScript.js @@ -0,0 +1,477 @@ +// shopReviewEntityScript.js +// +// This script handles the review phase in the vrShop. It starts entering into the entity holding in hand the item to review. +// Then the user can rate the item and record a review for that item. +// Finally the recording is stored into the asset and a link to that file is stored into the DB entity of that item. +// During the whole reviewing experience an (interactive) UI drives the user. + +// Created by Alessandro Signa and Edgar Pironti on 01/13/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 +// + +(function () { + var utilitiesScript = Script.resolvePath("../../libraries/utils.js"); + var overlayManagerScript = Script.resolvePath("../../libraries/overlayManager.js"); + Script.include(utilitiesScript); + Script.include(overlayManagerScript); + + var POINTER_ICON_URL = "https://dl.dropboxusercontent.com/u/14127429/FBX/VRshop/Pointer.png"; + var STAR_ON_URL = "https://dl.dropboxusercontent.com/u/14127429/FBX/VRshop/SingleStar_Yellow.png"; + var STAR_OFF_URL = "https://dl.dropboxusercontent.com/u/14127429/FBX/VRshop/SingleStar_Black.png"; + var RECORDING_ON_ICON_URL = "https://dl.dropboxusercontent.com/u/14127429/FBX/VRshop/REC.png"; + var ANCHOR_ENTITY_FOR_UI_NAME = "anchorEntityForReviewUI"; + var START_RECORDING_TEXT = "Press the bumper to start recording"; + var STOP_RECORDING_TEXT = "Press the bumper to stop recording"; + var SHOPPING_CART_NAME = "Shopping cart"; + var CAMERA_NAME = "CameraReview"; + + + var RIGHT_HAND = 1; + var LEFT_HAND = 0; + + var LINE_LENGTH = 100; + var COLOR = { + red: 165, + green: 199, + blue: 218 + }; + + + + var _this; + var dataBaseID = null; + var cameraEntity = null; + var scoreAssigned = null; + var hoveredButton = null; + var hoveredButtonIndex = -1; + var recording = false; + var workDone = false; + + var PENETRATION_THRESHOLD = 0.2; + + + var isUIWorking = false; + var wantToStopTrying = false; + var rightController = null; + var leftController = null; + var workingHand = null; + + var mainPanel = null; + var cameraPanel = null; + var buttons = []; + var onAirOverlay = null; + var instructionsOverlay = null; + + + var pointer = new Image3DOverlay({ + url: POINTER_ICON_URL, + dimensions: { + x: 0.015, + y: 0.015 + }, + alpha: 1, + emissive: true, + isFacingAvatar: false, + ignoreRayIntersection: true, + }) + + function ReviewZone() { + _this = this; + return; + }; + + function MyController(hand) { + this.hand = hand; + if (this.hand === RIGHT_HAND) { + this.getHandPosition = MyAvatar.getRightPalmPosition; + this.getHandRotation = MyAvatar.getRightPalmRotation; + this.bumper = Controller.Standard.RB; + } else { + this.getHandPosition = MyAvatar.getLeftPalmPosition; + this.getHandRotation = MyAvatar.getLeftPalmRotation; + this.bumper = Controller.Standard.LB; + } + + this.pickRay = null; // ray object + this.overlayLine = null; // id of line overlay + this.waitingForBumpReleased = false; + + this.overlayLineOn = function(closePoint, farPoint, color) { + if (this.overlayLine == null) { + var lineProperties = { + lineWidth: 5, + start: closePoint, + end: farPoint, + color: color, + ignoreRayIntersection: true, + visible: true, + alpha: 1 + }; + this.overlayLine = new Line3DOverlay(lineProperties); + } else { + this.overlayLine.start = closePoint; + this.overlayLine.end = farPoint; + } + }, + + this.updateRay = function() { + //update the ray object + this.pickRay = { + origin: this.getHandPosition(), + direction: Quat.getUp(this.getHandRotation()) + }; + //update the ray overlay and the pointer + + var rayPickResult = OverlayManager.findRayIntersection(this.pickRay); + if (rayPickResult.intersects) { + var normal = Vec3.multiply(Quat.getFront(Camera.getOrientation()), -1); + var offset = Vec3.multiply(normal, 0.001); + pointer.position = Vec3.sum(rayPickResult.intersection, offset); //pointer is a global Image3DOverlay + pointer.rotation = Camera.getOrientation(); + pointer.visible = true; + } else { + pointer.visible = false; + } + + var farPoint = rayPickResult.intersects ? rayPickResult.intersection : Vec3.sum(this.pickRay.origin, Vec3.multiply(this.pickRay.direction, LINE_LENGTH)); + this.overlayLineOn(this.pickRay.origin, farPoint, COLOR); + + }, + //the update of each hand has to update the ray belonging to that hand and handle the bumper event + this.updateHand = function() { + //detect the bumper event + var bumperPressed = Controller.getValue(this.bumper); + if (bumperPressed && this != workingHand) { + //mantain active one ray at a time + workingHand.clean(); + workingHand = this; + } else if (this != workingHand) { + return; + } + + if (!scoreAssigned) { + this.updateRay(); + + //manage event on UI + var lastHoveredButton = hoveredButton; + hoveredButton = OverlayManager.findOnRay(this.pickRay); + //print("hovered button: " + hoveredButton); + if (lastHoveredButton != hoveredButton) { + hoveredButtonIndex = -1; + if (hoveredButton) { + for (var i = 0; i < buttons.length; i++) { + if (buttons[i] == hoveredButton) { + //print("Adapting overlay rendering"); + hoveredButtonIndex = i; + } + } + } + adaptOverlayOnHover(hoveredButtonIndex); + } + } else if (!instructionsOverlay.visible) { + workingHand.clean(); + for (var i = 0; i < buttons.length; i++) { + buttons[i].destroy(); + } + buttons = []; + instructionsOverlay.visible = true; + } + + + if (bumperPressed && !this.waitingForBumpReleased) { + this.waitingForBumpReleased = true; + + if (hoveredButton) { + scoreAssigned = hoveredButtonIndex + 1; + hoveredButton = null; + } else if (scoreAssigned && !recording) { + instructionsOverlay.text = STOP_RECORDING_TEXT; + Recording.startRecording(); + onAirOverlay.visible = true; + recording = true; + } else if (scoreAssigned && recording) { + Recording.stopRecording(); + Recording.saveRecordingToAsset(saveDataIntoDB); + onAirOverlay.visible = false; + recording = false; + workDone = true; + _this.cleanUI(); + } + } else if (!bumperPressed && this.waitingForBumpReleased) { + this.waitingForBumpReleased = false; + } + }, + + this.clean = function() { + this.pickRay = null; + if (this.overlayLine) { + this.overlayLine.destroy(); + } + this.overlayLine = null; + pointer.visible = false; + } + }; + + function update(deltaTime) { + + if (!workDone) { + leftController.updateHand(); + rightController.updateHand(); + } else { + _this.cleanUI(); + Script.update.disconnect(update); + } + + + if (!insideZone && isUIWorking) { + // Destroy rays + _this.cleanUI(); + } + + }; + + //This method is the callback called once the recording is loaded into the asset + function saveDataIntoDB(url) { + // Feed the database + var dbObj = getEntityCustomData('infoKey', dataBaseID, null); + if(dbObj) { + var myName = MyAvatar.displayName ? MyAvatar.displayName : "Anonymous"; + dbObj.dbKey[dbObj.dbKey.length] = {name: myName, score: scoreAssigned, clip_url: url}; + setEntityCustomData('infoKey', dataBaseID, dbObj); + print("Feeded DB: " + url"); + } + }; + + // Find items in the zone. It returns a not null value if an item belonging to the user is found AND if a cart belonging to him is not found + function findItemToReview(searchingPointEntityID) { + var foundItemToReviewID = null; + var entitiesInZone = Entities.findEntities(Entities.getEntityProperties(searchingPointEntityID).position, 5); + for (var i = 0; i < entitiesInZone.length; i++) { + + var ownerObj = getEntityCustomData('ownerKey', entitiesInZone[i], null); + + if (ownerObj) { + print("Not sure if review. Check " + MyAvatar.sessionUUID); + if (ownerObj.ownerID === MyAvatar.sessionUUID) { + if (Entities.getEntityProperties(entitiesInZone[i]).name == SHOPPING_CART_NAME) { + //the user has a cart, he's not a reviewer + print("the user has a cart, it's not a reviewer"); + return null; + } else { + foundItemToReviewID = entitiesInZone[i]; + } + } + } + } + var res = null; + if (foundItemToReviewID) { + res = Entities.getEntityProperties(foundItemToReviewID).name; + print("Found an item to review: " + res); + //delete the item + Entities.deleteEntity(foundItemToReviewID); + } + return res; + }; + + function findAnchorEntityForUI(searchingPointEntityID) { + + var entitiesInZone = Entities.findEntities(Entities.getEntityProperties(searchingPointEntityID).position, 2); + for (var i = 0; i < entitiesInZone.length; i++) { + + if (Entities.getEntityProperties(entitiesInZone[i]).name == ANCHOR_ENTITY_FOR_UI_NAME) { + print("Anchor entity found " + entitiesInZone[i]); + return entitiesInZone[i]; + } + } + return null; + }; + + function findItemByName(searchingPointEntityID, itemName) { + // find the database entity + print("Looking for item: " + itemName); + var entitiesInZone = Entities.findEntities(Entities.getEntityProperties(searchingPointEntityID).position, (Entities.getEntityProperties(searchingPointEntityID).dimensions.x)*10); + + for (var i = 0; i < entitiesInZone.length; i++) { + if (Entities.getEntityProperties(entitiesInZone[i]).name == itemName) { + print(itemName + " found! " + entitiesInZone[i]); + return entitiesInZone[i]; + } + } + print("Item " + itemName + " not found"); + return null; + }; + + function adaptOverlayOnHover(hoveredButtonIndex) { + for (var i = buttons.length - 1; i >= 0; i--) { + if (i <= hoveredButtonIndex) { + buttons[i].url = STAR_ON_URL; + } else { + buttons[i].url = STAR_OFF_URL; + } + } + }; + + + ReviewZone.prototype = { + + preload: function (entityID) { + }, + + //When the avatar comes into the review zone the script looks for the actual item to review, the proper DB for that item and the entity camera. If all goes well the UI is created + enterEntity: function (entityID) { + print("entering in the review area"); + insideZone = true; + + var itemToReview = findItemToReview(entityID); //return the name or null + cameraEntity = findItemByName(entityID, CAMERA_NAME); + if (itemToReview && cameraEntity) { + dataBaseID = findItemByName(entityID, itemToReview + "DB"); //return the ID of the DB or null + if (dataBaseID) { + var anchorEntityForUI = findAnchorEntityForUI(entityID); + if (anchorEntityForUI) { + _this.createReviewUI(anchorEntityForUI); + rightController = new MyController(RIGHT_HAND); //rightController and leftController are two objects + leftController = new MyController(LEFT_HAND); + workingHand = rightController; + Script.update.connect(update); + } + } + } + }, + + leaveEntity: function (entityID) { + print("leaving the review area"); + if (!workDone) { + Script.update.disconnect(update); + } + if (recording) { + Recording.stopRecording(); + recording = false; + } + + if (cameraPanel) { + cameraPanel.destroy(); + cameraPanel = null; + } + workDone = false; + cameraEntity = null; + dataBaseID = null; + scoreAssigned = null; + hoveredButton = null; + hoveredButtonIndex = -1; + insideZone = false; + _this.cleanUI(); + }, + + createReviewUI : function(anchorEntityID) { + Entities.editEntity(anchorEntityID, { locked: false }); + var anchorEntityPosition = Entities.getEntityProperties(anchorEntityID).position; + anchorEntityPosition.y = MyAvatar.getHeadPosition().y; + Entities.editEntity(anchorEntityID, { position: anchorEntityPosition }); + Entities.editEntity(anchorEntityID, { locked: true }); + + //set the main panel to follow the inspect entity + mainPanel = new OverlayPanel({ + anchorPositionBinding: { entity: anchorEntityID }, + anchorRotationBinding: { entity: anchorEntityID }, + + isFacingAvatar: false + }); + + var offsetPositionY = 0.0; + var offsetPositionX = -0.3; + + for (var i = 0; i < 3; i++) { + buttons[i] = new Image3DOverlay({ + url: STAR_OFF_URL, + dimensions: { + x: 0.15, + y: 0.15 + }, + isFacingAvatar: false, + alpha: 0.8, + ignoreRayIntersection: false, + offsetPosition: { + x: offsetPositionX - (i * offsetPositionX), + y: offsetPositionY, + z: 0 + }, + emissive: true, + }); + mainPanel.addChild(buttons[i]); + } + + instructionsOverlay= new Text3DOverlay({ + text: START_RECORDING_TEXT, + isFacingAvatar: false, + alpha: 1.0, + ignoreRayIntersection: true, + offsetPosition: { + x: -0.5, + y: 0, + z: 0 + }, + dimensions: { x: 0, y: 0 }, + backgroundColor: { red: 0, green: 255, blue: 0 }, + color: { red: 0, green: 0, blue: 0 }, + topMargin: 0.00625, + leftMargin: 0.00625, + bottomMargin: 0.1, + rightMargin: 0.00625, + lineHeight: 0.06, + alpha: 1, + backgroundAlpha: 0.3, + visible: false + }); + + mainPanel.addChild(instructionsOverlay); + + cameraPanel = new OverlayPanel({ + anchorPositionBinding: { entity: cameraEntity }, + isFacingAvatar: false + }); + + onAirOverlay = new Image3DOverlay({ + url: RECORDING_ON_ICON_URL, + dimensions: { + x: 0.2, + y: 0.2 + }, + isFacingAvatar: false, + alpha: 0.8, + ignoreRayIntersection: true, + offsetPosition: { + x: 0, + y: 0.7, + z: 0 + }, + emissive: true, + visible: false, + }); + cameraPanel.addChild(onAirOverlay); + + isUIWorking = true; + }, + + cleanUI: function () { + workingHand.clean(); + if (mainPanel) { + mainPanel.destroy(); + } + mainPanel = null; + isUIWorking = false; + }, + + unload: function (entityID) { + this.cleanUI(); + if (cameraPanel) { + cameraPanel.destroy(); + } + } + } + + return new ReviewZone(); +}); \ No newline at end of file diff --git a/examples/vrShop/review/shopReviewerAC.js b/examples/vrShop/review/shopReviewerAC.js new file mode 100644 index 0000000000..7b7776fe98 --- /dev/null +++ b/examples/vrShop/review/shopReviewerAC.js @@ -0,0 +1,96 @@ +// shopReviewerAC.js +// + +// Created by Alessandro Signa and Edgar Pironti on 01/13/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 + + +var command = null; +var clip_url = null; + +var REVIEW_CHANNEL = "reviewChannel"; +var playFromCurrentLocation = true; +var useDisplayName = true; +var useAttachments = true; +var useAvatarModel = true; + +var totalTime = 0; +var subscribed = false; +var WAIT_FOR_AUDIO_MIXER = 1; + +var PLAY = "Play"; +var SHOW = "Show"; +var HIDE = "Hide"; + +function getAction(channel, message, senderID) { + if(subscribed) { + print("I'm the agent and I received this: " + message); + + if (Recording.isPlaying()) { + Recording.stopPlaying(); + } + + m = JSON.parse(message); + + command = m.command; + clip_url = m.clip_url; + + switch(command) { + case PLAY: + print("Play"); + if (!Recording.isPlaying()) { + Recording.setPlayerTime(0.0); + Recording.startPlaying(); + } + break; + + case SHOW: + print("Show"); + Recording.loadRecording(clip_url); + Agent.isAvatar = true; + Recording.setPlayerTime(0.0); + Recording.startPlaying(); + Recording.stopPlaying(); + break; + + case HIDE: + print("Hide"); + Agent.isAvatar = false; + break; + + default: + print("Unknown action: " + action); + break; + } + } +} + + +function update(deltaTime) { + + totalTime += deltaTime; + + if (totalTime > WAIT_FOR_AUDIO_MIXER) { + if (!subscribed) { + Messages.subscribe(REVIEW_CHANNEL); + subscribed = true; + Recording.setPlayFromCurrentLocation(playFromCurrentLocation); + Recording.setPlayerUseDisplayName(useDisplayName); + Recording.setPlayerUseAttachments(useAttachments); + Recording.setPlayerUseHeadModel(false); + Recording.setPlayerUseSkeletonModel(useAvatarModel); + } + } + +} + +Messages.messageReceived.connect(function (channel, message, senderID) { + if (channel == REVIEW_CHANNEL) { + getAction(channel, message, senderID); + } +}); + +Script.update.connect(update); \ No newline at end of file diff --git a/examples/vrShop/shop/shopGrabSwapperEntityScript.js b/examples/vrShop/shop/shopGrabSwapperEntityScript.js new file mode 100644 index 0000000000..b6f52b2731 --- /dev/null +++ b/examples/vrShop/shop/shopGrabSwapperEntityScript.js @@ -0,0 +1,55 @@ +// shopGrapSwapperEntityScript.js +// +// This script handle the transition from handControllerGrab to shopItemGrab +// When an avatar enters the zone a message is sent to the handControllerGrab script to disable itself and the shopItemGrab is loaded. +// When exit from the zone the handControllerGrab is re-enabled. +// This mechanism may not work well with the last changes to the animations in handControllerGrab, if it's the case please load this script manually and disable that one. + +// Created by Alessandro Signa and Edgar Pironti on 01/13/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 +// + +(function () { + var SHOP_GRAB_SCRIPT_URL = Script.resolvePath("../item/shopItemGrab.js"); + var SHOP_GRAB_CHANNEL = "Hifi-vrShop-Grab"; + var _this; + + function SwapGrabZone() { + _this = this; + return; + }; + + function isScriptRunning(script) { + script = script.toLowerCase().trim(); + var runningScripts = ScriptDiscoveryService.getRunning(); + for (i in runningScripts) { + if (runningScripts[i].url.toLowerCase().trim() == script) { + return true; + } + } + return false; + }; + + SwapGrabZone.prototype = { + + enterEntity: function (entityID) { + print("entering in the shop area"); + + if (!isScriptRunning(SHOP_GRAB_SCRIPT_URL)) { + Script.load(SHOP_GRAB_SCRIPT_URL); + } + + }, + + leaveEntity: function (entityID) { + print("leaving the shop area"); + Messages.sendMessage(SHOP_GRAB_CHANNEL, null); //signal to shopItemGrab that it has to kill itself + } + + } + + return new SwapGrabZone(); +}); \ No newline at end of file diff --git a/examples/vrShop/vendor/shopVendorAddItem.js b/examples/vrShop/vendor/shopVendorAddItem.js new file mode 100644 index 0000000000..a73fa97ba8 --- /dev/null +++ b/examples/vrShop/vendor/shopVendorAddItem.js @@ -0,0 +1,101 @@ +// shopVendorAddItem.js +// +// This shows a propt to allow the user to create a custom shop item, it also creates a DB entity for that object + +// Created by Alessandro Signa and Edgar Pironti on 01/13/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 +// + +var TITLE = "Add Item Form"; +var DEFAULT_ITEM_NAME = "New Item"; +var DEFAULT_ROOT_DIRECTORY_URL = "https://dl.dropboxusercontent.com/u/14127429/FBX/VRshop/"; +var DEFAULT_MODEL_1_RELATIVE_PATH = "cubeRed.fbx"; // those paths are relative to the root directory url +var DEFAULT_PREVIEW_1_RELATIVE_PATH = "previewRed.png"; +var DEFAULT_MODEL_2_RELATIVE_PATH = "cubeGreen.fbx"; +var DEFAULT_PREVIEW_2_RELATIVE_PATH = "previewGreen.png"; +var DEFAULT_MODEL_3_RELATIVE_PATH = "cubeBlue.fbx"; +var DEFAULT_PREVIEW_3_RELATIVE_PATH = "previewBlue.png"; +var DEFAULT_QUANTITY = 1; +var DEFAULT_PRICE = 1.00; +var DEFAULT_DESCRIPTION = "Description empty"; + + +var scriptURL = Script.resolvePath("../item/shopItemEntityScript.js"); +var rotation = Quat.safeEulerAngles(Camera.getOrientation()); +rotation = Quat.fromPitchYawRollDegrees(0, rotation.y, 0); +var position = Vec3.sum(MyAvatar.position, Vec3.multiply(1.5, Quat.getFront(rotation))); + +var form = [ + {"label": "Item Name", "value": DEFAULT_ITEM_NAME}, + + {"label": "Root directory URL", "value": DEFAULT_ROOT_DIRECTORY_URL}, + {"label": "Model 1 relative path", "value": DEFAULT_MODEL_1_RELATIVE_PATH}, + {"label": "Preview 1 relative path", "value": DEFAULT_PREVIEW_1_RELATIVE_PATH}, + {"label": "Model 2 relative path", "value": DEFAULT_MODEL_2_RELATIVE_PATH}, + {"label": "Preview 2 relative path", "value": DEFAULT_PREVIEW_2_RELATIVE_PATH}, + {"label": "Model 3 relative path", "value": DEFAULT_MODEL_3_RELATIVE_PATH}, + {"label": "Preview 3 relative path", "value": DEFAULT_PREVIEW_3_RELATIVE_PATH}, + + {"label": "Quantity", "value": DEFAULT_QUANTITY}, + {"label": "Price", "value": DEFAULT_PRICE}, + {"label": "Description", "value": DEFAULT_DESCRIPTION}, + {"label": "Wearable", "type": "checkbox", "value": false}, + +]; + +var accepted = Window.form(TITLE, form); + +if (accepted) { + var modelURL = "" + form[1].value + form[2].value; + var myEntity = Entities.addEntity({ + type: "Model", + name: form[0].value, + position: position, + script: scriptURL, + rotation: rotation, + collisionsWillMove: false, + ignoreForCollisions: true, + collisionMask: "static,dynamic,otherAvatar", + shapeType: "box", + modelURL: modelURL, + userData: JSON.stringify({ + infoKey: { + rootURL: form[1].value, + modelURLs: [ + form[2].value, + form[4].value, + form[6].value + ], + previewURLs: [ + form[3].value, + form[5].value, + form[7].value + ], + availability: form[8].value, + price: form[9].value, + description: form[10].value, + wearable: form[11].value + } + }) + }); + + var myEntityDB = Entities.addEntity({ + type: "Box", + name: form[0].value + "DB", + position: {x: 0, y: 0, z: 0}, + rotation: rotation, + collisionsWillMove: false, + ignoreForCollisions: true, + visible: false, + shapeType: "box", + userData: JSON.stringify({ + infoKey: { + dbKey: [ + ] + } + }) + }); +} \ No newline at end of file