diff --git a/examples/entityScripts/magBallEdge.js b/examples/entityScripts/magBallEdge.js deleted file mode 100644 index 5411a82088..0000000000 --- a/examples/entityScripts/magBallEdge.js +++ /dev/null @@ -1,124 +0,0 @@ -// -// Created by Bradley Austin Davis on 2015/08/29 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -(function(){ - this.preload = function(entityId) { - this.MIN_CHECK_INTERVAL = 0.05; - this.MAX_VARIANCE = 0.005; - this.ZERO_VECTOR = { x: 0, y: 0, z: 0 }; - - this.entityId = entityId; - var properties = Entities.getEntityProperties(this.entityId); - var userData = JSON.parse(properties.userData); - this.start = userData.magBalls.start; - this.end = userData.magBalls.end; - this.desiredLength = userData.magBalls.length; - this.timeSinceLastUpdate = 0; - this.nextCheckInterval = this.MIN_CHECK_INTERVAL; - - // FIXME do I really need to do this nonsense? - var _this = this; - this.updateWrapper = function(deltaTime) { - _this.onUpdate(deltaTime); - }; - Script.update.connect(this.updateWrapper); - - Script.scriptEnding.connect(function() { - _this.onCleanup(); - }); - Entities.deletingEntity.connect(function(entityId) { - _this.onCleanup(); - }); - }; - - this.onUpdate = function(deltaTime) { - this.timeSinceLastUpdate += deltaTime; - if (this.timeSinceLastUpdate > this.nextCheckInterval) { - this.updateProperties(); - this.timeSinceLastUpdate = 0; - var length = this.getLength(); - if (length == 0) { - this.onCleanup(); - return; - } - var variance = this.getVariance(length); - if (Math.abs(variance) <= this.MAX_VARIANCE) { - this.incrementCheckInterval(); - return; - } - this.decrementCheckInterval(); - var adjustmentVector = Vec3.multiply(variance / 4, this.vector); - var newPosition = Vec3.sum(Vec3.multiply(-1, adjustmentVector), this.position); - var newVector = Vec3.sum(Vec3.multiply(2, adjustmentVector), this.vector); - var newLength = Vec3.length(newVector); - var newVariance = this.getVariance(newLength); - Entities.editEntity(this.entityId, { - position: newPosition, - linePoints: [ this.ZERO_VECTOR, newVector ] - }); - Entities.editEntity(this.start, { - position: newPosition - }); - Entities.editEntity(this.end, { - position: Vec3.sum(newPosition, newVector) - }); - } - } - - this.incrementCheckInterval = function() { - this.nextCheckInterval = Math.min(this.nextCheckInterval * 2.0, 1.0); - } - - this.decrementCheckInterval = function() { - this.nextCheckInterval = 0.05; - } - - this.onCleanup = function() { - if (this.updateWrapper) { - Script.update.disconnect(this.updateWrapper); - delete this.updateWrapper; - } - } - - this.getVariance = function(length) { - if (!length) { - length = this.getLength(); - } - var difference = this.desiredLength - length; - return difference / this.desiredLength; - } - - this.getLength = function() { - return Vec3.length(this.vector); - } - - this.getPosition = function(entityId) { - var properties = Entities.getEntityProperties(entityId); - return properties.position; - } - - this.updateProperties = function() { - var properties = Entities.getEntityProperties(this.entityId); - var curStart = properties.position; - var curVector = properties.linePoints[1] - var curEnd = Vec3.sum(curVector, curStart); - var startPos = this.getPosition(this.start); - var endPos = this.getPosition(this.end); - var startError = Vec3.distance(curStart, startPos); - var endError = Vec3.distance(curEnd, endPos); - this.vector = Vec3.subtract(endPos, startPos); - if (startError > 0.005 || endError > 0.005) { - print("Fixing up edge"); - Entities.editEntity(this.entityId, { - position: startPos, - linePoints: [ this.ZERO_VECTOR, this.vector ] - }); - } - this.position = startPos; - } -}); diff --git a/examples/toys/magBalls.js b/examples/toys/magBalls.js deleted file mode 100644 index 46d4905d0e..0000000000 --- a/examples/toys/magBalls.js +++ /dev/null @@ -1,306 +0,0 @@ -// -// Created by Bradley Austin Davis on 2015/08/25 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -Script.include("magSticks/constants.js"); -Script.include("magSticks/utils.js"); -Script.include("magSticks/graph.js"); -Script.include("magSticks/highlighter.js"); -Script.include("magSticks/handController.js"); - -var BALL_NAME = "MagBall" -var EDGE_NAME = "MagStick" -var BALL_SELECTION_RADIUS = BALL_SIZE / 2.0 * 1.5; - -var BALL_DIMENSIONS = { - x: BALL_SIZE, - y: BALL_SIZE, - z: BALL_SIZE -}; - -var BALL_COLOR = { - red: 128, - green: 128, - blue: 128 -}; - -var STICK_DIMENSIONS = { - x: STICK_LENGTH / 6, - y: STICK_LENGTH / 6, - z: STICK_LENGTH -}; - -var BALL_DISTANCE = STICK_LENGTH + BALL_SIZE; - -var BALL_PROTOTYPE = { - type: "Sphere", - name: BALL_NAME, - dimensions: BALL_DIMENSIONS, - color: BALL_COLOR, - ignoreCollisions: true, - collisionsWillMove: false -}; - -// 2 millimeters -var EPSILON = (.002) / BALL_DISTANCE; - -var LINE_DIMENSIONS = { - x: 5, - y: 5, - z: 5 -} - -//var EDGE_ENTITY_SCRIPT_BASE = "file:/Users/bdavis/Git/hifi/examples/entityScripts"; -var EDGE_ENTITY_SCRIPT_BASE = "https://s3.amazonaws.com/hifi-public/scripts/entityScripts"; -var EDGE_ENTITY_SCRIPT = EDGE_ENTITY_SCRIPT_BASE + "/magBallEdge.js", - -var LINE_PROTOTYPE = { - type: "Line", - name: EDGE_NAME, - color: COLORS.CYAN, - dimensions: LINE_DIMENSIONS, - lineWidth: 5, - visible: true, - ignoreCollisions: true, - collisionsWillMove: false, - script: EDGE_ENTITY_SCRIPT -} - -var EDGE_PROTOTYPE = LINE_PROTOTYPE; - -// var EDGE_PROTOTYPE = { -// type: "Sphere", -// name: EDGE_NAME, -// color: { red: 0, green: 255, blue: 255 }, -// //dimensions: STICK_DIMENSIONS, -// dimensions: { x: 0.02, y: 0.02, z: 0.02 }, -// rotation: rotation, -// visible: true, -// ignoreCollisions: true, -// collisionsWillMove: false -// } - - -// A collection of balls and edges connecting them. -MagBalls = function() { - this.selectedNodes = {}; - Graph.call(this); - Script.scriptEnding.connect(function() { - _this.onCleanup(); - }); -} - -MagBalls.prototype = Object.create( Graph.prototype ); - -MagBalls.prototype.onUpdate = function(deltaTime) { - // FIXME move to a physics based implementation as soon as bullet - // is exposed to entities -} - -MagBalls.prototype.createNodeEntity = function(customProperies) { - return Entities.addEntity(mergeObjects(BALL_PROTOTYPE, customProperies)); -} - -MagBalls.prototype.createEdgeEntity = function(nodeIdA, nodeIdB) { - var apos = this.getNodePosition(nodeIdA); - var bpos = this.getNodePosition(nodeIdB); - return Entities.addEntity(mergeObjects(EDGE_PROTOTYPE, { - position: apos, - linePoints: [ ZERO_VECTOR, Vec3.subtract(bpos, apos) ], - userData: JSON.stringify({ - magBalls: { - start: nodeIdA, - end: nodeIdB, - length: BALL_DISTANCE - } - }) - })); -} - -MagBalls.prototype.findPotentialEdges = function(nodeId) { - var variances = {}; - for (var otherNodeId in this.nodes) { - // can't self connect - if (otherNodeId == nodeId) { - continue; - } - - // can't doubly connect - if (this.areConnected(otherNodeId, nodeId)) { - continue; - } - - // Too far to attempt - var distance = this.getNodeDistance(nodeId, otherNodeId); - var variance = this.getVariance(distance); - if (variance > 0.25) { - continue; - } - - variances[otherNodeId] = variance; - } - return variances; -} - -MagBalls.prototype.grabBall = function(position, maxDist) { - var selected = this.findNearestNode(position, maxDist); - if (!selected) { - selected = this.createNode({ position: position }); - } - if (selected) { - this.breakEdges(selected); - this.selectedNodes[selected] = true; - } - return selected; -} - -MagBalls.prototype.releaseBall = function(releasedBall) { - delete this.selectedNodes[releasedBall]; - logDebug("Released ball: " + releasedBall); - - // FIXME iterate through the other balls and ensure we don't intersect with - // any of them. If we do, just delete this ball and return. (play a pop - // sound) - - var releasePosition = this.getNodePosition(releasedBall); - - // Don't overlap other balls - for (var nodeId in this.nodes) { - if (nodeId == releasedBall) { - continue; - } - var distance = this.getNodeDistance(releasedBall, nodeId); - if (distance < BALL_SIZE / 2.0) { - this.destroyNode(nodeId); - return; - } - } - - var targets = this.findPotentialEdges(releasedBall); - for (var otherBallId in targets) { - this.createEdge(otherBallId, releasedBall); - } - this.clean(); - this.validate(); -} - - -MagBalls.prototype.getVariance = function(distance) { - // Given two points, how big is the difference between their distance - // and the desired length length - return (Math.abs(distance - BALL_DISTANCE)) / BALL_DISTANCE; -} - -// remove unconnected balls -MagBalls.prototype.clean = function() { - // do nothing unless there are at least 2 balls and one edge - if (Object.keys(this.nodes).length < 2 || !Object.keys(this.edges).length) { - return; - } - var disconnectedNodes = {}; - for (var nodeId in this.nodes) { - if (!Object.keys(this.nodes[nodeId]).length) { - disconnectedNodes[nodeId] = true; - } - } - for (var nodeId in disconnectedNodes) { - this.destroyNode(nodeId); - } -} - -// remove all balls -MagBalls.prototype.clear = function() { - if (DEBUG_MAGSTICKS) { - this.deleteAll(); - var ids = Entities.findEntities(MyAvatar.position, 50); - var result = []; - ids.forEach(function(id) { - var properties = Entities.getEntityProperties(id); - if (properties.name == BALL_NAME || properties.name == EDGE_NAME) { - Entities.deleteEntity(id); - } - }, this); - } -} - -var magBalls = new MagBalls(); - -// Clear any previous balls -// magBalls.clear(); - -// How close do we need to be to a ball to select it.... radius + 10% - -BallController = function(side) { - HandController.call(this, side); - this.highlighter = new Highlighter(); - this.highlighter.setSize(BALL_SIZE); - this.ghostEdges = {}; -} - -BallController.prototype = Object.create( HandController.prototype ); - -BallController.prototype.onUpdate = function(deltaTime) { - HandController.prototype.onUpdate.call(this, deltaTime); - if (!this.selected) { - // Find the highlight target and set it. - var target = magBalls.findNearestNode(this.tipPosition, BALL_SELECTION_RADIUS); - this.highlighter.highlight(target); - return; - } - this.highlighter.highlight(null); - Entities.editEntity(this.selected, { position: this.tipPosition }); - var targetBalls = magBalls.findPotentialEdges(this.selected); - for (var ballId in targetBalls) { - if (!this.ghostEdges[ballId]) { - // create the ovleray - this.ghostEdges[ballId] = Overlays.addOverlay("line3d", { - start: magBalls.getNodePosition(ballId), - end: this.tipPosition, - color: { red: 255, green: 0, blue: 0}, - alpha: 1, - lineWidth: 5, - visible: true, - }); - } else { - Overlays.editOverlay(this.ghostEdges[ballId], { - end: this.tipPosition, - }); - } - } - for (var ballId in this.ghostEdges) { - if (!targetBalls[ballId]) { - Overlays.deleteOverlay(this.ghostEdges[ballId]); - delete this.ghostEdges[ballId]; - } - } -} - -BallController.prototype.onClick = function() { - this.selected = magBalls.grabBall(this.tipPosition, BALL_SELECTION_RADIUS); - this.highlighter.highlight(null); -} - -BallController.prototype.onRelease = function() { - this.clearGhostEdges(); - magBalls.releaseBall(this.selected); - this.selected = null; -} - -BallController.prototype.clearGhostEdges = function() { - for(var ballId in this.ghostEdges) { - Overlays.deleteOverlay(this.ghostEdges[ballId]); - delete this.ghostEdges[ballId]; - } -} - -BallController.prototype.onCleanup = function() { - HandController.prototype.onCleanup.call(this); - this.clearGhostEdges(); -} - -// FIXME resolve some of the issues with dual controllers before allowing both controllers active -var handControllers = [new BallController(LEFT_CONTROLLER)]; //, new HandController(RIGHT) ]; diff --git a/examples/toys/magBalls/ballController.js b/examples/toys/magBalls/ballController.js new file mode 100644 index 0000000000..62e2e0a4d0 --- /dev/null +++ b/examples/toys/magBalls/ballController.js @@ -0,0 +1,74 @@ +Script.include("handController.js"); +Script.include("highlighter.js"); + +BallController = function(side, magBalls) { + HandController.call(this, side); + this.magBalls = magBalls; + this.highlighter = new Highlighter(); + this.highlighter.setSize(BALL_SIZE); + this.ghostEdges = {}; + this.lastUpdate = 0; + this.updateInterval = 0.05; +} + +BallController.prototype = Object.create( HandController.prototype ); + +BallController.prototype.onUpdate = function(deltaTime) { + HandController.prototype.onUpdate.call(this, deltaTime); + + if (!this.selected) { + // Find the highlight target and set it. + var target = this.magBalls.findNearestNode(this.tipPosition, BALL_SELECTION_RADIUS); + this.highlighter.highlight(target); + return; + } + this.highlighter.highlight(null); + Entities.editEntity(this.selected, { position: this.tipPosition }); + var targetBalls = this.magBalls.findPotentialEdges(this.selected); + for (var ballId in targetBalls) { + if (!this.ghostEdges[ballId]) { + // create the ovleray + this.ghostEdges[ballId] = Overlays.addOverlay("line3d", { + start: this.magBalls.getNodePosition(ballId), + end: this.tipPosition, + color: COLORS.RED, + alpha: 1, + lineWidth: 5, + visible: true, + }); + } else { + Overlays.editOverlay(this.ghostEdges[ballId], { + end: this.tipPosition, + }); + } + } + for (var ballId in this.ghostEdges) { + if (!targetBalls[ballId]) { + Overlays.deleteOverlay(this.ghostEdges[ballId]); + delete this.ghostEdges[ballId]; + } + } +} + +BallController.prototype.onClick = function() { + this.selected = this.magBalls.grabBall(this.tipPosition, BALL_SELECTION_RADIUS); + this.highlighter.highlight(null); +} + +BallController.prototype.onRelease = function() { + this.clearGhostEdges(); + this.magBalls.releaseBall(this.selected); + this.selected = null; +} + +BallController.prototype.clearGhostEdges = function() { + for(var ballId in this.ghostEdges) { + Overlays.deleteOverlay(this.ghostEdges[ballId]); + delete this.ghostEdges[ballId]; + } +} + +BallController.prototype.onCleanup = function() { + HandController.prototype.onCleanup.call(this); + this.clearGhostEdges(); +} diff --git a/examples/toys/magSticks/constants.js b/examples/toys/magBalls/constants.js similarity index 50% rename from examples/toys/magSticks/constants.js rename to examples/toys/magBalls/constants.js index 297fa51c6e..d154910f91 100644 --- a/examples/toys/magSticks/constants.js +++ b/examples/toys/magBalls/constants.js @@ -16,6 +16,10 @@ STICK_LENGTH = 0.24 * SCALE; DEBUG_MAGSTICKS = true; +CUSTOM_DATA_NAME = "magBalls"; +BALL_NAME = "MagBall"; +EDGE_NAME = "MagStick"; + ZERO_VECTOR = { x: 0, y: 0, z: 0 }; COLORS = { @@ -66,3 +70,71 @@ COLORS = { } } +BALL_RADIUS = BALL_SIZE / 2.0; + +BALL_SELECTION_RADIUS = BALL_RADIUS * 1.5; + +BALL_DIMENSIONS = { + x: BALL_SIZE, + y: BALL_SIZE, + z: BALL_SIZE +}; + +BALL_COLOR = { + red: 128, + green: 128, + blue: 128 +}; + +STICK_DIMENSIONS = { + x: STICK_LENGTH / 6, + y: STICK_LENGTH / 6, + z: STICK_LENGTH +}; + +BALL_DISTANCE = STICK_LENGTH + BALL_SIZE; + +BALL_PROTOTYPE = { + type: "Sphere", + name: BALL_NAME, + dimensions: BALL_DIMENSIONS, + color: BALL_COLOR, + ignoreCollisions: true, + collisionsWillMove: false +}; + +// 2 millimeters +BALL_EPSILON = (.002) / BALL_DISTANCE; + +LINE_DIMENSIONS = { + x: 5, + y: 5, + z: 5 +} + +LINE_PROTOTYPE = { + type: "Line", + name: EDGE_NAME, + color: COLORS.CYAN, + dimensions: LINE_DIMENSIONS, + lineWidth: 5, + visible: true, + ignoreCollisions: true, + collisionsWillMove: false, +} + +EDGE_PROTOTYPE = LINE_PROTOTYPE; + +// var EDGE_PROTOTYPE = { +// type: "Sphere", +// name: EDGE_NAME, +// color: { red: 0, green: 255, blue: 255 }, +// //dimensions: STICK_DIMENSIONS, +// dimensions: { x: 0.02, y: 0.02, z: 0.02 }, +// rotation: rotation, +// visible: true, +// ignoreCollisions: true, +// collisionsWillMove: false +// } + + diff --git a/examples/toys/magBalls/debugUtils.js b/examples/toys/magBalls/debugUtils.js new file mode 100644 index 0000000000..8dadd34679 --- /dev/null +++ b/examples/toys/magBalls/debugUtils.js @@ -0,0 +1,95 @@ +findMatchingNode = function(position, nodePositions) { + for (var nodeId in nodePositions) { + var nodePos = nodePositions[nodeId]; + var distance = Vec3.distance(position, nodePos); + if (distance < 0.03) { + return nodeId; + } + } +} + +repairConnections = function() { + var ids = Entities.findEntities(MyAvatar.position, 50); + + // Find all the balls and record their positions + var nodePositions = {}; + for (var i in ids) { + var id = ids[i]; + var properties = Entities.getEntityProperties(id); + if (properties.name == BALL_NAME) { + nodePositions[id] = properties.position; + } + } + + // Now check all the edges to see if they're valid (point to balls) + // and ensure that the balls point back to them + var ballsToEdges = {}; + for (var i in ids) { + var id = ids[i]; + var properties = Entities.getEntityProperties(id); + if (properties.name == EDGE_NAME) { + var startPos = properties.position; + var endPos = Vec3.sum(startPos, properties.linePoints[1]); + var magBallData = getMagBallsData(id); + var update = false; + if (!magBallData.start) { + var startNode = findMatchingNode(startPos, nodePositions); + if (startNode) { + logDebug("Found start node " + startNode) + magBallData.start = startNode; + update = true; + } + } + if (!magBallData.end) { + var endNode = findMatchingNode(endPos, nodePositions); + if (endNode) { + logDebug("Found end node " + endNode) + magBallData.end = endNode; + update = true; + } + } + if (!magBallData.start || !magBallData.end) { + logDebug("Didn't find both ends"); + Entities.deleteEntity(id); + continue; + } + if (!ballsToEdges[magBallData.start]) { + ballsToEdges[magBallData.start] = [ id ]; + } else { + ballsToEdges[magBallData.start].push(id); + } + if (!ballsToEdges[magBallData.end]) { + ballsToEdges[magBallData.end] = [ id ]; + } else { + ballsToEdges[magBallData.end].push(id); + } + if (update) { + logDebug("Updating incomplete edge " + id); + magBallData.length = BALL_DISTANCE; + setMagBallsData(id, magBallData); + } + } + } + for (var nodeId in ballsToEdges) { + var magBallData = getMagBallsData(nodeId); + var edges = magBallData.edges || []; + var edgeHash = {}; + for (var i in edges) { + edgeHash[edges[i]] = true; + } + var update = false; + for (var i in ballsToEdges[nodeId]) { + var edgeId = ballsToEdges[nodeId][i]; + if (!edgeHash[edgeId]) { + update = true; + edgeHash[edgeId] = true; + edges.push(edgeId); + } + } + if (update) { + logDebug("Fixing node with missing edge data"); + magBallData.edges = edges; + setMagBallsData(nodeId, magBallData); + } + } +} diff --git a/examples/toys/magBalls/edgeSpring.js b/examples/toys/magBalls/edgeSpring.js new file mode 100644 index 0000000000..852c9257c2 --- /dev/null +++ b/examples/toys/magBalls/edgeSpring.js @@ -0,0 +1,45 @@ + +EdgeSpring = function(edgeId, graph) { + this.edgeId = edgeId; + this.graph = graph; + + var magBallsData = getMagBallsData(this.edgeId); + this.start = magBallsData.start; + this.end = magBallsData.end; + this.desiredLength = magBallsData.length || BALL_DISTANCE; +} + +EdgeSpring.prototype.adjust = function(results) { + var startPos = this.getAdjustedPosition(this.start, results); + var endPos = this.getAdjustedPosition(this.end, results); + var vector = Vec3.subtract(endPos, startPos); + var length = Vec3.length(vector); + var variance = this.getVariance(length); + + if (Math.abs(variance) <= this.MAX_VARIANCE) { + return false; + } + + // adjust by halves until we fall below our variance + var adjustmentVector = Vec3.multiply(variance / 4, vector); + + var newStartPos = Vec3.sum(Vec3.multiply(-1, adjustmentVector), startPos); + var newEndPos = Vec3.sum(adjustmentVector, endPos); + results[this.start] = newStartPos; + results[this.end] = newEndPos; + return true; +} + +EdgeSpring.prototype.MAX_VARIANCE = 0.005; + +EdgeSpring.prototype.getAdjustedPosition = function(nodeId, results) { + if (results[nodeId]) { + return results[nodeId]; + } + return this.graph.getNodePosition(nodeId); +} + +EdgeSpring.prototype.getVariance = function(length) { + var difference = this.desiredLength - length; + return difference / this.desiredLength; +} diff --git a/examples/toys/magSticks/graph.js b/examples/toys/magBalls/graph.js similarity index 98% rename from examples/toys/magSticks/graph.js rename to examples/toys/magBalls/graph.js index 8ea9f97fc3..df02ee3628 100644 --- a/examples/toys/magSticks/graph.js +++ b/examples/toys/magBalls/graph.js @@ -16,12 +16,16 @@ Graph = function() { nodeId2: { edgeId1: true }, + // Nodes can many edges nodeId3: { edgeId2: true edgeId3: true edgeId4: true edgeId5: true }, + // Nodes can have 0 edges + nodeId5: { + }, ... } */ diff --git a/examples/toys/magSticks/handController.js b/examples/toys/magBalls/handController.js similarity index 96% rename from examples/toys/magSticks/handController.js rename to examples/toys/magBalls/handController.js index 57bef9be4a..3e54c7ed1b 100644 --- a/examples/toys/magSticks/handController.js +++ b/examples/toys/magBalls/handController.js @@ -54,7 +54,7 @@ HandController.prototype.setActive = function(active) { if (active == this.active) { return; } - logDebug("Setting active: " + active); + logDebug("Hand controller changing active state: " + active); this.active = active; Overlays.editOverlay(this.pointer, { visible: this.active @@ -68,12 +68,8 @@ HandController.prototype.updateControllerState = function() { this.palmPos = Controller.getSpatialControlPosition(this.palm); var tipPos = Controller.getSpatialControlPosition(this.tip); this.tipPosition = scaleLine(this.palmPos, tipPos, this.tipScale); - // When on the base hydras report a position of 0 this.setActive(Vec3.length(this.palmPos) > 0.001); - if (!this.active) { - return; - } } HandController.prototype.onCleanup = function() { diff --git a/examples/toys/magSticks/highlighter.js b/examples/toys/magBalls/highlighter.js similarity index 100% rename from examples/toys/magSticks/highlighter.js rename to examples/toys/magBalls/highlighter.js diff --git a/examples/toys/magBalls/magBalls.js b/examples/toys/magBalls/magBalls.js new file mode 100644 index 0000000000..9e0cbb4982 --- /dev/null +++ b/examples/toys/magBalls/magBalls.js @@ -0,0 +1,289 @@ +// +// Created by Bradley Austin Davis on 2015/08/29 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var UPDATE_INTERVAL = 0.1; + +Script.include("graph.js"); +Script.include("edgeSpring.js"); + +// A collection of balls and edges connecting them. +MagBalls = function() { + Graph.call(this); + + this.lastUpdateAge = 0; + this.stable = false; + this.selectedNodes = {}; + this.edgeObjects = {}; + + this.refresh(); + + var _this = this; + Script.update.connect(function(deltaTime) { + _this.onUpdate(deltaTime); + }); + + Script.scriptEnding.connect(function() { + _this.onCleanup(); + }); +} + +MagBalls.prototype = Object.create( Graph.prototype ); + +MagBalls.prototype.onUpdate = function(deltaTime) { + this.lastUpdateAge += deltaTime; + if (this.lastUpdateAge > UPDATE_INTERVAL) { + this.lastUpdateAge = 0; + if (!this.stable) { + // logDebug("Update"); + var adjusted = false; + var nodeAdjustResults = {}; + var fixupEdges = {}; + + for(var edgeId in this.edges) { + adjusted |= this.edgeObjects[edgeId].adjust(nodeAdjustResults); + } + for (var nodeId in nodeAdjustResults) { + var curPos = this.getNodePosition(nodeId); + var newPos = nodeAdjustResults[nodeId]; + var distance = Vec3.distance(curPos, newPos); + for (var edgeId in this.nodes[nodeId]) { + fixupEdges[edgeId] = true; + } + // logDebug("Moving node Id " + nodeId + " " + (distance * 1000).toFixed(3) + " mm"); + Entities.editEntity(nodeId, { position: newPos, color: COLORS.RED }); + } + + for (var edgeId in fixupEdges) { + this.fixupEdge(edgeId); + } + + Script.setTimeout(function(){ + for (var nodeId in nodeAdjustResults) { + Entities.editEntity(nodeId, { color: BALL_COLOR }); + } + }, ((UPDATE_INTERVAL * 1000) / 2)); + + if (!adjusted) { + this.stable = true; + } + } + } +} + +MagBalls.prototype.createNodeEntity = function(customProperies) { + var nodeId = Entities.addEntity(mergeObjects(BALL_PROTOTYPE, customProperies)); + return nodeId; +} + +MagBalls.prototype.createEdgeEntity = function(nodeIdA, nodeIdB) { + var apos = this.getNodePosition(nodeIdA); + var bpos = this.getNodePosition(nodeIdB); + var edgeId = Entities.addEntity(mergeObjects(EDGE_PROTOTYPE, { + position: apos, + linePoints: [ ZERO_VECTOR, Vec3.subtract(bpos, apos) ], + userData: JSON.stringify({ + magBalls: { + start: nodeIdA, + end: nodeIdB, + length: BALL_DISTANCE + } + }) + })); + this.edgeObjects[edgeId] = new EdgeSpring(edgeId, this); + return edgeId; +} + +MagBalls.prototype.findPotentialEdges = function(nodeId) { + var variances = {}; + for (var otherNodeId in this.nodes) { + // can't self connect + if (otherNodeId == nodeId) { + continue; + } + + // can't doubly connect + if (this.areConnected(otherNodeId, nodeId)) { + continue; + } + + // Check distance to attempt + var distance = this.getNodeDistance(nodeId, otherNodeId); + var variance = this.getVariance(distance); + if (Math.abs(variance) > 0.25) { + continue; + } + + variances[otherNodeId] = variance; + } + return variances; +} + +MagBalls.prototype.grabBall = function(position, maxDist) { + var selected = this.findNearestNode(position, maxDist); + if (!selected) { + selected = this.createNode({ position: position }); + } + if (selected) { + this.breakEdges(selected); + this.selectedNodes[selected] = true; + } + return selected; +} + +MagBalls.prototype.releaseBall = function(releasedBall) { + delete this.selectedNodes[releasedBall]; + logDebug("Released ball: " + releasedBall); + + this.stable = false; + + var releasePosition = this.getNodePosition(releasedBall); + + // iterate through the other balls and ensure we don't intersect with + // any of them. If we do, just delete this ball and return. + // FIXME (play a pop sound) + for (var nodeId in this.nodes) { + if (nodeId == releasedBall) { + continue; + } + var distance = this.getNodeDistance(releasedBall, nodeId); + if (distance < BALL_SIZE) { + this.destroyNode(releasedBall); + return; + } + } + + var targets = this.findPotentialEdges(releasedBall); + if (!targets || !Object.keys(targets).length) { + this.destroyNode(releasedBall); + } + for (var otherBallId in targets) { + this.createEdge(otherBallId, releasedBall); + } +// this.clean(); + this.validate(); +} + + +MagBalls.prototype.getVariance = function(distance) { + // FIXME different balls or edges might have different ideas of variance... + // let something else handle this + var offset = (BALL_DISTANCE - distance); + var variance = offset / BALL_DISTANCE + return variance; +} + +// remove unconnected balls +MagBalls.prototype.clean = function() { + // do nothing unless there are at least 2 balls and one edge + if (Object.keys(this.nodes).length < 2 || !Object.keys(this.edges).length) { + return; + } + var disconnectedNodes = {}; + for (var nodeId in this.nodes) { + if (!Object.keys(this.nodes[nodeId]).length) { + disconnectedNodes[nodeId] = true; + } + } + for (var nodeId in disconnectedNodes) { + this.destroyNode(nodeId); + } +} + +// remove all balls +MagBalls.prototype.clear = function() { + if (DEBUG_MAGSTICKS) { + this.deleteAll(); + var ids = Entities.findEntities(MyAvatar.position, 50); + var result = []; + ids.forEach(function(id) { + var properties = Entities.getEntityProperties(id); + if (properties.name == BALL_NAME || properties.name == EDGE_NAME) { + Entities.deleteEntity(id); + } + }, this); + } +} + +MagBalls.prototype.destroyEdge = function(edgeId) { + Graph.prototype.destroyEdge.call(this, edgeId); + delete this.edgeObjects[edgeId]; +} + +MagBalls.prototype.destroyNode = function(nodeId) { + Graph.prototype.destroyNode.call(this, nodeId); +} + +// Scan the entity tree and load all the objects in range +MagBalls.prototype.refresh = function() { + var ids = Entities.findEntities(MyAvatar.position, 50); + for (var i in ids) { + var id = ids[i]; + var properties = Entities.getEntityProperties(id); + if (properties.name == BALL_NAME) { + this.nodes[id] = {}; + } + } + + var deleteEdges = []; + for (var i in ids) { + var id = ids[i]; + var properties = Entities.getEntityProperties(id); + if (properties.name == EDGE_NAME) { + var edgeId = id; + this.edges[edgeId] = {}; + var magBallData = getMagBallsData(id); + if (!magBallData.start || !magBallData.end) { + logWarn("Edge information is missing for " + id); + continue; + } + if (!this.nodes[magBallData.start] || !this.nodes[magBallData.end]) { + logWarn("Edge " + id + " refers to unknown nodes: " + JSON.stringify(magBallData)); + Entities.editEntity(id, { color: COLORS.RED }); + deleteEdges.push(id); + continue; + } + this.nodes[magBallData.start][edgeId] = true; + this.nodes[magBallData.end][edgeId] = true; + this.edges[edgeId][magBallData.start] = true; + this.edges[edgeId][magBallData.end] = true; + this.edgeObjects[id] = new EdgeSpring(id, this); + } + } + + if (deleteEdges.length) { + Script.setTimeout(function() { + for (var i in deleteEdges) { + var edgeId = deleteEdges[i]; + logDebug("deleting invalid edge " + edgeId); + Entities.deleteEntity(edgeId); + } + }, 1000); + } + + var edgeCount = Object.keys(this.edges).length; + var nodeCount = Object.keys(this.nodes).length; + logDebug("Found " + nodeCount + " nodes and " + edgeCount + " edges "); + this.validate(); +} + + +MagBalls.prototype.findEdgeParams = function(startBall, endBall) { + var startBallPos = this.getNodePosition(startBall); + var endBallPos = this.getNodePosition(endBall); + var vector = Vec3.subtract(endBallPos, startBallPos); + return { + position: startBallPos, + linePoints: [ ZERO_VECTOR, vector ] + }; +} + +MagBalls.prototype.fixupEdge = function(edgeId) { + var ballsInEdge = Object.keys(this.edges[edgeId]); + Entities.editEntity(edgeId, this.findEdgeParams(ballsInEdge[0], ballsInEdge[1])); +} + diff --git a/examples/toys/magBalls/magBallsMain.js b/examples/toys/magBalls/magBallsMain.js new file mode 100644 index 0000000000..1c6bd2b159 --- /dev/null +++ b/examples/toys/magBalls/magBallsMain.js @@ -0,0 +1,26 @@ +// +// Created by Bradley Austin Davis on 2015/08/25 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include("constants.js"); +Script.include("utils.js"); +Script.include("magBalls.js"); + +Script.include("ballController.js"); + +var magBalls = new MagBalls(); + +// Clear any previous balls +// magBalls.clear(); + +MenuController = function(side) { + HandController.call(this, side); +} + + +// FIXME resolve some of the issues with dual controllers before allowing both controllers active +var handControllers = [new BallController(LEFT_CONTROLLER, magBalls)]; //, new HandController(RIGHT) ]; diff --git a/examples/toys/magSticks/utils.js b/examples/toys/magBalls/utils.js similarity index 82% rename from examples/toys/magSticks/utils.js rename to examples/toys/magBalls/utils.js index 8a522ac41f..ea1446f858 100644 --- a/examples/toys/magSticks/utils.js +++ b/examples/toys/magBalls/utils.js @@ -28,27 +28,6 @@ findAction = function(name) { return 0; } - -var LINE_DIMENSIONS = { - x: 5, - y: 5, - z: 5 -} - -var EDGE_NAME = "MagStick" - -var LINE_PROTOTYPE = { - type: "Line", - name: EDGE_NAME, - color: COLORS.CYAN, - dimensions: LINE_DIMENSIONS, - lineWidth: 5, - visible: true, - ignoreCollisions: true, - collisionsWillMove: false -} - - addLine = function(origin, vector, color) { if (!color) { color = COLORS.WHITE @@ -65,7 +44,8 @@ addLine = function(origin, vector, color) { // FIXME fetch from a subkey of user data to support non-destructive modifications setEntityUserData = function(id, data) { - Entities.editEntity(id, { userData: JSON.stringify(data) }); + var json = JSON.stringify(data) + Entities.editEntity(id, { userData: json }); } // FIXME do non-destructive modification of the existing user data @@ -73,9 +53,9 @@ getEntityUserData = function(id) { var results = null; var properties = Entities.getEntityProperties(id); if (properties.userData) { - results = JSON.parse(this.properties.userData); + results = JSON.parse(properties.userData); } - return results; + return results ? results : {}; } // Non-destructively modify the user data of an entity. @@ -86,10 +66,18 @@ setEntityCustomData = function(customKey, id, data) { } getEntityCustomData = function(customKey, id, defaultValue) { - var userData = getEntityUserData(); + var userData = getEntityUserData(id); return userData[customKey] ? userData[customKey] : defaultValue; } +getMagBallsData = function(id) { + return getEntityCustomData(CUSTOM_DATA_NAME, id, {}); +} + +setMagBallsData = function(id, value) { + setEntityCustomData(CUSTOM_DATA_NAME, id, value); +} + mergeObjects = function(proto, custom) { var result = {}; for (var attrname in proto) {