mirror of
https://github.com/overte-org/overte.git
synced 2025-04-21 08:04:01 +02:00
More magball work
This commit is contained in:
parent
38b62108af
commit
ebb530aaac
7 changed files with 1049 additions and 378 deletions
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
270
examples/toys/magSticks/graph.js
Normal file
270
examples/toys/magSticks/graph.js
Normal file
|
@ -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;
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
//};
|
||||
|
|
143
examples/toys/magSticks/springEdgeEntity.js
Normal file
143
examples/toys/magSticks/springEdgeEntity.js
Normal file
|
@ -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+")");
|
||||
};
|
||||
});
|
|
@ -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);
|
||||
}
|
Loading…
Reference in a new issue