babylon-navigation-mesh
Version:
A toolkit to move on navigation mesh with BABYLONJS
1,622 lines (1,281 loc) • 1.87 MB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Navigation = f()}})(function(){var define,module,exports;return (function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}return e})()({1:[function(require,module,exports){
"use strict";
var Class = require("abitbol");
var BABYLON = require("babylonjs");
var BinaryHeap = require("./BinaryHeap.js");
var Astar = Class.$extend({
init: function init(graph) {
for (var x = 0; x < graph.length; x++) {
//for(var x in graph) {
var node = graph[x];
node.f = 0;
node.g = 0;
node.h = 0;
node.cost = 1.0;
node.visited = false;
node.closed = false;
node.parent = null;
}
},
cleanUp: function cleanUp(graph) {
for (var x = 0; x < graph.length; x++) {
var node = graph[x];
delete node.f;
delete node.g;
delete node.h;
delete node.cost;
delete node.visited;
delete node.closed;
delete node.parent;
}
},
heap: function heap() {
return new BinaryHeap(function (node) {
return node.f;
});
},
search: function search(graph, start, end) {
this.init(graph);
//heuristic = heuristic || astar.manhattan;
var openHeap = this.heap();
openHeap.push(start);
while (openHeap.size() > 0) {
// Grab the lowest f(x) to process next. Heap keeps this sorted for us.
var currentNode = openHeap.pop();
// End case -- result has been found, return the traced path.
if (currentNode === end) {
var curr = currentNode;
var ret = [];
while (curr.parent) {
ret.push(curr);
curr = curr.parent;
}
this.cleanUp(ret);
return ret.reverse();
}
// Normal case -- move currentNode from open to closed, process each of its neighbours.
currentNode.closed = true;
// Find all neighbours for the current node. Optionally find diagonal neighbours as well (false by default).
var neighbours = this.neighbours(graph, currentNode);
for (var i = 0, il = neighbours.length; i < il; i++) {
var neighbour = neighbours[i];
if (neighbour.closed) {
// Not a valid node to process, skip to next neighbour.
continue;
}
// The g score is the shortest distance from start to current node.
// We need to check if the path we have arrived at this neighbour is the shortest one we have seen yet.
var gScore = currentNode.g + neighbour.cost;
var beenVisited = neighbour.visited;
if (!beenVisited || gScore < neighbour.g) {
// Found an optimal (so far) path to this node. Take score for node to see how good it is.
neighbour.visited = true;
neighbour.parent = currentNode;
if (!neighbour.centroid || !end.centroid) debugger;
neighbour.h = neighbour.h || this.heuristic(neighbour.centroid, end.centroid);
neighbour.g = gScore;
neighbour.f = neighbour.g + neighbour.h;
if (!beenVisited) {
// Pushing to heap will put it in proper place based on the 'f' value.
openHeap.push(neighbour);
} else {
// Already seen the node, but since it has been rescored we need to reorder it in the heap
openHeap.rescoreElement(neighbour);
}
}
}
}
// No result was found - empty array signifies failure to find path.
return [];
},
heuristic: function heuristic(pos1, pos2) {
return BABYLON.Vector3.DistanceSquared(pos1, pos2);
},
neighbours: function neighbours(graph, node) {
var ret = [];
for (var e = 0; e < node.neighbours.length; e++) {
ret.push(graph[node.neighbours[e]]);
}
return ret;
}
});
module.exports = Astar;
},{"./BinaryHeap.js":2,"abitbol":5,"babylonjs":7}],2:[function(require,module,exports){
"use strict";
var Class = require("abitbol");
var BinaryHeap = Class.$extend({
__init__: function __init__(scoreFunction) {
this.content = [];
this.scoreFunction = scoreFunction || new function () {}();
},
push: function push(element) {
// Add the new element to the end of the array.
this.content.push(element);
// Allow it to sink down.
this.sinkDown(this.content.length - 1);
},
pop: function pop() {
// Store the first element so we can return it later.
var result = this.content[0];
// Get the element at the end of the array.
var end = this.content.pop();
// If there are any elements left, put the end element at the
// start, and let it bubble up.
if (this.content.length > 0) {
this.content[0] = end;
this.bubbleUp(0);
}
return result;
},
remove: function remove(node) {
var i = this.content.indexOf(node);
// When it is found, the process seen in 'pop' is repeated
// to fill up the hole.
var end = this.content.pop();
if (i !== this.content.length - 1) {
this.content[i] = end;
if (this.scoreFunction(end) < this.scoreFunction(node)) {
this.sinkDown(i);
} else {
this.bubbleUp(i);
}
}
},
size: function size() {
return this.content.length;
},
rescoreElement: function rescoreElement(node) {
this.sinkDown(this.content.indexOf(node));
},
sinkDown: function sinkDown(n) {
// Fetch the element that has to be sunk.
var element = this.content[n];
// When at 0, an element can not sink any further.
while (n > 0) {
// Compute the parent element's index, and fetch it.
var parentN = (n + 1 >> 1) - 1,
parent = this.content[parentN];
// Swap the elements if the parent is greater.
if (this.scoreFunction(element) < this.scoreFunction(parent)) {
this.content[parentN] = element;
this.content[n] = parent;
// Update 'n' to continue at the new position.
n = parentN;
}
// Found a parent that is less, no need to sink any further.
else {
break;
}
}
},
bubbleUp: function bubbleUp(n) {
// Look up the target element and its score.
var length = this.content.length,
element = this.content[n],
elemScore = this.scoreFunction(element);
while (true) {
// Compute the indices of the child elements.
var child2N = n + 1 << 1,
child1N = child2N - 1;
// This is used to store the new position of the element,
// if any.
var swap = null;
// If the first child exists (is inside the array)...
if (child1N < length) {
// Look it up and compute its score.
var child1 = this.content[child1N],
child1Score = this.scoreFunction(child1);
// If the score is less than our element's, we need to swap.
if (child1Score < elemScore) swap = child1N;
}
// Do the same checks for the other child.
if (child2N < length) {
var child2 = this.content[child2N],
child2Score = this.scoreFunction(child2);
if (child2Score < (swap === null ? elemScore : child1Score)) {
swap = child2N;
}
}
// If the element needs to be moved, swap it, and continue.
if (swap !== null) {
this.content[n] = this.content[swap];
this.content[swap] = element;
n = swap;
}
// Otherwise, we are done.
else {
break;
}
}
}
});
module.exports = BinaryHeap;
},{"abitbol":5}],3:[function(require,module,exports){
"use strict";
var Class = require("abitbol");
var BABYLON = require("babylonjs");
var Channel = Class.$extend({
__init__: function __init__() {
this.portals = [];
},
push: function push(p1, p2) {
if (p2 === undefined) p2 = p1;
this.portals.push({
left: p1,
right: p2
});
},
_vequal: function _vequal(a, b) {
return BABYLON.Vector3.DistanceSquared(a, b) < 0.00001;
},
_triarea2: function _triarea2(a, b, c) {
var ax = b.x - a.x;
var az = b.z - a.z;
var bx = c.x - a.x;
var bz = c.z - a.z;
return bx * az - ax * bz;
},
stringPull: function stringPull() {
var portals = this.portals;
var pts = [];
// Init scan state
var portalApex, portalLeft, portalRight;
var apexIndex = 0,
leftIndex = 0,
rightIndex = 0;
portalApex = portals[0].left;
portalLeft = portals[0].left;
portalRight = portals[0].right;
// Add start point.
pts.push(portalApex);
for (var i = 1; i < portals.length; i++) {
var left = portals[i].left;
var right = portals[i].right;
// Update right vertex.
if (this._triarea2(portalApex, portalRight, right) >= 0.0) {
if (this._vequal(portalApex, portalRight) || this._triarea2(portalApex, portalLeft, right) < 0.0) {
// Tighten the funnel.
portalRight = right;
rightIndex = i;
} else {
// Right over left, insert left to path and restart scan from portal left point.
pts.push(portalLeft);
// Make current left the new apex.
portalApex = portalLeft;
apexIndex = leftIndex;
// Reset portal
portalLeft = portalApex;
portalRight = portalApex;
leftIndex = apexIndex;
rightIndex = apexIndex;
// Restart scan
i = apexIndex;
continue;
}
}
// Update left vertex.
if (this._triarea2(portalApex, portalLeft, left) <= 0.0) {
if (this._vequal(portalApex, portalLeft) || this._triarea2(portalApex, portalRight, left) > 0.0) {
// Tighten the funnel.
portalLeft = left;
leftIndex = i;
} else {
// Left over right, insert right to path and restart scan from portal right point.
pts.push(portalRight);
// Make current right the new apex.
portalApex = portalRight;
apexIndex = rightIndex;
// Reset portal
portalLeft = portalApex;
portalRight = portalApex;
leftIndex = apexIndex;
rightIndex = apexIndex;
// Restart scan
i = apexIndex;
continue;
}
}
}
if (pts.length === 0 || !this._vequal(pts[pts.length - 1], portals[portals.length - 1].left)) {
// Append last point to path.
pts.push(portals[portals.length - 1].left);
}
this.path = pts;
return pts;
}
});
module.exports = Channel;
},{"abitbol":5,"babylonjs":7}],4:[function(require,module,exports){
"use strict";
var Class = require("abitbol");
var _ = require("lodash");
var Astar = require("./Astar.js");
var Channel = require("./Channel.js");
var BABYLON = require("babylonjs");
/**
* This component generates screenshots of 3D models, in order to preview them when they are rendered.
*
* @class Focus3D
* @constructor
*/
var Navigation = Class.$extend({
__init__: function __init__() {
this.zoneNodes = {};
this.astar = new Astar();
this.yTolerance = 1;
},
buildNodes: function buildNodes(mesh) {
var navigationMesh = this._buildNavigationMesh(mesh.geometry);
var zoneNodes = this._groupNavMesh(navigationMesh);
return zoneNodes;
},
setZoneData: function setZoneData(zone, data) {
this.zoneNodes[zone] = data;
},
setHeightTolerance: function setHeightTolerance(tolerance) {
this.yTolerance = tolerance;
},
getGroup: function getGroup(zone, position) {
if (!this.zoneNodes[zone]) {
return null;
}
var closestNodeGroup = null;
var distance = Infinity;
_.each(this.zoneNodes[zone].groups, function (group, index) {
_.each(group, function (node) {
var measuredDistance = BABYLON.Vector3.DistanceSquared(node.centroid, position);
if (measuredDistance < distance) {
closestNodeGroup = index;
distance = measuredDistance;
}
});
});
return closestNodeGroup;
},
getRandomNode: function getRandomNode(zone, group, nearPosition, nearRange) {
if (!this.zoneNodes[zone]) return new BABYLON.Vector3();
nearPosition = nearPosition || null;
nearRange = nearRange || 0;
var candidates = [];
var polygons = this.zoneNodes[zone].groups[group];
_.each(polygons, function (p) {
if (nearPosition && nearRange) {
if (BABYLON.Vector3.DistanceSquared(nearPosition, p.centroid) < nearRange * nearRange) {
candidates.push(p.centroid);
}
} else {
candidates.push(p.centroid);
}
});
return _.sample(candidates) || new BABYLON.Vector3();
},
projectOnNavmesh: function projectOnNavmesh(position, zone, group) {
var allNodes = this.zoneNodes[zone].groups[group];
var vertices = this.zoneNodes[zone].vertices;
var closestNode = null;
var distance = Infinity;
var finalProj = null,
proj = null,
node = null,
measuredDistance = 0;
for (var i = 0; i < allNodes.length; i++) {
node = allNodes[i];
proj = this._getProjectionOnNode(position, node, vertices);
measuredDistance = BABYLON.Vector3.DistanceSquared(proj, position);
if (measuredDistance < distance) {
distance = measuredDistance;
//this.meshes[3].position.copyFrom(proj);
finalProj = proj;
closestNode = node;
}
}
return finalProj;
},
_projectPointOnPlane: function _projectPointOnPlane(point, plane) {
var coef = BABYLON.Vector3.Dot(point, plane.normal) + plane.d;
var proj = point.subtract(plane.normal.scale(coef));
return proj;
},
_getProjectionOnNode: function _getProjectionOnNode(position, node, vertices) {
var A = this.getVectorFrom(vertices, node.vertexIds[0]);
var B = this.getVectorFrom(vertices, node.vertexIds[1]);
var C = this.getVectorFrom(vertices, node.vertexIds[2]);
var u = B.subtract(A);
var v = C.subtract(A);
var n = BABYLON.Vector3.Cross(u, v).normalize();
var plane = {
normal: n,
d: -BABYLON.Vector3.Dot(A, n)
};
var p = this._projectPointOnPlane(position, plane);
// Compute barycentric coordinates (u, v, w) for
// point p with respect to triangle (a, b, c)
var barycentric = function barycentric(p, a, b, c) {
var ret = {};
var v0 = c.subtract(a),
v1 = b.subtract(a),
v2 = p.subtract(a);
var d00 = BABYLON.Vector3.Dot(v0, v0);
var d01 = BABYLON.Vector3.Dot(v0, v1);
var d02 = BABYLON.Vector3.Dot(v0, v2);
var d11 = BABYLON.Vector3.Dot(v1, v1);
var d12 = BABYLON.Vector3.Dot(v1, v2);
var denom = d00 * d11 - d01 * d01;
ret.u = (d11 * d02 - d01 * d12) / denom;
ret.v = (d00 * d12 - d01 * d02) / denom;
ret.w = 1 - ret.u - ret.v;
return ret;
};
var bary = barycentric(p, A, B, C);
bary.u = Math.min(Math.max(bary.u, 0), 1);
bary.v = Math.min(Math.max(bary.v, 0), 1);
if (bary.u + bary.v >= 1) {
var sum = bary.u + bary.v;
bary.u /= sum;
bary.v /= sum;
}
var proj = A.add(B.subtract(A).scale(bary.v).add(C.subtract(A).scale(bary.u)));
return proj;
},
findPath: function findPath(startPosition, targetPosition, zone, group) {
var allNodes = this.zoneNodes[zone].groups[group];
var vertices = this.zoneNodes[zone].vertices;
var startingNode = null;
for (var i = 0; i < allNodes.length; i++) {
if (this._isVectorInPolygon(startPosition, allNodes[i], vertices)) {
startingNode = allNodes[i];
break;
}
}
var endNode = null;
for (var i = 0; i < allNodes.length; i++) {
if (this._isVectorInPolygon(targetPosition, allNodes[i], vertices)) {
endNode = allNodes[i];
break;
}
}
// If we can't find any node, theres no path to target
if (!startingNode || !endNode) {
return null;
}
if (startingNode.id != endNode.id) {
// if the starting node and target node are at the same polygon skip searching and funneling as there is no obstacle.
var paths = this.astar.search(allNodes, startingNode, endNode);
} else {
vectors = [];
vectors.push(new BABYLON.Vector3(targetPosition.x, targetPosition.y, targetPosition.z));
return vectors;
}
var getPortalFromTo = function getPortalFromTo(a, b) {
for (var i = 0; i < a.neighbours.length; i++) {
if (a.neighbours[i] === b.id) {
return a.portals[i];
}
}
};
// We got the corridor
// Now pull the rope
var channel = new Channel();
channel.push(startPosition);
for (var i = 0; i < paths.length; i++) {
var polygon = paths[i];
var nextPolygon = paths[i + 1];
if (nextPolygon) {
var portals = getPortalFromTo(polygon, nextPolygon);
channel.push(this.getVectorFrom(vertices, portals[0]), this.getVectorFrom(vertices, portals[1]));
}
}
channel.push(targetPosition);
channel.stringPull();
var vectors = [];
channel.path.forEach(function (c) {
var vec = new BABYLON.Vector3(c.x, c.y, c.z);
// console.log(vec.clone().sub(startPosition).length());
// Ensure the intermediate steps aren't too close to the start position
// var dist = vec.clone().sub(startPosition).lengthSq();
// if (dist > 0.01 * 0.01) {
vectors.push(vec);
// }
});
// We don't need the first one, as we already know our start position
vectors.shift();
return vectors;
},
_isPointInPoly: function _isPointInPoly(poly, pt) {
for (var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) {
(poly[i].z <= pt.z && pt.z < poly[j].z || poly[j].z <= pt.z && pt.z < poly[i].z) && pt.x < (poly[j].x - poly[i].x) * (pt.z - poly[i].z) / (poly[j].z - poly[i].z) + poly[i].x && (c = !c);
}return c;
},
_isVectorInPolygon: function _isVectorInPolygon(vector, polygon, vertices) {
// reference point will be the centroid of the polygon
// We need to rotate the vector as well as all the points which the polygon uses
var lowestPoint = 100000;
var highestPoint = -100000;
var polygonVertices = [];
_.each(polygon.vertexIds, function (vId) {
var point = this.getVectorFrom(vertices, vId);
lowestPoint = Math.min(point.y, lowestPoint);
highestPoint = Math.max(point.y, highestPoint);
polygonVertices.push(point);
}.bind(this));
if (vector.y < highestPoint + this.yTolerance && vector.y > lowestPoint - this.yTolerance && this._isPointInPoly(polygonVertices, vector)) {
return true;
}
return false;
},
_computeCentroids: function _computeCentroids(geometry) {
var centroids = [];
var indices = geometry.getIndices();
var vertices = geometry.getVerticesData(BABYLON.VertexBuffer.PositionKind);
var c = new BABYLON.Vector3(0, 0, 0);
for (var f = 0; f < indices.length; f += 3) {
var p1 = this.getVectorFrom(vertices, indices[f]);
var p2 = this.getVectorFrom(vertices, indices[f + 1]);
var p3 = this.getVectorFrom(vertices, indices[f + 2]);
c.copyFromFloats(0, 0, 0);
c.addInPlace(p1);
c.addInPlace(p2);
c.addInPlace(p3);
c.scaleInPlace(1 / 3);
centroids.push(c.clone());
}
geometry.centroids = centroids;
},
_roundNumber: function _roundNumber(number, decimals) {
var newnumber = new Number(number + '').toFixed(parseInt(decimals));
return parseFloat(newnumber);
},
_mergeVertexIds: function _mergeVertexIds(aList, bList) {
var sharedVertices = [];
aList.forEach(function (vId) {
if (_.includes(bList, vId)) {
sharedVertices.push(vId);
}
});
if (sharedVertices.length < 2) return [];
// console.log("TRYING aList:", aList, ", bList:", bList, ", sharedVertices:", sharedVertices);
if (_.includes(sharedVertices, aList[0]) && _.includes(sharedVertices, aList[aList.length - 1])) {
// Vertices on both edges are bad, so shift them once to the left
aList.push(aList.shift());
}
if (_.includes(sharedVertices, bList[0]) && _.includes(sharedVertices, bList[bList.length - 1])) {
// Vertices on both edges are bad, so shift them once to the left
bList.push(bList.shift());
}
// Again!
sharedVertices = [];
aList.forEach(function (vId) {
if (_.includes(bList, vId)) {
sharedVertices.push(vId);
}
});
var clockwiseMostSharedVertex = sharedVertices[1];
var counterClockwiseMostSharedVertex = sharedVertices[0];
var cList = _.clone(aList);
while (cList[0] !== clockwiseMostSharedVertex) {
cList.push(cList.shift());
}
var c = 0;
var temp = _.clone(bList);
while (temp[0] !== counterClockwiseMostSharedVertex) {
temp.push(temp.shift());
if (c++ > 10) break;
}
// Shave
temp.shift();
temp.pop();
cList = cList.concat(temp);
// console.log("aList:", aList, ", bList:", bList, ", cList:", cList, ", sharedVertices:", sharedVertices);
return cList;
},
_setPolygonCentroid: function _setPolygonCentroid(polygon, navigationMesh) {
var sum = new BABYLON.Vector3(0, 0, 0);
var vertices = navigationMesh.vertices;
_.each(polygon.vertexIds, function (vId) {
sum.x += vertices[vId * 3];
sum.y += vertices[vId * 3 + 1];
sum.z += vertices[vId * 3 + 2];
});
sum.scaleInPlace(1 / polygon.vertexIds.length);
polygon.centroid.copyFrom(sum);
},
getVectorFrom: function getVectorFrom(vertices, id, _vector) {
if (_vector) {
_vector.copyFromFloats(vertices[id * 3], vertices[id * 3 + 1], vertices[id * 3 + 2]);
return _vector;
}
return new BABYLON.Vector3(vertices[id * 3], vertices[id * 3 + 1], vertices[id * 3 + 2]);
},
_cleanPolygon: function _cleanPolygon(polygon, navigationMesh) {
var newVertexIds = [];
var vertices = navigationMesh.vertices;
for (var i = 0; i < polygon.vertexIds.length; i++) {
var vertex = this.getVectorFrom(vertices, polygon.vertexIds[i]);
var nextVertexId, previousVertexId;
var nextVertex, previousVertex;
// console.log("nextVertex: ", nextVertex);
if (i === 0) {
nextVertexId = polygon.vertexIds[1];
previousVertexId = polygon.vertexIds[polygon.vertexIds.length - 1];
} else if (i === polygon.vertexIds.length - 1) {
nextVertexId = polygon.vertexIds[0];
previousVertexId = polygon.vertexIds[polygon.vertexIds.length - 2];
} else {
nextVertexId = polygon.vertexIds[i + 1];
previousVertexId = polygon.vertexIds[i - 1];
}
nextVertex = this.getVectorFrom(vertices, nextVertexId);
previousVertex = this.getVectorFrom(vertices, previousVertexId);
var a = nextVertex.clone().sub(vertex);
var b = previousVertex.clone().sub(vertex);
var angle = a.angleTo(b);
// console.log(angle);
if (angle > Math.PI - 0.01 && angle < Math.PI + 0.01) {
// Unneccesary vertex
// console.log("Unneccesary vertex: ", polygon.vertexIds[i]);
// console.log("Angle between "+previousVertexId+", "+polygon.vertexIds[i]+" "+nextVertexId+" was: ", angle);
// Remove the neighbours who had this vertex
var goodNeighbours = [];
polygon.neighbours.forEach(function (neighbour) {
if (!_.includes(neighbour.vertexIds, polygon.vertexIds[i])) {
goodNeighbours.push(neighbour);
}
});
polygon.neighbours = goodNeighbours;
// TODO cleanup the list of vertices and rebuild vertexIds for all polygons
} else {
newVertexIds.push(polygon.vertexIds[i]);
}
}
// console.log("New vertexIds: ", newVertexIds);
polygon.vertexIds = newVertexIds;
this._setPolygonCentroid(polygon, navigationMesh);
},
_isConvex: function _isConvex(polygon, navigationMesh) {
var vertices = navigationMesh.vertices;
if (polygon.vertexIds.length < 3) return false;
var convex = true;
var total = 0;
var results = [];
for (var i = 0; i < polygon.vertexIds.length; i++) {
var vertex = this.getVectorFrom(vertices, polygon.vertexIds[i]);
var nextVertex, previousVertex;
// console.log("nextVertex: ", nextVertex);
if (i === 0) {
nextVertex = this.getVectorFrom(vertices, polygon.vertexIds[1]);
previousVertex = this.getVectorFrom(vertices, polygon.vertexIds[polygon.vertexIds.length - 1]);
} else if (i === polygon.vertexIds.length - 1) {
nextVertex = this.getVectorFrom(vertices, polygon.vertexIds[0]);
previousVertex = this.getVectorFrom(vertices, polygon.vertexIds[polygon.vertexIds.length - 2]);
} else {
nextVertex = this.getVectorFrom(vertices, polygon.vertexIds[i + 1]);
previousVertex = this.getVectorFrom(vertices, polygon.vertexIds[i - 1]);
}
var a = nextVertex.clone().sub(vertex);
var b = previousVertex.clone().sub(vertex);
var angle = a.angleTo(b);
total += angle;
// console.log(angle);
if (angle === Math.PI || angle === 0) return false;
var r = BABYLON.Vector3.Cross(a, b).y;
results.push(r);
// console.log("pushed: ", r);
}
// if ( total > (polygon.vertexIds.length-2)*Math.PI ) return false;
results.forEach(function (r) {
if (r === 0) convex = false;
});
if (results[0] > 0) {
results.forEach(function (r) {
if (r < 0) convex = false;
});
} else {
results.forEach(function (r) {
if (r > 0) convex = false;
});
}
// console.log("allowed: "+total+", max: "+(polygon.vertexIds.length-2)*Math.PI);
// if ( total > (polygon.vertexIds.length-2)*Math.PI ) convex = false;
// console.log("Convex: "+(convex ? "true": "false"));
return convex;
},
_buildPolygonGroups: function _buildPolygonGroups(navigationMesh) {
var polygons = navigationMesh.polygons;
var polygonGroups = [];
var groupCount = 0;
var spreadGroupId = function spreadGroupId(polygon) {
_.each(polygon.neighbours, function (neighbour) {
if (_.isUndefined(neighbour.group)) {
neighbour.group = polygon.group;
spreadGroupId(neighbour);
}
});
};
_.each(polygons, function (polygon) {
if (_.isUndefined(polygon.group)) {
polygon.group = groupCount++;
// Spread it
spreadGroupId(polygon);
}
if (!polygonGroups[polygon.group]) polygonGroups[polygon.group] = [];
polygonGroups[polygon.group].push(polygon);
});
console.log("Groups built: ", polygonGroups.length);
return polygonGroups;
},
_array_intersect: function _array_intersect() {
var i,
shortest,
nShortest,
n,
len,
ret = [],
obj = {},
nOthers;
nOthers = arguments.length - 1;
nShortest = arguments[0].length;
shortest = 0;
for (i = 0; i <= nOthers; i++) {
n = arguments[i].length;
if (n < nShortest) {
shortest = i;
nShortest = n;
}
}
for (i = 0; i <= nOthers; i++) {
n = i === shortest ? 0 : i || shortest; //Read the shortest array first. Read the first array instead of the shortest
len = arguments[n].length;
for (var j = 0; j < len; j++) {
var elem = arguments[n][j];
if (obj[elem] === i - 1) {
if (i === nOthers) {
ret.push(elem);
obj[elem] = 0;
} else {
obj[elem] = i;
}
} else if (i === 0) {
obj[elem] = 0;
}
}
}
return ret;
},
_buildPolygonNeighbours: function _buildPolygonNeighbours(polygon, navigationMesh) {
polygon.neighbours = [];
// All other nodes that contain at least two of our vertices are our neighbours
for (var i = 0, len = navigationMesh.polygons.length; i < len; i++) {
if (polygon === navigationMesh.polygons[i]) continue;
// Don't check polygons that are too far, since the intersection tests take a long time
if (BABYLON.Vector3.DistanceSquared(polygon.centroid, navigationMesh.polygons[i].centroid) > 100 * 100) continue;
var matches = this._array_intersect(polygon.vertexIds, navigationMesh.polygons[i].vertexIds);
// var matches = _.intersection(polygon.vertexIds, navigationMesh.polygons[i].vertexIds);
if (matches.length >= 2) {
polygon.neighbours.push(navigationMesh.polygons[i]);
}
}
},
_buildPolygonsFromGeometry: function _buildPolygonsFromGeometry(geometry) {
var polygons = [];
var vertices = geometry.getVerticesData(BABYLON.VertexBuffer.PositionKind);
var indices = geometry.getIndices();
var polygonId = 1;
console.log("Vertices:", vertices.length / 3, "polygons:", indices.length / 3);
// Convert the faces into a custom format that supports more than 3 vertices
for (var i = 0; i < indices.length; i += 3) {
var a = this.getVectorFrom(vertices, indices[i]);
var b = this.getVectorFrom(vertices, indices[i + 1]);
var c = this.getVectorFrom(vertices, indices[i + 2]);
var normal = BABYLON.Vector3.Cross(b.subtract(a), b.subtract(c)).normalize();
polygons.push({
id: polygonId++,
vertexIds: [indices[i], indices[i + 1], indices[i + 2]],
centroid: geometry.centroids[i / 3],
normal: normal,
neighbours: []
});
}
var navigationMesh = {
polygons: polygons,
vertices: vertices
};
// Build a list of adjacent polygons
_.each(polygons, function (polygon) {
this._buildPolygonNeighbours(polygon, navigationMesh);
}.bind(this));
return navigationMesh;
},
_cleanNavigationMesh: function _cleanNavigationMesh(navigationMesh) {
var polygons = navigationMesh.polygons;
var vertices = navigationMesh.vertices;
// Remove steep triangles
var up = new BABYLON.Vector3(0, 1, 0);
polygons = _.filter(polygons, function (polygon) {
var angle = Math.acos(BABYLON.Vector3.Dot(up, polygon.normal));
return angle < Math.PI / 4;
});
// Remove unnecessary edges using the Hertel-Mehlhorn algorithm
// 1. Find a pair of adjacent nodes (i.e., two nodes that share an edge between them)
// whose normals are nearly identical (i.e., their surfaces face the same direction).
var newPolygons = [];
_.each(polygons, function (polygon) {
if (polygon.toBeDeleted) return;
var keepLooking = true;
while (keepLooking) {
keepLooking = false;
_.each(polygon.neighbours, function (otherPolygon) {
if (polygon === otherPolygon) return;
if (Math.acos(BABYLON.Vector3.Dot(polygon.normal, otherPolygon.normal)) < 0.01) {
// That's pretty equal alright!
// Merge otherPolygon with polygon
var testPolygon = {
vertexIds: this._mergeVertexIds(polygon.vertexIds, otherPolygon.vertexIds),
neighbours: polygon.neighbours,
normal: polygon.normal.clone(),
centroid: polygon.centroid.clone()
};
this._cleanPolygon(testPolygon, navigationMesh);
if (this._isConvex(testPolygon, navigationMesh)) {
otherPolygon.toBeDeleted = true;
// Inherit the neighbours from the to be merged polygon, except ourself
_.each(otherPolygon.neighbours, function (otherPolygonNeighbour) {
// Set this poly to be merged to be no longer our neighbour
otherPolygonNeighbour.neighbours = _.without(otherPolygonNeighbour.neighbours, otherPolygon);
if (otherPolygonNeighbour !== polygon) {
// Tell the old Polygon's neighbours about the new neighbour who has merged
otherPolygonNeighbour.neighbours.push(polygon);
} else {
// For ourself, we don't need to know about ourselves
// But we inherit the old neighbours
polygon.neighbours = polygon.neighbours.concat(otherPolygon.neighbours);
polygon.neighbours = _.uniq(polygon.neighbours);
// Without ourselves in it!
polygon.neighbours = _.without(polygon.neighbours, polygon);
}
});
polygon.vertexIds = this._mergeVertexIds(polygon.vertexIds, otherPolygon.vertexIds);
this._cleanPolygon(polygon, navigationMesh);
keepLooking = true;
}
}
}.bind(this));
}
if (!polygon.toBeDeleted) {
newPolygons.push(polygon);
}
});
var isUsed = function isUsed(vId) {
var contains = false;
_.each(newPolygons, function (p) {
if (!contains && _.includes(p.vertexIds, vId)) {
contains = true;
}
});
return contains;
};
// Clean vertices
for (var i = 0; i < vertices.length; i++) {
if (!isUsed(i)) {
// Decrement all vertices that are higher than i
_.each(newPolygons, function (p) {
for (var j = 0; j < p.vertexIds.length; j++) {
if (p.vertexIds[j] > i) {
p.vertexIds[j]--;
}
}
});
vertices.splice(i, 1);
i--;
}
}
navigationMesh.polygons = newPolygons;
navigationMesh.vertices = vertices;
},
_buildNavigationMesh: function _buildNavigationMesh(geometry) {
// Prepare geometry
this._computeCentroids(geometry);
this._mergeVertices(geometry);
// BABYLON.GeometryUtils.triangulateQuads(geometry);
// console.log("vertices:", geometry.vertices.length, "polygons:", geometry.faces.length);
var navigationMesh = this._buildPolygonsFromGeometry(geometry);
// cleanNavigationMesh(navigationMesh);
// console.log("Pre-clean:", navigationMesh.polygons.length, "polygons,", navigationMesh.vertices.length, "vertices.");
// console.log("")
// console.log("Vertices:", navigationMesh.vertices.length, "polygons,", navigationMesh.polygons.length, "vertices.");
return navigationMesh;
},
_mergeVertices: function _mergeVertices(geometry) {
var verticesMap = {}; // Hashmap for looking up vertices by position coordinates (and making sure they are unique)
var unique = [],
changes = [];
var v, key;
var precisionPoints = 4; // number of decimal points, e.g. 4 for epsilon of 0.0001
var precision = Math.pow(10, precisionPoints);
var indices;
var ind = geometry.getIndices(),
vert = geometry.getVerticesData(BABYLON.VertexBuffer.PositionKind);
for (var i = 0; i < vert.length; i += 3) {
v = new BABYLON.Vector3(vert[i], vert[i + 1], vert[i + 2]);
key = Math.round(v.x * precision) + '_' + Math.round(v.y * precision) + '_' + Math.round(v.z * precision);
if (verticesMap[key] === undefined) {
verticesMap[key] = i / 3;
unique.push(v.clone());
changes[i / 3] = unique.length - 1;
} else {
//console.log('Duplicate vertex found. ', i, ' could be using ', verticesMap[key]);
changes[i / 3] = changes[verticesMap[key]];
}
}
// if faces are completely degenerate after merging vertices, we
// have to remove them from the geometry.
var faceIndicesToRemove = [];
for (i = 0; i < ind.length; i += 3) {
ind[i] = changes[ind[i]];
ind[i + 1] = changes[ind[i + 1]];
ind[i + 2] = changes[ind[i + 2]];
indices = [ind[i], ind[i + 1], ind[i + 2]];
var dupIndex = -1;
// if any duplicate vertices are found in a Face3
// we have to remove the face as nothing can be saved
for (var n = 0; n < 3; n++) {
if (indices[n] === indices[(n + 1) % 3]) {
dupIndex = n;
faceIndicesToRemove.push(i);
break;
}
}
}
for (i = faceIndicesToRemove.length - 1; i >= 0; i--) {
var idx = faceIndicesToRemove[i];
ind.splice(idx, 3);
}
// Use unique set of vertices
var diff = vert.length / 3 - unique.length;
vert = [];
for (i = 0; i < unique.length; i++) {
vert.push(unique[i].x, unique[i].y, unique[i].z);
}
geometry.setIndices(ind);
geometry.setVerticesData(BABYLON.VertexBuffer.PositionKind, vert);
return diff;
},
_getSharedVerticesInOrder: function _getSharedVerticesInOrder(a, b) {
var aList = a.vertexIds;
var bList = b.vertexIds;
var sharedVertices = [];
_.each(aList, function (vId) {
if (_.includes(bList, vId)) {
sharedVertices.push(vId);
}
});
if (sharedVertices.length < 2) return [];
// console.log("TRYING aList:", aList, ", bList:", bList, ", sharedVertices:", sharedVertices);
if (_.includes(sharedVertices, aList[0]) && _.includes(sharedVertices, aList[aList.length - 1])) {
// Vertices on both edges are bad, so shift them once to the left
aList.push(aList.shift());
}
if (_.includes(sharedVertices, bList[0]) && _.includes(sharedVertices, bList[bList.length - 1])) {
// Vertices on both edges are bad, so shift them once to the left
bList.push(bList.shift());
}
// Again!
sharedVertices = [];
_.each(aList, function (vId) {
if (_.includes(bList, vId)) {
sharedVertices.push(vId);
}
});
return sharedVertices;
},
_groupNavMesh: function _groupNavMesh(navigationMesh) {
var saveObj = {};
_.each(navigationMesh.vertices, function (v) {
v = this._roundNumber(v, 2);
}.bind(this));
saveObj.vertices = navigationMesh.vertices;
var groups = this._buildPolygonGroups(navigationMesh);
saveObj.groups = [];
var findPolygonIndex = function findPolygonIndex(group, p) {
for (var i = 0; i < group.length; i++) {
if (p === group[i]) return i;
}
};
_.each(groups, function (group) {
var newGroup = [];
_.each(group, function (p) {
var neighbours = [];
_.each(p.neighbours, function (n) {
neighbours.push(findPolygonIndex(group, n));
});
// Build a portal list to each neighbour
var portals = [];
_.each(p.neighbours, function (n) {
portals.push(this._getSharedVerticesInOrder(p, n));
}.bind(this));
p.centroid.x = this._roundNumber(p.centroid.x, 2);
p.centroid.y = this._roundNumber(p.centroid.y, 2);
p.centroid.z = this._roundNumber(p.centroid.z, 2);
newGroup.push({
id: findPolygonIndex(group, p),
neighbours: neighbours,
vertexIds: p.vertexIds,
centroid: p.centroid,
portals: portals
});
}.bind(this));
saveObj.groups.push(newGroup);
}.bind(this));
return saveObj;
}
});
module.exports = Navigation;
},{"./Astar.js":1,"./Channel.js":3,"abitbol":5,"babylonjs":7,"lodash":8}],5:[function(require,module,exports){
"use strict";
var extractAnnotations = require("./annotation.js");
var _disableConstructor = false;
// Inherit from a class without calling its constructor.
function inherit(SuperClass) {
_disableConstructor = true;
var __class__ = new SuperClass();
_disableConstructor = false;
return __class__;
}
// Checks if the given function uses abitbol special properties ($super, $name,...)
function usesSpecialProperty(fn) {
return Boolean(fn.toString().match(/.*(\$super|\$name|\$computedPropertyName).*/));
}
var Class = function () {};
Object.defineProperty(Class, "$class", {
enumerable: false,
value: Class
});
Object.defineProperty(Class, "$map", {
enumerable: false,
value: {
attributes: {},
methods: {},
computedProperties: {}
}
});
Object.defineProperty(Class, "$extend", {
enumerable: false,
value: function (properties) {
var _superClass = this;
var _classMap = JSON.parse(JSON.stringify(_superClass.$map)); // not pretty :s
// New class
var __class__ = function () {
if (_disableConstructor) {
return;
}
// Abitbol special properties
Object.defineProperty(this, "$class", {
enumerable: false,
value: __class__
});
Object.defineProperty(this, "$map", {
enumerable: false,
value: _classMap
});
Object.defineProperty(this, "$data", {
enumerable: false,
value: {}
});
// Computed properties
for (var property in _classMap.computedProperties) {
Object.defineProperty(this, property, {
enumerable: true,
configurable: false,
get: (_classMap.computedProperties[property].get !== undefined) ? (function (accessorName) {
return function () {
return this[accessorName].apply(this, arguments);
};
})(_classMap.computedProperties[property].get) : undefined, // jshint ignore:line
set: (_classMap.computedProperties[property].set !== undefined) ? (function (mutatorName) {
return function () {
return this[mutatorName].apply(this, arguments);
};
})(_classMap.computedProperties[property].set) : undefined // jshint ignore:line
});
}
// Bind this
for (var method in _classMap.methods) {
this[method] = this[method].bind(this);
}
// Call the constructor if any
if (this.__init__) {
this.__init__.apply(this, arguments);
}
return this;
};
// Inheritance
__class__.prototype = inherit(this.$class);
properties = properties || {};
var property;
var computedPropertyName;
var annotations;
var i;
var mixin;
// Copy properties from mixins
if (properties.__include__) {
for (i = properties.__include__.length - 1 ; i >= 0 ; i--) {
mixin = properties.__include__[i];
for (property in mixin) {
if (property == "__classvars__") {
continue;
} else if (properties[property] === undefined) {
properties[property] = mixin[property];
}
}
// Merging mixin's static properties
if (mixin.__classvars__) {
if (!properties.__classvars__) {
properties.__classvars__ = {};
}
for (property in mixin.__classvars__) {
if (properties.__classvars__[property] === undefined) {
properties.__classvars__[property] = mixin.__classvars__[property];
}
}
}
}
}
// Add properties
for (property in properties || {}) {
if (property == "__include__" || property == "__classvars__") {
continue;
}
if (typeof properties[property] == "function") {
computedPropertyName = undefined;
_classMap.methods[property] = {annotations: {}};
// Accessors / Mutators
if (property.indexOf("get") === 0) {
computedPropertyName = property.slice(3, 4).toLowerCase() + property.slice(4, property.length);
if (!_classMap.computedProperties[computedPropertyName]) {
_classMap.computedProperties[computedPropertyName] = {annotations: {}};
}
_classMap.computedProperties[computedPropertyName].get = property;
} else if (property.indexOf("set") === 0) {
computedPropertyName = property.slice(3, 4).toLowerCase() + property.slice(4, property.length);
if (!_classMap.computedProperties[computedPropertyName]) {
_classMap.computedProperties[computedPropertyName] = {annotations: {}};
}
_classMap.computedProperties[computedPropertyName].set = property;
} else if (property.indexOf("has") === 0) {
computedPropertyName = property.slice(3, 4).toLowerCase() + property.slice(4, property.length);
if (!_classMap.computedProperties[computedPropertyName]) {
_classMap.computedProperties[computedPropertyName] = {annotations: {}};
}
_classMap.computedProperties[computedPropertyName].get = property;
} else if (property.indexOf("is") === 0) {
computedPropertyName = property.slice(2, 3).toLowerCase() + property.slice(3, property.length);
if (!_classMap.computedProperties[computedPropertyName]) {
_classMap.computedProperties[computedPropertyName] = {annotations: {}};
}
_classMap.computedProperties[computedPropertyName].get = property;
}
// Annotations
annotations = extractAnnotations(properties[property]);
for (var annotation in annotations) {
_classMap.methods[property].annotations[annotation] = annotations[annotation];
if (computedPropertyName) {
_classMap.computedProperties[computedPropertyName]
.annotations[annotation] = annotations[annotation];
}
}
// Wrapped method
if (usesSpecialProperty(properties[property])) {
__class__.prototype[property] = (function (method, propertyName, computedPropertyName) {
return function () {
var _oldSuper = this.$super;
var _oldName = this.$name;
var _oldComputedPropertyName = this.$computedPropertyName;
this.$super = _superClass.prototype[propertyName];
this.$name = propertyName;
this.$computedPropertyName = computedPropertyName;
try {
return method.apply(this, arguments);
} finally {
if (_oldSuper) {
this.$super = _oldSuper;
} else {
delete this.$super;
}
if (_oldName) {
this.$name = _oldName;
} else {
delete this.$name;
}
if (_oldComputedPropertyName) {
this.$computedPropertyName = _oldComputedPropertyName;
} else {
delete this.$computedPropertyName;
}
}
};
})(properties[property], property, computedPropertyName); // jshint ignore:line
// Simple methods
} else {
__class__.prototype[property] = properties[property];
}
} else {
_classMap.attributes[property] = true;
__class__.prototype[property] = properties[property];
}
}
// Copy super class static properties
var scStaticProps = Object.getOwnPropertyNames(_superClass);
// Removes caller, callee and arguments from the list (strict mode)
// Removes non enumerable Abitbol properties too
scStaticProps = scStaticProps.filter(function (value) {
return (["caller", "callee", "arguments", "$class", "$extend", "$map"].indexOf(value) == -1);
});
for (i = 0 ; i < scStaticProps.length ; i++) {
if (__class__[scStaticProps[i]] === undefined) {
__class__[scStaticProps[i]] = _superClass[scStaticProps[i]];
}
}
// Add static properties
if (properties.__classvars__) {
for (property in properties.__classvars__) {
__class__[property] = properties.__classvars__[property];
}
}
// Add abitbol static properties
Object.defineProperty(__class__, "$class", {
enumerable: false,
value: __class__
});
Object.defineProperty(__class__, "$extend", {
enumerable: false,
value: Class.$extend
});
Object.defineProperty(__class__, "$map", {
enumerable: false,
value: _classMap
});
return __class__;
}
});
module.exports = Class;
},{"./annotation.js":6}],6:[function(require,module,exports){
"use strict";
function cleanJs(js) {
// remove function fn(param) {
// or fn(param) {
// or (param) => {
var c;
var p = 0;
for (var i = 0 ; i < js.length ; i++) {
c = js[i];
if (c == "(") {
++p;
} else if (c == ")") {
--p;
} else if (c == "{" && p === 0) {
js = js.slice(i + 1);
break;
}
}
// remove comments (not super safe but should work in most cases)
js = js.replace(/\/\*(.|\r|\n)*?\*\//g, "");
js = js.replace(/\/\/.*?\r?\n