diff --git a/BUILD_WIN.md b/BUILD_WIN.md index b6ec31b713..d291cef4f2 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -71,7 +71,7 @@ Your system may already have several versions of the OpenSSL DLL's (ssleay32.dll To prevent these problems, install OpenSSL yourself. Download the following binary packages [from this website](http://slproweb.com/products/Win32OpenSSL.html): * Visual C++ 2008 Redistributables -* Win32 OpenSSL v1.0.1m +* Win32 OpenSSL v1.0.1p Install OpenSSL into the Windows system directory, to make sure that Qt uses the version that you've just installed, and not some other version. diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 49b7f2e183..614b0a1528 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1744,13 +1744,26 @@ void DomainServer::addStaticAssignmentsToQueue() { // if the domain-server has just restarted, // check if there are static assignments that we need to throw into the assignment queue - QHash staticHashCopy = _allAssignments; - QHash::iterator staticAssignment = staticHashCopy.begin(); - while (staticAssignment != staticHashCopy.end()) { + auto sharedAssignments = _allAssignments.values(); + + // sort the assignments to put the server/mixer assignments first + qSort(sharedAssignments.begin(), sharedAssignments.end(), [](SharedAssignmentPointer a, SharedAssignmentPointer b){ + if (a->getType() == b->getType()) { + return true; + } else if (a->getType() != Assignment::AgentType && b->getType() != Assignment::AgentType) { + return a->getType() < b->getType(); + } else { + return a->getType() != Assignment::AgentType; + } + }); + + auto staticAssignment = sharedAssignments.begin(); + + while (staticAssignment != sharedAssignments.end()) { // add any of the un-matched static assignments to the queue // enumerate the nodes and check if there is one with an attached assignment with matching UUID - if (!DependencyManager::get()->nodeWithUUID(staticAssignment->data()->getUUID())) { + if (!DependencyManager::get()->nodeWithUUID((*staticAssignment)->getUUID())) { // this assignment has not been fulfilled - reset the UUID and add it to the assignment queue refreshStaticAssignmentAndAddToQueue(*staticAssignment); } diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index f38d4a1008..5818815bb2 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -8,74 +8,91 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - +/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, randFloat, randInt, pointInExtents, vec3equal, setEntityCustomData, getEntityCustomData */ Script.include("../libraries/utils.js"); -var RADIUS_FACTOR = 4; +///////////////////////////////////////////////////////////////// +// +// these tune time-averaging and "on" value for analog trigger +// -var RIGHT_HAND_CLICK = Controller.findAction("RIGHT_HAND_CLICK"); -var rightTriggerAction = RIGHT_HAND_CLICK; +var TRIGGER_SMOOTH_RATIO = 0.1; // 0.0 disables smoothing of trigger value +var TRIGGER_ON_VALUE = 0.2; -var GRAB_USER_DATA_KEY = "grabKey"; +///////////////////////////////////////////////////////////////// +// +// distant manipulation +// -var LEFT_HAND_CLICK = Controller.findAction("LEFT_HAND_CLICK"); -var leftTriggerAction = LEFT_HAND_CLICK; +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 LIFETIME = 10; -var EXTRA_TIME = 5; -var POINTER_CHECK_TIME = 5000; + +///////////////////////////////////////////////////////////////// +// +// near grabbing +// + +var GRAB_RADIUS = 0.3; // 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.6; // max length of pick-ray for close grabbing to be selected +var RELEASE_VELOCITY_MULTIPLIER = 1.5; // affects throwing things + +///////////////////////////////////////////////////////////////// +// +// other constants +// + +var RIGHT_HAND = 1; +var LEFT_HAND = 0; var ZERO_VEC = { x: 0, y: 0, z: 0 -} -var LINE_LENGTH = 500; -var THICK_LINE_WIDTH = 7; -var THIN_LINE_WIDTH = 2; - -var NO_INTERSECT_COLOR = { - red: 10, - green: 10, - blue: 255 -}; -var INTERSECT_COLOR = { - red: 250, - green: 10, - blue: 10 }; +var NULL_ACTION_ID = "{00000000-0000-0000-000000000000}"; +var MSEC_PER_SEC = 1000.0; -var GRAB_RADIUS = 0.3; - -var GRAB_COLOR = { - red: 250, - green: 10, - blue: 250 -}; -var SHOW_LINE_THRESHOLD = 0.2; -var DISTANCE_HOLD_THRESHOLD = 0.8; - -var right4Action = 18; -var left4Action = 17; - -var RIGHT = 1; -var LEFT = 0; -var rightController = new controller(RIGHT, rightTriggerAction, right4Action, "right"); -var leftController = new controller(LEFT, leftTriggerAction, left4Action, "left"); +// these control how long an abandoned pointer line will hang around var startTime = Date.now(); +var LIFETIME = 10; + +// states for the state machine +var STATE_SEARCHING = 0; +var STATE_DISTANCE_HOLDING = 1; +var STATE_CONTINUE_DISTANCE_HOLDING = 2; +var STATE_NEAR_GRABBING = 3; +var STATE_CONTINUE_NEAR_GRABBING = 4; +var STATE_NEAR_GRABBING_NON_COLLIDING = 5; +var STATE_CONTINUE_NEAR_GRABBING_NON_COLLIDING = 6; +var STATE_RELEASE = 7; -//Need to wait before calling these methods for some reason... -Script.setTimeout(function() { - rightController.checkPointer(); - leftController.checkPointer(); -}, 100) +var GRAB_USER_DATA_KEY = "grabKey"; -function controller(side, triggerAction, pullAction, hand) { +function controller(hand, triggerAction) { this.hand = hand; - if (hand === "right") { + if (this.hand === RIGHT_HAND) { this.getHandPosition = MyAvatar.getRightPalmPosition; this.getHandRotation = MyAvatar.getRightPalmRotation; } else { @@ -83,309 +100,469 @@ function controller(side, triggerAction, pullAction, hand) { this.getHandRotation = MyAvatar.getLeftPalmRotation; } this.triggerAction = triggerAction; - this.pullAction = pullAction; - this.actionID = null; - this.distanceHolding = false; - this.closeGrabbing = false; - this.triggerValue = 0; - this.prevTriggerValue = 0; - this.palm = 2 * side; - this.tip = 2 * side + 1; - this.pointer = null; -} + this.palm = 2 * hand; + // this.tip = 2 * hand + 1; // unused, but I'm leaving this here for fear it will be needed - -controller.prototype.updateLine = function() { - if (this.pointer != null) { - if (Entities.getEntityProperties(this.pointer).id != this.pointer) { - this.pointer = null; + this.actionID = null; // action this script created... + this.grabbedEntity = null; // on this entity. + this.grabbedVelocity = ZERO_VEC; // rolling average of held object's velocity + this.state = 0; + this.pointer = null; // entity-id of line object + this.triggerValue = 0; // rolling average of trigger value + var _this = this; + this.update = function () { + switch (this.state) { + case STATE_SEARCHING: + this.search(); + this.touchTest(); + break; + case STATE_DISTANCE_HOLDING: + this.distanceHolding(); + break; + case STATE_CONTINUE_DISTANCE_HOLDING: + this.continueDistanceHolding(); + break; + case STATE_NEAR_GRABBING: + this.nearGrabbing(); + break; + case STATE_CONTINUE_NEAR_GRABBING: + this.continueNearGrabbing(); + break; + case STATE_NEAR_GRABBING_NON_COLLIDING: + this.nearGrabbingNonColliding(); + break; + case STATE_CONTINUE_NEAR_GRABBING_NON_COLLIDING: + this.continueNearGrabbingNonColliding(); + break; + case STATE_RELEASE: + this.release(); + break; } - } - - if (this.pointer == null) { - this.lineCreationTime = Date.now(); - this.pointer = Entities.addEntity({ - type: "Line", - name: "pointer", - color: NO_INTERSECT_COLOR, - dimensions: { - x: 1000, - y: 1000, - z: 1000 - }, - visible: true, - lifetime: LIFETIME - }); - } - - var handPosition = this.getHandPosition(); - var direction = Quat.getUp(this.getHandRotation()); - - //only check if we havent already grabbed an object - if (this.distanceHolding) { - Entities.editEntity(this.pointer, { - position: handPosition, - linePoints: [ ZERO_VEC, Vec3.subtract(Entities.getEntityProperties(this.grabbedEntity).position, handPosition) ], - lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME - }); - - return; - } - - Entities.editEntity(this.pointer, { - position: handPosition, - linePoints: [ ZERO_VEC, Vec3.multiply(direction, LINE_LENGTH) ], - lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME - }); - - if (this.checkForIntersections(handPosition, direction)) { - Entities.editEntity(this.pointer, { - color: INTERSECT_COLOR, - }); - } else { - Entities.editEntity(this.pointer, { - color: NO_INTERSECT_COLOR, - }); - } -} - - -controller.prototype.checkPointer = function() { - var self = this; - Script.setTimeout(function() { - var props = Entities.getEntityProperties(self.pointer); - Entities.editEntity(self.pointer, { - lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME - }); - self.checkPointer(); - }, POINTER_CHECK_TIME); -} - -controller.prototype.checkForIntersections = function(origin, direction) { - var pickRay = { - origin: origin, - direction: direction }; - - var intersection = Entities.findRayIntersection(pickRay, true); - if (intersection.intersects && intersection.properties.collisionsWillMove === 1) { - var handPosition = Controller.getSpatialControlPosition(this.palm); - this.distanceToEntity = Vec3.distance(handPosition, intersection.properties.position); - var intersectionDistance = Vec3.distance(handPosition, intersection.intersection); - - if (intersectionDistance < 0.6) { - //We are grabbing an entity, so let it know we've grabbed it - this.grabbedEntity = intersection.entityID; - this.activateEntity(this.grabbedEntity); - this.hidePointer(); - this.shouldDisplayLine = false; - this.grabEntity(); - return true; + _this.pointerIDs = []; + this.lineOn = function (closePoint, farPoint, color) { + // draw a line + if (this.pointer === null) { + this.pointer = Entities.addEntity({ + type: "Line", + name: "pointer", + dimensions: LINE_ENTITY_DIMENSIONS, + visible: true, + position: closePoint, + linePoints: [ZERO_VEC, farPoint], + color: color, + lifetime: LIFETIME + }); + _this.pointerIDs.push(this.pointer); } else { Entities.editEntity(this.pointer, { - linePoints: [ - ZERO_VEC, - Vec3.multiply(direction, this.distanceToEntity) - ] + position: closePoint, + linePoints: [ZERO_VEC, farPoint], + color: color, + lifetime: (Date.now() - startTime) / MSEC_PER_SEC + LIFETIME }); + } + + }; + + + this.lineOff = function () { + if (this.pointer !== null) { + Entities.deleteEntity(this.pointer); + } + var index = _this.pointerIDs.indexOf(this.pointer); + if (index > -1) { + _this.pointerIDs.splice(index, 1); + } + this.pointer = null; + }; + + + this.triggerSmoothedSqueezed = function () { + var triggerValue = Controller.getActionValue(this.triggerAction); + // smooth out trigger value + this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) + + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); + return this.triggerValue > TRIGGER_ON_VALUE; + }; + + + this.triggerSqueezed = function () { + var triggerValue = Controller.getActionValue(this.triggerAction); + return triggerValue > TRIGGER_ON_VALUE; + }; + + + this.search = function () { + + + if (!this.triggerSmoothedSqueezed()) { + this.state = STATE_RELEASE; + return; + } + + // the trigger is being pressed, do a ray test + var handPosition = this.getHandPosition(); + var pickRay = { + origin: handPosition, + direction: Quat.getUp(this.getHandRotation()) + }; + var intersection = Entities.findRayIntersection(pickRay, true); + if (intersection.intersects && + intersection.properties.collisionsWillMove === 1 && + intersection.properties.locked === 0) { + // the ray is intersecting something we can move. + var handControllerPosition = Controller.getSpatialControlPosition(this.palm); + var intersectionDistance = Vec3.distance(handControllerPosition, intersection.intersection); this.grabbedEntity = intersection.entityID; - return true; + if (intersectionDistance < NEAR_PICK_MAX_DISTANCE) { + // the hand is very close to the intersected object. go into close-grabbing mode. + this.state = STATE_NEAR_GRABBING; + } else { + // the hand is far from the intersected object. go into distance-holding mode + this.state = STATE_DISTANCE_HOLDING; + this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + } + } else { + // forward ray test failed, try sphere test. + var nearbyEntities = Entities.findEntities(handPosition, GRAB_RADIUS); + var minDistance = GRAB_RADIUS; + var i, props, distance; + for (i = 0; i < nearbyEntities.length; i++) { + props = Entities.getEntityProperties(nearbyEntities[i], ["position", "name", "collisionsWillMove", "locked"]); + distance = Vec3.distance(props.position, handPosition); + if (distance < minDistance && props.name !== "pointer") { + this.grabbedEntity = nearbyEntities[i]; + minDistance = distance; + } + } + if (this.grabbedEntity === null) { + this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + } else if (props.locked === 0 && props.collisionsWillMove === 1) { + this.state = STATE_NEAR_GRABBING; + } else if (props.collisionsWillMove === 0) { + // We have grabbed a non-physical object, so we want to trigger a non-colliding event as opposed to a grab event + this.state = STATE_NEAR_GRABBING_NON_COLLIDING; + } } - } - return false; -} + + }; -controller.prototype.attemptMove = function() { - if (this.grabbedEntity || this.distanceHolding) { - var handPosition = Controller.getSpatialControlPosition(this.palm); + this.distanceHolding = function () { + var handControllerPosition = Controller.getSpatialControlPosition(this.palm); var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm)); + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position", "rotation"]); - this.distanceHolding = true; - if (this.actionID === null) { - this.currentObjectPosition = Entities.getEntityProperties(this.grabbedEntity).position; - this.currentObjectRotation = Entities.getEntityProperties(this.grabbedEntity).rotation; + // add the action and initialize some variables + this.currentObjectPosition = grabbedProperties.position; + this.currentObjectRotation = grabbedProperties.rotation; + this.currentObjectTime = Date.now(); + this.handPreviousPosition = handControllerPosition; + this.handPreviousRotation = handRotation; - this.handPreviousPosition = handPosition; - this.handPreviousRotation = handRotation; - - this.actionID = Entities.addAction("spring", this.grabbedEntity, { - targetPosition: this.currentObjectPosition, - linearTimeScale: .1, - targetRotation: this.currentObjectRotation, - angularTimeScale: .1 - }); - } else { - var radius = Math.max(Vec3.distance(this.currentObjectPosition, handPosition) * RADIUS_FACTOR, 1.0); - - var handMoved = Vec3.subtract(handPosition, this.handPreviousPosition); - this.handPreviousPosition = handPosition; - var superHandMoved = Vec3.multiply(handMoved, radius); - this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, superHandMoved); - - // ---------------- this tracks hand rotation - // var handChange = Quat.multiply(handRotation, Quat.inverse(this.handPreviousRotation)); - // this.handPreviousRotation = handRotation; - // this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); - // ---------------- - - // ---------------- this doubles hand rotation - var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, handRotation, 2.0), - Quat.inverse(this.handPreviousRotation)); - this.handPreviousRotation = handRotation; - this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); - // ---------------- - - - Entities.updateAction(this.grabbedEntity, this.actionID, { - targetPosition: this.currentObjectPosition, linearTimeScale: .1, - targetRotation: this.currentObjectRotation, angularTimeScale: .1 - }); + this.actionID = Entities.addAction("spring", this.grabbedEntity, { + targetPosition: this.currentObjectPosition, + linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, + targetRotation: this.currentObjectRotation, + angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME + }); + if (this.actionID === NULL_ACTION_ID) { + this.actionID = null; } - } -} -controller.prototype.showPointer = function() { - Entities.editEntity(this.pointer, { - visible: true - }); - -} - -controller.prototype.hidePointer = function() { - Entities.editEntity(this.pointer, { - visible: false - }); -} - - -controller.prototype.letGo = function() { - if (this.grabbedEntity && this.actionID) { - this.deactivateEntity(this.grabbedEntity); - Entities.deleteAction(this.grabbedEntity, this.actionID); - } - this.grabbedEntity = null; - this.actionID = null; - this.distanceHolding = false; - this.closeGrabbing = false; -} - -controller.prototype.update = function() { - this.triggerValue = Controller.getActionValue(this.triggerAction); - if (this.triggerValue > SHOW_LINE_THRESHOLD && this.prevTriggerValue < SHOW_LINE_THRESHOLD) { - //First check if an object is within close range and then run the close grabbing logic - if (this.checkForInRangeObject()) { - this.grabEntity(); - } else { - this.showPointer(); - this.shouldDisplayLine = true; + if (this.actionID !== null) { + this.state = STATE_CONTINUE_DISTANCE_HOLDING; + this.activateEntity(this.grabbedEntity); + if (this.hand === RIGHT_HAND) { + Entities.callEntityMethod(this.grabbedEntity, "setRightHand"); + } else { + Entities.callEntityMethod(this.grabbedEntity, "setLeftHand"); + } } - } else if (this.triggerValue < SHOW_LINE_THRESHOLD && this.prevTriggerValue > SHOW_LINE_THRESHOLD) { - this.hidePointer(); - this.letGo(); - this.shouldDisplayLine = false; - } + Entities.callEntityMethod(this.grabbedEntity, "startDistantGrab"); - if (this.shouldDisplayLine) { - this.updateLine(); - } - if (this.triggerValue > DISTANCE_HOLD_THRESHOLD && !this.closeGrabbing) { - this.attemptMove(); - } - this.prevTriggerValue = this.triggerValue; -} + }; -controller.prototype.grabEntity = function() { - var handRotation = this.getHandRotation(); - var handPosition = this.getHandPosition(); - this.closeGrabbing = true; - //check if our entity has instructions on how to be grabbed, otherwise, just use default relative position and rotation - var userData = getEntityUserData(this.grabbedEntity); - var objectRotation = Entities.getEntityProperties(this.grabbedEntity).rotation; - var offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); - - var objectPosition = Entities.getEntityProperties(this.grabbedEntity).position; - var offset = Vec3.subtract(objectPosition, handPosition); - var offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, offsetRotation)), offset); - - var relativePosition = offsetPosition; - var relativeRotation = offsetRotation; - if (userData.grabFrame) { - if (userData.grabFrame.relativePosition) { - relativePosition = userData.grabFrame.relativePosition; + this.continueDistanceHolding = function () { + if (!this.triggerSmoothedSqueezed()) { + this.state = STATE_RELEASE; + return; } - if (userData.grabFrame.relativeRotation) { - relativeRotation = userData.grabFrame.relativeRotation; - } - } - this.actionID = Entities.addAction("hold", this.grabbedEntity, { - hand: this.hand, - timeScale: 0.05, - relativePosition: relativePosition, - relativeRotation: relativeRotation - }); -} + + var handPosition = this.getHandPosition(); + var handControllerPosition = Controller.getSpatialControlPosition(this.palm); + var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm)); + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position", "rotation"]); + + this.lineOn(handPosition, Vec3.subtract(grabbedProperties.position, handPosition), INTERSECT_COLOR); + + // the action was set up on a previous call. update the targets. + var radius = Math.max(Vec3.distance(this.currentObjectPosition, + handControllerPosition) * DISTANCE_HOLDING_RADIUS_FACTOR, + DISTANCE_HOLDING_RADIUS_FACTOR); + + var handMoved = Vec3.subtract(handControllerPosition, this.handPreviousPosition); + this.handPreviousPosition = handControllerPosition; + var superHandMoved = Vec3.multiply(handMoved, radius); + + var newObjectPosition = Vec3.sum(this.currentObjectPosition, superHandMoved); + var deltaPosition = Vec3.subtract(newObjectPosition, this.currentObjectPosition); // meters + var now = Date.now(); + var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds + this.computeReleaseVelocity(deltaPosition, deltaTime, false); + + this.currentObjectPosition = newObjectPosition; + this.currentObjectTime = now; + + // this doubles hand rotation + var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, handRotation, + DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), + Quat.inverse(this.handPreviousRotation)); + this.handPreviousRotation = handRotation; + this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); + + Entities.callEntityMethod(this.grabbedEntity, "continueDistantGrab"); + + Entities.updateAction(this.grabbedEntity, this.actionID, { + targetPosition: this.currentObjectPosition, + linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, + targetRotation: this.currentObjectRotation, + angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME + }); + }; -controller.prototype.checkForInRangeObject = function() { - var handPosition = Controller.getSpatialControlPosition(this.palm); - var entities = Entities.findEntities(handPosition, GRAB_RADIUS); - var minDistance = GRAB_RADIUS; - var grabbedEntity = null; - //Get nearby entities and assign nearest - for (var i = 0; i < entities.length; i++) { - var props = Entities.getEntityProperties(entities[i]); - var distance = Vec3.distance(props.position, handPosition); - if (distance < minDistance && props.name !== "pointer" && props.collisionsWillMove === 1) { - grabbedEntity = entities[i]; - minDistance = distance; + this.nearGrabbing = function () { + if (!this.triggerSmoothedSqueezed()) { + this.state = STATE_RELEASE; + return; } - } - if (grabbedEntity === null) { - return false; - } else { - //We are grabbing an entity, so let it know we've grabbed it - this.grabbedEntity = grabbedEntity; + + this.lineOff(); + this.activateEntity(this.grabbedEntity); - return true; - } -} + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position", "rotation"]); -controller.prototype.activateEntity = function(entity) { - var data = { - activated: true, - avatarId: MyAvatar.sessionUUID + var handRotation = this.getHandRotation(); + var handPosition = this.getHandPosition(); + + var objectRotation = grabbedProperties.rotation; + var offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); + + var currentObjectPosition = grabbedProperties.position; + var offset = Vec3.subtract(currentObjectPosition, handPosition); + var offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, offsetRotation)), offset); + + this.actionID = Entities.addAction("hold", this.grabbedEntity, { + hand: this.hand === RIGHT_HAND ? "right" : "left", + timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, + relativePosition: offsetPosition, + relativeRotation: offsetRotation + }); + if (this.actionID === NULL_ACTION_ID) { + this.actionID = null; + } else { + this.state = STATE_CONTINUE_NEAR_GRABBING; + if (this.hand === RIGHT_HAND) { + Entities.callEntityMethod(this.grabbedEntity, "setRightHand"); + } else { + Entities.callEntityMethod(this.grabbedEntity, "setLeftHand"); + } + Entities.callEntityMethod(this.grabbedEntity, "startNearGrab"); + + } + + this.currentHandControllerPosition = Controller.getSpatialControlPosition(this.palm); + this.currentObjectTime = Date.now(); }; - setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data); -} -controller.prototype.deactivateEntity = function(entity) { - var data = { - activated: false, - avatarId: null + this.continueNearGrabbing = function () { + if (!this.triggerSmoothedSqueezed()) { + this.state = STATE_RELEASE; + return; + } + + // keep track of the measured velocity of the held object + var handControllerPosition = Controller.getSpatialControlPosition(this.palm); + var now = Date.now(); + + var deltaPosition = Vec3.subtract(handControllerPosition, this.currentHandControllerPosition); // meters + var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds + this.computeReleaseVelocity(deltaPosition, deltaTime, true); + + this.currentHandControllerPosition = handControllerPosition; + this.currentObjectTime = now; + Entities.callEntityMethod(this.grabbedEntity, "continueNearGrab"); + }; + + this.nearGrabbingNonColliding = function () { + if (!this.triggerSmoothedSqueezed()) { + this.state = STATE_RELEASE; + return; + } + Entities.callEntityMethod(this.grabbedEntity, "startNearGrabNonColliding"); + this.state = STATE_CONTINUE_NEAR_GRABBING_NON_COLLIDING; + }; + + this.continueNearGrabbingNonColliding = function () { + if (!this.triggerSmoothedSqueezed()) { + this.state = STATE_RELEASE; + return; + } + Entities.callEntityMethod(this.grabbedEntity, "continueNearGrabbingNonColliding"); + }; + + _this.allTouchedIDs = {}; + this.touchTest = function () { + //print('touch test'); + 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] === true) { + // we have been touched before and are still being touched + // continue touch + _this.continueTouch(id); + } else if (_this.allTouchedIDs[id] === true) { + delete _this.allTouchedIDs[id]; + _this.stopTouch(id); + + } else { + //we are in another state + return; + } + } + + }); + + }; + + this.startTouch = function (entityID) { + // print('START TOUCH' + entityID); + Entities.callEntityMethod(entityID, "startTouch"); + }; + + this.continueTouch = function (entityID) { + // print('CONTINUE TOUCH' + entityID); + Entities.callEntityMethod(entityID, "continueTouch"); + }; + + this.stopTouch = function (entityID) { + // print('STOP TOUCH' + entityID); + Entities.callEntityMethod(entityID, "stopTouch"); + + }; + + this.computeReleaseVelocity = function (deltaPosition, deltaTime, useMultiplier) { + if (deltaTime > 0.0 && !vec3equal(deltaPosition, ZERO_VEC)) { + var grabbedVelocity = Vec3.multiply(deltaPosition, 1.0 / deltaTime); + // don't update grabbedVelocity if the trigger is off. the smoothing of the trigger + // value would otherwise give the held object time to slow down. + if (this.triggerSqueezed()) { + this.grabbedVelocity = + Vec3.sum(Vec3.multiply(this.grabbedVelocity, (1.0 - NEAR_GRABBING_VELOCITY_SMOOTH_RATIO)), + Vec3.multiply(grabbedVelocity, NEAR_GRABBING_VELOCITY_SMOOTH_RATIO)); + } + + if (useMultiplier) { + this.grabbedVelocity = Vec3.multiply(this.grabbedVelocity, RELEASE_VELOCITY_MULTIPLIER); + } + } + }; + + + this.release = function () { + this.lineOff(); + + if (this.grabbedEntity !== null && this.actionID !== null) { + Entities.deleteAction(this.grabbedEntity, this.actionID); + Entities.callEntityMethod(this.grabbedEntity, "releaseGrab"); + } + + // the action will tend to quickly bring an object's velocity to zero. now that + // the action is gone, set the objects velocity to something the holder might expect. + Entities.editEntity(this.grabbedEntity, { + velocity: this.grabbedVelocity + }); + this.deactivateEntity(this.grabbedEntity); + + this.grabbedVelocity = ZERO_VEC; + this.grabbedEntity = null; + this.actionID = null; + this.state = STATE_SEARCHING; + }; + + + this.cleanup = function () { + this.release(); + }; + + this.activateEntity = function () { + var data = { + activated: true, + avatarId: MyAvatar.sessionUUID + }; + setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data); + }; + + this.deactivateEntity = function () { + var data = { + activated: false, + avatarId: null + }; + setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data); }; - setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data); } -controller.prototype.cleanup = function() { - Entities.deleteEntity(this.pointer); - if (this.grabbedEntity) { - Entities.deleteAction(this.grabbedEntity, this.actionID); - } -} + +var rightController = new controller(RIGHT_HAND, Controller.findAction("RIGHT_HAND_CLICK")); +var leftController = new controller(LEFT_HAND, Controller.findAction("LEFT_HAND_CLICK")); + function update() { rightController.update(); leftController.update(); } + function cleanup() { rightController.cleanup(); leftController.cleanup(); } + Script.scriptEnding.connect(cleanup); -Script.update.connect(update) +Script.update.connect(update); \ No newline at end of file diff --git a/examples/cubePerfTest.js b/examples/cubePerfTest.js index bdf123ae33..f2f4d48b22 100644 --- a/examples/cubePerfTest.js +++ b/examples/cubePerfTest.js @@ -16,7 +16,7 @@ var PARTICLE_MAX_SIZE = 2.50; var LIFETIME = 600; var boxes = []; -var ids = Entities.findEntities({ x: 512, y: 512, z: 512 }, 50); +var ids = Entities.findEntities(MyAvatar.position, 50); for (var i = 0; i < ids.length; i++) { var id = ids[i]; var properties = Entities.getEntityProperties(id); @@ -33,7 +33,7 @@ for (var x = 0; x < SIDE_SIZE; x++) { var gray = Math.random() * 155; var cube = Math.random() > 0.5; var color = { red: 100 + gray, green: 100 + gray, blue: 100 + gray }; - var position = { x: 512 + x * 0.2, y: 512 + y * 0.2, z: 512 + z * 0.2}; + var position = Vec3.sum(MyAvatar.position, { x: x * 0.2, y: y * 0.2, z: z * 0.2}); var radius = Math.random() * 0.1; boxes.push(Entities.addEntity({ type: cube ? "Box" : "Sphere", @@ -52,7 +52,7 @@ for (var x = 0; x < SIDE_SIZE; x++) { function scriptEnding() { for (var i = 0; i < boxes.length; i++) { - //Entities.deleteEntity(boxes[i]); + Entities.deleteEntity(boxes[i]); } } Script.scriptEnding.connect(scriptEnding); diff --git a/examples/edit.js b/examples/edit.js index d778ff324d..0d1164685a 100644 --- a/examples/edit.js +++ b/examples/edit.js @@ -245,6 +245,10 @@ var toolBar = (function () { that.setActive(false); } + that.clearEntityList = function() { + entityListTool.clearEntityList(); + }; + that.setActive = function(active) { if (active != isActive) { if (active && !Entities.canAdjustLocks()) { @@ -510,6 +514,7 @@ var toolBar = (function () { Window.domainChanged.connect(function() { that.setActive(false); + that.clearEntityList(); }); Entities.canAdjustLocksChanged.connect(function(canAdjustLocks) { @@ -1315,7 +1320,7 @@ PropertiesTool = function(opts) { if (data.action == "moveSelectionToGrid") { if (selectionManager.hasSelection()) { selectionManager.saveProperties(); - var dY = grid.getOrigin().y - (selectionManager.worldPosition.y - selectionManager.worldDimensions.y / 2), + var dY = grid.getOrigin().y - (selectionManager.worldPosition.y - selectionManager.worldDimensions.y / 2); var diff = { x: 0, y: dY, z: 0 }; for (var i = 0; i < selectionManager.selections.length; i++) { var properties = selectionManager.savedProperties[selectionManager.selections[i]]; diff --git a/examples/entityScripts/changeColorOnTouch.js b/examples/entityScripts/changeColorOnTouch.js new file mode 100644 index 0000000000..b3082fa9d5 --- /dev/null +++ b/examples/entityScripts/changeColorOnTouch.js @@ -0,0 +1,71 @@ +// +// changeColorOnTouch.js +// examples/entityScripts +// +// Created by Brad Hefta-Gaub on 11/1/14. +// Additions by James B. Pollack @imgntn on 9/23/2015 +// Copyright 2014 High Fidelity, Inc. +// +// ATTENTION: Requires you to run handControllerGrab.js +// This is an example of an entity script which when assigned to a non-model entity like a box or sphere, will +// change the color of the entity when you touch it. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function () { + ChangeColorOnTouch = function () { + this.oldColor = {}; + this.oldColorKnown = false; + }; + + ChangeColorOnTouch.prototype = { + + storeOldColor: function (entityID) { + var oldProperties = Entities.getEntityProperties(entityID); + this.oldColor = oldProperties.color; + this.oldColorKnown = true; + print("storing old color... this.oldColor=" + this.oldColor.red + "," + this.oldColor.green + "," + this.oldColor.blue); + }, + + preload: function (entityID) { + print("preload"); + this.entityID = entityID; + this.storeOldColor(entityID); + }, + + startTouch: function () { + print("startTouch"); + if (!this.oldColorKnown) { + this.storeOldColor(this.entityID); + } + Entities.editEntity(this.entityID, { + color: { + red: 0, + green: 255, + blue: 255 + } + }); + }, + + continueTouch: function () { + //unused here + return; + }, + + stopTouch: function () { + print("stopTouch"); + if (this.oldColorKnown) { + print("leave restoring old color... this.oldColor=" + this.oldColor.red + "," + this.oldColor.green + "," + this.oldColor.blue); + Entities.editEntity(this.entityID, { + color: this.oldColor + }); + } + } + + + }; + + return new ChangeColorOnTouch(); +}) \ No newline at end of file diff --git a/examples/entityScripts/detectGrabExample.js b/examples/entityScripts/detectGrabExample.js index cdc79e119d..3ff5ba8da2 100644 --- a/examples/entityScripts/detectGrabExample.js +++ b/examples/entityScripts/detectGrabExample.js @@ -12,7 +12,6 @@ // (function() { - Script.include("../libraries/utils.js"); var _this; @@ -24,39 +23,29 @@ DetectGrabbed.prototype = { - // update() will be called regulary, because we've hooked the update signal in our preload() function - // we will check out userData for the grabData. In the case of the hydraGrab script, it will tell us - // if we're currently being grabbed and if the person grabbing us is the current interfaces avatar. - // we will watch this for state changes and print out if we're being grabbed or released when it changes. - update: function() { - var GRAB_USER_DATA_KEY = "grabKey"; + setRightHand: function () { + print("I am being held in a right hand... entity:" + this.entityID); + }, + setLeftHand: function () { + print("I am being held in a left hand... entity:" + this.entityID); + }, - // because the update() signal doesn't have a valid this, we need to use our memorized _this to access our entityID - var entityID = _this.entityID; + startDistantGrab: function () { + print("I am being distance held... entity:" + this.entityID); + }, + continueDistantGrab: function () { + print("I continue to be distance held... entity:" + this.entityID); + }, - // we want to assume that if there is no grab data, then we are not being grabbed - var defaultGrabData = { activated: false, avatarId: null }; + startNearGrab: function () { + print("I was just grabbed... entity:" + this.entityID); + }, + continueNearGrab: function () { + print("I am still being grabbed... entity:" + this.entityID); + }, - // this handy function getEntityCustomData() is available in utils.js and it will return just the specific section - // of user data we asked for. If it's not available it returns our default data. - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, defaultGrabData); - - // if the grabData says we're being grabbed, and the owner ID is our session, then we are being grabbed by this interface - if (grabData.activated && grabData.avatarId == MyAvatar.sessionUUID) { - - // remember we're being grabbed so we can detect being released - _this.beingGrabbed = true; - - // print out that we're being grabbed - print("I'm being grabbed..."); - - } else if (_this.beingGrabbed) { - - // if we are not being grabbed, and we previously were, then we were just released, remember that - // and print out a message - _this.beingGrabbed = false; - print("I'm was released..."); - } + releaseGrab: function () { + print("I was released... entity:" + this.entityID); }, // preload() will be called when the entity has become visible (or known) to the interface @@ -65,14 +54,6 @@ // * connecting to the update signal so we can check our grabbed state preload: function(entityID) { this.entityID = entityID; - Script.update.connect(this.update); - }, - - // unload() will be called when our entity is no longer available. It may be because we were deleted, - // or because we've left the domain or quit the application. In all cases we want to unhook our connection - // to the update signal - unload: function(entityID) { - Script.update.disconnect(this.update); }, }; diff --git a/examples/entityScripts/sprayPaintCan.js b/examples/entityScripts/sprayPaintCan.js index 4407140184..aa04e94341 100644 --- a/examples/entityScripts/sprayPaintCan.js +++ b/examples/entityScripts/sprayPaintCan.js @@ -1,7 +1,8 @@ (function() { // Script.include("../libraries/utils.js"); //Need absolute path for now, for testing before PR merge and s3 cloning. Will change post-merge - Script.include("https://hifi-public.s3.amazonaws.com/scripts/libraries/utils.js"); + + Script.include("../libraries/utils.js"); GRAB_FRAME_USER_DATA_KEY = "grabFrame"; this.userData = {}; @@ -56,26 +57,21 @@ timeSinceLastMoved = 0; } - if (self.userData.grabKey && self.userData.grabKey.activated === true) { + //Only activate for the user who grabbed the object + if (self.userData.grabKey && self.userData.grabKey.activated === true && self.userData.grabKey.avatarId == MyAvatar.sessionUUID) { if (self.activated !== true) { //We were just grabbed, so create a particle system self.grab(); - Entities.editEntity(self.paintStream, { - animationSettings: startSetting - }); } //Move emitter to where entity is always when its activated self.sprayStream(); } else if (self.userData.grabKey && self.userData.grabKey.activated === false && self.activated) { - Entities.editEntity(self.paintStream, { - animationSettings: stopSetting - }); - self.activated = false; + self.letGo(); } } this.grab = function() { - self.activated = true; + this.activated = true; var animationSettings = JSON.stringify({ fps: 30, loop: true, @@ -92,9 +88,9 @@ emitVelocity: ZERO_VEC, emitAcceleration: ZERO_VEC, velocitySpread: { - x: .02, - y: .02, - z: 0.02 + x: .1, + y: .1, + z: 0.1 }, emitRate: 100, particleRadius: 0.01, @@ -103,14 +99,14 @@ green: 20, blue: 150 }, - lifetime: 500, //probably wont be holding longer than this straight + lifetime: 50, //probably wont be holding longer than this straight }); - } this.letGo = function() { - self.activated = false; + this.activated = false; Entities.deleteEntity(this.paintStream); + this.paintStream = null; } this.reset = function() { @@ -123,8 +119,7 @@ } this.sprayStream = function() { - var forwardVec = Quat.getFront(self.properties.rotation); - forwardVec = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, 90, 0), forwardVec); + var forwardVec = Quat.getFront(Quat.multiply(self.properties.rotation , Quat.fromPitchYawRollDegrees(0, 90, 0))); forwardVec = Vec3.normalize(forwardVec); var upVec = Quat.getUp(self.properties.rotation); @@ -132,11 +127,10 @@ position = Vec3.sum(position, Vec3.multiply(upVec, TIP_OFFSET_Y)) Entities.editEntity(self.paintStream, { position: position, - emitVelocity: Vec3.multiply(forwardVec, 4) + emitVelocity: Vec3.multiply(5, forwardVec) }); //Now check for an intersection with an entity - //move forward so ray doesnt intersect with gun var origin = Vec3.sum(position, forwardVec); var pickRay = { @@ -216,6 +210,8 @@ this.entityId = entityId; this.properties = Entities.getEntityProperties(self.entityId); this.getUserData(); + + //Only activate for the avatar who is grabbing the can! if (this.userData.grabKey && this.userData.grabKey.activated) { this.activated = true; } @@ -235,7 +231,9 @@ this.unload = function() { Script.update.disconnect(this.update); - Entities.deleteEntity(this.paintStream); + if(this.paintStream) { + Entities.deleteEntity(this.paintStream); + } this.strokes.forEach(function(stroke) { Entities.deleteEntity(stroke); }); @@ -244,6 +242,7 @@ }); + function randFloat(min, max) { return Math.random() * (max - min) + min; } diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js new file mode 100644 index 0000000000..1748198cce --- /dev/null +++ b/examples/example/entities/platform.js @@ -0,0 +1,1221 @@ +// +// platform.js +// +// Created by Seiji Emery on 8/19/15 +// Copyright 2015 High Fidelity, Inc. +// +// Entity stress test / procedural demo. +// Spawns a platform under your avatar made up of randomly sized and colored boxes or spheres. The platform follows your avatar +// around, and comes with a UI to update the platform's properties (radius, entity density, color distribution, etc) in real time. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +// UI and debug console implemented using uiwidgets / 2d overlays +Script.include("../../libraries/uiwidgets.js"); +if (typeof(UI) === 'undefined') { // backup link in case the user downloaded this somewhere + print("Missing library script -- loading from public.highfidelity.io"); + Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js'); + if (typeof(UI) === 'undefined') { + print("Cannot load UIWidgets library -- check your internet connection", COLORS.RED); + throw new Error("Could not load uiwidgets.js"); + } +} + +// Platform script +(function () { +var SCRIPT_NAME = "platform.js"; +var USE_DEBUG_LOG = true; // Turns on the 2dOverlay-based debug log. If false, just redirects to print. +var NUM_DEBUG_LOG_LINES = 10; +var LOG_ENTITY_CREATION_MESSAGES = false; // detailed debugging (init) +var LOG_UPDATE_STATUS_MESSAGES = false; // detailed debugging (startup) + +var MAX_UPDATE_INTERVAL = 0.2; // restrict to 5 updates / sec +var AVATAR_HEIGHT_OFFSET = 1.5; // offset to make the platform spawn under your feet. Might need to be adjusted for unusually proportioned avatars. + +var USE_ENTITY_TIMEOUTS = true; +var ENTITY_TIMEOUT_DURATION = 30.0; // kill entities in 30 secs if they don't get any updates +var ENTITY_REFRESH_INTERVAL = 10.0; // poke the entities every 10s so they don't die until we're done with them + +// Initial state +var NUM_PLATFORM_ENTITIES = 400; +var RADIUS = 5.0; + +// Defines min/max for onscreen platform radius, density, and entity width/height/depth sliders. +// Color limits are hardcoded at [0, 255]. +var PLATFORM_RADIUS_RANGE = [ 1.0, 15.0 ]; +var PLATFORM_DENSITY_RANGE = [ 0.0, 35.0 ]; // do NOT increase this above 40! (~20k limit). Entity count = Math.PI * radius * radius * density. +var PLATFORM_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension limits + +// Utils +(function () { + if (typeof(Math.randRange) === 'undefined') { + Math.randRange = function (min, max) { + return Math.random() * (max - min) + min; + } + } + if (typeof(Math.randInt) === 'undefined') { + Math.randInt = function (n) { + return Math.floor(Math.random() * n) | 0; + } + } + function fromComponents (r, g, b, a) { + this.red = r; + this.green = g; + this.blue = b; + this.alpha = a || 1.0; + } + function fromHex (c) { + this.red = parseInt(c[1] + c[2], 16); + this.green = parseInt(c[3] + c[4], 16); + this.blue = parseInt(c[5] + c[6], 16); + } + var Color = this.Color = function () { + if (arguments.length >= 3) { + fromComponents.apply(this, arguments); + } else if (arguments.length == 1 && arguments[0].length == 7 && arguments[0][0] == '#') { + fromHex.apply(this, arguments); + } else { + throw new Error("Invalid arguments to new Color(): " + JSON.stringify(arguments)); + } + } + Color.prototype.toString = function () { + return "[Color: " + JSON.stringify(this) + "]"; + } +})(); + +// RNG models +(function () { + /// Encapsulates a simple color model that generates colors using a linear, pseudo-random color distribution. + var RandomColorModel = this.RandomColorModel = function () { + this.shadeRange = 0; // = 200; + this.minColor = 55; // = 100; + this.redRange = 255; // = 200; + this.greenRange = 0; // = 10; + this.blueRange = 0; // = 0; + }; + /// Generates 4 numbers in [0, 1] corresponding to each color attribute (uniform shade and additive red, green, blue). + /// This is done in a separate step from actually generating the colors, since it allows us to either A) completely + /// rebuild / re-randomize the color values, or B) reuse the RNG values but with different color parameters, which + /// enables us to do realtime color editing on the same visuals (awesome!). + RandomColorModel.prototype.generateSeed = function () { + return [ Math.random(), Math.random(), Math.random(), Math.random() ]; + }; + /// Takes a random 'seed' (4 floats from this.generateSeed()) and calculates a pseudo-random + /// color by combining that with the color model's current parameters. + RandomColorModel.prototype.getRandom = function (r) { + // logMessage("color seed values " + JSON.stringify(r)); + var shade = Math.min(255, this.minColor + r[0] * this.shadeRange); + + // No clamping on the color components, so they may overflow. + // However, this creates some pretty interesting visuals, so we're not "fixing" this. + var color = { + red: shade + r[1] * this.redRange, + green: shade + r[2] * this.greenRange, + blue: shade + r[3] * this.blueRange + }; + // logMessage("this: " + JSON.stringify(this)); + // logMessage("color: " + JSON.stringify(color), COLORS.RED); + return color; + }; + /// Custom property iterator used to setup UI (sliders, etc) + RandomColorModel.prototype.setupUI = function (callback) { + var _this = this; + [ + ['shadeRange', 'shade range'], + ['minColor', 'shade min'], + ['redRange', 'red (additive)'], + ['greenRange', 'green (additive)'], + ['blueRange', 'blue (additive)'] + ].forEach(function (v) { + // name, value, min, max, onValueChanged + callback(v[1], _this[v[0]], 0, 255, function (value) { _this[v[0]] = value }); + }); + } + + /// Generates pseudo-random dimensions for our cubes / shapes. + var RandomShapeModel = this.RandomShapeModel = function () { + this.widthRange = [ 0.3, 0.7 ]; + this.depthRange = [ 0.5, 0.8 ]; + this.heightRange = [ 0.01, 0.08 ]; + }; + /// Generates 3 seed numbers in [0, 1] + RandomShapeModel.prototype.generateSeed = function () { + return [ Math.random(), Math.random(), Math.random() ]; + } + /// Combines seed values with width/height/depth ranges to produce vec3 dimensions for a cube / sphere. + RandomShapeModel.prototype.getRandom = function (r) { + return { + x: r[0] * (this.widthRange[1] - this.widthRange[0]) + this.widthRange[0], + y: r[1] * (this.heightRange[1] - this.heightRange[0]) + this.heightRange[0], + z: r[2] * (this.depthRange[1] - this.depthRange[0]) + this.depthRange[0] + }; + } + /// Custom property iterator used to setup UI (sliders, etc) + RandomShapeModel.prototype.setupUI = function (callback) { + var _this = this; + var dimensionsMin = PLATFORM_SHAPE_DIMENSIONS_RANGE[0]; + var dimensionsMax = PLATFORM_SHAPE_DIMENSIONS_RANGE[1]; + [ + ['widthRange', 'width'], + ['depthRange', 'depth'], + ['heightRange', 'height'] + ].forEach(function (v) { + // name, value, min, max, onValueChanged + callback(v[1], _this[v[0]], dimensionsMin, dimensionsMax, function (value) { _this[v[0]] = value }); + }); + } + + /// Combines color + shape PRNG models and hides their implementation details. + var RandomAttribModel = this.RandomAttribModel = function () { + this.colorModel = new RandomColorModel(); + this.shapeModel = new RandomShapeModel(); + } + /// Completely re-randomizes obj's `color` and `dimensions` parameters based on the current model params. + RandomAttribModel.prototype.randomizeShapeAndColor = function (obj) { + // logMessage("randomizing " + JSON.stringify(obj)); + obj._colorSeed = this.colorModel.generateSeed(); + obj._shapeSeed = this.shapeModel.generateSeed(); + this.updateShapeAndColor(obj); + // logMessage("color seed: " + JSON.stringify(obj._colorSeed), COLORS.RED); + // logMessage("randomized color: " + JSON.stringify(obj.color), COLORS.RED); + // logMessage("randomized: " + JSON.stringify(obj)); + return obj; + } + /// Updates obj's `color` and `dimensions` params to use the current model params. + /// Reuses hidden seed attribs; _must_ have called randomizeShapeAndColor(obj) at some point before + /// calling this. + RandomAttribModel.prototype.updateShapeAndColor = function (obj) { + try { + // logMessage("update shape and color: " + this.colorModel); + obj.color = this.colorModel.getRandom(obj._colorSeed); + obj.dimensions = this.shapeModel.getRandom(obj._shapeSeed); + } catch (e) { + logMessage("update shape / color failed", COLORS.RED); + logMessage('' + e, COLORS.RED); + logMessage("obj._colorSeed = " + JSON.stringify(obj._colorSeed)); + logMessage("obj._shapeSeed = " + JSON.stringify(obj._shapeSeed)); + // logMessage("obj = " + JSON.stringify(obj)); + throw e; + } + return obj; + } +})(); + +// Status / logging UI (ignore this) +(function () { + var COLORS = this.COLORS = { + 'GREEN': new Color("#2D870C"), + 'RED': new Color("#AF1E07"), + 'LIGHT_GRAY': new Color("#CCCCCC"), + 'DARK_GRAY': new Color("#4E4E4E") + }; + function buildDebugLog () { + var LINE_WIDTH = 400; + var LINE_HEIGHT = 20; + + var lines = []; + var lineIndex = 0; + for (var i = 0; i < NUM_DEBUG_LOG_LINES; ++i) { + lines.push(new UI.Label({ + text: " ", visible: false, + width: LINE_WIDTH, height: LINE_HEIGHT, + })); + } + var title = new UI.Label({ + text: SCRIPT_NAME, visible: true, + width: LINE_WIDTH, height: LINE_HEIGHT, + }); + + var overlay = new UI.Box({ + visible: true, + width: LINE_WIDTH, height: 0, + backgroundColor: COLORS.DARK_GRAY, + backgroundAlpha: 0.3 + }); + overlay.setPosition(280, 10); + relayoutFrom(0); + UI.updateLayout(); + + function relayoutFrom (n) { + var layoutPos = { + x: overlay.position.x, + y: overlay.position.y + }; + + title.setPosition(layoutPos.x, layoutPos.y); + layoutPos.y += LINE_HEIGHT; + + // for (var i = n; i >= 0; --i) { + for (var i = n + 1; i < lines.length; ++i) { + if (lines[i].visible) { + lines[i].setPosition(layoutPos.x, layoutPos.y); + layoutPos.y += LINE_HEIGHT; + } + } + // for (var i = lines.length - 1; i > n; --i) { + for (var i = 0; i <= n; ++i) { + if (lines[i].visible) { + lines[i].setPosition(layoutPos.x, layoutPos.y); + layoutPos.y += LINE_HEIGHT; + } + } + overlay.height = (layoutPos.y - overlay.position.y + 10); + overlay.getOverlay().update({ + height: overlay.height + }); + } + this.logMessage = function (text, color, alpha) { + lines[lineIndex].setVisible(true); + relayoutFrom(lineIndex); + + lines[lineIndex].getOverlay().update({ + text: text, + visible: true, + color: color || COLORS.LIGHT_GRAY, + alpha: alpha !== undefined ? alpha : 1.0, + x: lines[lineIndex].position.x, + y: lines[lineIndex].position.y + }); + lineIndex = (lineIndex + 1) % lines.length; + UI.updateLayout(); + } + } + if (USE_DEBUG_LOG) { + buildDebugLog(); + } else { + this.logMessage = function (msg) { + print(SCRIPT_NAME + ": " + msg); + } + } +})(); + +// Utils (ignore) +(function () { + // Utility function + var withDefaults = this.withDefaults = function (properties, defaults) { + // logMessage("withDefaults: " + JSON.stringify(properties) + JSON.stringify(defaults)); + properties = properties || {}; + if (defaults) { + for (var k in defaults) { + properties[k] = defaults[k]; + } + } + return properties; + } + var withReadonlyProp = this.withReadonlyProp = function (propname, value, obj) { + Object.defineProperty(obj, propname, { + value: value, + writable: false + }); + return obj; + } + + // Math utils + if (typeof(Math.randRange) === 'undefined') { + Math.randRange = function (min, max) { + return Math.random() * (max - min) + min; + } + } + if (typeof(Math.randInt) === 'undefined') { + Math.randInt = function (n) { + return Math.floor(Math.random() * n) | 0; + } + } + + /// Random distrib: Get a random point within a circle on the xz plane with radius r, center p. + this.randomCirclePoint = function (r, pos) { + var a = Math.random(), b = Math.random(); + if (b < a) { + var tmp = b; + b = a; + a = tmp; + } + var point = { + x: pos.x + b * r * Math.cos(2 * Math.PI * a / b), + y: pos.y, + z: pos.z + b * r * Math.sin(2 * Math.PI * a / b) + }; + if (LOG_ENTITY_CREATION_MESSAGES) { + // logMessage("input params: " + JSON.stringify({ radius: r, position: pos }), COLORS.GREEN); + // logMessage("a = " + a + ", b = " + b); + logMessage("generated point: " + JSON.stringify(point), COLORS.RED); + } + return point; + } + + // Entity utils. NOT using overlayManager for... reasons >.> + var makeEntity = this.makeEntity = function (properties) { + if (LOG_ENTITY_CREATION_MESSAGES) { + logMessage("Creating entity: " + JSON.stringify(properties)); + } + var entity = Entities.addEntity(properties); + return withReadonlyProp("type", properties.type, { + update: function (properties) { + Entities.editEntity(entity, properties); + }, + destroy: function () { + Entities.deleteEntity(entity) + }, + getId: function () { + return entity; + } + }); + } + // this.makeLight = function (properties) { + // return makeEntity(withDefaults(properties, { + // type: "Light", + // isSpotlight: false, + // diffuseColor: { red: 255, green: 100, blue: 100 }, + // ambientColor: { red: 200, green: 80, blue: 80 } + // })); + // } + this.makeBox = function (properties) { + // logMessage("Creating box: " + JSON.stringify(properties)); + return makeEntity(withDefaults(properties, { + type: "Box" + })); + } +})(); + +// Platform +(function () { + /// Encapsulates a platform 'piece'. Owns an entity (`box`), and handles destruction and some other state. + var PlatformComponent = this.PlatformComponent = function (properties) { + // logMessage("Platform component initialized with " + Object.keys(properties), COLORS.GREEN); + this.position = properties.position || null; + this.color = properties.color || null; + this.dimensions = properties.dimensions || null; + this.entityType = properties.type || "Box"; + + // logMessage("Spawning with type: '" + this.entityType + "' (properties.type = '" + properties.type + "')", COLORS.GREEN); + + if (properties._colorSeed) + this._colorSeed = properties._colorSeed; + if (properties._shapeSeed) + this._shapeSeed = properties._shapeSeed; + + // logMessage("dimensions: " + JSON.stringify(this.dimensions)); + // logMessage("color: " + JSON.stringify(this.color)); + + this.cachedEntity = null; + this.activeEntity = this.spawnEntity(this.entityType); + }; + PlatformComponent.prototype.spawnEntity = function (type) { + return makeEntity({ + type: type, + position: this.position, + dimensions: this.dimensions, + color: this.color, + lifetime: USE_ENTITY_TIMEOUTS ? ENTITY_TIMEOUT_DURATION : -1.0, + alpha: 0.5 + }); + } + if (USE_ENTITY_TIMEOUTS) { + PlatformComponent.prototype.pokeEntity = function () { + // Kinda inefficient, but there's no way to get around this :/ + var age = Entities.getEntityProperties(this.activeEntity.getId()).age; + this.activeEntity.update({ lifetime: ENTITY_TIMEOUT_DURATION + age }); + } + } else { + PlatformComponent.prototype.pokeEntity = function () {} + } + /// Updates platform to be at position p, and calls .update() with the current + /// position, color, and dimensions parameters. + PlatformComponent.prototype.update = function (position) { + if (position) + this.position = position; + // logMessage("updating with " + JSON.stringify(this)); + this.activeEntity.update(this); + } + function swap (a, b) { + var tmp = a; + a = b; + b = tmp; + } + PlatformComponent.prototype.swapEntityType = function (newType) { + if (this.entityType !== newType) { + this.entityType = newType; + // logMessage("Destroying active entity and rebuilding it (newtype = '" + newType + "')"); + if (this.activeEntity) { + this.activeEntity.destroy(); + } + this.activeEntity = this.spawnEntity(newType); + // if (this.cachedEntity && this.cachedEntity.type == newType) { + // this.cachedEntity.update({ visible: true }); + // this.activeEntity.update({ visible: false }); + // swap(this.cachedEntity, this.activeEntity); + // this.update(this.position); + // } else { + // this.activeEntity.update({ visible: false }); + // this.cachedEntity = this.activeEntity; + // this.activeEntity = spawnEntity(newType); + // } + } + } + /// Swap state with another component + PlatformComponent.prototype.swap = function (other) { + swap(this.position, other.position); + swap(this.dimensions, other.dimensions); + swap(this.color, other.color); + swap(this.entityType, other.entityType); + swap(this.activeEntity, other.activeEntity); + swap(this._colorSeed, other._colorSeed); + swap(this._shapeSeed, other._shapeSeed); + } + PlatformComponent.prototype.destroy = function () { + if (this.activeEntity) { + this.activeEntity.destroy(); + this.activeEntity = null; + } + if (this.cachedEntity) { + this.cachedEntity.destroy(); + this.cachedEntity = null; + } + } + + // util + function inRange (p1, p2, radius) { + return Vec3.distance(p1, p2) < Math.abs(radius); + } + + /// Encapsulates a moving platform that follows the avatar around (mostly). + var DynamicPlatform = this.DynamicPlatform = function (n, position, radius) { + this.position = position; + this.radius = radius; + this.randomizer = new RandomAttribModel(); + this.boxType = "Box"; + this.boxTypes = [ "Box", "Sphere" ]; + + logMessage("Spawning " + n + " entities", COLORS.GREEN); + var boxes = this.boxes = []; + while (n > 0) { + boxes.push(this.spawnEntity()); + --n; + } + this.targetDensity = this.getEntityDensity(); + this.pendingUpdates = {}; + this.updateTimer = 0.0; + + this.platformHeight = position.y; + this.oldPos = { x: position.x, y: position.y, z: position.z }; + this.oldRadius = radius; + + // this.sendPokes(); + } + DynamicPlatform.prototype.toString = function () { + return "[DynamicPlatform (" + this.boxes.length + " entities)]"; + } + DynamicPlatform.prototype.spawnEntity = function () { + // logMessage("Called spawn entity. this.boxType = '" + this.boxType + "'") + var properties = { position: this.randomPoint(), type: this.boxType }; + this.randomizer.randomizeShapeAndColor(properties); + return new PlatformComponent(properties); + } + DynamicPlatform.prototype.updateEntityAttribs = function () { + var _this = this; + this.setPendingUpdate('updateEntityAttribs', function () { + // logMessage("updating model", COLORS.GREEN); + _this.boxes.forEach(function (box) { + this.randomizer.updateShapeAndColor(box); + box.update(); + }, _this); + }); + } + DynamicPlatform.prototype.toggleBoxType = function () { + var _this = this; + this.setPendingUpdate('toggleBoxType', function () { + // Swap / cycle through types: find index of current type and set next type to idx+1 + for (var idx = 0; idx < _this.boxTypes.length; ++idx) { + if (_this.boxTypes[idx] === _this.boxType) { + var nextIndex = (idx + 1) % _this.boxTypes.length; + logMessage("swapping box type from '" + _this.boxType + "' to '" + _this.boxTypes[nextIndex] + "'", COLORS.GREEN); + _this.boxType = _this.boxTypes[nextIndex]; + break; + } + } + _this.boxes.forEach(function (box) { + box.swapEntityType(_this.boxType); + }, _this); + }); + } + DynamicPlatform.prototype.getBoxType = function () { + return this.boxType; + } + + // if (USE_ENTITY_TIMEOUTS) { + // DynamicPlatform.prototype.sendPokes = function () { + // var _this = this; + // function poke () { + // logMessage("Poking entities so they don't die", COLORS.GREEN); + // _this.boxes.forEach(function (box) { + // box.pokeEntity(); + // }, _this); + + + // if (_this.pendingUpdates['keepalive']) { + // logMessage("previous timer: " + _this.pendingUpdates['keepalive'].timer + "; new timer: " + ENTITY_REFRESH_INTERVAL) + // } + // _this.pendingUpdates['keepalive'] = { + // callback: poke, + // timer: ENTITY_REFRESH_INTERVAL, + // skippedUpdates: 0 + // }; + // // _this.setPendingUpdate('keepalive', poke); + // // _this.pendingUpdates['keepalive'].timer = ENTITY_REFRESH_INTERVAL; + // } + // poke(); + // } + // } else { + // DynamicPlatform.prototype.sendPokes = function () {}; + // } + + /// Queue impl that uses the update loop to limit potentially expensive updates to only execute every x seconds (default: 200 ms). + /// This is to prevent UI code from running full entity updates every 10 ms (or whatever). + DynamicPlatform.prototype.setPendingUpdate = function (name, callback) { + if (!this.pendingUpdates[name]) { + // logMessage("Queued update for " + name, COLORS.GREEN); + this.pendingUpdates[name] = { + callback: callback, + timer: 0.0, + skippedUpdates: 0 + } + } else { + // logMessage("Deferred update for " + name, COLORS.GREEN); + this.pendingUpdates[name].callback = callback; + this.pendingUpdates[name].skippedUpdates++; + // logMessage("scheduling update for \"" + name + "\" to run in " + this.pendingUpdates[name].timer + " seconds"); + } + } + /// Runs all queued updates as soon as they can execute (each one has a cooldown timer). + DynamicPlatform.prototype.processPendingUpdates = function (dt) { + for (var k in this.pendingUpdates) { + if (this.pendingUpdates[k].timer >= 0.0) + this.pendingUpdates[k].timer -= dt; + + if (this.pendingUpdates[k].callback && this.pendingUpdates[k].timer < 0.0) { + // logMessage("Dispatching update for " + k); + try { + this.pendingUpdates[k].callback(); + } catch (e) { + logMessage("update for \"" + k + "\" failed: " + e, COLORS.RED); + } + this.pendingUpdates[k].timer = MAX_UPDATE_INTERVAL; + this.pendingUpdates[k].skippedUpdates = 0; + this.pendingUpdates[k].callback = null; + } else { + // logMessage("Deferred update for " + k + " for " + this.pendingUpdates[k].timer + " seconds"); + } + } + } + + /// Updates the platform based on the avatar's current position (spawning / despawning entities as needed), + /// and calls processPendingUpdates() once this is done. + /// Does NOT have any update interval limits (it just updates every time it gets run), but these are not full + /// updates (they're incremental), so the network will not get flooded so long as the avatar is moving at a + /// normal walking / flying speed. + DynamicPlatform.prototype.updatePosition = function (dt, position) { + // logMessage("updating " + this); + position.y = this.platformHeight; + this.position = position; + + var toUpdate = []; + this.boxes.forEach(function (box, i) { + // if (Math.abs(box.position.y - position.y) > HEIGHT_TOLERANCE || !inRange(box, position, radius)) { + if (!inRange(box.position, this.position, this.radius)) { + toUpdate.push(i); + } + }, this); + + var MAX_TRIES = toUpdate.length * 8; + var tries = MAX_TRIES; + var moved = 0; + var recalcs = 0; + toUpdate.forEach(function (index) { + if ((index % 2 == 0) || tries > 0) { + do { + var randomPoint = this.randomPoint(this.position, this.radius); + ++recalcs + } while (--tries > 0 && inRange(randomPoint, this.oldPos, this.oldRadiuss)); + + if (LOG_UPDATE_STATUS_MESSAGES && tries <= 0) { + logMessage("updatePlatform() gave up after " + MAX_TRIES + " iterations (" + moved + " / " + toUpdate.length + " successful updates)", COLORS.RED); + logMessage("old pos: " + JSON.stringify(this.oldPos) + ", old radius: " + this.oldRadius); + logMessage("new pos: " + JSON.stringify(this.position) + ", new radius: " + this.radius); + } + } else { + var randomPoint = this.randomPoint(position, this.radius); + } + + this.randomizer.randomizeShapeAndColor(this.boxes[index]); + this.boxes[index].update(randomPoint); + // this.boxes[index].setValues({ + // position: randomPoint, + // // dimensions: this.randomDimensions(), + // // color: this.randomColor() + // }); + ++moved; + }, this); + recalcs = recalcs - toUpdate.length; + + this.oldPos = position; + this.oldRadius = this.radius; + if (LOG_UPDATE_STATUS_MESSAGES && toUpdate.length > 0) { + logMessage("updated " + toUpdate.length + " entities w/ " + recalcs + " recalcs"); + } + } + + DynamicPlatform.prototype.update = function (dt, position) { + this.updatePosition(dt, position); + this.processPendingUpdates(dt); + this.sendPokes(dt); + } + + if (USE_ENTITY_TIMEOUTS) { + DynamicPlatform.prototype.sendPokes = function (dt) { + logMessage("starting keepalive", COLORS.GREEN); + // logMessage("dt = " + dt, COLORS.RED); + // var original = this.sendPokes; + var pokeTimer = 0.0; + this.sendPokes = function (dt) { + // logMessage("dt = " + dt); + if ((pokeTimer -= dt) < 0.0) { + // logMessage("Poking entities so they don't die", COLORS.GREEN); + this.boxes.forEach(function (box) { + box.pokeEntity(); + }, this); + pokeTimer = ENTITY_REFRESH_INTERVAL; + } else { + // logMessage("Poking entities in " + pokeTimer + " seconds"); + } + } + // logMessage("this.sendPokes === past this.sendPokes? " + (this.sendPokes === original), COLORS.GREEN); + this.sendPokes(dt); + } + } else { + DynamicPlatform.prototype.sendPokes = function () {}; + } + DynamicPlatform.prototype.getEntityCount = function () { + return this.boxes.length; + } + DynamicPlatform.prototype.getEntityCountWithRadius = function (radius) { + var est = Math.floor((radius * radius) / (this.radius * this.radius) * this.getEntityCount()); + var actual = Math.floor(Math.PI * radius * radius * this.getEntityDensity()); + + if (est != actual) { + logMessage("assert failed: getEntityCountWithRadius() -- est " + est + " != actual " + actual); + } + return est; + } + DynamicPlatform.prototype.getEntityCountWithDensity = function (density) { + return Math.floor(Math.PI * this.radius * this.radius * density); + } + + /// Sets the entity count to n. Don't call this directly -- use setRadius / density instead. + DynamicPlatform.prototype.setEntityCount = function (n) { + if (n > this.boxes.length) { + // logMessage("Setting entity count to " + n + " (adding " + (n - this.boxes.length) + " entities)", COLORS.GREEN); + + // Spawn new boxes + n = n - this.boxes.length; + for (; n > 0; --n) { + // var properties = { position: this.randomPoint() }; + // this.randomizer.randomizeShapeAndColor(properties); + // this.boxes.push(new PlatformComponent(properties)); + this.boxes.push(this.spawnEntity()); + } + } else if (n < this.boxes.length) { + // logMessage("Setting entity count to " + n + " (removing " + (this.boxes.length - n) + " entities)", COLORS.GREEN); + + // Destroy random boxes (technically, the most recent ones, but it should be sorta random) + n = this.boxes.length - n; + for (; n > 0; --n) { + this.boxes.pop().destroy(); + } + } + } + /// Calculate the entity density based on radial surface area. + DynamicPlatform.prototype.getEntityDensity = function () { + return (this.boxes.length * 1.0) / (Math.PI * this.radius * this.radius); + } + /// Queues a setDensity update. This is expensive, so we don't call it directly from UI. + DynamicPlatform.prototype.setDensityOnNextUpdate = function (density) { + var _this = this; + this.targetDensity = density; + this.setPendingUpdate('density', function () { + _this.updateEntityDensity(density); + }); + } + DynamicPlatform.prototype.updateEntityDensity = function (density) { + this.setEntityCount(Math.floor(density * Math.PI * this.radius * this.radius)); + } + DynamicPlatform.prototype.getRadius = function () { + return this.radius; + } + /// Queues a setRadius update. This is expensive, so we don't call it directly from UI. + DynamicPlatform.prototype.setRadiusOnNextUpdate = function (radius) { + var _this = this; + this.setPendingUpdate('radius', function () { + _this.setRadius(radius); + }); + } + var DEBUG_RADIUS_RECALC = false; + DynamicPlatform.prototype.setRadius = function (radius) { + if (radius < this.radius) { // Reduce case + // logMessage("Setting radius to " + radius + " (shrink by " + (this.radius - radius) + ")", COLORS.GREEN ); + this.radius = radius; + + // Remove all entities outside of current bounds. Requires swapping, since we want to maintain a contiguous array. + // Algorithm: two pointers at front and back. We traverse fwd and back, swapping elems so that all entities in bounds + // are at the front of the array, and all entities out of bounds are at the back. We then pop + destroy all entities + // at the back to reduce the entity count. + var count = this.boxes.length; + var toDelete = 0; + var swapList = []; + if (DEBUG_RADIUS_RECALC) { + logMessage("starting at i = 0, j = " + (count - 1)); + } + for (var i = 0, j = count - 1; i < j; ) { + // Find first elem outside of bounds that we can move to the back + while (inRange(this.boxes[i].position, this.position, this.radius) && i < j) { + ++i; + } + // Find first elem in bounds that we can move to the front + while (!inRange(this.boxes[j].position, this.position, this.radius) && i < j) { + --j; ++toDelete; + } + if (i < j) { + // swapList.push([i, j]); + if (DEBUG_RADIUS_RECALC) { + logMessage("swapping " + i + ", " + j); + } + this.boxes[i].swap(this.boxes[j]); + ++i, --j; ++toDelete; + } else { + if (DEBUG_RADIUS_RECALC) { + logMessage("terminated at i = " + i + ", j = " + j, COLORS.RED); + } + } + } + if (DEBUG_RADIUS_RECALC) { + logMessage("toDelete = " + toDelete, COLORS.RED); + } + // Sanity check + if (toDelete > this.boxes.length) { + logMessage("Error: toDelete " + toDelete + " > entity count " + this.boxes.length + " (setRadius algorithm)", COLORS.RED); + toDelete = this.boxes.length; + } + if (toDelete > 0) { + // logMessage("Deleting " + toDelete + " entities as part of radius resize", COLORS.GREEN); + } + // Delete cleared boxes + for (; toDelete > 0; --toDelete) { + this.boxes.pop().destroy(); + } + // fix entity density (just in case -- we may have uneven entity distribution) + this.updateEntityDensity(this.targetDensity); + } else if (radius > this.radius) { + // Grow case (much simpler) + // logMessage("Setting radius to " + radius + " (grow by " + (radius - this.radius) + ")", COLORS.GREEN); + + // Add entities based on entity density + // var density = this.getEntityDensity(); + var density = this.targetDensity; + var oldArea = Math.PI * this.radius * this.radius; + var n = Math.floor(density * Math.PI * (radius * radius - this.radius * this.radius)); + + if (n > 0) { + // logMessage("Adding " + n + " entities", COLORS.GREEN); + + // Add entities (we use a slightly different algorithm to place them in the area between two concentric circles. + // This is *slightly* less uniform (the reason we're not using this everywhere is entities would be tightly clustered + // at the platform center and become spread out as the radius increases), but the use-case here is just incremental + // radius resizes and the user's not likely to notice the difference). + for (; n > 0; --n) { + var theta = Math.randRange(0.0, Math.PI * 2.0); + var r = Math.randRange(this.radius, radius); + // logMessage("theta = " + theta + ", r = " + r); + var pos = { + x: Math.cos(theta) * r + this.position.x, + y: this.position.y, + z: Math.sin(theta) * r + this.position.y + }; + + // var properties = { position: pos }; + // this.randomizer.randomizeShapeAndColor(properties); + // this.boxes.push(new PlatformComponent(properties)); + this.boxes.push(this.spawnEntity()); + } + } + this.radius = radius; + } + } + DynamicPlatform.prototype.updateHeight = function (height) { + logMessage("Setting platform height to " + height); + this.platformHeight = height; + + // Invalidate current boxes to trigger a rebuild + this.boxes.forEach(function (box) { + box.position.x += this.oldRadius * 100; + }); + // this.update(dt, position, radius); + } + /// Gets a random point within the platform bounds. + /// Should maybe get moved to the RandomAttribModel (would be much cleaner), but this works for now. + DynamicPlatform.prototype.randomPoint = function (position, radius) { + position = position || this.position; + radius = radius !== undefined ? radius : this.radius; + return randomCirclePoint(radius, position); + } + /// Old. The RandomAttribModel replaces this and enables realtime editing of the *****_RANGE params. + // DynamicPlatform.prototype.randomDimensions = function () { + // return { + // x: Math.randRange(WIDTH_RANGE[0], WIDTH_RANGE[1]), + // y: Math.randRange(HEIGHT_RANGE[0], HEIGHT_RANGE[1]), + // z: Math.randRange(DEPTH_RANGE[0], DEPTH_RANGE[1]) + // }; + // } + // DynamicPlatform.prototype.randomColor = function () { + // var shade = Math.randRange(SHADE_RANGE[0], SHADE_RANGE[1]); + // // var h = HUE_RANGE; + // return { + // red: shade + Math.randRange(RED_RANGE[0], RED_RANGE[1]) | 0, + // green: shade + Math.randRange(GREEN_RANGE[0], GREEN_RANGE[1]) | 0, + // blue: shade + Math.randRange(BLUE_RANGE[0], BLUE_RANGE[1]) | 0 + // } + // // return COLORS[Math.randInt(COLORS.length)] + // } + + /// Cleanup. + DynamicPlatform.prototype.destroy = function () { + this.boxes.forEach(function (box) { + box.destroy(); + }); + this.boxes = []; + } +})(); + +// UI +(function () { + var CATCH_SETUP_ERRORS = true; + + // Util functions for setting up widgets (the widget library is intended to be used like this) + function makePanel (dir, properties) { + return new UI.WidgetStack(withDefaults(properties, { + dir: dir + })); + } + function addSpacing (parent, width, height) { + parent.add(new UI.Box({ + backgroundAlpha: 0.0, + width: width, height: height + })); + } + function addLabel (parent, text) { + return parent.add(new UI.Label({ + text: text, + width: 200, + height: 20 + })); + } + function addSlider (parent, label, min, max, getValue, onValueChanged) { + try { + var layout = parent.add(new UI.WidgetStack({ dir: "+x" })); + var textLabel = layout.add(new UI.Label({ + text: label, + width: 130, + height: 20 + })); + var valueLabel = layout.add(new UI.Label({ + text: "" + (+getValue().toFixed(1)), + width: 60, + height: 20 + })); + var slider = layout.add(new UI.Slider({ + value: getValue(), minValue: min, maxValue: max, + width: 300, height: 20, + slider: { + width: 30, + height: 18 + }, + onValueChanged: function (value) { + valueLabel.setText("" + (+value.toFixed(1))); + onValueChanged(value, slider); + UI.updateLayout(); + } + })); + return slider; + } catch (e) { + logMessage("" + e, COLORS.RED); + logMessage("parent: " + parent, COLORS.RED); + logMessage("label: " + label, COLORS.RED); + logMessage("min: " + min, COLORS.RED); + logMessage("max: " + max, COLORS.RED); + logMessage("getValue: " + getValue, COLORS.RED); + logMessage("onValueChanged: " + onValueChanged, COLORS.RED); + throw e; + } + } + function addButton (parent, label, onClicked) { + var button = parent.add(new UI.Box({ + text: label, + width: 160, + height: 26, + leftMargin: 8, + topMargin: 3 + })); + button.addAction('onClick', onClicked); + return button; + } + function moveToBottomLeftScreenCorner (widget) { + var border = 5; + var pos = { + x: border, + y: Controller.getViewportDimensions().y - widget.getHeight() - border + }; + if (widget.position.x != pos.x || widget.position.y != pos.y) { + widget.setPosition(pos.x, pos.y); + UI.updateLayout(); + } + } + var _export = this; + + /// Setup the UI. Creates a bunch of sliders for setting the platform radius, density, and entity color / shape properties. + /// The entityCount slider is readonly. + function _setupUI (platform) { + var layoutContainer = makePanel("+y", { visible: false }); + // layoutContainer.setPosition(10, 280); + // makeDraggable(layoutContainer); + _export.onScreenResize = function () { + moveToBottomLeftScreenCorner(layoutContainer); + } + var topSection = layoutContainer.add(makePanel("+x")); addSpacing(layoutContainer, 1, 5); + var btmSection = layoutContainer.add(makePanel("+x")); + + var controls = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1); + var buttons = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1); + + var colorControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1); + var shapeControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1); + + // Top controls + addLabel(controls, "Platform (platform.js)"); + controls.radiusSlider = addSlider(controls, "radius", PLATFORM_RADIUS_RANGE[0], PLATFORM_RADIUS_RANGE[1], function () { return platform.getRadius() }, + function (value) { + platform.setRadiusOnNextUpdate(value); + controls.entityCountSlider.setValue(platform.getEntityCountWithRadius(value)); + }); + addSpacing(controls, 1, 2); + controls.densitySlider = addSlider(controls, "entity density", PLATFORM_DENSITY_RANGE[0], PLATFORM_DENSITY_RANGE[1], function () { return platform.getEntityDensity() }, + function (value) { + platform.setDensityOnNextUpdate(value); + controls.entityCountSlider.setValue(platform.getEntityCountWithDensity(value)); + }); + addSpacing(controls, 1, 2); + + var minEntities = Math.PI * PLATFORM_RADIUS_RANGE[0] * PLATFORM_RADIUS_RANGE[0] * PLATFORM_DENSITY_RANGE[0]; + var maxEntities = Math.PI * PLATFORM_RADIUS_RANGE[1] * PLATFORM_RADIUS_RANGE[1] * PLATFORM_DENSITY_RANGE[1]; + controls.entityCountSlider = addSlider(controls, "entity count", minEntities, maxEntities, function () { return platform.getEntityCount() }, + function (value) {}); + controls.entityCountSlider.actions = {}; // hack: make this slider readonly (clears all attached actions) + controls.entityCountSlider.slider.actions = {}; + + // Buttons + addSpacing(buttons, 1, 22); + addButton(buttons, 'rebuild', function () { + platform.updateHeight(MyAvatar.position.y - AVATAR_HEIGHT_OFFSET); + }); + addSpacing(buttons, 1, 2); + addButton(buttons, 'toggle entity type', function () { + platform.toggleBoxType(); + }); + + // Bottom controls + + // Iterate over controls (making sliders) for the RNG shape / dimensions model + platform.randomizer.shapeModel.setupUI(function (name, value, min, max, setValue) { + // logMessage("platform.randomizer.shapeModel." + name + " = " + value); + var internal = { + avg: (value[0] + value[1]) * 0.5, + range: Math.abs(value[0] - value[1]) + }; + // logMessage(JSON.stringify(internal), COLORS.GREEN); + addSlider(shapeControls, name + ' avg', min, max, function () { return internal.avg; }, function (value) { + internal.avg = value; + setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]); + platform.updateEntityAttribs(); + }); + addSpacing(shapeControls, 1, 2); + addSlider(shapeControls, name + ' range', min, max, function () { return internal.range }, function (value) { + internal.range = value; + setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]); + platform.updateEntityAttribs(); + }); + addSpacing(shapeControls, 1, 2); + }); + // Do the same for the color model + platform.randomizer.colorModel.setupUI(function (name, value, min, max, setValue) { + // logMessage("platform.randomizer.colorModel." + name + " = " + value); + addSlider(colorControls, name, min, max, function () { return value; }, function (value) { + setValue(value); + platform.updateEntityAttribs(); + }); + addSpacing(colorControls, 1, 2); + }); + + moveToBottomLeftScreenCorner(layoutContainer); + layoutContainer.setVisible(true); + } + this.setupUI = function (platform) { + if (CATCH_SETUP_ERRORS) { + try { + _setupUI(platform); + } catch (e) { + logMessage("Error setting up ui: " + e, COLORS.RED); + } + } else { + _setupUI(platform); + } + } +})(); + +// Error handling w/ explicit try / catch blocks. Good for catching unexpected errors with the onscreen debugLog +// (if it's enabled); bad for detailed debugging since you lose the file and line num even if the error gets rethrown. + +// Catch errors from init +var CATCH_INIT_ERRORS = true; + +// Catch errors from everything (technically, Script and Controller signals that runs platform / ui code) +var CATCH_ERRORS_FROM_EVENT_UPDATES = false; + +// Setup everything +(function () { + var doLater = null; + if (CATCH_ERRORS_FROM_EVENT_UPDATES) { + // Decorates a function w/ explicit error catching + printing to the debug log. + function catchErrors (fcn) { + return function () { + try { + fcn.apply(this, arguments); + } catch (e) { + logMessage('' + e, COLORS.RED); + logMessage("while calling " + fcn); + logMessage("Called by: " + arguments.callee.caller); + } + } + } + // We need to do this after the functions are registered... + doLater = function () { + // Intercept errors from functions called by Script.update and Script.ScriptEnding. + [ 'teardown', 'startup', 'update', 'initPlatform', 'setupUI' ].forEach(function (fcn) { + this[fcn] = catchErrors(this[fcn]); + }); + }; + // These need to be wrapped first though: + + // Intercept errors from UI functions called by Controller.****Event. + [ 'handleMousePress', 'handleMouseMove', 'handleMouseRelease' ].forEach(function (fcn) { + UI[fcn] = catchErrors(UI[fcn]); + }); + } + + function getTargetPlatformPosition () { + var pos = MyAvatar.position; + pos.y -= AVATAR_HEIGHT_OFFSET; + return pos; + } + + // Program state + var platform = this.platform = null; + var lastHeight = null; + + // Init + this.initPlatform = function () { + platform = new DynamicPlatform(NUM_PLATFORM_ENTITIES, getTargetPlatformPosition(), RADIUS); + lastHeight = getTargetPlatformPosition().y; + } + + // Handle relative screen positioning (UI) + var lastDimensions = Controller.getViewportDimensions(); + function checkScreenDimensions () { + var dimensions = Controller.getViewportDimensions(); + if (dimensions.x != lastDimensions.x || dimensions.y != lastDimensions.y) { + onScreenResize(dimensions.x, dimensions.y); + } + lastDimensions = dimensions; + } + + // Update + this.update = function (dt) { + checkScreenDimensions(); + var pos = getTargetPlatformPosition(); + platform.update(dt, getTargetPlatformPosition(), platform.getRadius()); + } + + // Teardown + this.teardown = function () { + try { + platform.destroy(); + UI.teardown(); + + Controller.mousePressEvent.disconnect(UI.handleMousePress); + Controller.mouseMoveEvent.disconnect(UI.handleMouseMove); + Controller.mouseReleaseEvent.disconnect(UI.handleMouseRelease); + } catch (e) { + logMessage("" + e, COLORS.RED); + } + } + + if (doLater) { + doLater(); + } + + // Delays startup until / if entities can be spawned. + this.startup = function (dt) { + if (Entities.canAdjustLocks() && Entities.canRez()) { + Script.update.disconnect(this.startup); + + function init () { + logMessage("initializing..."); + + this.initPlatform(); + + Script.update.connect(this.update); + Script.scriptEnding.connect(this.teardown); + + this.setupUI(platform); + + logMessage("finished initializing.", COLORS.GREEN); + } + if (CATCH_INIT_ERRORS) { + try { + init(); + } catch (error) { + logMessage("" + error, COLORS.RED); + } + } else { + init(); + } + + Controller.mousePressEvent.connect(UI.handleMousePress); + Controller.mouseMoveEvent.connect(UI.handleMouseMove); + Controller.mouseReleaseEvent.connect(UI.handleMouseRelease); + } else { + if (!startup.printedWarnMsg) { + startup.timer = startup.timer || startup.ENTITY_SERVER_WAIT_TIME; + if ((startup.timer -= dt) < 0.0) { + logMessage("Waiting for entity server"); + startup.printedWarnMsg = true; + } + + } + } + } + startup.ENTITY_SERVER_WAIT_TIME = 0.2; // print "waiting for entity server" if more than this time has elapsed in startup() + + Script.update.connect(this.startup); +})(); + +})(); diff --git a/examples/faceBlendCoefficients.js b/examples/faceBlendCoefficients.js new file mode 100644 index 0000000000..6756be548f --- /dev/null +++ b/examples/faceBlendCoefficients.js @@ -0,0 +1,98 @@ +// +// faceBlendCoefficients.js +// +// version 2.0 +// +// Created by Bob Long, 9/14/2015 +// A simple panel that can select and display the blending coefficient of the Avatar's face model. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include('utilities/tools/cookies.js') + +var panel; +var coeff; +var interval; +var item = 0; +var DEVELOPER_MENU = "Developer"; +var AVATAR_MENU = DEVELOPER_MENU + " > Avatar"; +var SHOW_FACE_BLEND_COEFFICIENTS = "Show face blend coefficients" + +function MenuConnect(menuItem) { + if (menuItem == SHOW_FACE_BLEND_COEFFICIENTS) { + if(Menu.isOptionChecked(SHOW_FACE_BLEND_COEFFICIENTS)) { + panel.show(); + Overlays.editOverlay(coeff, { visible : true }); + } else { + panel.hide(); + Overlays.editOverlay(coeff, { visible : false }); + } + } +} + +// Add a menu item to show/hide the coefficients +function setupMenu() { + if (!Menu.menuExists(DEVELOPER_MENU)) { + Menu.addMenu(DEVELOPER_MENU); + } + + if (!Menu.menuExists(AVATAR_MENU)) { + Menu.addMenu(AVATAR_MENU); + } + + Menu.addMenuItem({ menuName: AVATAR_MENU, menuItemName: SHOW_FACE_BLEND_COEFFICIENTS, isCheckable: true, isChecked: true }); + Menu.menuItemEvent.connect(MenuConnect); +} + +function setupPanel() { + panel = new Panel(10, 400); + + // Slider to select which coefficient to display + panel.newSlider("Select Coefficient Index", + 0, + 100, + function(value) { item = value.toFixed(0); }, + function() { return item; }, + function(value) { return "index = " + item; } + ); + + // The raw overlay used to show the actual coefficient value + coeff = Overlays.addOverlay("text", { + x: 10, + y: 420, + width: 300, + height: 50, + color: { red: 255, green: 255, blue: 255 }, + alpha: 1.0, + backgroundColor: { red: 127, green: 127, blue: 127 }, + backgroundAlpha: 0.5, + topMargin: 15, + leftMargin: 20, + text: "Coefficient: 0.0" + }); + + // Set up the interval (0.5 sec) to update the coefficient. + interval = Script.setInterval(function() { + Overlays.editOverlay(coeff, { text: "Coefficient: " + MyAvatar.getFaceBlendCoef(item).toFixed(4) }); + }, 500); + + // Mouse event setup + Controller.mouseMoveEvent.connect(function panelMouseMoveEvent(event) { return panel.mouseMoveEvent(event); }); + Controller.mousePressEvent.connect( function panelMousePressEvent(event) { return panel.mousePressEvent(event); }); + Controller.mouseReleaseEvent.connect(function(event) { return panel.mouseReleaseEvent(event); }); +} + +// Clean up +function scriptEnding() { + panel.destroy(); + Overlays.deleteOverlay(coeff); + Script.clearInterval(interval); + + Menu.removeMenuItem(AVATAR_MENU, SHOW_FACE_BLEND_COEFFICIENTS); +} + +setupMenu(); +setupPanel(); +Script.scriptEnding.connect(scriptEnding); \ No newline at end of file diff --git a/examples/html/entityList.html b/examples/html/entityList.html index a1ba167652..3a1eeedf95 100644 --- a/examples/html/entityList.html +++ b/examples/html/entityList.html @@ -201,7 +201,9 @@ EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); - if (data.type == "selectionUpdate") { + if (data.type === "clearEntityList") { + clearEntities(); + } else if (data.type == "selectionUpdate") { var notFound = updateSelectedEntities(data.selectedIDs); if (notFound) { refreshEntities(); diff --git a/examples/html/entityProperties.html b/examples/html/entityProperties.html index ad489afddf..268a2fb7f2 100644 --- a/examples/html/entityProperties.html +++ b/examples/html/entityProperties.html @@ -1,5 +1,6 @@ + Properties @@ -961,7 +962,7 @@
-
@@ -970,7 +971,7 @@
Name
- +
@@ -1003,13 +1004,13 @@
Href
- +
Description
- +
@@ -1021,9 +1022,9 @@
Position
-
X
-
Y
-
Z
+
X
+
Y
+
Z
@@ -1034,26 +1035,26 @@
Registration
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Dimensions
-
X
-
Y
-
Z
+
X
+
Y
+
Z
- % + %
- +
@@ -1061,9 +1062,9 @@
Voxel Volume Size
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Surface Extractor
@@ -1078,26 +1079,26 @@
X-axis Texture URL
- +
Y-axis Texture URL
- +
Z-axis Texture URL
- +
Rotation
-
Pitch
-
Yaw
-
Roll
+
Pitch
+
Yaw
+
Roll
@@ -1109,66 +1110,66 @@
Linear Velocity
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Linear Damping
- +
Angular Velocity
-
Pitch
-
Yaw
-
Roll
+
Pitch
+
Yaw
+
Roll
Angular Damping
- +
Restitution
- +
Friction
- +
Gravity
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Acceleration
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Density
- +
@@ -1176,9 +1177,9 @@
Color
-
R
-
G
-
B
+
R
+
G
+
B
@@ -1190,38 +1191,38 @@
Ignore For Collisions - +
Collisions Will Move - +
Collision Sound URL
- +
Lifetime
- +
Script URL - - + +
- +
@@ -1233,14 +1234,14 @@
Model URL
- +
Shape Type
- @@ -1251,13 +1252,13 @@
Compound Shape URL
- +
Animation URL
- +
@@ -1269,13 +1270,13 @@
Animation FPS
- +
Animation Frame
- +
@@ -1305,7 +1306,7 @@
Source URL
- +
@@ -1317,45 +1318,45 @@
Max Particles
- +
Particle Life Span
- +
Particle Emission Rate
- +
Particle Emission Direction
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Particle Emission Strength
- +
Particle Local Gravity
- +
Particle Radius
- +
@@ -1367,31 +1368,31 @@
Text Content
- +
Line Height
- +
Text Color
-
R
-
G
-
B
+
R
+
G
+
B
Background Color
-
R
-
G
-
B
+
R
+
G
+
B
@@ -1410,27 +1411,27 @@
Color
-
R
-
G
-
B
+
R
+
G
+
B
Intensity
- +
Spot Light Exponent
- +
Spot Light Cutoff (degrees)
- +
@@ -1450,48 +1451,48 @@
Key Light Color
-
R
-
G
-
B
+
R
+
G
+
B
Key Light Intensity
- +
Key Light Ambient Intensity
- +
Key Light Direction
-
Pitch
-
Yaw
-
Roll
+
Pitch
+
Yaw
+
Roll
Stage Latitude
- +
Stage Longitude
- +
Stage Altitude
- +
@@ -1505,20 +1506,20 @@
Stage Day
- +
Stage Hour
- +
Background Mode
- @@ -1535,15 +1536,15 @@
Skybox Color
-
R
-
G
-
B
+
R
+
G
+
B
Skybox URL
- +
@@ -1555,9 +1556,9 @@
Atmosphere Center
-
X
-
Y
-
Z
+
X
+
Y
+
Z
@@ -1566,33 +1567,33 @@
Atmosphere Inner Radius
- +
Atmosphere Outer Radius
- +
Atmosphere Mie Scattering
- +
Atmosphere Rayleigh Scattering
- +
Atmosphere Scattering Wavelenghts
-
X
-
Y
-
Z
+
X
+
Y
+
Z