Magnetic sticks and balls

This commit is contained in:
Brad Davis 2015-08-25 18:54:48 -07:00
parent 08986dcb17
commit 38b62108af
6 changed files with 767 additions and 0 deletions

View file

@ -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) ];

View file

@ -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;

View file

@ -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");
}

View file

@ -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
});
}
}

View file

@ -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;
//};

View file

@ -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);
}