const Event = require('r3-event'); /** OPTIONS_START OPTIONS_END **/ class Utils { //CONSTRUCTOR_TEMPLATE_START constructor(options) { Event.Emit(Event.OBJECT_CREATED, this); //OPTIONS_INIT_START if (typeof options === 'undefined') { options = {}; } //OPTIONS_INIT_END //CUSTOM_OPTIONS_INIT_START //CUSTOM_OPTIONS_INIT_END Object.assign(this, options); //CUSTOM_BEFORE_INIT_START //CUSTOM_BEFORE_INIT_END Event.Emit(Event.OBJECT_INITIALIZED, this); } //CONSTRUCTOR_TEMPLATE_END //CUSTOM_IMPLEMENTATION_START static GetFirstParent(object, constructor) { if (Utils.UndefinedOrNull(constructor)) { throw new Error('You need to specify a constructor'); } if (object.parent === null) { return null; } if (object.parent instanceof constructor) { return object.parent; } else { return Utils.GetFirstParent(object.parent, constructor); } }; static SyntaxHighlight(json) { if (typeof json != 'string') { json = JSON.stringify(json, undefined, 2); } json = json.replace(/&/g, '&').replace(//g, '>'); return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { let cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return '' + match + ''; }); }; static GetParentProject(component) { if (Utils.UndefinedOrNull(component.parent)) { throw new Error('Parent not found'); } if (component.parent instanceof R3.Project) { return component.parent; } return Utils.GetParentProject(component.parent); }; static GetParents(component, parents) { if (Utils.UndefinedOrNull(parents)) { parents = []; } if (Utils.UndefinedOrNull(component.parent)) { return parents; } parents.push(component.parent); return Utils.GetParents(component.parent, parents); }; /** * @return {boolean} */ static Instance(component) { return Utils.Defined(component) && Utils.Defined(component.instance); }; /** * Utils.RemoveFromSelect * @param select * @param id * @returns {boolean} * @constructor */ static RemoveFromSelect(select, id) { let i; for (i = 0; i < select.options.length; i++) { if (select.options[i].value === id) { select.remove(i); return true; } } return false; }; /** * Utils.GetSelectIndex * * Get the select index of given id * * @param select * @param id * @returns boolean true if successful * * @constructor */ static SetSelectIndex(select, id) { for (let i = 0; i < select.options.length; i++) { if (select.options[i].value === id) { select.selectedIndex = i; return true; } } return false; }; static SortSelect(select) { let tmp = []; let i; for (i = 1; i < select.options.length; i++) { tmp[i-1] = []; tmp[i-1][0] = select.options[i].text; tmp[i-1][1] = select.options[i].value; } tmp.sort(); select.options = [select.options[0]]; for (i = 0; i < tmp.length; i++) { select.options[i+1] = new Option(tmp[i][0], tmp[i][1]); } return; }; /** * Gets the parent of object whith property of optional type constructor. If index is specified, get the parent of the * object with property[index] - which means the property should be an array * @param object * @param property * @param index * @param constructor * @returns {*} * @constructor */ static GetParent(object, property, index, constructor) { if (Utils.UndefinedOrNull(constructor)) { constructor = null; } if (Utils.UndefinedOrNull(index)) { index = null; } if (object.parent) { /** * Parent defined */ if (object.parent.hasOwnProperty(property)) { if (constructor) { if (index) { if (object.parent[property][index] instanceof constructor) { return object.parent[property][index]; } else { if (typeof object.parent.getParent === 'function') { return object.parent.getParent(property, index, constructor); } else { console.warn('getParent not defined on API object : ' + object.parent + ' - you should avoid having these messsages'); return null; } } } else { if (object.parent[property] instanceof constructor) { return object.parent[property]; } else { if (typeof object.parent.getParent === 'function') { return object.parent.getParent(property, index, constructor); } else { console.warn('getParent not defined on API object : ' + object.parent + ' - you should avoid having these messsages'); return null; } } } } else { if (index) { return object.parent[property][index]; } else { return object.parent[property]; } } } else { /** * This parent does not have the property - go a level higher */ if (typeof object.parent.getParent === 'function') { return object.parent.getParent(property, index, constructor); } else { console.warn('getParent not defined on API object : ' + object.parent + ' - you should avoid having these messsages'); return null; } } } else { /** * No parent defined */ console.warn('property : ' + property + ' of type ' + constructor + ' was not found in the parent chain'); return null; } }; /** * Strips image extension from given path * @param imagePath * @constructor */ static StripImageExtension(imagePath) { return imagePath.replace(/(\.png$|\.gif$|\.jpeg$|\.jpg$)/,'') }; /** * Returns true if unloaded * @param component * @returns {boolean} * @constructor */ static Unloaded(component) { if ( Utils.UndefinedOrNull(component) || Utils.UndefinedOrNull(component.instance) ) { return true; } return false; }; /** * * @param component * @returns {boolean} * @constructor */ static Loaded(component) { if (component && component.instance) { return true; } return false; }; static BuildVectorSource(result, name, dimension) { if (dimension === 2) { result[name] = {}; result[name].x = false; result[name].y = false; return; } if (dimension === 3) { result[name] = {}; result[name].x = false; result[name].y = false; result[name].y = false; return; } if (dimension === 4) { result[name] = {}; result[name].x = false; result[name].y = false; result[name].z = false; result[name].w = false; return; } console.warn('unknown dimension : ' + dimension); }; /** * Returns all 'instances' of the array, or null if an 'instance' is undefined * @constructor * @param array */ static GetArrayInstances(array) { return array.reduce( function(result, object) { if (result === null) { return result; } if (Utils.UndefinedOrNull(object.instance)) { result = null; } else { result.push(object.instance); } return result; }, [] ); }; static SortFacesByMaterialIndex(faces) { /** * Sorts faces according to material index because later we will create * groups for each vertice group */ faces.sort(function(a, b) { if (a.materialIndex < b.materialIndex) { return -1; } if (a.materialIndex > b.materialIndex) { return 1; } return 0; }); return faces; }; static BuildQuaternionSource(result, name) { result[name] = {}; result[name].axis = {}; result[name].axis.x = false; result[name].axis.y = false; result[name].axis.z = false; result[name].angle = false; result[name].x = false; result[name].y = false; result[name].z = false; result[name].w = false; }; static ObjectPropertiesAsBoolean(object) { return Object.keys(object).reduce( function(result, propertyId) { if (typeof object[propertyId] === 'function') { return result; } result[propertyId] = false; // if (object[propertyId] instanceof R3.Vector2) { // Utils.BuildVectorSource(result, propertyId, 2); // } // // if (object[propertyId] instanceof R3.Vector3) { // Utils.BuildVectorSource(result, propertyId, 3); // } // // if (object[propertyId] instanceof R3.Vector4) { // Utils.BuildVectorSource(result, propertyId, 4); // } // // if (object[propertyId] instanceof R3.Quaternion) { // Utils.BuildQuaternionSource(result, propertyId); // } return result; }.bind(this), {} ); }; static GetRuntime() { let result = null; R3.Event.Emit( R3.Event.GET_RUNTIME, null, function(runtime) { result = runtime; } ); return result; }; /** * Returns the window size or null * @returns {*} * @constructor */ static GetWindowSize() { let size = null; R3.Event.Emit( R3.Event.GET_WINDOW_SIZE, null, function(data) { size = data; }.bind(this) ); return size; }; /** * Convenience function to update object width and height members with window size * @param object * @constructor */ static UpdateWindowSize(object) { let size = Utils.GetWindowSize(); object.width = size.width; object.height = size.height; }; /** * Returns id of object with the name if it exists in the array, otherwise null * @param name * @param array * @returns {*} * @constructor */ static ObjectIdWithNameInArray(name, array) { return array.reduce( function(result, object) { if (result) { return result; } if (name === object.name) { return object.id; } return null; }, null ); }; static LoadIdsFromArrayToIdObject(array, idToObject) { }; static LoadIdsFromObjectToIdObject(object, idToObject) { }; /** * Gets random int exclusive of maximum but inclusive of minimum * @param min * @param max * @returns {*} * @constructor */ static GetRandomInt(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive }; /** * Gets random int inclusive of minimum and maximum * @param min * @param max * @returns {*} * @constructor */ static GetRandomIntInclusive(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; //The maximum is inclusive and the minimum is inclusive }; static InterpolateArray(data, fitCount) { let linearInterpolate = function(before, after, atPoint) { return before + (after - before) * atPoint; }; let newData = []; let springFactor = Number((data.length - 1) / (fitCount - 1)); newData[0] = data[0]; // for new allocation for ( let i = 1; i < fitCount - 1; i++) { let tmp = i * springFactor; let before = Number(Math.floor(tmp)).toFixed(); let after = Number(Math.ceil(tmp)).toFixed(); let atPoint = tmp - before; newData[i] = linearInterpolate(data[before], data[after], atPoint); } newData[fitCount - 1] = data[data.length - 1]; // for new allocation return newData; }; /** * Undefined or null check * @param variable * @returns {boolean} * @constructor */ static UndefinedOrNull( variable ) { return typeof variable === 'undefined' || variable === null; }; /** * The variable is not undefined and not null * @param variable * @returns {boolean} * @constructor */ static Defined( variable ) { return typeof variable !== 'undefined' && variable !== null; }; /** * Gets function parameters * @param fn * @constructor */ static GetParameters(fn) { let FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; let FN_ARG_SPLIT = /,/; let FN_ARG = /^\s*(_?)(.+?)\1\s*$/; let STRIP_COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/mg; let parameters, fnText, argDecl; if (typeof fn !== 'function') { parameters = []; fnText = fn.toString().replace(STRIP_COMMENTS, ''); argDecl = fnText.match(FN_ARGS); argDecl[1].split(FN_ARG_SPLIT).forEach(function(arg) { arg.replace(FN_ARG, function(all, underscore, name) { parameters.push(name); }); }); } else { throw Error("not a function") } return parameters; }; /** * Returns either an ID of the object or Null * @param object * @returns {null} * @constructor */ static IdOrNull(object) { if (Utils.UndefinedOrNull(object)) { return null; } else { if (Utils.UndefinedOrNull(object.id)) { console.warn('saving an object reference with no ID : ', object); return null; } return object.id; } }; /** * Limit a property to values between -pi and +pi * @param property * @param objectProperty * @returns {{configurable?: boolean, enumerable?: boolean, value?, writable?: boolean, get?: Function, set?: Function}} * @constructor */ static LimitToPI(property, objectProperty) { let store = objectProperty; return { get : function() { return store; }, set : function(value) { while (value > Math.PI) { value -= (Math.PI * 2); } while (value < -(Math.PI)) { value += (Math.PI * 2); } store = value; } }; }; /** * Returns an array of IDs representing the objects * @param array * @returns [] * @constructor */ static IdArrayOrEmptyArray(array) { if (Utils.UndefinedOrNull(array)) { return []; } else { return array.map(function(item) { if (Utils.UndefinedOrNull(item.id)) { throw new Error('No ID found while trying to store IDs to array'); } return item.id }); } }; /** * Links an object to its parent through idToObject array * @param propertyString * @param idToObject * @param parentObject * @param id * @constructor */ static Link(propertyString, idToObject, parentObject, id) { if (!Utils.UndefinedOrNull(parentObject[propertyString])) { if (!idToObject.hasOwnProperty(id)) { console.warn('Linking failed for object:' + parentObject.name); } parentObject[propertyString] = idToObject[id]; } }; /** * Generates a random ID * @returns {string} * @constructor */ static RandomId(length) { if (Utils.UndefinedOrNull(length)) { length = 10; } return Math.random().toString(36).substr(2, length); }; static InvertWindingOrder(triangles) { for (let i = 0; i < triangles.length; i++) { let v1 = triangles[i].v1; triangles[i].v1 = triangles[i].v2; triangles[i].v2 = v1; let backupUV = triangles[i].triangle.v1uv; triangles[i].triangle.v1uv = triangles[i].triangle.v2uv; triangles[i].triangle.v2uv = backupUV; } return triangles; }; /** * Inverts a mesh winding order (and its instance) * @param mesh R3.D3.Mesh * @returns {*} * @constructor */ static InvertMeshWindingOrder(mesh) { mesh.faces.forEach( function(face) { let tmpV1 = face.v1; face.v1 = face.v2; face.v2 = tmpV1; let tmpV1uv = face.v1uv; face.v1uv = face.v2uv; face.v2uv = tmpV1uv; }.bind(this) ); //mesh.computeNormals = true; //mesh.createInstance(); }; /** * This function resets a the winding order of a mesh from a reference point V (the average center of the mesh) */ static ResetWindingOrder(faces, vertices) { let vertexList = new R3.API.Vector3.Points(); for (let v = 0; v < vertices.length; v++) { vertexList.add(new R3.API.Vector3( vertices[v].position.x, vertices[v].position.y, vertices[v].position.z )); } let V = vertexList.average(); let triangles = []; for (let s = 0; s < faces.length; s += 3) { let v0 = faces[s]; let v1 = faces[s+1]; let v2 = faces[s+2]; triangles.push( { v0 : v0, v1 : v1, v2 : v2, edges : [ {v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v0} ], winding : 0, edgeIndex : -1, processed : false } ); } for (let i = 0; i < triangles.length; i++) { if ( R3.API.Vector3.clockwise( vertices[triangles[i].v0].position, vertices[triangles[i].v1].position, vertices[triangles[i].v2].position, V ) ) { console.log('clockwise'); let bv1 = triangles[i].v1; triangles[i].v1 = triangles[i].v2; triangles[i].v2 = bv1; } else { console.log('not clockwise'); } } return triangles; }; /** * This function resets the winding order for triangles in faces, given an initial triangle and orientation edge * used pseudocode from * http://stackoverflow.com/questions/17036970/how-to-correct-winding-of-triangles-to-counter-clockwise-direction-of-a-3d-mesh * We need to use a graph traversal algorithm, * lets assume we have method that returns neighbor of triangle on given edge * * neighbor_on_egde( next_tria, edge ) * * to_process = set of pairs triangle and orientation edge, initial state is one good oriented triangle with any edge on it * processed = set of processed triangles; initial empty * * while to_process is not empty: * next_tria, orientation_edge = to_process.pop() * add next_tria in processed * if next_tria is not opposite oriented than orientation_edge: * change next_tria (ABC) orientation (B<->C) * for each edge (AB) in next_tria: * neighbor_tria = neighbor_on_egde( next_tria, edge ) * if neighbor_tria exists and neighbor_tria not in processed: * to_process add (neighbor_tria, edge opposite oriented (BA)) * @param faces R3.D3.Face[] * @param orientationEdge R3.API.Vector2 * @returns {Array} */ static FixWindingOrder(faces, orientationEdge) { /** * Checks if a Face belonging to a TriangleEdge has already been processed * @param processed TriangleEdge[] * @param triangle Face * @returns {boolean} */ function inProcessed(processed, triangle) { for (let i = 0; i < processed.length; i++) { if (processed[i].triangle.equals(triangle)) { return true; } } return false; } /** * Returns a neighbouring triangle on a specific edge - preserving the edge orientation * @param edge R3.API.Vector2 * @param faces R3.D3.Face[] * @param currentTriangle * @returns {*} */ function neighbourOnEdge(edge, faces, currentTriangle) { for (let i = 0; i < faces.length; i++) { if ( (faces[i].v0 === edge.x && faces[i].v1 === edge.y) || (faces[i].v1 === edge.x && faces[i].v2 === edge.y) || (faces[i].v2 === edge.x && faces[i].v0 === edge.y) || (faces[i].v0 === edge.y && faces[i].v1 === edge.x) || (faces[i].v1 === edge.y && faces[i].v2 === edge.x) || (faces[i].v2 === edge.y && faces[i].v0 === edge.x) ) { let triangle = new R3.D3.API.Face( null, null, faces[i].v0index, faces[i].v1index, faces[i].v2index, faces[i].materialIndex, faces[i].uvs ); if (triangle.equals(currentTriangle)) { continue; } return new R3.D3.TriangleEdge( triangle, edge ); } } return null; } let toProcess = [ new R3.D3.TriangleEdge( new R3.D3.API.Face( null, null, faces[0].v0index, faces[0].v1index, faces[0].v2index, faces[0].materialIndex, faces[0].uvs ), orientationEdge ) ]; let processed = []; while (toProcess.length > 0) { let triangleEdge = toProcess.pop(); /** * If edge is the same orientation (i.e. the edge order is the same as the given triangle edge) it needs to be reversed * to have the same winding order) */ if ( (triangleEdge.triangle.v0index === triangleEdge.edge.x && triangleEdge.triangle.v1index === triangleEdge.edge.y) || (triangleEdge.triangle.v1index === triangleEdge.edge.x && triangleEdge.triangle.v2index === triangleEdge.edge.y) || (triangleEdge.triangle.v2index === triangleEdge.edge.x && triangleEdge.triangle.v0index === triangleEdge.edge.y) ) { let backupV = triangleEdge.triangle.v1index; triangleEdge.triangle.v1index = triangleEdge.triangle.v2index; triangleEdge.triangle.v2index = backupV; // let backupUV = triangleEdge.triangle.v1uv; // triangleEdge.triangle.v1uv = triangleEdge.triangle.v2uv; // triangleEdge.triangle.v2uv = backupUV; // let backupUV = triangleEdge.triangle.uvs[0][1]; triangleEdge.triangle.uvs[0][1] = triangleEdge.triangle.uvs[0][2]; triangleEdge.triangle.uvs[0][2] = backupUV; } processed.push(triangleEdge); let edges = [ new R3.API.Vector2( triangleEdge.triangle.v0index, triangleEdge.triangle.v1index ), new R3.API.Vector2( triangleEdge.triangle.v1index, triangleEdge.triangle.v2index ), new R3.API.Vector2( triangleEdge.triangle.v2index, triangleEdge.triangle.v0index ) ]; for (let j = 0; j < edges.length; j++) { let neighbour = neighbourOnEdge(edges[j], faces, triangleEdge.triangle); if (neighbour && !inProcessed(processed, neighbour.triangle)) { toProcess.push(neighbour); } } } /** * In processed - we will have some duplicates - only add the unique ones * @type {Array} */ let triangles = []; for (let i = 0; i < processed.length; i++) { let found = false; for (let k = 0; k < triangles.length; k++) { if (triangles[k].equals(processed[i].triangle)){ found = true; break; } } if (!found) { triangles.push(processed[i].triangle); } } return triangles; }; /** * This is a work-around function to fix polys which don't triangulate because * they could lie on Z-plane (XZ or YZ)) - we translate the poly to the origin, systematically rotate the poly around * Z then Y axis * @param verticesFlat [] * @param grain is the amount to systematically rotate the poly by - a finer grain means a more accurate maximum XY * @return [] */ static FixPolyZPlane(verticesFlat, grain) { if ((verticesFlat.length % 3) !== 0 && !(verticesFlat.length > 9)) { console.log("The vertices are not in the right length : " + verticesFlat.length); } let vertices = []; let points = new R3.API.Quaternion.Points(); for (let i = 0; i < verticesFlat.length; i += 3) { points.add(new R3.API.Vector3( verticesFlat[i], verticesFlat[i + 1], verticesFlat[i + 2] )); } points.toOrigin(); points.maximizeXDistance(grain); points.maximizeYDistance(grain); for (i = 0; i < points.vectors.length; i++) { vertices.push( [ points.vectors[i].x, points.vectors[i].y ] ); } return vertices; }; static MovingAverage(period) { let nums = []; return function(num) { nums.push(num); if (nums.length > period) nums.splice(0,1); // remove the first element of the array let sum = 0; for (let i in nums) sum += nums[i]; let n = period; if (nums.length < period) n = nums.length; return(sum/n); } }; static Intersect(a, b) { let t; /** * Loop over shortest array */ if (b.length > a.length) { t = b; b = a; a = t; } return a.filter( /** * Check if exists * @param e * @returns {boolean} */ function(e) { return (b.indexOf(e) > -1); } ).filter( /** * Remove Duplicates * @param e * @param i * @param c * @returns {boolean} */ function(e, i, c) { return c.indexOf(e) === i; } ); }; static Difference(a, b) { let t; /** * Loop over shortest array */ if (b.length > a.length) { t = b; b = a; a = t; } return a.filter( /** * Check if exists * @param e * @returns {boolean} */ function(e) { return (b.indexOf(e) === -1); } ).filter( /** * Remove Duplicates * @param e * @param i * @param c * @returns {boolean} */ function(e, i, c) { return c.indexOf(e) === i; } ); }; /** * Push only if not in there already * @param array * @param object * @constructor */ static PushUnique(array, object) { if (array.indexOf(object) === -1) { array.push(object); } }; /** * Checks whether or not the object is empty * @param obj * @returns {boolean} * @constructor */ static IsEmpty(obj) { return (Object.keys(obj).length === 0 && obj.constructor === Object); }; static IsString(member) { return (typeof member === 'string'); }; static IsBoolean(member) { return (member === true || member === false); }; static IsColor(member) { return (member instanceof R3.Color); }; static IsNumber(member) { return (typeof member === 'number'); }; static IsVector2(member) { return ( member instanceof R3.API.Vector2 || member instanceof R3.Vector2 ); }; static IsVector3(member) { return ( member instanceof R3.API.Vector3 || member instanceof R3.Vector3 ); }; static IsVector4(member) { return ( member instanceof R3.API.Vector4 || member instanceof R3.Vector4 || member instanceof R3.API.Quaternion || member instanceof R3.Quaternion ); }; static IsObject(member) { let type = typeof member; return type === 'function' || type === 'object' && !!member; }; /** * @return {string} */ static LowerUnderscore(name) { let string = name.toLowerCase().replace(/\s+/g, '_'); string = string.replace(/-/g, '_'); string = string.replace(/\_+/g, '_'); return string; }; static UpperCaseWordsSpaces(input) { let word = input.replace(/[-_]/g, ' '); word = word.replace(/\s+/, ' '); let words = word.split(' '); return words.reduce( function(result, word) { result += word[0].toUpperCase() + word.substr(1); return result + ' '; }, '' ).trim(); }; /** * @return {string} */ static UpperCaseUnderscore(word) { let str = ''; word.split('').map( function(letter){ if (letter === letter.toUpperCase()) { str += '_' + letter; } else { str += letter.toUpperCase(); } }); str = str.replace(new RegExp('^_'),''); return str; }; /** * Returns Left Padded Text - ex. length 5, padchar 0, string abc = '00abc' * @param length * @param padChar * @param string * @returns {string} * @constructor */ static PaddedText(length, padChar, string) { let pad = ""; for (let x = 0; x < length; x++) { pad += padChar; } return pad.substring(0, pad.length - string.length) + string; }; //CUSTOM_IMPLEMENTATION_END } //CUSTOM_OUT_OF_CLASS_IMPLEMENTATION_START //CUSTOM_OUT_OF_CLASS_IMPLEMENTATION_END module.exports = Utils;