From 38b62108afd49586e8cfca71ea9e44688005fc50 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 25 Aug 2015 18:54:48 -0700 Subject: [PATCH 1/6] Magnetic sticks and balls --- examples/toys/magSticks.js | 92 +++++ examples/toys/magSticks/constants.js | 16 + examples/toys/magSticks/handController.js | 94 +++++ examples/toys/magSticks/highlighter.js | 63 +++ examples/toys/magSticks/magBalls.js | 454 ++++++++++++++++++++++ examples/toys/magSticks/utils.js | 48 +++ 6 files changed, 767 insertions(+) create mode 100644 examples/toys/magSticks.js create mode 100644 examples/toys/magSticks/constants.js create mode 100644 examples/toys/magSticks/handController.js create mode 100644 examples/toys/magSticks/highlighter.js create mode 100644 examples/toys/magSticks/magBalls.js create mode 100644 examples/toys/magSticks/utils.js diff --git a/examples/toys/magSticks.js b/examples/toys/magSticks.js new file mode 100644 index 0000000000..7381754de0 --- /dev/null +++ b/examples/toys/magSticks.js @@ -0,0 +1,92 @@ +// +// 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/utils.js"); +Script.include("magSticks/constants.js"); +Script.include("magSticks/magBalls.js"); +Script.include("magSticks/highlighter.js"); +Script.include("magSticks/handController.js"); + +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% +var BALL_SELECTION_RADIUS = BALL_SIZE / 2.0 * 1.1; + +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.updateControllerState.call(this, deltaTime); + if (!this.selected) { + // Find the highlight target and set it. + var target = magBalls.findNearestBall(this.tipPosition, BALL_SELECTION_RADIUS); + this.highlighter.highlight(target); + return; + } + this.highlighter.highlight(null); + Entities.editEntity(this.selected, { position: this.tipPosition }); + var targetBalls = magBalls.findMatches(this.selected); + for (var ballId in targetBalls) { + if (!this.ghostEdges[ballId]) { + // create the ovleray + this.ghostEdges[ballId] = Overlays.addOverlay("line3d", { + start: magBalls.getBallPosition(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.updateControllerState.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/magSticks/constants.js b/examples/toys/magSticks/constants.js new file mode 100644 index 0000000000..0453d29a79 --- /dev/null +++ b/examples/toys/magSticks/constants.js @@ -0,0 +1,16 @@ +// +// Created by Bradley Austin Davis on 2015/08/27 +// 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 +// + +HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; +STICK_URL = HIFI_PUBLIC_BUCKET + "models/props/geo_stick.fbx"; + +// FIXME make this editable through some script UI, so the user can customize the size of the structure built +SCALE = 0.5; +BALL_SIZE = 0.08 * SCALE; +STICK_LENGTH = 0.24 * SCALE; + diff --git a/examples/toys/magSticks/handController.js b/examples/toys/magSticks/handController.js new file mode 100644 index 0000000000..4f893c9cf2 --- /dev/null +++ b/examples/toys/magSticks/handController.js @@ -0,0 +1,94 @@ + +LEFT_CONTROLLER = 0; +RIGHT_CONTROLLER = 1; + + +HandController = function(side) { + this.side = side; + this.palm = 2 * side; + this.tip = 2 * side + 1; + this.action = findAction(side ? "ACTION2" : "ACTION1"); + this.active = false; + this.pointer = Overlays.addOverlay("sphere", { + position: { + x: 0, + y: 0, + z: 0 + }, + size: 0.01, + color: { + red: 255, + green: 255, + blue: 0 + }, + alpha: 1.0, + solid: true, + visible: false, + }); + + // Connect to desired events + var _this = this; + Controller.actionEvent.connect(function(action, state) { + _this.onActionEvent(action, state); + }); + + Script.update.connect(function(deltaTime) { + _this.onUpdate(deltaTime); + }); + + Script.scriptEnding.connect(function() { + _this.onCleanup(); + }); +} + +HandController.prototype.onActionEvent = function(action, state) { + if (action == this.action) { + if (state) { + this.onClick(); + } else { + this.onRelease(); + } + } +} + +HandController.prototype.setActive = function(active) { + if (active == this.active) { + return; + } + debugPrint("Setting active: " + active); + this.active = active; + Overlays.editOverlay(this.pointer, { + position: this.tipPosition, + visible: this.active + }); +} + +HandController.prototype.updateControllerState = function() { + var palmPos = Controller.getSpatialControlPosition(this.palm); + // When on the base hydras report a position of 0 + this.setActive(Vec3.length(palmPos) > 0.001); + if (!this.active) { + return; + } + var tipPos = Controller.getSpatialControlPosition(this.tip); + this.tipPosition = scaleLine(palmPos, tipPos, 1.4); + Overlays.editOverlay(this.pointer, { + position: this.tipPosition + }); +} + +HandController.prototype.onCleanup = function() { + Overlays.deleteOverlay(this.pointer); +} + +HandController.prototype.onUpdate = function(deltaTime) { + this.updateControllerState(); +} + +HandController.prototype.onClick = function() { + debugPrint("Base hand controller does nothing on click"); +} + +HandController.prototype.onRelease = function() { + debugPrint("Base hand controller does nothing on release"); +} diff --git a/examples/toys/magSticks/highlighter.js b/examples/toys/magSticks/highlighter.js new file mode 100644 index 0000000000..7316549c84 --- /dev/null +++ b/examples/toys/magSticks/highlighter.js @@ -0,0 +1,63 @@ +var SELECTION_OVERLAY = { + position: { + x: 0, + y: 0, + z: 0 + }, + color: { + red: 255, + green: 255, + blue: 0 + }, + alpha: 1, + size: 1.0, + solid: false, + //colorPulse: 1.0, + //pulseMin: 0.5, + //pulseMax: 1.0, + visible: false, + lineWidth: 1.0, + borderSize: 1.4, +}; + +Highlighter = function() { + this.highlightCube = Overlays.addOverlay("cube", this.SELECTION_OVERLAY); + this.hightlighted = null; + var _this = this; + Script.scriptEnding.connect(function() { + _this.onCleanup(); + }); +}; + +Highlighter.prototype.onCleanup = function() { + Overlays.deleteOverlay(this.highlightCube); +} + +Highlighter.prototype.highlight = function(entityId) { + if (entityId != this.hightlighted) { + this.hightlighted = entityId; + this.updateHighlight(); + } +} + +Highlighter.prototype.setSize = function(newSize) { + Overlays.editOverlay(this.highlightCube, { + size: newSize + }); +} + +Highlighter.prototype.updateHighlight = function() { + if (this.hightlighted) { + var properties = Entities.getEntityProperties(this.hightlighted); + // logDebug("Making highlight " + this.highlightCube + " visible @ " + vec3toStr(properties.position)); + Overlays.editOverlay(this.highlightCube, { + position: properties.position, + visible: true + }); + } else { + // logDebug("Making highlight invisible"); + Overlays.editOverlay(this.highlightCube, { + visible: false + }); + } +} \ No newline at end of file diff --git a/examples/toys/magSticks/magBalls.js b/examples/toys/magSticks/magBalls.js new file mode 100644 index 0000000000..9d67e0e0b7 --- /dev/null +++ b/examples/toys/magSticks/magBalls.js @@ -0,0 +1,454 @@ +var BALL_NAME = "MagBall" + +var EDGE_NAME = "MagStick" + +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 +}; + +// A collection of balls and edges connecting them. +MagBalls = function() { + /* + this.balls: { + ballId1: { + edgeId1: true + } + ballId2: { + edgeId1: true + }, + ballId3: { + edgeId2: true + edgeId3: true + edgeId4: true + edgeId5: true + }, + ... + } + */ + this.balls = {}; + /* + this.edges: { + edgeId1: { + ballId1: true + ballId2: true + }, + edgeId2: { + ballId3: true + ballId4: true + }, + ... + } + */ + + // FIXME initialize from nearby entities + this.edges = {}; + this.selectedBalls = {}; + + var _this = this; + Script.update.connect(function(deltaTime) { + _this.onUpdate(deltaTime); + }); + + Script.scriptEnding.connect(function() { + _this.onCleanup(); + }); +} + +MagBalls.prototype.findNearestBall = function(position, maxDist) { + var resultId = null; + var resultDist = 0; + for (var id in this.balls) { + var properties = Entities.getEntityProperties(id); + var curDist = Vec3.distance(properties.position, position); + if (!maxDist || curDist <= maxDist) { + if (!resultId || curDist < resultDist) { + resultId = id; + resultDist = curDist; + } + } + } + return resultId; +} + +// FIXME move to a physics based implementation as soon as bullet +// is exposed to entities +MagBalls.prototype.onUpdate = function(deltaTime) { +} + +function mergeObjects(proto, custom) { + var result = {}; + for (var attrname in proto) { + result[attrname] = proto[attrname]; + } + for (var attrname in custom) { + result[attrname] = custom[attrname]; + } + return result; +} + +MagBalls.prototype.createBall = function(customProperies) { + var ballId = Entities.addEntity(mergeObjects(BALL_PROTOTYPE, customProperies)); + this.balls[ballId] = {}; + this.validate(); + return ballId; +} + +MagBalls.prototype.grabBall = function(position, maxDist) { + var selected = this.findNearestBall(position, maxDist); + if (!selected) { + selected = this.createBall({ position: position }); + } + if (selected) { + this.breakEdges(selected); + this.selectedBalls[selected] = true; + } + return selected; +} + +MagBalls.prototype.findMatches = function(ballId) { + var variances = {}; + for (var otherBallId in this.balls) { + if (otherBallId == ballId || this.areConnected(otherBallId, ballId)) { + // can't self connect or doubly connect + continue; + } + var variance = this.getStickLengthVariance(ballId, otherBallId); + if (variance > BALL_DISTANCE / 4) { + continue; + } + variances[otherBallId] = variance; + } + return variances; +} + +MagBalls.prototype.releaseBall = function(releasedBall) { + delete this.selectedBalls[releasedBall]; + debugPrint("Released ball: " + releasedBall); + + // sort other balls by distance from the stick length + var edgeTargetBall = null; + do { + var releasePosition = this.getBallPosition(releasedBall); + // Get the list of candidate connections + var variances = this.findMatches(releasedBall); + // sort them by the difference from an ideal distance + var sortedBalls = Object.keys(variances); + if (!sortedBalls.length) { + return; + } + sortedBalls.sort(function(a, b){ + return variances[a] - variances[b]; + }); + // find the nearest matching unconnected ball + edgeTargetBall = sortedBalls[0]; + // note that createEdge will preferentially move the second parameter, the target ball + } while(this.createEdge(edgeTargetBall, releasedBall)) + + this.clean(); +} + +// FIXME the Quat should be able to do this +function findOrientation(from, to) { + //float m = sqrt(2.f + 2.f * dot(u, v)); + //vec3 w = (1.f / m) * cross(u, v); + //return quat(0.5f * m, w.x, w.y, w.z); + var v2 = Vec3.normalize(Vec3.subtract(to, from)); + var v1 = { x: 0.0, y: 1.0, z: 0.0 }; + var m = Math.sqrt(2 + 2 * Vec3.dot(v1, v2)); + var w = Vec3.multiply(1.0 / m, Vec3.cross(v1, v2)); + return { + w: 0.5 * m, + x: w.x, + y: w.y, + z: w.z + }; +} + +var LINE_DIMENSIONS = 5; + +var EDGE_PROTOTYPE = { + type: "Line", + name: EDGE_NAME, + color: { red: 0, green: 255, blue: 255 }, + dimensions: { + x: LINE_DIMENSIONS, + y: LINE_DIMENSIONS, + z: LINE_DIMENSIONS + }, + lineWidth: 5, + visible: true, + ignoreCollisions: true, + collisionsWillMove: false +} + +var ZERO_VECTOR = { + x: 0, + y: 0, + z: 0 +}; + +//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 +//} + +MagBalls.prototype.createEdge = function(from, to) { + // FIXME find the constraints on from an to and determine if there is an intersection. + // Do only first order scanning for now, unless we can expose a mechanism for interacting + // to reach equilibrium via Bullet + // * a ball zero edges is fully free... + // * a ball with one edge free to move on a sphere section + // * a ball with two edges is free to move in a circle + // * a ball with more than two edges is not free + + var fromPos = this.getBallPosition(from); + var toPos = this.getBallPosition(to); + var vector = Vec3.subtract(toPos, fromPos); + var originalLength = Vec3.length(originalLength); + + // if they're already at a close enough distance, just create the edge + if ((originalLength - BALL_DISTANCE) > (BALL_DISTANCE * 0.01)) { + //code + } else { + // Attempt to move the ball to match the distance + vector = Vec3.multiply(BALL_DISTANCE, Vec3.normalize(vector)); + // Zero edges for the destination + var edgeCount = Object.keys(this.balls[to]).length; + if (!edgeCount) { + // update the entity + var newPosition = Vec3.sum(vector, fromPos); + Entities.editEntity(to, { position: newPosition }); + } else if (1 == edgeCount) { + // FIXME + // find the other end of the edge already connected, call it ball2 + // given two spheres of radius BALL_DISTANCE centered at fromPos and ball2.position, + // find the closest point of intersection to toPos + // move the ball to toPos + } else if (2 == edgeCount) { + // FIXME + } else { + // FIXME check for the ability to move fromPos + return false; + } + } + + + // Fixup existing edges + for (var edgeId in this.balls[to]) { + this.fixupEdge(edgeId); + } + + // FIXME, find the correct orientation for a box or model between the two balls + // for now use a line + var newEdge = Entities.addEntity(mergeObjects(EDGE_PROTOTYPE, this.findEdgeParams(from, to))); + + this.edges[newEdge] = {}; + this.edges[newEdge][from] = true; + this.edges[newEdge][to] = true; + this.balls[from][newEdge] = true; + this.balls[to][newEdge] = true; + this.validate(); + return true; +} + +MagBalls.prototype.findEdgeParams = function(startBall, endBall) { + var startBallPos = this.getBallPosition(startBall); + var endBallPos = this.getBallPosition(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])); +} + +// Given two balls, how big is the difference between their distances and the stick length +MagBalls.prototype.getStickLengthVariance = function(a, b) { + var apos = this.getBallPosition(a); + var bpos = this.getBallPosition(b); + var distance = Vec3.distance(apos, bpos); + var variance = Math.abs(distance - BALL_DISTANCE); + return variance; +} + +// FIXME remove unconnected balls +MagBalls.prototype.clean = function() { + //var deletedBalls = {}; + //if (Object.keys(this.balls).length > 1) { + // for (var ball in this.balls) { + // if (!this.getConnections(ball)) { + // deletedBalls[ball] = true; + // Entities.deleteEntity(ball); + // } + // } + //} + //for (var ball in deletedBalls) { + // delete this.balls[ball]; + //} +} + +MagBalls.prototype.getBallPosition = function(ball) { + var properties = Entities.getEntityProperties(ball); + return properties.position; +} + +MagBalls.prototype.breakEdges = function(ballId) { + for (var edgeId in this.balls[ballId]) { + this.destroyEdge(edgeId); + } + // This shouldn't be necessary + this.balls[ballId] = {}; +} + +MagBalls.prototype.destroyEdge = function(edgeId) { + logDebug("Deleting edge " + edgeId); + // Delete the edge from other balls + for (var edgeBallId in this.edges[edgeId]) { + delete this.balls[edgeBallId][edgeId]; + } + delete this.edges[edgeId]; + Entities.deleteEntity(edgeId); + this.validate(); +} + +MagBalls.prototype.destroyBall = function(ballId) { + logDebug("Deleting ball " + ballId); + breakEdges(ballId); + Entities.deleteEntity(ballId); +} + +MagBalls.prototype.clear = function() { + if (DEBUG_MAGSTICKS) { + 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.areConnected = function(a, b) { + for (var edge in this.balls[a]) { + // edge already exists + if (this.balls[b][edge]) { + return true; + } + } + return false; +} + +MagBalls.prototype.validate = function() { + var error = false; + for (ballId in this.balls) { + for (edgeId in this.balls[ballId]) { + var edge = this.edges[edgeId]; + if (!edge) { + logError("Error: ball " + ballId + " refers to unknown edge " + edgeId); + error = true; + continue; + } + if (!edge[ballId]) { + logError("Error: ball " + ballId + " refers to edge " + edgeId + " but not vice versa"); + error = true; + continue; + } + } + } + + for (edgeId in this.edges) { + for (ballId in this.edges[edgeId]) { + var ball = this.balls[ballId]; + if (!ball) { + logError("Error: edge " + edgeId + " refers to unknown ball " + ballId); + error = true; + continue; + } + if (!ball[edgeId]) { + logError("Error: edge " + edgeId + " refers to ball " + ballId + " but not vice versa"); + error = true; + continue; + } + } + } + if (error) { + logDebug(JSON.stringify({ edges: this.edges, balls: this.balls }, null, 2)); + } +} + +// FIXME fetch from a subkey of user data to support non-destructive modifications +MagBalls.prototype.setUserData = function(id, data) { + Entities.editEntity(id, { userData: JSON.stringify(data) }); +} + +// FIXME do non-destructive modification of the existing user data +MagBalls.prototype.getUserData = function(id) { + var results = null; + var properties = Entities.getEntityProperties(id); + if (properties.userData) { + results = JSON.parse(this.properties.userData); + } + return results; +} + +//MagBalls.prototype.findBalls = function() { +// var ids = Entities.findEntities(MyAvatar.position, 50); +// var result = []; +// ids.forEach(function(id) { +// var properties = Entities.getEntityProperties(id); +// if (properties.name == BALL_NAME) { +// result.push(id); +// } +// }, this); +// return result; +//}; +// +//MagBalls.prototype.findEdges = function() { +// var ids = Entities.findEntities(MyAvatar.position, 50); +// var result = []; +// ids.forEach(function(id) { +// var properties = Entities.getEntityProperties(id); +// if (properties.name == EDGE_NAME) { +// result.push(id); +// } +// }, this); +// return result; +//}; diff --git a/examples/toys/magSticks/utils.js b/examples/toys/magSticks/utils.js new file mode 100644 index 0000000000..43e4f800fb --- /dev/null +++ b/examples/toys/magSticks/utils.js @@ -0,0 +1,48 @@ + +DEBUG_MAGSTICKS = true; + +debugPrint = function (str) { + if (DEBUG_MAGSTICKS) { + print(str); + } +} + +vec3toStr = function (v) { + return "{ " + + (Math.round(v.x*1000)/1000) + ", " + + (Math.round(v.y*1000)/1000) + ", " + + (Math.round(v.z*1000)/1000) + " }"; +} + +scaleLine = function (start, end, scale) { + var v = Vec3.subtract(end, start); + var length = Vec3.length(v); + v = Vec3.multiply(scale, v); + return Vec3.sum(start, v); +} + +findAction = function(name) { + var actions = Controller.getAllActions(); + for (var i = 0; i < actions.length; i++) { + if (actions[i].actionName == name) { + return i; + } + } + return 0; +} + +logWarn = function(str) { + print(str); +} + +logError = function(str) { + print(str); +} + +logInfo = function(str) { + print(str); +} + +logDebug = function(str) { + debugPrint(str); +} \ No newline at end of file From ebb530aaace0ba8d03ea8d6307176cdfaf4463ab Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 28 Aug 2015 18:36:21 -0700 Subject: [PATCH 2/6] More magball work --- examples/toys/magSticks.js | 15 +- examples/toys/magSticks/constants.js | 52 ++ examples/toys/magSticks/graph.js | 270 +++++++ examples/toys/magSticks/handController.js | 65 +- examples/toys/magSticks/magBalls.js | 791 +++++++++++--------- examples/toys/magSticks/springEdgeEntity.js | 143 ++++ examples/toys/magSticks/utils.js | 91 ++- 7 files changed, 1049 insertions(+), 378 deletions(-) create mode 100644 examples/toys/magSticks/graph.js create mode 100644 examples/toys/magSticks/springEdgeEntity.js diff --git a/examples/toys/magSticks.js b/examples/toys/magSticks.js index 7381754de0..9461ffd047 100644 --- a/examples/toys/magSticks.js +++ b/examples/toys/magSticks.js @@ -6,8 +6,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -Script.include("magSticks/utils.js"); Script.include("magSticks/constants.js"); +Script.include("magSticks/utils.js"); +Script.include("magSticks/graph.js"); Script.include("magSticks/magBalls.js"); Script.include("magSticks/highlighter.js"); Script.include("magSticks/handController.js"); @@ -18,7 +19,7 @@ var magBalls = new MagBalls(); magBalls.clear(); // How close do we need to be to a ball to select it.... radius + 10% -var BALL_SELECTION_RADIUS = BALL_SIZE / 2.0 * 1.1; +var BALL_SELECTION_RADIUS = BALL_SIZE / 2.0 * 1.5; BallController = function(side) { HandController.call(this, side); @@ -30,21 +31,21 @@ BallController = function(side) { BallController.prototype = Object.create( HandController.prototype ); BallController.prototype.onUpdate = function(deltaTime) { - HandController.prototype.updateControllerState.call(this, deltaTime); + HandController.prototype.onUpdate.call(this, deltaTime); if (!this.selected) { // Find the highlight target and set it. - var target = magBalls.findNearestBall(this.tipPosition, BALL_SELECTION_RADIUS); + 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.findMatches(this.selected); + 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.getBallPosition(ballId), + start: magBalls.getNodePosition(ballId), end: this.tipPosition, color: { red: 255, green: 0, blue: 0}, alpha: 1, @@ -84,7 +85,7 @@ BallController.prototype.clearGhostEdges = function() { } BallController.prototype.onCleanup = function() { - HandController.prototype.updateControllerState.call(this); + HandController.prototype.onCleanup.call(this); this.clearGhostEdges(); } diff --git a/examples/toys/magSticks/constants.js b/examples/toys/magSticks/constants.js index 0453d29a79..297fa51c6e 100644 --- a/examples/toys/magSticks/constants.js +++ b/examples/toys/magSticks/constants.js @@ -14,3 +14,55 @@ SCALE = 0.5; BALL_SIZE = 0.08 * SCALE; STICK_LENGTH = 0.24 * SCALE; +DEBUG_MAGSTICKS = true; + +ZERO_VECTOR = { x: 0, y: 0, z: 0 }; + +COLORS = { + WHITE: { + red: 255, + green: 255, + blue: 255, + }, + BLACK: { + red: 0, + green: 0, + blue: 0, + }, + GREY: { + red: 128, + green: 128, + blue: 128, + }, + RED: { + red: 255, + green: 0, + blue: 0 + }, + BLUE: { + red: 0, + green: 0, + blue: 255 + }, + GREEN: { + red: 0, + green: 255, + blue: 0 + }, + CYAN: { + red: 0, + green: 255, + blue: 255 + }, + YELLOW: { + red: 255, + green: 255, + blue: 0 + }, + MAGENTA: { + red: 255, + green: 0, + blue: 255 + } +} + diff --git a/examples/toys/magSticks/graph.js b/examples/toys/magSticks/graph.js new file mode 100644 index 0000000000..8a5c00451d --- /dev/null +++ b/examples/toys/magSticks/graph.js @@ -0,0 +1,270 @@ + +// A collection of nodes and edges connecting them. +Graph = function() { + /* Structure of nodes tree + this.nodes: { + nodeId1: { + edgeId1: true + } + nodeId2: { + edgeId1: true + }, + nodeId3: { + edgeId2: true + edgeId3: true + edgeId4: true + edgeId5: true + }, + ... + } + */ + this.nodes = {}; + /* Structure of edge tree + this.edges: { + edgeId1: { + // Every edge should have exactly two + nodeId1: true + nodeId2: true + }, + edgeId2: { + nodeId3: true + nodeId4: true + }, + ... + } + */ + this.edges = {}; +} + +Graph.prototype.createNodeEntity = function(properties) { + throw "Unimplemented"; +} + +Graph.prototype.createNode = function(properties) { + var nodeId = this.createNodeEntity(properties); + this.nodes[nodeId] = {}; + this.validate(); + return nodeId; +} + +Graph.prototype.createEdgeEntity = function(nodeA, nodeB) { + throw "Unimplemented"; +} + +Graph.prototype.createEdge = function(nodeA, nodeB) { + if (nodeA == nodeB) { + throw "Error: self connection not supported"; + } + var newEdgeId = this.createEdgeEntity(nodeA, nodeB); + + // Create the bidirectional linkage + this.edges[newEdgeId] = {}; + this.edges[newEdgeId][nodeA] = true; + this.edges[newEdgeId][nodeB] = true; + this.nodes[nodeA][newEdgeId] = true; + this.nodes[nodeB][newEdgeId] = true; + + this.validate(); +} + +Graph.prototype.getEdges = function(nodeId) { + var edges = this.nodes[nodeId]; + var result = {}; + for (var edgeId in edges) { + for (var otherNodeId in this.edges[edgeId]) { + if (otherNodeId != nodeId) { + result[edgeId] = otherNodeId; + } + } + } + return result; +} + +Graph.prototype.getConnectedNodes = function(nodeId) { + var edges = this.getEdges(nodeId); + var result = {}; + for (var edgeId in edges) { + var otherNodeId = edges[edgeId]; + result[otherNodeId] = edgeId; + } + return result; +} + +Graph.prototype.getEdgeLength = function(edgeId) { + var nodesInEdge = Object.keys(this.edges[edgeId]); + return this.getNodeDistance(nodesInEdge[0], nodesInEdge[1]); +} + +Graph.prototype.getNodeDistance = function(a, b) { + var apos = this.getNodePosition(a); + var bpos = this.getNodePosition(b); + return Vec3.distance(apos, bpos); +} + +Graph.prototype.getNodePosition = function(node) { + var properties = Entities.getEntityProperties(node); + return properties.position; +} + +Graph.prototype.breakEdges = function(nodeId) { + for (var edgeId in this.nodes[nodeId]) { + this.destroyEdge(edgeId); + } +} + +Graph.prototype.findNearestNode = function(position, maxDist) { + var resultId = null; + var resultDist = 0; + for (var nodeId in this.nodes) { + var nodePosition = this.getNodePosition(nodeId); + var curDist = Vec3.distance(nodePosition, position); + if (!maxDist || curDist <= maxDist) { + if (!resultId || curDist < resultDist) { + resultId = nodeId; + resultDist = curDist; + } + } + } + return resultId; +} + +Graph.prototype.findMatchingNodes = function(selector) { + var result = {}; + for (var nodeId in this.nodes) { + if (selector(nodeId)) { + result[nodeId] = true; + } + } + return result; +} + +Graph.prototype.destroyEdge = function(edgeId) { + logDebug("Deleting edge " + edgeId); + for (var nodeId in this.edges[edgeId]) { + delete this.nodes[nodeId][edgeId]; + } + delete this.edges[edgeId]; + Entities.deleteEntity(edgeId); + this.validate(); +} + +Graph.prototype.destroyNode = function(nodeId) { + logDebug("Deleting node " + nodeId); + this.breakEdges(nodeId); + delete this.nodes[nodeId]; + Entities.deleteEntity(nodeId); + this.validate(); +} + +Graph.prototype.deleteAll = function() { + var nodeIds = Object.keys(this.nodes); + for (var i in nodeIds) { + var nodeId = nodeIds[i]; + this.destroyNode(nodeId); + } +} + +Graph.prototype.areConnected = function(nodeIdA, nodeIdB) { + for (var edgeId in this.nodes[nodeIdA]) { + if (this.nodes[nodeIdB][edgeId]) { + return true; + } + } + return false; +} + +forEachValue = function(val, operation) { + if( typeof val === 'string' ) { + operation(val); + } else if (typeof val === 'object') { + if (val.constructor === Array) { + for (var i in val) { + operation(val[i]); + } + } else { + for (var v in val) { + operation(v); + } + } + } +} + +Graph.prototype.findShortestPath = function(start, end, options) { + var queue = [ start ]; + var prev = {}; + if (options && options.exclude) { + forEachValue(options.exclude, function(value) { + prev[value] = value; + }); + logDebug("exclude " + prev); + } + var found = false; + while (!found && Object.keys(queue).length) { + var current = queue.shift(); + for (var ballId in this.getConnectedNodes(current)) { + if (prev[ballId]) { + // already visited node + continue; + } + // record optimal path + prev[ballId] = current; + if (ballId == end) { + found = true; + break; + } + queue.push(ballId); + } + } + + if (!found) { + logDebug("Exhausted search"); + return; + } + + var result = [ end ]; + while (result[0] != start) { + result.unshift(prev[result[0]]); + } + + return result; +} + +Graph.prototype.validate = function() { + var error = false; + for (nodeId in this.nodes) { + for (edgeId in this.nodes[nodeId]) { + var edge = this.edges[edgeId]; + if (!edge) { + logError("Error: node " + nodeId + " refers to unknown edge " + edgeId); + error = true; + continue; + } + if (!edge[nodeId]) { + logError("Error: node " + nodeId + " refers to edge " + edgeId + " but not vice versa"); + error = true; + continue; + } + } + } + + for (edgeId in this.edges) { + for (nodeId in this.edges[edgeId]) { + var node = this.nodes[nodeId]; + if (!node) { + logError("Error: edge " + edgeId + " refers to unknown node " + nodeId); + error = true; + continue; + } + if (!node[edgeId]) { + logError("Error: edge " + edgeId + " refers to node " + nodeId + " but not vice versa"); + error = true; + continue; + } + } + } + + if (error) { + logDebug(JSON.stringify({ edges: this.edges, balls: this.nodes }, null, 2)); + } + return error; +} \ No newline at end of file diff --git a/examples/toys/magSticks/handController.js b/examples/toys/magSticks/handController.js index 4f893c9cf2..cf3ef96b4a 100644 --- a/examples/toys/magSticks/handController.js +++ b/examples/toys/magSticks/handController.js @@ -2,6 +2,26 @@ LEFT_CONTROLLER = 0; RIGHT_CONTROLLER = 1; +WAND_LIFETIME = 30; + +var WAND_PROPERTIES = { + type: "Model", + modelURL: "file:///f:/Downloads/wand.FBX", + ignoreForCollisions: true, + dimensions: { + x: 0.1, + y: 0.1, + z: 0.1 + }, + lifetime: 30 +}; + +if (!Date.now) { + Date.now = function now() { + return new Date().getTime(); + }; +} + HandController = function(side) { this.side = side; @@ -9,22 +29,17 @@ HandController = function(side) { this.tip = 2 * side + 1; this.action = findAction(side ? "ACTION2" : "ACTION1"); this.active = false; + this.tipScale = 1.4; this.pointer = Overlays.addOverlay("sphere", { - position: { - x: 0, - y: 0, - z: 0 - }, + position: ZERO_VECTOR, size: 0.01, - color: { - red: 255, - green: 255, - blue: 0 - }, + color: COLORS.YELLOW, alpha: 1.0, solid: true, visible: false, }); + //this.wand = Entities.addEntity(WAND_PROPERTIES); + // Connect to desired events var _this = this; @@ -55,26 +70,26 @@ HandController.prototype.setActive = function(active) { if (active == this.active) { return; } - debugPrint("Setting active: " + active); + logDebug("Setting active: " + active); this.active = active; Overlays.editOverlay(this.pointer, { - position: this.tipPosition, + visible: this.active + }); + Entities.editEntity(this.wand, { visible: this.active }); } HandController.prototype.updateControllerState = function() { - var palmPos = Controller.getSpatialControlPosition(this.palm); + 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(palmPos) > 0.001); + this.setActive(Vec3.length(this.palmPos) > 0.001); if (!this.active) { return; } - var tipPos = Controller.getSpatialControlPosition(this.tip); - this.tipPosition = scaleLine(palmPos, tipPos, 1.4); - Overlays.editOverlay(this.pointer, { - position: this.tipPosition - }); } HandController.prototype.onCleanup = function() { @@ -83,12 +98,20 @@ HandController.prototype.onCleanup = function() { HandController.prototype.onUpdate = function(deltaTime) { this.updateControllerState(); + if (this.active) { + Overlays.editOverlay(this.pointer, { + position: this.tipPosition + }); + Entities.editEntity(this.wand, { + position: this.tipPosition + }); + } } HandController.prototype.onClick = function() { - debugPrint("Base hand controller does nothing on click"); + logDebug("Base hand controller does nothing on click"); } HandController.prototype.onRelease = function() { - debugPrint("Base hand controller does nothing on release"); + logDebug("Base hand controller does nothing on release"); } diff --git a/examples/toys/magSticks/magBalls.js b/examples/toys/magSticks/magBalls.js index 9d67e0e0b7..9a0465c582 100644 --- a/examples/toys/magSticks/magBalls.js +++ b/examples/toys/magSticks/magBalls.js @@ -1,5 +1,5 @@ -var BALL_NAME = "MagBall" +var BALL_NAME = "MagBall" var EDGE_NAME = "MagStick" var BALL_DIMENSIONS = { @@ -31,44 +31,48 @@ var BALL_PROTOTYPE = { collisionsWillMove: false }; +// 2 millimeters +var EPSILON = (.002) / BALL_DISTANCE; + +var LINE_DIMENSIONS = { + x: 5, + y: 5, + z: 5 +} + +var LINE_PROTOTYPE = { + type: "Line", + name: EDGE_NAME, + color: COLORS.CYAN, + dimensions: LINE_DIMENSIONS, + lineWidth: 5, + visible: true, + ignoreCollisions: true, + collisionsWillMove: false, + script: "file:/Users/bdavis/Git/hifi/examples/toys/magSticks/springEdgeEntity.js" +} + +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.balls: { - ballId1: { - edgeId1: true - } - ballId2: { - edgeId1: true - }, - ballId3: { - edgeId2: true - edgeId3: true - edgeId4: true - edgeId5: true - }, - ... - } - */ - this.balls = {}; - /* - this.edges: { - edgeId1: { - ballId1: true - ballId2: true - }, - edgeId2: { - ballId3: true - ballId4: true - }, - ... - } - */ - - // FIXME initialize from nearby entities - this.edges = {}; - this.selectedBalls = {}; - + this.selectedNodes = {}; + + Graph.call(this); + var _this = this; Script.update.connect(function(deltaTime) { _this.onUpdate(deltaTime); @@ -79,282 +83,463 @@ MagBalls = function() { }); } -MagBalls.prototype.findNearestBall = function(position, maxDist) { - var resultId = null; - var resultDist = 0; - for (var id in this.balls) { - var properties = Entities.getEntityProperties(id); - var curDist = Vec3.distance(properties.position, position); - if (!maxDist || curDist <= maxDist) { - if (!resultId || curDist < resultDist) { - resultId = id; - resultDist = curDist; - } - } - } - return resultId; -} +MagBalls.prototype = Object.create( Graph.prototype ); -// FIXME move to a physics based implementation as soon as bullet -// is exposed to entities MagBalls.prototype.onUpdate = function(deltaTime) { + // FIXME move to a physics based implementation as soon as bullet + // is exposed to entities } -function mergeObjects(proto, custom) { - var result = {}; - for (var attrname in proto) { - result[attrname] = proto[attrname]; - } - for (var attrname in custom) { - result[attrname] = custom[attrname]; - } - return result; +MagBalls.prototype.createNodeEntity = function(customProperies) { + return Entities.addEntity(mergeObjects(BALL_PROTOTYPE, customProperies)); } -MagBalls.prototype.createBall = function(customProperies) { - var ballId = Entities.addEntity(mergeObjects(BALL_PROTOTYPE, customProperies)); - this.balls[ballId] = {}; - this.validate(); - return ballId; +MagBalls.prototype.getEdgeProperties = function(nodeIdA, nodeIdB) { + var apos = this.getNodePosition(nodeIdA); + var bpos = this.getNodePosition(nodeIdB); + return { + position: apos, + linePoints: [ ZERO_VECTOR, Vec3.subtract(bpos, apos) ], + userData: JSON.stringify({ + magBalls: { + start: nodeIdA, + end: nodeIdB, + length: BALL_DISTANCE + } + }) + }; } -MagBalls.prototype.grabBall = function(position, maxDist) { - var selected = this.findNearestBall(position, maxDist); - if (!selected) { - selected = this.createBall({ position: position }); - } - if (selected) { - this.breakEdges(selected); - this.selectedBalls[selected] = true; - } - return selected; +MagBalls.prototype.createEdgeEntity = function(nodeIdA, nodeIdB) { + var customProperties = this.getEdgeProperties(nodeIdA, nodeIdB) + return Entities.addEntity(mergeObjects(EDGE_PROTOTYPE, customProperties)); } -MagBalls.prototype.findMatches = function(ballId) { +MagBalls.prototype.findPotentialEdges = function(nodeId) { var variances = {}; - for (var otherBallId in this.balls) { - if (otherBallId == ballId || this.areConnected(otherBallId, ballId)) { - // can't self connect or doubly connect + for (var otherNodeId in this.nodes) { + // can't self connect + if (otherNodeId == nodeId) { continue; } - var variance = this.getStickLengthVariance(ballId, otherBallId); - if (variance > BALL_DISTANCE / 4) { + + // can't doubly connect + if (this.areConnected(otherNodeId, nodeId)) { continue; } - variances[otherBallId] = variance; + + // 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.releaseBall = function(releasedBall) { - delete this.selectedBalls[releasedBall]; - debugPrint("Released ball: " + releasedBall); +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; +} - // sort other balls by distance from the stick length - var edgeTargetBall = null; - do { - var releasePosition = this.getBallPosition(releasedBall); - // Get the list of candidate connections - var variances = this.findMatches(releasedBall); - // sort them by the difference from an ideal distance - var sortedBalls = Object.keys(variances); - if (!sortedBalls.length) { +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; } - sortedBalls.sort(function(a, b){ + } + + var targets = this.findPotentialEdges(releasedBall); + for (var otherBallId in targets) { + this.createEdge(otherBallId, releasedBall); + } + this.clean(); + return; + + // sort other balls by distance from the stick length + var createdEdge = false; + while (true) { + // Get the list of candidate connections + var variances = this.findPotentialEdges(releasedBall); + // sort them by the difference from an ideal distance + var targetBalls = Object.keys(variances); + if (!targetBalls.length) { + break; + } + + // special case when there are 2 potential matches, + // try to create a ring on a plane + if (targetBalls.length == 2 && this.tryCreateRing(targetBalls, releasedBall)) { + createdEdge = true; + break; + } + + // special case when there are 3 potential matches, + // try create a fan + if (targetBalls.length == 3 && this.tryCreateFan(targetBalls, releasedBall)) { + createdEdge = true; + break; + } + + targetBalls.sort(function(a, b){ return variances[a] - variances[b]; }); - // find the nearest matching unconnected ball - edgeTargetBall = sortedBalls[0]; - // note that createEdge will preferentially move the second parameter, the target ball - } while(this.createEdge(edgeTargetBall, releasedBall)) + + // find the nearest matching unconnected ball and create an edge to it + // if possible + // note that createEdge will preferentially move the second entity, the + // released ball + // in order to create a fit + var str = "Attempt to create edge between " + targetBalls[0] + " and " + releasedBall; + if (!this.tryCreateEdge(targetBalls[0], releasedBall)) { + logDebug(str + "failed"); + var nodeDistance = this.getNodeDistance(targetBalls[0], releasedBall); + var variance = this.getVariance(nodeDistance); + logDebug("Distance was " + (nodeDistance * 100).toFixed(2) + "cm with a variance of " + variance); + break; + } + logDebug(str + " succeeded"); + + releasePosition = this.getNodePosition(releasedBall); + + // Record that we created at least one edge + createdEdge = true; + } + + if (createdEdge) { + // FIXME play a snap sound + } this.clean(); + this.validate(); } -// FIXME the Quat should be able to do this -function findOrientation(from, to) { - //float m = sqrt(2.f + 2.f * dot(u, v)); - //vec3 w = (1.f / m) * cross(u, v); - //return quat(0.5f * m, w.x, w.y, w.z); - var v2 = Vec3.normalize(Vec3.subtract(to, from)); - var v1 = { x: 0.0, y: 1.0, z: 0.0 }; - var m = Math.sqrt(2 + 2 * Vec3.dot(v1, v2)); - var w = Vec3.multiply(1.0 / m, Vec3.cross(v1, v2)); - return { - w: 0.5 * m, - x: w.x, - y: w.y, - z: w.z - }; -} - -var LINE_DIMENSIONS = 5; - -var EDGE_PROTOTYPE = { - type: "Line", - name: EDGE_NAME, - color: { red: 0, green: 255, blue: 255 }, - dimensions: { - x: LINE_DIMENSIONS, - y: LINE_DIMENSIONS, - z: LINE_DIMENSIONS - }, - lineWidth: 5, - visible: true, - ignoreCollisions: true, - collisionsWillMove: false -} - -var ZERO_VECTOR = { - x: 0, - y: 0, - z: 0 -}; - -//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 -//} - -MagBalls.prototype.createEdge = function(from, to) { - // FIXME find the constraints on from an to and determine if there is an intersection. - // Do only first order scanning for now, unless we can expose a mechanism for interacting - // to reach equilibrium via Bullet - // * a ball zero edges is fully free... - // * a ball with one edge free to move on a sphere section - // * a ball with two edges is free to move in a circle - // * a ball with more than two edges is not free - - var fromPos = this.getBallPosition(from); - var toPos = this.getBallPosition(to); +MagBalls.prototype.tryCreateEdge = function(from, to) { + var fromPos = this.getNodePosition(from); + var toPos = this.getNodePosition(to); var vector = Vec3.subtract(toPos, fromPos); - var originalLength = Vec3.length(originalLength); - + var originalLength = Vec3.length(vector); + + var variance = this.getVariance(originalLength); // if they're already at a close enough distance, just create the edge - if ((originalLength - BALL_DISTANCE) > (BALL_DISTANCE * 0.01)) { - //code - } else { - // Attempt to move the ball to match the distance + if (variance < EPSILON) { + logDebug("Length " + originalLength + " with variance of " + (variance * 100).toFixed(2) + " is within epislon " + EPSILON) ; + // close enough for government work + this.createEdge(from, to); + return true; + } + + // FIXME find the constraints on `from` and `to` and determine if there is a + // new positiong + // for 'to' that keeps it's current connections and connects with 'from' + // Do only first order scanning for now, unless we can expose a mechanism + // for interacting + // to reach equilibrium via Bullet + // * a ball zero edges is fully free... + // * a ball with one edge free to move on a sphere section + // * a ball with two edges is free to move in a circle + // * a ball with more than two edges is not free to move + + // Zero edges for the destination + var existingEdges = Object.keys(this.nodes[to]); + var edgeCount = existingEdges.length; + if (!edgeCount) { + // Easy case 1: unconnected ball + // Move the ball along it's current path to match the desired distance vector = Vec3.multiply(BALL_DISTANCE, Vec3.normalize(vector)); - // Zero edges for the destination - var edgeCount = Object.keys(this.balls[to]).length; - if (!edgeCount) { + // Add the vector to the starting position to find the new position + var newPosition = Vec3.sum(vector, fromPos); + // update the entity + Entities.editEntity(to, { position: newPosition }); + moved = true; + } else if (edgeCount > 2) { + // Easy case 2: locked position ball + // FIXME should check the target ball to see if it can be moved. + // Possible easy solution is to recurse into this.createEdge and swap + // the parameters, + // but need to prevert infinite recursion + // for now... + return false; + } else { + var connectedBalls = this.getConnectedNodes(to); + // find the other balls connected, will be either 1 or 2 + var origin = { x: 0, y: 0, z: 0 }; + for (var nodeId in connectedBalls) { + origin = Vec3.sum(origin, this.getNodePosition(nodeId)); + } + + if (edgeCount > 1) { + origin = Vec3.multiply(origin, 1 / edgeCount); + } + // logDebug("Using origin " + vec3toStr(origin)); + + if (edgeCount == 1) { + // vectors from the temp origin to the two balls. + var v1 = Vec3.subtract(toPos, origin); + var v2 = Vec3.subtract(fromPos, origin); + + // ortogonal to the solution plane + var o1 = Vec3.normalize(Vec3.cross(Vec3.normalize(v2), Vec3.normalize(v1))); + // addLine(origin, o1, COLORS.RED); // debugging + + // orthogonal to o1, lying on the solution plane + var o2 = Vec3.normalize(Vec3.cross(o1, Vec3.normalize(v2))); + // addLine(origin, o2, COLORS.YELLOW); // debugging + + // The adjacent side of a right triangle containg the + // solution as one of the points + var v3 = Vec3.multiply(0.5, v2); + + // The length of the adjacent side of the triangle + var l1 = Vec3.length(v3); + // The length of the hypotenuse + var r = BALL_DISTANCE; + + // No connection possible + if (l1 > r) { + return false; + } + + // The length of the opposite side + var l2 = Math.sqrt(r * r - l1 * l1); + + // vector with the length and direction of the opposite side + var v4 = Vec3.multiply(l2, Vec3.normalize(o2)); + + // Add the adjacent vector and the opposite vector to get the + // hypotenuse vector + var result = Vec3.sum(v3, v4); + // move back into world space + result = Vec3.sum(origin, result); // update the entity - var newPosition = Vec3.sum(vector, fromPos); - Entities.editEntity(to, { position: newPosition }); - } else if (1 == edgeCount) { - // FIXME - // find the other end of the edge already connected, call it ball2 - // given two spheres of radius BALL_DISTANCE centered at fromPos and ball2.position, - // find the closest point of intersection to toPos - // move the ball to toPos - } else if (2 == edgeCount) { - // FIXME + Entities.editEntity(to, { position: result }); } else { - // FIXME check for the ability to move fromPos - return false; + // Has a bug of some kind... validation fails after this + + // Debugging marker + //Entities.addEntity(mergeObjects(BALL_PROTOTYPE, { + // position: origin, + // color: COLORS.YELLOW, + // dimensions: Vec3.multiply(0.4, BALL_DIMENSIONS) + //})); + + var v1 = Vec3.subtract(fromPos, origin); + // addLine(origin, v1, COLORS.RED); // debugging + + + var v2 = Vec3.subtract(toPos, origin); + // addLine(origin, v2, COLORS.GREEN); // debugging + + // the lengths of v1 and v2 represent the lengths of two sides + // of the triangle we need to build. + var l1 = Vec3.length(v1); + var l2 = Vec3.length(v2); + // The remaining side is the edge we are trying to create, so + // it will be of length BALL_DISTANCE + var l3 = BALL_DISTANCE; + // given this triangle, we want to know the angle between l1 and l2 + // (this is NOT the same as the angle between v1 and v2, because we + // are trying to rotate v2 around o1 to find a solution + // Use law of cosines to find the angle: cos A = (b^2 + c^2 − a^2) / + // 2bc + var cosA = (l1 * l1 + l2 * l2 - l3 * l3) / (2.0 * l1 * l2); + + // Having this angle gives us all three angles of the right triangle + // containing + // the solution, along with the length of the hypotenuse, which is + // l2 + var hyp = l2; + // We need to find the length of the adjacent and opposite sides + // since cos(A) = adjacent / hypotenuse, then adjacent = hypotenuse + // * cos(A) + var adj = hyp * cosA; + // Pythagoras gives us the opposite side length + var opp = Math.sqrt(hyp * hyp - adj * adj); + + // v1 is the direction vector we need for the adjacent side, so + // resize it to + // the proper length + v1 = Vec3.multiply(adj, Vec3.normalize(v1)); + // addLine(origin, v1, COLORS.GREEN); // debugging + + // FIXME, these are not the right normals, because the ball needs to rotate around the origin + + // This is the normal to the plane on which our solution lies + var o1 = Vec3.cross(v1, v2); + + // Our final side is a normal to the plane defined by o1 and v1 + // and is of length opp + var o2 = Vec3.multiply(opp, Vec3.normalize(Vec3.cross(o1, v1))); + + // Our final result is the sum of v1 and o2 (opposite side vector + + // adjacent side vector) + var result = Vec3.sum(v1, o2); + // Move back into world space + result = Vec3.sum(origin, result); + // update the entity + Entities.editEntity(to, { position: result }); } } - - // Fixup existing edges - for (var edgeId in this.balls[to]) { + // Fixup existing edges if we moved the ball + for (var edgeId in this.nodes[to]) { this.fixupEdge(edgeId); } - - // FIXME, find the correct orientation for a box or model between the two balls - // for now use a line - var newEdge = Entities.addEntity(mergeObjects(EDGE_PROTOTYPE, this.findEdgeParams(from, to))); - - this.edges[newEdge] = {}; - this.edges[newEdge][from] = true; - this.edges[newEdge][to] = true; - this.balls[from][newEdge] = true; - this.balls[to][newEdge] = true; - this.validate(); + + this.createEdge(from, to); return true; } -MagBalls.prototype.findEdgeParams = function(startBall, endBall) { - var startBallPos = this.getBallPosition(startBall); - var endBallPos = this.getBallPosition(endBall); - var vector = Vec3.subtract(endBallPos, startBallPos); - return { - position: startBallPos, - linePoints: [ ZERO_VECTOR, vector ] - }; +MagBalls.prototype.tryCreateRing = function(fromBalls, to) { + // FIXME, if the user tries to connect two points, attempt to + // walk the graph and see if they're creating a ring of 4 or + // more vertices, if so and they're within N percent of lying + // on a plane, then adjust them all so they lie on a plance + return false; +} + +function pausecomp(millis) { + var date = new Date(); + var curDate = null; + do { curDate = new Date(); } while(curDate-date < millis); +} + +// find a normal between three points +function findNormal(a, b, c) { + var aa = Vec3.subtract(a, b); + var cc = Vec3.subtract(c, b); + return Vec3.cross(aa, cc); +} + +MagBalls.prototype.tryCreateFan = function(fromBalls, to) { + logDebug("Attempting to create fan"); + // if the user tries to connect three points, attempt to + // walk the graph and see if they're creating fan, adjust all the + // points to lie on a plane an equidistant from the shared vertex + + // A fan may exist if given three potential connections, two of the connection + // share and edge with the third + var a = fromBalls[0]; + var b = fromBalls[1]; + var c = fromBalls[2]; + var ab = this.areConnected(a, b); + var bc = this.areConnected(b, c); + var ca = this.areConnected(c, a); + if (ab && bc && ca) { + // tetrahedron, let the generic code handle it + return false; + } + var crux = null; + var left = null; + var right = null; + if (ab && bc) { + crux = b; + left = a; + right = c; + } else if (bc && ca) { + crux = a; + left = b; + right = a; + } else if (ca && ab) { + crux = a; + left = c; + right = b; + } + if (crux == null) { + // we don't have two nodes which share edges with the third, so fail + return false; + } + var loop = this.findShortestPath(left, right, { exclude: crux }); + if (!loop) { + return false; + } + + // find the normal to the target plane + var origin = this.getNodePosition(crux); + var normals = []; + var averageNormal = ZERO_VECTOR; + for (var i = 0; i < loop.length - 2; ++i) { + var a = loop[i]; + var b = loop[i + 1]; + var c = loop[i + 2]; + var apos = this.getNodePosition(a); + var bpos = this.getNodePosition(b); + var cpos = this.getNodePosition(c); + var normal = Vec3.normalize(findNormal(apos, bpos, cpos)); + averageNormal = Vec3.sum(averageNormal, normal); + addLine(bpos, normal, COLORS.YELLOW); + normals.push(normal); + } + averageNormal = Vec3.normalize(Vec3.multiply(1 / normals.length, averageNormal)); + + addLine(origin, averageNormal, COLORS.RED); + + // FIXME need to account for locked nodes... if there are 3 locked nodes on the loop, + // then find their cross product + // if there are more than 3 locked nodes on the loop, check if they have matching cross + // products, otherwise fail + + return false; } MagBalls.prototype.fixupEdge = function(edgeId) { var ballsInEdge = Object.keys(this.edges[edgeId]); - Entities.editEntity(edgeId, this.findEdgeParams(ballsInEdge[0], ballsInEdge[1])); + var customProperties = this.getEdgeProperties(ballsInEdge[0], ballsInEdge[1]); + Entities.editEntity(edgeId, customProperties); } -// Given two balls, how big is the difference between their distances and the stick length -MagBalls.prototype.getStickLengthVariance = function(a, b) { - var apos = this.getBallPosition(a); - var bpos = this.getBallPosition(b); - var distance = Vec3.distance(apos, bpos); - var variance = Math.abs(distance - BALL_DISTANCE); - return variance; +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; } -// FIXME remove unconnected balls +// remove unconnected balls MagBalls.prototype.clean = function() { - //var deletedBalls = {}; - //if (Object.keys(this.balls).length > 1) { - // for (var ball in this.balls) { - // if (!this.getConnections(ball)) { - // deletedBalls[ball] = true; - // Entities.deleteEntity(ball); - // } - // } - //} - //for (var ball in deletedBalls) { - // delete this.balls[ball]; - //} -} - -MagBalls.prototype.getBallPosition = function(ball) { - var properties = Entities.getEntityProperties(ball); - return properties.position; -} - -MagBalls.prototype.breakEdges = function(ballId) { - for (var edgeId in this.balls[ballId]) { - this.destroyEdge(edgeId); + // 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; } - // This shouldn't be necessary - this.balls[ballId] = {}; -} - -MagBalls.prototype.destroyEdge = function(edgeId) { - logDebug("Deleting edge " + edgeId); - // Delete the edge from other balls - for (var edgeBallId in this.edges[edgeId]) { - delete this.balls[edgeBallId][edgeId]; + 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); } - delete this.edges[edgeId]; - Entities.deleteEntity(edgeId); - this.validate(); -} - -MagBalls.prototype.destroyBall = function(ballId) { - logDebug("Deleting ball " + ballId); - breakEdges(ballId); - Entities.deleteEntity(ballId); } +// 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) { @@ -366,89 +551,23 @@ MagBalls.prototype.clear = function() { } } -MagBalls.prototype.areConnected = function(a, b) { - for (var edge in this.balls[a]) { - // edge already exists - if (this.balls[b][edge]) { - return true; - } - } - return false; -} - +// Override to check lengths as well as connection consistency MagBalls.prototype.validate = function() { - var error = false; - for (ballId in this.balls) { - for (edgeId in this.balls[ballId]) { - var edge = this.edges[edgeId]; - if (!edge) { - logError("Error: ball " + ballId + " refers to unknown edge " + edgeId); + var error = Graph.prototype.validate.call(this); + + if (!error) { + for (edgeId in this.edges) { + var length = this.getEdgeLength(edgeId); + var variance = this.getVariance(length); + if (variance > EPSILON) { + Entities.editEntity(edgeId, { color: COLORS.RED }); + + logDebug("Edge " + edgeId + " length " + (length * 100).toFixed(2) + " cm variance " + (variance * 100).toFixed(3) + "%"); error = true; - continue; - } - if (!edge[ballId]) { - logError("Error: ball " + ballId + " refers to edge " + edgeId + " but not vice versa"); - error = true; - continue; } } - } - - for (edgeId in this.edges) { - for (ballId in this.edges[edgeId]) { - var ball = this.balls[ballId]; - if (!ball) { - logError("Error: edge " + edgeId + " refers to unknown ball " + ballId); - error = true; - continue; - } - if (!ball[edgeId]) { - logError("Error: edge " + edgeId + " refers to ball " + ballId + " but not vice versa"); - error = true; - continue; - } + if (error) { + logDebug(EPSILON); } } - if (error) { - logDebug(JSON.stringify({ edges: this.edges, balls: this.balls }, null, 2)); - } } - -// FIXME fetch from a subkey of user data to support non-destructive modifications -MagBalls.prototype.setUserData = function(id, data) { - Entities.editEntity(id, { userData: JSON.stringify(data) }); -} - -// FIXME do non-destructive modification of the existing user data -MagBalls.prototype.getUserData = function(id) { - var results = null; - var properties = Entities.getEntityProperties(id); - if (properties.userData) { - results = JSON.parse(this.properties.userData); - } - return results; -} - -//MagBalls.prototype.findBalls = function() { -// var ids = Entities.findEntities(MyAvatar.position, 50); -// var result = []; -// ids.forEach(function(id) { -// var properties = Entities.getEntityProperties(id); -// if (properties.name == BALL_NAME) { -// result.push(id); -// } -// }, this); -// return result; -//}; -// -//MagBalls.prototype.findEdges = function() { -// var ids = Entities.findEntities(MyAvatar.position, 50); -// var result = []; -// ids.forEach(function(id) { -// var properties = Entities.getEntityProperties(id); -// if (properties.name == EDGE_NAME) { -// result.push(id); -// } -// }, this); -// return result; -//}; diff --git a/examples/toys/magSticks/springEdgeEntity.js b/examples/toys/magSticks/springEdgeEntity.js new file mode 100644 index 0000000000..9e0ebd2115 --- /dev/null +++ b/examples/toys/magSticks/springEdgeEntity.js @@ -0,0 +1,143 @@ +// +// 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.originalColor = properties.color; + this.desiredLength = userData.magBalls.length; + this.timeSinceLastUpdate = 0; + this.nextCheckInterval = this.MIN_CHECK_INTERVAL; + + print("preload("+entityId+") " + this.start + " -> " + this.end + " " + this.desiredLength); + + 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) { + if (_this.entityId == 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(); + print("Length is wrong: " + (length * 100).toFixed(1) + "cm variance " + variance); + + 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); + var color = { color: this.originalColor } + if (Math.abs(newVariance) > this.MAX_VARIANCE) { + color = { red: 255, green: 0, blue: 0 }; + } + print("Updating entity to new variance " + newVariance); + Entities.editEntity(this.entityId, { + color: color, + 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() { + print("Stopping spring script"); + Script.update.disconnect(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) { + Entities.editEntity(this.entityId, { + position: startPos, + linePoints: [ this.ZERO_VECTOR, this.vector ] + }); + } + this.position = startPos; + } + + this.enterEntity = function(entityId) { + print("enterEntity("+entityId+")"); + }; + + this.leaveEntity = function(entityId) { + print("leaveEntity("+entityId+")"); + }; +}); diff --git a/examples/toys/magSticks/utils.js b/examples/toys/magSticks/utils.js index 43e4f800fb..5eaf1791fb 100644 --- a/examples/toys/magSticks/utils.js +++ b/examples/toys/magSticks/utils.js @@ -1,17 +1,7 @@ -DEBUG_MAGSTICKS = true; - -debugPrint = function (str) { - if (DEBUG_MAGSTICKS) { - print(str); - } -} - -vec3toStr = function (v) { - return "{ " + - (Math.round(v.x*1000)/1000) + ", " + - (Math.round(v.y*1000)/1000) + ", " + - (Math.round(v.z*1000)/1000) + " }"; +vec3toStr = function (v, digits) { + if (!digits) { digits = 3; } + return "{ " + v.x.toFixed(digits) + ", " + v.y.toFixed(digits) + ", " + v.z.toFixed(digits)+ " }"; } scaleLine = function (start, end, scale) { @@ -31,6 +21,79 @@ 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 + } + return Entities.addEntity(mergeObjects(LINE_PROTOTYPE, { + position: origin, + linePoints: [ + ZERO_VECTOR, + vector, + ], + color: 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) }); +} + +// FIXME do non-destructive modification of the existing user data +getEntityUserData = function(id) { + var results = null; + var properties = Entities.getEntityProperties(id); + if (properties.userData) { + results = JSON.parse(this.properties.userData); + } + return results; +} + +// Non-destructively modify the user data of an entity. +setEntityCustomData = function(customKey, id, data) { + var userData = getEntityUserData(id); + userData[customKey] = data; + setEntityUserData(id, userData); +} + +getEntityCustomData = function(customKey, id, defaultValue) { + var userData = getEntityUserData(); + return userData[customKey] ? userData[customKey] : defaultValue; +} + +mergeObjects = function(proto, custom) { + var result = {}; + for (var attrname in proto) { + result[attrname] = proto[attrname]; + } + for (var attrname in custom) { + result[attrname] = custom[attrname]; + } + return result; +} + logWarn = function(str) { print(str); } @@ -44,5 +107,5 @@ logInfo = function(str) { } logDebug = function(str) { - debugPrint(str); + print(str); } \ No newline at end of file From cffb0be38487e0a38f83dd5a8081eef0d0efd011 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sat, 29 Aug 2015 22:49:14 -0700 Subject: [PATCH 3/6] Removing edge finding calculations --- examples/toys/magSticks.js | 2 +- examples/toys/magSticks/graph.js | 7 + examples/toys/magSticks/handController.js | 32 +- examples/toys/magSticks/highlighter.js | 7 + examples/toys/magSticks/magBalls.js | 376 +------------------- examples/toys/magSticks/springEdgeEntity.js | 34 +- examples/toys/magSticks/utils.js | 7 + 7 files changed, 47 insertions(+), 418 deletions(-) diff --git a/examples/toys/magSticks.js b/examples/toys/magSticks.js index 9461ffd047..1c51d1d0da 100644 --- a/examples/toys/magSticks.js +++ b/examples/toys/magSticks.js @@ -16,7 +16,7 @@ Script.include("magSticks/handController.js"); var magBalls = new MagBalls(); // Clear any previous balls -magBalls.clear(); +// magBalls.clear(); // How close do we need to be to a ball to select it.... radius + 10% var BALL_SELECTION_RADIUS = BALL_SIZE / 2.0 * 1.5; diff --git a/examples/toys/magSticks/graph.js b/examples/toys/magSticks/graph.js index 8a5c00451d..8ea9f97fc3 100644 --- a/examples/toys/magSticks/graph.js +++ b/examples/toys/magSticks/graph.js @@ -1,3 +1,10 @@ +// +// 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 +// // A collection of nodes and edges connecting them. Graph = function() { diff --git a/examples/toys/magSticks/handController.js b/examples/toys/magSticks/handController.js index cf3ef96b4a..57bef9be4a 100644 --- a/examples/toys/magSticks/handController.js +++ b/examples/toys/magSticks/handController.js @@ -1,28 +1,14 @@ - +// +// 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 +// LEFT_CONTROLLER = 0; RIGHT_CONTROLLER = 1; -WAND_LIFETIME = 30; - -var WAND_PROPERTIES = { - type: "Model", - modelURL: "file:///f:/Downloads/wand.FBX", - ignoreForCollisions: true, - dimensions: { - x: 0.1, - y: 0.1, - z: 0.1 - }, - lifetime: 30 -}; - -if (!Date.now) { - Date.now = function now() { - return new Date().getTime(); - }; -} - - +// FIXME add a customizable wand model and a mechanism to switch between wands HandController = function(side) { this.side = side; this.palm = 2 * side; @@ -38,9 +24,7 @@ HandController = function(side) { solid: true, visible: false, }); - //this.wand = Entities.addEntity(WAND_PROPERTIES); - // Connect to desired events var _this = this; Controller.actionEvent.connect(function(action, state) { diff --git a/examples/toys/magSticks/highlighter.js b/examples/toys/magSticks/highlighter.js index 7316549c84..149d9ec5b7 100644 --- a/examples/toys/magSticks/highlighter.js +++ b/examples/toys/magSticks/highlighter.js @@ -1,3 +1,10 @@ +// +// 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 SELECTION_OVERLAY = { position: { x: 0, diff --git a/examples/toys/magSticks/magBalls.js b/examples/toys/magSticks/magBalls.js index 9a0465c582..ca843fc2ef 100644 --- a/examples/toys/magSticks/magBalls.js +++ b/examples/toys/magSticks/magBalls.js @@ -1,3 +1,10 @@ +// +// 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 BALL_NAME = "MagBall" var EDGE_NAME = "MagStick" @@ -70,14 +77,7 @@ var EDGE_PROTOTYPE = LINE_PROTOTYPE; // A collection of balls and edges connecting them. MagBalls = function() { this.selectedNodes = {}; - Graph.call(this); - - var _this = this; - Script.update.connect(function(deltaTime) { - _this.onUpdate(deltaTime); - }); - Script.scriptEnding.connect(function() { _this.onCleanup(); }); @@ -94,10 +94,10 @@ MagBalls.prototype.createNodeEntity = function(customProperies) { return Entities.addEntity(mergeObjects(BALL_PROTOTYPE, customProperies)); } -MagBalls.prototype.getEdgeProperties = function(nodeIdA, nodeIdB) { +MagBalls.prototype.createEdgeEntity = function(nodeIdA, nodeIdB) { var apos = this.getNodePosition(nodeIdA); var bpos = this.getNodePosition(nodeIdB); - return { + return Entities.addEntity(mergeObjects(EDGE_PROTOTYPE, { position: apos, linePoints: [ ZERO_VECTOR, Vec3.subtract(bpos, apos) ], userData: JSON.stringify({ @@ -107,12 +107,7 @@ MagBalls.prototype.getEdgeProperties = function(nodeIdA, nodeIdB) { length: BALL_DISTANCE } }) - }; -} - -MagBalls.prototype.createEdgeEntity = function(nodeIdA, nodeIdB) { - var customProperties = this.getEdgeProperties(nodeIdA, nodeIdB) - return Entities.addEntity(mergeObjects(EDGE_PROTOTYPE, customProperties)); + })); } MagBalls.prototype.findPotentialEdges = function(nodeId) { @@ -178,340 +173,10 @@ MagBalls.prototype.releaseBall = function(releasedBall) { for (var otherBallId in targets) { this.createEdge(otherBallId, releasedBall); } - this.clean(); - return; - - // sort other balls by distance from the stick length - var createdEdge = false; - while (true) { - // Get the list of candidate connections - var variances = this.findPotentialEdges(releasedBall); - // sort them by the difference from an ideal distance - var targetBalls = Object.keys(variances); - if (!targetBalls.length) { - break; - } - - // special case when there are 2 potential matches, - // try to create a ring on a plane - if (targetBalls.length == 2 && this.tryCreateRing(targetBalls, releasedBall)) { - createdEdge = true; - break; - } - - // special case when there are 3 potential matches, - // try create a fan - if (targetBalls.length == 3 && this.tryCreateFan(targetBalls, releasedBall)) { - createdEdge = true; - break; - } - - targetBalls.sort(function(a, b){ - return variances[a] - variances[b]; - }); - - // find the nearest matching unconnected ball and create an edge to it - // if possible - // note that createEdge will preferentially move the second entity, the - // released ball - // in order to create a fit - var str = "Attempt to create edge between " + targetBalls[0] + " and " + releasedBall; - if (!this.tryCreateEdge(targetBalls[0], releasedBall)) { - logDebug(str + "failed"); - var nodeDistance = this.getNodeDistance(targetBalls[0], releasedBall); - var variance = this.getVariance(nodeDistance); - logDebug("Distance was " + (nodeDistance * 100).toFixed(2) + "cm with a variance of " + variance); - break; - } - logDebug(str + " succeeded"); - - releasePosition = this.getNodePosition(releasedBall); - - // Record that we created at least one edge - createdEdge = true; - } - - if (createdEdge) { - // FIXME play a snap sound - } - this.clean(); this.validate(); } -MagBalls.prototype.tryCreateEdge = function(from, to) { - var fromPos = this.getNodePosition(from); - var toPos = this.getNodePosition(to); - var vector = Vec3.subtract(toPos, fromPos); - var originalLength = Vec3.length(vector); - - var variance = this.getVariance(originalLength); - // if they're already at a close enough distance, just create the edge - if (variance < EPSILON) { - logDebug("Length " + originalLength + " with variance of " + (variance * 100).toFixed(2) + " is within epislon " + EPSILON) ; - // close enough for government work - this.createEdge(from, to); - return true; - } - - // FIXME find the constraints on `from` and `to` and determine if there is a - // new positiong - // for 'to' that keeps it's current connections and connects with 'from' - // Do only first order scanning for now, unless we can expose a mechanism - // for interacting - // to reach equilibrium via Bullet - // * a ball zero edges is fully free... - // * a ball with one edge free to move on a sphere section - // * a ball with two edges is free to move in a circle - // * a ball with more than two edges is not free to move - - // Zero edges for the destination - var existingEdges = Object.keys(this.nodes[to]); - var edgeCount = existingEdges.length; - if (!edgeCount) { - // Easy case 1: unconnected ball - // Move the ball along it's current path to match the desired distance - vector = Vec3.multiply(BALL_DISTANCE, Vec3.normalize(vector)); - // Add the vector to the starting position to find the new position - var newPosition = Vec3.sum(vector, fromPos); - // update the entity - Entities.editEntity(to, { position: newPosition }); - moved = true; - } else if (edgeCount > 2) { - // Easy case 2: locked position ball - // FIXME should check the target ball to see if it can be moved. - // Possible easy solution is to recurse into this.createEdge and swap - // the parameters, - // but need to prevert infinite recursion - // for now... - return false; - } else { - var connectedBalls = this.getConnectedNodes(to); - // find the other balls connected, will be either 1 or 2 - var origin = { x: 0, y: 0, z: 0 }; - for (var nodeId in connectedBalls) { - origin = Vec3.sum(origin, this.getNodePosition(nodeId)); - } - - if (edgeCount > 1) { - origin = Vec3.multiply(origin, 1 / edgeCount); - } - // logDebug("Using origin " + vec3toStr(origin)); - - if (edgeCount == 1) { - // vectors from the temp origin to the two balls. - var v1 = Vec3.subtract(toPos, origin); - var v2 = Vec3.subtract(fromPos, origin); - - // ortogonal to the solution plane - var o1 = Vec3.normalize(Vec3.cross(Vec3.normalize(v2), Vec3.normalize(v1))); - // addLine(origin, o1, COLORS.RED); // debugging - - // orthogonal to o1, lying on the solution plane - var o2 = Vec3.normalize(Vec3.cross(o1, Vec3.normalize(v2))); - // addLine(origin, o2, COLORS.YELLOW); // debugging - - // The adjacent side of a right triangle containg the - // solution as one of the points - var v3 = Vec3.multiply(0.5, v2); - - // The length of the adjacent side of the triangle - var l1 = Vec3.length(v3); - // The length of the hypotenuse - var r = BALL_DISTANCE; - - // No connection possible - if (l1 > r) { - return false; - } - - // The length of the opposite side - var l2 = Math.sqrt(r * r - l1 * l1); - - // vector with the length and direction of the opposite side - var v4 = Vec3.multiply(l2, Vec3.normalize(o2)); - - // Add the adjacent vector and the opposite vector to get the - // hypotenuse vector - var result = Vec3.sum(v3, v4); - // move back into world space - result = Vec3.sum(origin, result); - // update the entity - Entities.editEntity(to, { position: result }); - } else { - // Has a bug of some kind... validation fails after this - - // Debugging marker - //Entities.addEntity(mergeObjects(BALL_PROTOTYPE, { - // position: origin, - // color: COLORS.YELLOW, - // dimensions: Vec3.multiply(0.4, BALL_DIMENSIONS) - //})); - - var v1 = Vec3.subtract(fromPos, origin); - // addLine(origin, v1, COLORS.RED); // debugging - - - var v2 = Vec3.subtract(toPos, origin); - // addLine(origin, v2, COLORS.GREEN); // debugging - - // the lengths of v1 and v2 represent the lengths of two sides - // of the triangle we need to build. - var l1 = Vec3.length(v1); - var l2 = Vec3.length(v2); - // The remaining side is the edge we are trying to create, so - // it will be of length BALL_DISTANCE - var l3 = BALL_DISTANCE; - // given this triangle, we want to know the angle between l1 and l2 - // (this is NOT the same as the angle between v1 and v2, because we - // are trying to rotate v2 around o1 to find a solution - // Use law of cosines to find the angle: cos A = (b^2 + c^2 − a^2) / - // 2bc - var cosA = (l1 * l1 + l2 * l2 - l3 * l3) / (2.0 * l1 * l2); - - // Having this angle gives us all three angles of the right triangle - // containing - // the solution, along with the length of the hypotenuse, which is - // l2 - var hyp = l2; - // We need to find the length of the adjacent and opposite sides - // since cos(A) = adjacent / hypotenuse, then adjacent = hypotenuse - // * cos(A) - var adj = hyp * cosA; - // Pythagoras gives us the opposite side length - var opp = Math.sqrt(hyp * hyp - adj * adj); - - // v1 is the direction vector we need for the adjacent side, so - // resize it to - // the proper length - v1 = Vec3.multiply(adj, Vec3.normalize(v1)); - // addLine(origin, v1, COLORS.GREEN); // debugging - - // FIXME, these are not the right normals, because the ball needs to rotate around the origin - - // This is the normal to the plane on which our solution lies - var o1 = Vec3.cross(v1, v2); - - // Our final side is a normal to the plane defined by o1 and v1 - // and is of length opp - var o2 = Vec3.multiply(opp, Vec3.normalize(Vec3.cross(o1, v1))); - - // Our final result is the sum of v1 and o2 (opposite side vector + - // adjacent side vector) - var result = Vec3.sum(v1, o2); - // Move back into world space - result = Vec3.sum(origin, result); - // update the entity - Entities.editEntity(to, { position: result }); - } - } - - // Fixup existing edges if we moved the ball - for (var edgeId in this.nodes[to]) { - this.fixupEdge(edgeId); - } - - this.createEdge(from, to); - return true; -} - -MagBalls.prototype.tryCreateRing = function(fromBalls, to) { - // FIXME, if the user tries to connect two points, attempt to - // walk the graph and see if they're creating a ring of 4 or - // more vertices, if so and they're within N percent of lying - // on a plane, then adjust them all so they lie on a plance - return false; -} - -function pausecomp(millis) { - var date = new Date(); - var curDate = null; - do { curDate = new Date(); } while(curDate-date < millis); -} - -// find a normal between three points -function findNormal(a, b, c) { - var aa = Vec3.subtract(a, b); - var cc = Vec3.subtract(c, b); - return Vec3.cross(aa, cc); -} - -MagBalls.prototype.tryCreateFan = function(fromBalls, to) { - logDebug("Attempting to create fan"); - // if the user tries to connect three points, attempt to - // walk the graph and see if they're creating fan, adjust all the - // points to lie on a plane an equidistant from the shared vertex - - // A fan may exist if given three potential connections, two of the connection - // share and edge with the third - var a = fromBalls[0]; - var b = fromBalls[1]; - var c = fromBalls[2]; - var ab = this.areConnected(a, b); - var bc = this.areConnected(b, c); - var ca = this.areConnected(c, a); - if (ab && bc && ca) { - // tetrahedron, let the generic code handle it - return false; - } - var crux = null; - var left = null; - var right = null; - if (ab && bc) { - crux = b; - left = a; - right = c; - } else if (bc && ca) { - crux = a; - left = b; - right = a; - } else if (ca && ab) { - crux = a; - left = c; - right = b; - } - if (crux == null) { - // we don't have two nodes which share edges with the third, so fail - return false; - } - var loop = this.findShortestPath(left, right, { exclude: crux }); - if (!loop) { - return false; - } - - // find the normal to the target plane - var origin = this.getNodePosition(crux); - var normals = []; - var averageNormal = ZERO_VECTOR; - for (var i = 0; i < loop.length - 2; ++i) { - var a = loop[i]; - var b = loop[i + 1]; - var c = loop[i + 2]; - var apos = this.getNodePosition(a); - var bpos = this.getNodePosition(b); - var cpos = this.getNodePosition(c); - var normal = Vec3.normalize(findNormal(apos, bpos, cpos)); - averageNormal = Vec3.sum(averageNormal, normal); - addLine(bpos, normal, COLORS.YELLOW); - normals.push(normal); - } - averageNormal = Vec3.normalize(Vec3.multiply(1 / normals.length, averageNormal)); - - addLine(origin, averageNormal, COLORS.RED); - - // FIXME need to account for locked nodes... if there are 3 locked nodes on the loop, - // then find their cross product - // if there are more than 3 locked nodes on the loop, check if they have matching cross - // products, otherwise fail - - return false; -} - -MagBalls.prototype.fixupEdge = function(edgeId) { - var ballsInEdge = Object.keys(this.edges[edgeId]); - var customProperties = this.getEdgeProperties(ballsInEdge[0], ballsInEdge[1]); - Entities.editEntity(edgeId, customProperties); -} MagBalls.prototype.getVariance = function(distance) { // Given two points, how big is the difference between their distance @@ -550,24 +215,3 @@ MagBalls.prototype.clear = function() { }, this); } } - -// Override to check lengths as well as connection consistency -MagBalls.prototype.validate = function() { - var error = Graph.prototype.validate.call(this); - - if (!error) { - for (edgeId in this.edges) { - var length = this.getEdgeLength(edgeId); - var variance = this.getVariance(length); - if (variance > EPSILON) { - Entities.editEntity(edgeId, { color: COLORS.RED }); - - logDebug("Edge " + edgeId + " length " + (length * 100).toFixed(2) + " cm variance " + (variance * 100).toFixed(3) + "%"); - error = true; - } - } - if (error) { - logDebug(EPSILON); - } - } -} diff --git a/examples/toys/magSticks/springEdgeEntity.js b/examples/toys/magSticks/springEdgeEntity.js index 9e0ebd2115..56ef55e5d2 100644 --- a/examples/toys/magSticks/springEdgeEntity.js +++ b/examples/toys/magSticks/springEdgeEntity.js @@ -6,7 +6,6 @@ // 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; @@ -18,26 +17,22 @@ var userData = JSON.parse(properties.userData); this.start = userData.magBalls.start; this.end = userData.magBalls.end; - this.originalColor = properties.color; this.desiredLength = userData.magBalls.length; this.timeSinceLastUpdate = 0; this.nextCheckInterval = this.MIN_CHECK_INTERVAL; - print("preload("+entityId+") " + this.start + " -> " + this.end + " " + this.desiredLength); - + // 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) { - if (_this.entityId == entityId) { - _this.onCleanup(); - } + _this.onCleanup(); }); }; @@ -57,20 +52,12 @@ return; } this.decrementCheckInterval(); - print("Length is wrong: " + (length * 100).toFixed(1) + "cm variance " + variance); - 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); - var color = { color: this.originalColor } - if (Math.abs(newVariance) > this.MAX_VARIANCE) { - color = { red: 255, green: 0, blue: 0 }; - } - print("Updating entity to new variance " + newVariance); Entities.editEntity(this.entityId, { - color: color, position: newPosition, linePoints: [ this.ZERO_VECTOR, newVector ] }); @@ -80,7 +67,6 @@ Entities.editEntity(this.end, { position: Vec3.sum(newPosition, newVector) }); - } } @@ -93,8 +79,10 @@ } this.onCleanup = function() { - print("Stopping spring script"); - Script.update.disconnect(this.updateWrapper); + if (this.updateWrapper) { + Script.update.disconnect(this.updateWrapper); + delete this.updateWrapper; + } } this.getVariance = function(length) { @@ -132,12 +120,4 @@ } this.position = startPos; } - - this.enterEntity = function(entityId) { - print("enterEntity("+entityId+")"); - }; - - this.leaveEntity = function(entityId) { - print("leaveEntity("+entityId+")"); - }; }); diff --git a/examples/toys/magSticks/utils.js b/examples/toys/magSticks/utils.js index 5eaf1791fb..8a522ac41f 100644 --- a/examples/toys/magSticks/utils.js +++ b/examples/toys/magSticks/utils.js @@ -1,3 +1,10 @@ +// +// 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 +// vec3toStr = function (v, digits) { if (!digits) { digits = 3; } From a574251173e78477043adb73f224d7faf584102f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sat, 29 Aug 2015 23:06:01 -0700 Subject: [PATCH 4/6] Cleaning up and moving entity script --- .../magBallEdge.js} | 1 + examples/toys/{magSticks => }/magBalls.js | 93 ++++++++++++++++++- examples/toys/magSticks.js | 93 ------------------- 3 files changed, 92 insertions(+), 95 deletions(-) rename examples/{toys/magSticks/springEdgeEntity.js => entityScripts/magBallEdge.js} (99%) rename examples/toys/{magSticks => }/magBalls.js (63%) delete mode 100644 examples/toys/magSticks.js diff --git a/examples/toys/magSticks/springEdgeEntity.js b/examples/entityScripts/magBallEdge.js similarity index 99% rename from examples/toys/magSticks/springEdgeEntity.js rename to examples/entityScripts/magBallEdge.js index 56ef55e5d2..5411a82088 100644 --- a/examples/toys/magSticks/springEdgeEntity.js +++ b/examples/entityScripts/magBallEdge.js @@ -113,6 +113,7 @@ 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 ] diff --git a/examples/toys/magSticks/magBalls.js b/examples/toys/magBalls.js similarity index 63% rename from examples/toys/magSticks/magBalls.js rename to examples/toys/magBalls.js index ca843fc2ef..46d4905d0e 100644 --- a/examples/toys/magSticks/magBalls.js +++ b/examples/toys/magBalls.js @@ -1,13 +1,20 @@ // -// Created by Bradley Austin Davis on 2015/08/29 +// 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, @@ -47,6 +54,10 @@ var LINE_DIMENSIONS = { 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, @@ -56,7 +67,7 @@ var LINE_PROTOTYPE = { visible: true, ignoreCollisions: true, collisionsWillMove: false, - script: "file:/Users/bdavis/Git/hifi/examples/toys/magSticks/springEdgeEntity.js" + script: EDGE_ENTITY_SCRIPT } var EDGE_PROTOTYPE = LINE_PROTOTYPE; @@ -215,3 +226,81 @@ MagBalls.prototype.clear = function() { }, 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/magSticks.js b/examples/toys/magSticks.js deleted file mode 100644 index 1c51d1d0da..0000000000 --- a/examples/toys/magSticks.js +++ /dev/null @@ -1,93 +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/magBalls.js"); -Script.include("magSticks/highlighter.js"); -Script.include("magSticks/handController.js"); - -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% -var BALL_SELECTION_RADIUS = BALL_SIZE / 2.0 * 1.5; - -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) ]; From 9d07bf1585ece2eb407abe53102d158d0953304f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 30 Aug 2015 19:20:47 -0700 Subject: [PATCH 5/6] Magballs: Removing entity scripts, using onUpdate --- examples/entityScripts/magBallEdge.js | 124 ------- examples/toys/magBalls.js | 306 ------------------ examples/toys/magBalls/ballController.js | 74 +++++ .../toys/{magSticks => magBalls}/constants.js | 72 +++++ examples/toys/magBalls/debugUtils.js | 95 ++++++ examples/toys/magBalls/edgeSpring.js | 45 +++ .../toys/{magSticks => magBalls}/graph.js | 4 + .../{magSticks => magBalls}/handController.js | 6 +- .../{magSticks => magBalls}/highlighter.js | 0 examples/toys/magBalls/magBalls.js | 289 +++++++++++++++++ examples/toys/magBalls/magBallsMain.js | 26 ++ .../toys/{magSticks => magBalls}/utils.js | 38 +-- 12 files changed, 619 insertions(+), 460 deletions(-) delete mode 100644 examples/entityScripts/magBallEdge.js delete mode 100644 examples/toys/magBalls.js create mode 100644 examples/toys/magBalls/ballController.js rename examples/toys/{magSticks => magBalls}/constants.js (50%) create mode 100644 examples/toys/magBalls/debugUtils.js create mode 100644 examples/toys/magBalls/edgeSpring.js rename examples/toys/{magSticks => magBalls}/graph.js (98%) rename examples/toys/{magSticks => magBalls}/handController.js (96%) rename examples/toys/{magSticks => magBalls}/highlighter.js (100%) create mode 100644 examples/toys/magBalls/magBalls.js create mode 100644 examples/toys/magBalls/magBallsMain.js rename examples/toys/{magSticks => magBalls}/utils.js (82%) 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) { From dc7d20ff86a75a6c9228d4dee4646e9645ce8278 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 30 Aug 2015 23:49:47 -0700 Subject: [PATCH 6/6] Working on ui --- examples/toys/magBalls/ballController.js | 33 +++++++++++- examples/toys/magBalls/handController.js | 32 +++++++++++- examples/toys/magBalls/magBalls.js | 12 +++-- examples/toys/magBalls/magBallsMain.js | 1 - examples/toys/magBalls/menuController.js | 66 ++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 examples/toys/magBalls/menuController.js diff --git a/examples/toys/magBalls/ballController.js b/examples/toys/magBalls/ballController.js index 62e2e0a4d0..0f178b2804 100644 --- a/examples/toys/magBalls/ballController.js +++ b/examples/toys/magBalls/ballController.js @@ -7,8 +7,6 @@ BallController = function(side, 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 ); @@ -72,3 +70,34 @@ BallController.prototype.onCleanup = function() { HandController.prototype.onCleanup.call(this); this.clearGhostEdges(); } + +BallController.prototype.onAltClick = function() { + return; + var target = this.magBalls.findNearestNode(this.tipPosition, BALL_SELECTION_RADIUS); + if (!target) { + logDebug(target); + return; + } + + // FIXME move to delete shape + var toDelete = {}; + var deleteQueue = [ target ]; + while (deleteQueue.length) { + var curNode = deleteQueue.shift(); + if (toDelete[curNode]) { + continue; + } + toDelete[curNode] = true; + for (var nodeId in this.magBalls.getConnectedNodes(curNode)) { + deleteQueue.push(nodeId); + } + } + for (var nodeId in toDelete) { + this.magBalls.destroyNode(nodeId); + } +} + + + +BallController.prototype.onAltRelease = function() { +} diff --git a/examples/toys/magBalls/handController.js b/examples/toys/magBalls/handController.js index 3e54c7ed1b..998d22c6f8 100644 --- a/examples/toys/magBalls/handController.js +++ b/examples/toys/magBalls/handController.js @@ -14,6 +14,7 @@ HandController = function(side) { this.palm = 2 * side; this.tip = 2 * side + 1; this.action = findAction(side ? "ACTION2" : "ACTION1"); + this.altAction = findAction(side ? "ACTION1" : "ACTION2"); this.active = false; this.tipScale = 1.4; this.pointer = Overlays.addOverlay("sphere", { @@ -41,6 +42,10 @@ HandController = function(side) { } HandController.prototype.onActionEvent = function(action, state) { + var spatialControlCount = Controller.getNumberOfSpatialControls(); + // If only 2 spacial controls, then we only have one controller active, so use either button + // otherwise, only use the specified action + if (action == this.action) { if (state) { this.onClick(); @@ -48,6 +53,14 @@ HandController.prototype.onActionEvent = function(action, state) { this.onRelease(); } } + + if (action == this.altAction) { + if (state) { + this.onAltClick(); + } else { + this.onAltRelease(); + } + } } HandController.prototype.setActive = function(active) { @@ -65,11 +78,18 @@ HandController.prototype.setActive = function(active) { } HandController.prototype.updateControllerState = function() { + // FIXME this returns data if either the left or right controller is not on the base 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 + // When on the base, hydras report a position of 0 this.setActive(Vec3.length(this.palmPos) > 0.001); + + //logDebug(Controller.getTriggerValue(0) + " " + Controller.getTriggerValue(1)); + + //if (this.active) { + // logDebug("#ctrls " + Controller.getNumberOfSpatialControls() + " Side: " + this.side + " Palm: " + this.palm + " " + vec3toStr(this.palmPos)) + //} } HandController.prototype.onCleanup = function() { @@ -95,3 +115,13 @@ HandController.prototype.onClick = function() { HandController.prototype.onRelease = function() { logDebug("Base hand controller does nothing on release"); } + +HandController.prototype.onAltClick = function() { + logDebug("Base hand controller does nothing on alt click"); +} + +HandController.prototype.onAltRelease = function() { + logDebug("Base hand controller does nothing on alt click"); +} + + diff --git a/examples/toys/magBalls/magBalls.js b/examples/toys/magBalls/magBalls.js index 9e0cbb4982..187c550073 100644 --- a/examples/toys/magBalls/magBalls.js +++ b/examples/toys/magBalls/magBalls.js @@ -15,8 +15,10 @@ Script.include("edgeSpring.js"); MagBalls = function() { Graph.call(this); + this.MAX_ADJUST_ITERATIONS = 100; this.lastUpdateAge = 0; this.stable = false; + this.adjustIterations = 0; this.selectedNodes = {}; this.edgeObjects = {}; @@ -39,6 +41,7 @@ MagBalls.prototype.onUpdate = function(deltaTime) { if (this.lastUpdateAge > UPDATE_INTERVAL) { this.lastUpdateAge = 0; if (!this.stable) { + this.adjustIterations += 1; // logDebug("Update"); var adjusted = false; var nodeAdjustResults = {}; @@ -68,9 +71,10 @@ MagBalls.prototype.onUpdate = function(deltaTime) { } }, ((UPDATE_INTERVAL * 1000) / 2)); - if (!adjusted) { + if (!adjusted || this.adjustIterations > this.MAX_ADJUST_ITERATIONS) { + this.adjustIterations = 0; this.stable = true; - } + } } } } @@ -129,6 +133,7 @@ MagBalls.prototype.grabBall = function(position, maxDist) { selected = this.createNode({ position: position }); } if (selected) { + this.stable = true; this.breakEdges(selected); this.selectedNodes[selected] = true; } @@ -159,12 +164,11 @@ MagBalls.prototype.releaseBall = function(releasedBall) { var targets = this.findPotentialEdges(releasedBall); if (!targets || !Object.keys(targets).length) { - this.destroyNode(releasedBall); +// this.destroyNode(releasedBall); } for (var otherBallId in targets) { this.createEdge(otherBallId, releasedBall); } -// this.clean(); this.validate(); } diff --git a/examples/toys/magBalls/magBallsMain.js b/examples/toys/magBalls/magBallsMain.js index 1c6bd2b159..e54b818e4a 100644 --- a/examples/toys/magBalls/magBallsMain.js +++ b/examples/toys/magBalls/magBallsMain.js @@ -21,6 +21,5 @@ 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/magBalls/menuController.js b/examples/toys/magBalls/menuController.js new file mode 100644 index 0000000000..0a076d1ff8 --- /dev/null +++ b/examples/toys/magBalls/menuController.js @@ -0,0 +1,66 @@ +Script.include("handController.js"); + +MenuController = function(side, magBalls) { + HandController.call(this, side); +} + +MenuController.prototype = Object.create( HandController.prototype ); + +MenuController.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]; + } + } +} + +MenuController.prototype.onClick = function() { + this.selected = this.magBalls.grabBall(this.tipPosition, BALL_SELECTION_RADIUS); + this.highlighter.highlight(null); +} + +MenuController.prototype.onRelease = function() { + this.clearGhostEdges(); + this.magBalls.releaseBall(this.selected); + this.selected = null; +} + +MenuController.prototype.clearGhostEdges = function() { + for(var ballId in this.ghostEdges) { + Overlays.deleteOverlay(this.ghostEdges[ballId]); + delete this.ghostEdges[ballId]; + } +} + +MenuController.prototype.onCleanup = function() { + HandController.prototype.onCleanup.call(this); + this.clearGhostEdges(); +}