diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 213377a4f..12457bc87 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -19,7 +19,7 @@ "dompurify": "^2.2.4", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", - "marked": "^1.2.6", + "marked": "^2.0.0", "numeral": "^2.0.6", "portal-vue": "^2.1.7", "tween": "^0.9.0", diff --git a/packages/viewer/src/modules/converter/Converter.js b/packages/viewer/src/modules/converter/Converter.js index c6950c761..bd580e3fe 100644 --- a/packages/viewer/src/modules/converter/Converter.js +++ b/packages/viewer/src/modules/converter/Converter.js @@ -5,6 +5,7 @@ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUti import ObjectWrapper from './ObjectWrapper' import { getConversionFactor } from './Units' +import MeshTriangulationHelper from './MeshTriangulationHelper' /** * Utility class providing some top level conversion methods. @@ -68,7 +69,7 @@ export default class Coverter { // If we can convert it, we should invoke the respective conversion routine. const type = this.getSpeckleType( obj ) - + if ( this[`${type}ToBufferGeometry`] ) { try { await callback( await this[`${type}ToBufferGeometry`]( obj.data || obj, scale ) ) @@ -310,16 +311,13 @@ export default class Coverter { let n = faces[ k ] if ( n <= 3 ) n += 3 // 0 -> 3, 1 -> 4 - if ( n === 4 ) { // QUAD FACE + if ( n === 3 ) { // TRIANGLE FACE indices.push( faces[ k + 1 ], faces[ k + 2 ], faces[ k + 3 ] ) - indices.push( faces[ k + 1 ], faces[ k + 3 ], faces[ k + 4 ] ) - } else if ( n === 3 ) { // TRIANGLE FACE - indices.push( faces[ k + 1 ], faces[ k + 2 ], faces[ k + 3 ] ) - } else { //N-GON FACE - //TODO triangulate n-gon face. - - //There is no need to throw an exception here, unsupported faces will simply be ignored. - //throw new Error( `Mesh type not supported. Face topology indicator: ${faces[k]}` ) + } else { //Quad or N-GON FACE + const triangulation = MeshTriangulationHelper.triangulateFace( k, faces, vertices ) + for( let t = 0; t < triangulation.length; t += 3 ) { + indices.push( triangulation[ t ], triangulation[ t + 1 ], triangulation[ t + 2 ] ) + } } k += n + 1 diff --git a/packages/viewer/src/modules/converter/MeshTriangulationHelper.js b/packages/viewer/src/modules/converter/MeshTriangulationHelper.js new file mode 100644 index 000000000..fa5c4406f --- /dev/null +++ b/packages/viewer/src/modules/converter/MeshTriangulationHelper.js @@ -0,0 +1,213 @@ +/** + * Set of functions to triangulate n-gon faces (i.e. polygon faces with an arbitrary (n) number of vertices). + * This class is a JavaScript port of https://github.com/specklesystems/speckle-sharp/blob/main/Objects/Objects/Utils/MeshTriangulationHelper.cs + */ +export default class MeshTriangulationHelper { + + /** + * Calculates the triangulation of the face at given faceIndex. + * @remarks This implementation is based the ear clipping method proposed by "Christer Ericson (2005) Real-Time Collision Detection. + * @param {Number} faceIndex The index of the face's cardinality indicator `n` + * @param {Number[]} faces The list of faces in the mesh + * @param {Number[]} vertices The list of vertices in the mesh + * @return {Number[]} flat list of triangle faces (without cardinality indicators) + */ + static triangulateFace( faceIndex, faces, vertices ) { + let n = faces[faceIndex] + if ( n <= 3 ) n += 3 // 0 -> 3, 1 -> 4 + + //Converts from relative to absolute index (returns index in mesh.vertices list) + function asIndex( v ) { + return faceIndex + v + 1 + } + + //Gets vertex from a relative vert index + function V( v ) + { + let index = faces[ asIndex( v ) ] * 3 + return new Vector3( vertices[index], vertices[index + 1], vertices[index + 2] ) + } + + let triangleFaces = Array( ( n - 2 ) * 3 ) + + //Calculate face normal using the Newell Method + let faceNormal = new Vector3( 0,0, 0 ) + for ( let ii = n - 1, jj = 0; jj < n; ii = jj, jj++ ) + { + let iPos = V( ii ) + let jPos = V( jj ) + faceNormal.x += ( jPos.y - iPos.y ) * ( iPos.z + jPos.z ) // projection on yz + faceNormal.y += ( jPos.z - iPos.z ) * ( iPos.x + jPos.x ) // projection on xz + faceNormal.z += ( jPos.x - iPos.x ) * ( iPos.y + jPos.y ) // projection on xy + } + faceNormal.normalize() + + //Set up previous and next links to effectively form a double-linked vertex list + const prev = Array( n ) + const next = Array( n ) + for ( let j = 0; j < n; j++ ) + { + prev[j] = j - 1 + next[j] = j + 1 + } + prev[0] = n - 1 + next[n - 1] = 0 + + + //Start clipping ears until we are left with a triangle + let i = 0 + let counter = 0 + while ( n >= 3 ) + { + let isEar = true + + //If we are the last triangle or we have exhausted our vertices, the below statement will be false + if ( n > 3 && counter < n ) + { + const prevVertex = V( prev[i] ) + const earVertex = V( i ) + const nextVertex = V( next[i] ) + + if ( this.triangleIsCCW( faceNormal, prevVertex, earVertex, nextVertex ) ) + { + let k = next[next[i]] + + do + { + if ( this.testPointTriangle( V( k ), prevVertex, earVertex, nextVertex ) ) + { + isEar = false + break + } + + k = next[k] + } while ( k !== prev[i] ) + } + else + { + isEar = false + } + } + + if ( isEar ) + { + const a = faces[asIndex( i )] + const b = faces[asIndex( next[i] )] + const c = faces[asIndex( prev[i] )] + triangleFaces.push( a, b, c ) + + next[prev[i]] = next[i] + prev[next[i]] = prev[i] + n-- + i = prev[i] + counter = 0 + } + else + { + i = next[i] + counter++ + } + } + + return triangleFaces + } + + /** + * Tests if point v is within the triangle *abc*. + * @param {Vector3} v + * @param {Vector3} a + * @param {Vector3} b + * @param {Vector3} c + * @returns {boolean} true if v is within triangle. + */ + static testPointTriangle( v, a, b, c ) + { + function Test( _v, _a, _b ) + { + let crossA = _v.cross( _a ) + let crossB = _v.cross( _b ) + let dotWithEpsilon = Number.EPSILON + crossA.dot( crossB ) + return Math.sign( dotWithEpsilon ) !== -1 + } + + return Test( b.sub( a ), v.sub( a ), c.sub( a ) ) + && Test( c.sub( b ), v.sub( b ), a.sub( b ) ) + && Test( a.sub( c ), v.sub( c ), b.sub( c ) ) + } + + /** + * Checks that triangle abc is clockwise with reference to referenceNormal. + * @param {Vector3} referenceNormal The normal direction of the face. + * @param {Vector3} a + * @param {Vector3} b + * @param {Vector3} c + * @returns {boolean} true if triangle is ccw + */ + static triangleIsCCW( referenceNormal, a, b, c ) + { + const triangleNormal = c.sub( a ).cross( b.sub( a ) ).getNormal() + return referenceNormal.dot( triangleNormal ) > 0.0 + } + +} + +/** + * Encapsulates vector maths operations required for polygon triangulation + */ +class Vector3 { + + constructor( x, y, z ) { + this.x = x + this.y = y + this.z = z + } + + + getNormal( ) + { + const scale = 1.0 / Math.sqrt( this.squareSum() ) + return new Vector3( this.x * scale, this.y * scale, this.z * scale ) + } + + add( v ) { + this.x += v.x + this.y += v.y + this.z += v.z + return this + } + + sub( v ) { + this.x -= v.x + this.y -= v.y + this.z -= v.z + return this + } + + mul( n ) { + this.x *= n + this.y *= n + this.z *= n + return this + } + + dot( v ) { return this.x * v.x + this.y * v.y + this.z * v.z } + + cross( v ) { + this.x = this.y * v.z - this.z * v.y + this.y = this.z * v.x - this.x * v.z + this.z = this.x * v.y - this.y * v.x + + return this + } + + squareSum( ) { + return this.x * this.x + this.y * this.y + this.z * this.z + } + + normalize() { + const scale = 1.0 / Math.sqrt( this.squareSum() ) + return this.mul( scale ) + } + + +} diff --git a/packages/webhook-service/src/webhookCaller.js b/packages/webhook-service/src/webhookCaller.js index e73db3a24..40ae32cdb 100644 --- a/packages/webhook-service/src/webhookCaller.js +++ b/packages/webhook-service/src/webhookCaller.js @@ -26,7 +26,7 @@ async function isLocalNetworkUrl( url ) { } async function makeNetworkRequest( { url, data, headersData } ) { - if ( process.env.ALLOW_LOCAL_NETWORK !== 'true' && isLocalNetworkUrl( url ) ) { + if ( process.env.ALLOW_LOCAL_NETWORK !== 'true' && ( await isLocalNetworkUrl( url ) ) ) { return { success: false, error: 'Local network requests are not allowed. To allow, use ALLOW_LOCAL_NETWORK=true environment variable', diff --git a/readme.md b/readme.md index 69d84f0a4..901646133 100644 --- a/readme.md +++ b/readme.md @@ -91,3 +91,4 @@ For any security vulnerabilities or concerns, please contact us directly at secu ### License Unless otherwise described, the code in this repository is licensed under the Apache-2.0 License. Please note that some modules, extensions or code herein might be otherwise licensed. This is indicated either in the root of the containing folder under a different license file, or in the respective file's header. If you have any questions, don't hesitate to get in touch with us via [email](mailto:hello@speckle.systems). +