From 97558ab6006ed4dac928f401b2eecaea384848a5 Mon Sep 17 00:00:00 2001 From: Thomas FORGIONE Date: Mon, 4 Jan 2016 17:49:05 +0100 Subject: [PATCH] Loads of documentation, and improved dev utils --- server/geo/Geo.js | 1 + server/geo/Makefile | 6 + server/geo/Mesh.js | 52 +++- server/geo/MeshContainer.js | 56 ++++- server/geo/MeshStreamer.js | 473 ++++++------------------------------ server/server.js | 4 + 6 files changed, 173 insertions(+), 419 deletions(-) diff --git a/server/geo/Geo.js b/server/geo/Geo.js index 57ceded..59037c9 100644 --- a/server/geo/Geo.js +++ b/server/geo/Geo.js @@ -2,6 +2,7 @@ var fs = require('fs'); /** * @namespace + * @description Contains all functions and classes relative to meshes and streaming (server-side) */ var geo = {}; diff --git a/server/geo/Makefile b/server/geo/Makefile index 4322f3e..1f19108 100644 --- a/server/geo/Makefile +++ b/server/geo/Makefile @@ -11,6 +11,12 @@ all: Geo Geo: $(CLOSURE) $(OPT) \ --js Geo.js \ + --js ConfigGenerators/ConfigGenerator.js \ + --js ConfigGenerators/NV_PN.js \ + --js ConfigGenerators/V_PP.js \ + --js ConfigGenerators/V_PD.js \ + --js ConfigGenerators/V_PP_PD.js \ + --js ConfigGenerators/ConfigGeneratorEnd.js \ --js Mesh.js \ --js MeshContainer.js \ --js MeshStreamer.js \ diff --git a/server/geo/Mesh.js b/server/geo/Mesh.js index a1c53cc..2c51425 100644 --- a/server/geo/Mesh.js +++ b/server/geo/Mesh.js @@ -1,16 +1,53 @@ /** - * Reprensents a mesh + * Reprensents an elementary mesh (only one material) * @constructor * @memberOf geo */ geo.Mesh = function() { + /** + * @type {geo.Vertex[]} + * @description All the vertices of the mesh + * @deprecated Prefer the use of {@link geo.MeshContainer}.vertices + */ this.vertices = []; + + /** + * @type {geo.Face[]} + * @description All the faces of the mesh + */ this.faces = []; + + /** + * @type {geo.TexCoord[]} + * @description All the textures coordinates of the mesh + */ this.texCoords = []; + + /** + * @type {geo.Normal[]} + * @description All the normals of the mesh + */ this.normals = []; + + /** + * @deprecated You should use your own counter + */ this.faceIndex = 0; + + /** + * @type {String} + * @description Name of the material of the current mesh + */ this.material = null; + + /** + * @deprecated You should use your own attributes + */ this.started = false; + + /** + * @deprecated You should use your own attributes + */ this.finished = false; }; @@ -58,7 +95,7 @@ geo.Mesh.prototype.addFaces = function(face) { if (face instanceof geo.Face) { this.faces.push(face); } else if (typeof face === 'string' || face instanceof String) { - faces = parseFace(face); + faces = geo.parseFace(face); this.faces = this.faces.concat(faces); } else { console.error("Can only add face from geo.Face or string"); @@ -106,6 +143,9 @@ geo.Mesh.prototype.addNormal = function(normal) { return this.normals[this.normals.length - 1]; }; +/** + * @deprecated + */ geo.Mesh.prototype.isFinished = function() { return this.faceIndex === this.faces.length; }; @@ -113,6 +153,7 @@ geo.Mesh.prototype.isFinished = function() { /** * Represent a 3D vertex * @constructor + * @param {String} A string like in the .obj file (e.g. 'v 1.1 0.2 3.4') * @memberOf geo */ geo.Vertex = function() { @@ -175,6 +216,7 @@ geo.Vertex.prototype.toString = function() { * @constructor * @memberOf geo * @augments geo.Vertex + * @param {String} A string like in the .obj file (e.g. 'vn 1.1 2.2 3.3') */ geo.Normal = function() { geo.Vertex.apply(this, arguments); @@ -216,6 +258,7 @@ geo.Normal.prototype.toString = function() { * Represent a texture coordinate element * @constructor * @memberOf geo + * @param {String} a string like in the .obj file (e.g. 'vt 0.5 0.5') */ geo.TexCoord = function() { if (typeof arguments[0] === 'string' || arguments[0] instanceof String) { @@ -268,9 +311,10 @@ geo.TexCoord.prototype.toString = function() { /** - * Represents a face + * Represents a face. Only triangles are supported. For quadrangular polygons, see {@link geo.parseFace} * @constructor * @memberOf geo + * @param {String} A string like in a .obj file (e.g. 'f 1/1/1 2/2/2 3/3/3' or 'f 1 2 3'). */ geo.Face = function() { var split; @@ -368,7 +412,7 @@ geo.Face = function() { * @returns {Face[]} a single 3-vertices face or two 3-vertices face if it was * a 4-vertices face */ -var parseFace = function(arg) { +geo.parseFace = function(arg) { var split = arg.trim().split(' '); var ret = []; diff --git a/server/geo/MeshContainer.js b/server/geo/MeshContainer.js index 3222916..fb2a8f3 100644 --- a/server/geo/MeshContainer.js +++ b/server/geo/MeshContainer.js @@ -2,10 +2,25 @@ var Log = require('../lib/NodeLog.js'); var L3D = require('../../static/js/l3d.min.js'); var THREE = require('three'); +/** + * Clones a vector + * @private + * @param {Object} vec an object with attributes x, y, and z + * @return {Object} a new object with the same x, y, and z attributes + */ function clone(vec) { return {x : vec.x, y : vec.y, z : vec.z}; } +/** + * Rotates a vector, three.js style + * @private + * @param {Object} vec1 an object with attributes x, y, and z + * @param {Number} x three.js's rotateX value + * @param {Number} y three.js's rotateY value + * @param {Number} z three.js's rotateZ value + * @return {Object} a new vector corresponding to the rotated vector + */ function rotation(vec1, x, y, z) { var cos = Math.cos(z); @@ -39,6 +54,18 @@ function rotation(vec1, x, y, z) { return clone(newVec); } +/** + * Applies a transformation to a vector + * @param {Object} vector an object with attributes x, y, and z + * @param {Object} transfo an object with attributes + * + * @see {@link rotation} + * @return {Object} a new object with attributes x, y and z corresponding to the transformation applied to vector + */ function applyTransformation(vector, transfo) { var ret = rotation(vector, transfo.rotation.x, transfo.rotation.y, transfo.rotation.z); @@ -57,6 +84,9 @@ function applyTransformation(vector, transfo) { * Represents a mesh. All meshes are loaded once in geo.availableMesh to avoid * loading at each mesh request * @constructor + * @param {String} path path to the .obj file + * @param {Object} transfo a transformation object to apply during the loading + * @param {function} callback callback to call on the mesh * @memberOf geo */ geo.MeshContainer = function(path, transfo, callback) { @@ -71,31 +101,31 @@ geo.MeshContainer = function(path, transfo, callback) { } /** - * array of each part of the mesh + * Array of each part of the mesh * @type {geo.Mesh[]} */ this.meshes = []; /** - * array of the vertices of the meshes (all merged) + * Array of the vertices of the meshes (all merged) * @type {geo.Vertex[]} */ this.vertices = []; /** - * array of the faces of the meshes (all merged) + * Array of the faces of the meshes (all merged) * @type {geo.Face[]} */ this.faces = []; /** - * array of the normals of the meshes (all merged) + * Array of the normals of the meshes (all merged) * @type {geo.Normal[]} */ this.normals = []; /** - * array of the texture coordinates (all merged) + * Array of the texture coordinates (all merged) * @type {geo.TexCoord[]} */ this.texCoords = []; @@ -106,8 +136,17 @@ geo.MeshContainer = function(path, transfo, callback) { */ this.numberOfFaces = 0; + /** + * Transformation that should be applied to the mesh when loading it + * @type {Object} + * @see {@link applyTransformation} + */ this.transfo = transfo; + /** + * Function to call on the mesh once it is loaded + * @type {function} + */ this.callback = callback; if (path !== undefined) { @@ -119,7 +158,7 @@ geo.MeshContainer = function(path, transfo, callback) { }; /** - * Loads a obj file + * Loads a obj file and apply the transformation * @param {string} path the path to the file */ geo.MeshContainer.prototype.loadFromFile = function(path) { @@ -195,10 +234,7 @@ geo.MeshContainer.prototype.loadFromFile = function(path) { } else if (line[0] === 'u') { - // usemtl - // If a current mesh exists, finish it - - // Create a new mesh + // usemtl : create a new mesh currentMesh = new geo.Mesh(); self.meshes.push(currentMesh); currentMesh.material = (new geo.Material(line)).name; diff --git a/server/geo/MeshStreamer.js b/server/geo/MeshStreamer.js index 01a7999..3103302 100644 --- a/server/geo/MeshStreamer.js +++ b/server/geo/MeshStreamer.js @@ -3,12 +3,23 @@ var THREE = require('three'); var L3D = require('../../static/js/l3d.min.js'); function readIt(sceneNumber, recoId) { - return { + var toZip = { triangles : JSON.parse(fs.readFileSync('./geo/generated/scene' + sceneNumber + '/triangles' + recoId + '.json')), areas : JSON.parse(fs.readFileSync('./geo/generated/scene' + sceneNumber + '/areas' + recoId + '.json')) }; + + var ret = []; + + for (var i = 0; i < toZip.triangles.length; i++) { + ret.push({ + index: toZip.triangles[i], + area: toZip.areas[i] + }); + } + + return ret; } numberOfReco = [0, 0, 12, 12, 11, 2]; @@ -46,6 +57,13 @@ try predictionTables = []; } +/** + * Checks quickly if a triangle might be in a frustum + * @private + * @param {Object[]} element array of thre 3 vertices of the triangle to test + * @param {Object[]} planes array of planes (Object with normal and constant values) + * @return {Boolean} false if we can be sure that the triangle is not in the frustum, true oherwise + */ function isInFrustum(element, planes) { if (element instanceof Array) { @@ -99,47 +117,6 @@ function isInFrustum(element, planes) { } -/** - * @private - */ -function bisect(items, x, lo, hi) { - var mid; - if (typeof(lo) == 'undefined') lo = 0; - if (typeof(hi) == 'undefined') hi = items.length; - while (lo < hi) { - mid = Math.floor((lo + hi) / 2); - if (x < items[mid]) hi = mid; - else lo = mid + 1; - } - return lo; -} - -/** - * @private - */ -function insort(items, x) { - items.splice(bisect(items, x), 0, x); -} - -/** - * @private - */ -function partialSort(items, k, comparator) { - var smallest = items.slice(0, k).sort(), - max = smallest[k-1]; - - for (var i = k, len = items.length; i < len; ++i) { - var item = items[i]; - var cond = comparator === undefined ? item < max : comparator(item, max) < 0; - if (cond) { - insort(smallest, item); - smallest.length = k; - max = smallest[k-1]; - } - } - return smallest; -} - /** * A class that streams easily a mesh via socket.io * @memberOf geo @@ -184,12 +161,6 @@ geo.MeshStreamer = function(path) { */ this.texCoords = []; - this.minThreshold = 0.75; - this.maxThreshold = 0.85; - this.currentlyPrefetching = false; - - this.beginning = false; - this.beginningThreshold = 0.9; this.frustumPercentage = 0.6; @@ -212,6 +183,12 @@ geo.MeshStreamer = function(path) { }; +/** + * Checks if a face is oriented towards the camera + * @param {Object} camera a camera (with a position, and a direction) + * @param {geo.Face} the face to test + * @return {Boolean} true if the face is in the good orientation, face otherwise + */ geo.MeshStreamer.prototype.isBackFace = function(camera, face) { var directionCamera = L3D.Tools.diff( @@ -244,18 +221,6 @@ geo.MeshStreamer.prototype.faceComparator = function(camera) { var self = this; - // var direction = { - // x: camera.target.x - camera.position.x, - // y: camera.target.y - camera.position.y, - // z: camera.target.z - camera.position.z - // }; - - // var norm = Math.sqrt(direction.x * direction.x + direction.y * direction.y + direction.z * direction.z); - - // direction.x /= norm; - // direction.y /= norm; - // direction.z /= norm; - return function(face1, face2) { var center1 = { @@ -271,12 +236,6 @@ geo.MeshStreamer.prototype.faceComparator = function(camera) { z: center1.z - camera.position.z }; - // var norm1 = Math.sqrt(dir1.x * dir1.x + dir1.y * dir1.y + dir1.z + dir1.z); - - // dir1.x /= norm1; - // dir1.y /= norm1; - // dir1.z /= norm1; - var dot1 = dir1.x * dir1.x + dir1.y * dir1.y + dir1.z * dir1.z; var center2 = { @@ -291,12 +250,6 @@ geo.MeshStreamer.prototype.faceComparator = function(camera) { z: center2.z - camera.position.z }; - // var norm2 = Math.sqrt(dir2.x * dir2.x + dir2.y * dir2.y + dir2.z + dir2.z); - - // dir2.x /= norm2; - // dir2.y /= norm2; - // dir2.z /= norm2; - var dot2 = dir2.x * dir2.x + dir2.y * dir2.y + dir2.z * dir2.z; // Decreasing order @@ -312,12 +265,11 @@ geo.MeshStreamer.prototype.faceComparator = function(camera) { }; /** - * Initialize the socket.io callback + * Initialize the socket.io callbacks * @param {socket} socket the socket to initialize */ geo.MeshStreamer.prototype.start = function(socket) { - this.meshIndex = 0; this.socket = socket; var self = this; @@ -328,8 +280,6 @@ geo.MeshStreamer.prototype.start = function(socket) { self.chunk = 1; } - self.prefetch = prefetch; - self.mesh = geo.availableMeshes[path]; switch (path) { @@ -356,6 +306,9 @@ geo.MeshStreamer.prototype.start = function(socket) { self.predictionTable = predictionTables[3]; }; + self.generator = geo.ConfigGenerator.createFromString(prefetch, self); + self.backupGenerator = new geo.ConfigGenerator(self); + if (self.mesh === undefined) { process.stderr.write('Wrong path for model : ' + path); socket.emit('refused'); @@ -434,103 +387,45 @@ geo.MeshStreamer.prototype.start = function(socket) { } - // Create config for proportions of chunks - var config; - var didPrefetch = false; + if (cameraExists) { - // if (false) { + // Create config for proportions of chunks + var didPrefetch = false; + var config = self.generator.generateMainConfig(cameraFrustum, recommendationClicked); - if (cameraExists) { + // Send next elements + var oldTime = Date.now(); + var next = self.nextElements(config); - switch (self.prefetch) { - case 'V-PP': - config = self.generateConfig_V_PP(cameraFrustum, recommendationClicked); - break; - case 'V-PD': - config = self.generateConfig_V_PD(cameraFrustum, recommendationClicked); - break; - case 'V-PP+PD': - config = self.generateConfig_V_PP_PD(cameraFrustum, recommendationClicked); - break; - case 'NV-PN': - default: - config = self.generateConfig_NV_PN(cameraFrustum, recommendationClicked); - break; - // console.log(self.prefetch) - // process.exit(-1); - - } - - // Send next elements - var oldTime = Date.now(); - var next = self.nextElements(config); - - // console.log( - // 'Adding ' + - // next.size + - // ' for newConfig : ' - // + JSON.stringify(config.map(function(o) { return o.proportion})) - // ); + // console.log( + // 'Adding ' + + // next.size + + // ' for newConfig : ' + // + JSON.stringify(config.map(function(o) { return o.proportion})) + // ); - if (self.beginning === true && next.size < self.chunk) { - self.beginning = false; + if (self.beginning === true && next.size < self.chunk) { - switch (self.prefetch) { - case 'V-PP': - config = self.generateConfig_V_PP(cameraFrustum, recommendationClicked); - break; - case 'V-PD': - config = self.generateConfig_V_PD(cameraFrustum, recommendationClicked); - break; - case 'V-PP+PD': - config = self.generateConfig_V_PP_PD(cameraFrustum, recommendationClicked); - break; - case 'NV-PN': - default: - config = self.generateConfig_NV_PN(cameraFrustum, recommendationClicked); - break; - } + self.beginning = false; + config = self.generator.generateMainConfig(cameraFrustum, recommendationClicked); - var fillElements = self.nextElements(config, self.chunk - next.size); + } - next.configSizes = fillElements.configSizes; - next.data.push.apply(next.data, fillElements.data); - next.size += fillElements.size; - } + var fillElements = self.nextElements(config, self.chunk - next.size); + next.configSizes = fillElements.configSizes; + next.data.push.apply(next.data, fillElements.data); + next.size += fillElements.size; - // Chunk is not empty, compute fill config - if (next.size < self.chunk) { + // Chunk is not empty, compute fill config + if (next.size < self.chunk) { - switch (self.prefetch) { - case 'V-PP': - config = self.generateFillConfig_V_PP(config, next, cameraFrustum, recommendationClicked); - break; - case 'V-PD': - config = self.generateFillConfig_V_PD(config, next, cameraFrustum, recommendationClicked); - break; - case 'V-PP+PD': - config = self.generateFillConfig_V_PP_PD(config, next, cameraFrustum, recommendationClicked); - break; - case 'NV-PN': - default: - config = self.generateFillConfig_NV_PN(config, next, cameraFrustum, recommendationClicked); - break; + config = self.generator.generateFillingConfig(config, next, cameraFrustum, recommendationClicked); + fillElements = self.nextElements(config, self.chunk - next.size); - } - - - fillElements = self.nextElements(config, self.chunk - next.size); - - next.data.push.apply(next.data, fillElements.data); - next.size += fillElements.size; - - } - - } else { - - next = { data : [], size : 0 }; + next.data.push.apply(next.data, fillElements.data); + next.size += fillElements.size; } @@ -544,10 +439,12 @@ geo.MeshStreamer.prototype.start = function(socket) { } - // } + } else { - // var config = [{proportion: 1, smart: true, recommendationId: 1}]; - // var next = self.nextElements(config); + config = self.backupGenerator.generateMainConfig(); + next = self.nextElements(config, self.chunk); + + } console.log('Chunk of size ' + next.size + ' (generated in ' + (Date.now() - oldTime) + 'ms)'); @@ -595,241 +492,11 @@ geo.MeshStreamer.prototype.nextMaterials = function() { }; -geo.MeshStreamer.prototype.generateConfig_NV_PN = function(cameraFrustum) { - - var config; - - // if (this.beginning === true) { - - // console.log('Begining : full init'); - // config = [{recommendationId : 0, proportion:1, smart: true}]; - - - // } else { - - // Case without prefetch - console.log("No prefetching"); - config = [{ frustum: cameraFrustum, proportion: 1}]; - - // } - - return config; -}; - -geo.MeshStreamer.prototype.generateConfig_V_PD = function(cameraFrustum, recommendationClicked) { - - var config; - if (recommendationClicked != null) { - - if (this.beginning === true) { - this.beginning = false; - } - - // Case full reco - console.log("Going to " + recommendationClicked); - console.log("Recommendation is clicking : full for " + JSON.stringify(this.mesh.recommendations[recommendationClicked].position)); - config = [{recommendationId : recommendationClicked + 1, proportion: 1, smart:true}]; - - } else if (this.beginning === true) { - - console.log('Begining : full init'); - config = [{recommendationId : 0, proportion:1, smart: true}]; - - - } else { - - // Case without prefetch - console.log("No prefetching"); - config = [{ frustum: cameraFrustum, proportion: 1}]; - - } - - return config; -}; - -geo.MeshStreamer.prototype.generateConfig_V_PP = function(cameraFrustum) { - - var config; - - if (this.beginning === true) { - - console.log('Begining : full init'); - config = [{recommendationId : 0, proportion:1, smart: true}]; - - } else { - - // Case full prefetch - console.log("Allow some prefetching"); - - didPrefetch = true; - config = [{ frustum: cameraFrustum, proportion : this.frustumPercentage}]; - - if (this.predictionTable !== undefined) { - - var sum = 0; - - for (var i = 1; i <= this.mesh.recommendations.length; i++) { - - sum += this.predictionTable[this.previousReco][i]; - - } - - for (var i = 1; i <= this.mesh.recommendations.length; i++) { - - if (this.predictionTable[this.previousReco][i] > 0) { - - config.push({ - - proportion : this.predictionTable[this.previousReco][i] * this.prefetchPercentage / sum, - recommendationId : i, - smart: true - - }); - - } - - } - - } else { - - process.stderr.write('ERROR : PREDICTION TABLE IF UNDEFINED'); - - } - - } - - return config; - -}; - -geo.MeshStreamer.prototype.generateConfig_V_PP_PD = function(cameraFrustum, recommendationClicked) { - - var config; - - if (recommendationClicked != null) { - - if (this.beginning === true) { - this.beginning = false; - } - - // Case full reco - console.log("Going to " + recommendationClicked); - console.log("Recommendation is clicking : full for " + JSON.stringify(this.mesh.recommendations[recommendationClicked].position)); - config = [{recommendationId : recommendationClicked + 1, proportion: 1, smart:true}]; - - - - } else if (this.beginning === true) { - - console.log('Begining : full init'); - config = [{recommendationId : 0, proportion:1, smart: true}]; - - - } else { - - // Case full prefetch - console.log("Allow some prefetching"); - - config = [{ frustum: cameraFrustum, proportion : this.frustumPercentage}]; - - // Find best recommendation - var bestReco; - var bestScore = -Infinity; - var bestIndex = null; - - if (this.predictionTable !== undefined) { - - var sum = 0; - - for (var i = 1; i <= this.mesh.recommendations.length; i++) { - - sum += this.predictionTable[this.previousReco][i]; - - } - - for (var i = 1; i <= this.mesh.recommendations.length; i++) { - - if (this.predictionTable[this.previousReco][i] > 0) { - - config.push({ - - proportion : this.predictionTable[this.previousReco][i] * this.prefetchPercentage / sum, - recommendationId : i, - smart: true - - }); - - } - - } - - // if (score > this.maxThreshold) - // this.currentlyPrefetching = true; - - } else { - - process.stderr.write('ERROR : PREDICTION TABLE IF UNDEFINED'); - - } - - } - - return config; - -}; - -geo.MeshStreamer.prototype.generateFillConfig_NV_PN = - function(previousConfig, previousResult, cameraFrustum, recommendationClicked) { - - // Nothing to do better than linear, let default fill do its work - return {data:[], size: 0}; - -}; - -geo.MeshStreamer.prototype.generateFillConfig_V_PP = - function(previousConfig, previousResult, cameraFrustum, recommendationClicked) { - - var sum = 0; - var newConfig = []; - - for (var i = 0; i < previousConfig.length; i++) { - - // Check if previousConfig was full - if (previousResult.configSizes[i] >= this.chunk * previousConfig[i].proportion) { - - newConfig.push(previousConfig[i]); - sum += previousConfig[i].proportion; - - } - - } - - // Normalize previousConfig probabilities - for (var i = 0; i < newConfig.length; i++) { - - newConfig[i].proportion /= sum; - - } - - return newConfig; - -}; - -geo.MeshStreamer.prototype.generateFillConfig_V_PP_PD = - geo.MeshStreamer.prototype.generateFillConfig_V_PP; - -geo.MeshStreamer.prototype.generateFillConfig_V_PD = - function(previousConfig, previousResult, cameraFrustum, recommendationClicked) { - - return [{proportion:1, frustum: cameraFrustum}]; - -}; - /** * Prepare the next elements - * @param {camera} _camera a camera that can be usefull to do smart streaming (stream - * only interesting parts according to the camera + * @param {Object[]} config a configuration list * @returns {array} an array of elements ready to send + * @see {@link https://github.com/DragonRock/3dinterface/wiki/Streaming-configuration|Configuration list documentation} */ geo.MeshStreamer.prototype.nextElements = function(config, chunk) { @@ -915,18 +582,15 @@ geo.MeshStreamer.prototype.nextElements = function(config, chunk) { var currentArea = 0; // Fill buffer using facesToSend - for (var faceIndex = 0; faceIndex < this.facesToSend[currentConfig.recommendationId].triangles.length; faceIndex++) { + for (var faceIndex = 0; faceIndex < this.facesToSend[currentConfig.recommendationId].length; faceIndex++) { - var faceInfo = { - index:this.facesToSend[currentConfig.recommendationId].triangles[faceIndex], - area: this.facesToSend[currentConfig.recommendationId].areas[faceIndex] - }; + var faceInfo = this.facesToSend[currentConfig.recommendationId][faceIndex]; area += faceInfo.area; - // if (area > 0.6) { - // break; - // } + if (area > 0.9) { + break; + } if (this.faces[faceInfo.index] !== true) { @@ -1091,7 +755,6 @@ geo.MeshStreamer.prototype.pushFace = function(face, buffer) { } buffer.push(face.toList()); - // this.meshFaces[meshIndex] = this.meshFaces[meshIndex] || []; this.faces[face.index] = true; totalSize+=3; diff --git a/server/server.js b/server/server.js index 0f4409b..0a01aac 100644 --- a/server/server.js +++ b/server/server.js @@ -122,3 +122,7 @@ function main() { } module.exports = main; + +if (require.main === module) { + main(); +}