diff --git a/examples/toys/magBalls/ballController.js b/examples/toys/magBalls/ballController.js new file mode 100644 index 0000000000..0f178b2804 --- /dev/null +++ b/examples/toys/magBalls/ballController.js @@ -0,0 +1,103 @@ +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 = {}; +} + +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(); +} + +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/constants.js b/examples/toys/magBalls/constants.js new file mode 100644 index 0000000000..d154910f91 --- /dev/null +++ b/examples/toys/magBalls/constants.js @@ -0,0 +1,140 @@ +// +// 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; + +DEBUG_MAGSTICKS = true; + +CUSTOM_DATA_NAME = "magBalls"; +BALL_NAME = "MagBall"; +EDGE_NAME = "MagStick"; + +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 + } +} + +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/magBalls/graph.js b/examples/toys/magBalls/graph.js new file mode 100644 index 0000000000..df02ee3628 --- /dev/null +++ b/examples/toys/magBalls/graph.js @@ -0,0 +1,281 @@ +// +// 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() { + /* Structure of nodes tree + this.nodes: { + nodeId1: { + edgeId1: true + } + nodeId2: { + edgeId1: true + }, + // Nodes can many edges + nodeId3: { + edgeId2: true + edgeId3: true + edgeId4: true + edgeId5: true + }, + // Nodes can have 0 edges + nodeId5: { + }, + ... + } + */ + 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/magBalls/handController.js b/examples/toys/magBalls/handController.js new file mode 100644 index 0000000000..998d22c6f8 --- /dev/null +++ b/examples/toys/magBalls/handController.js @@ -0,0 +1,127 @@ +// +// 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; + +// FIXME add a customizable wand model and a mechanism to switch between wands +HandController = function(side) { + this.side = 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", { + position: ZERO_VECTOR, + size: 0.01, + color: COLORS.YELLOW, + 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) { + 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(); + } else { + this.onRelease(); + } + } + + if (action == this.altAction) { + if (state) { + this.onAltClick(); + } else { + this.onAltRelease(); + } + } +} + +HandController.prototype.setActive = function(active) { + if (active == this.active) { + return; + } + logDebug("Hand controller changing active state: " + active); + this.active = active; + Overlays.editOverlay(this.pointer, { + visible: this.active + }); + Entities.editEntity(this.wand, { + visible: this.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 + 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() { + Overlays.deleteOverlay(this.pointer); +} + +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() { + logDebug("Base hand controller does nothing on click"); +} + +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/highlighter.js b/examples/toys/magBalls/highlighter.js new file mode 100644 index 0000000000..149d9ec5b7 --- /dev/null +++ b/examples/toys/magBalls/highlighter.js @@ -0,0 +1,70 @@ +// +// 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, + 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/magBalls/magBalls.js b/examples/toys/magBalls/magBalls.js new file mode 100644 index 0000000000..187c550073 --- /dev/null +++ b/examples/toys/magBalls/magBalls.js @@ -0,0 +1,293 @@ +// +// 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.MAX_ADJUST_ITERATIONS = 100; + this.lastUpdateAge = 0; + this.stable = false; + this.adjustIterations = 0; + 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) { + this.adjustIterations += 1; + // 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.adjustIterations > this.MAX_ADJUST_ITERATIONS) { + this.adjustIterations = 0; + 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.stable = true; + 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.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..e54b818e4a --- /dev/null +++ b/examples/toys/magBalls/magBallsMain.js @@ -0,0 +1,25 @@ +// +// 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/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(); +} diff --git a/examples/toys/magBalls/utils.js b/examples/toys/magBalls/utils.js new file mode 100644 index 0000000000..ea1446f858 --- /dev/null +++ b/examples/toys/magBalls/utils.js @@ -0,0 +1,106 @@ +// +// 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; } + return "{ " + v.x.toFixed(digits) + ", " + v.y.toFixed(digits) + ", " + v.z.toFixed(digits)+ " }"; +} + +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; +} + +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) { + var json = JSON.stringify(data) + Entities.editEntity(id, { userData: json }); +} + +// 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(properties.userData); + } + return results ? 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(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) { + result[attrname] = proto[attrname]; + } + for (var attrname in custom) { + result[attrname] = custom[attrname]; + } + return result; +} + +logWarn = function(str) { + print(str); +} + +logError = function(str) { + print(str); +} + +logInfo = function(str) { + print(str); +} + +logDebug = function(str) { + print(str); +} \ No newline at end of file