├── .gitignore ├── jest.config.js ├── src ├── index.ts ├── trianglesIntersect.ts ├── testUtils │ └── matchers.ts ├── utils.ts ├── utils.test.ts ├── crossIntersect.ts ├── coplanarIntersect.ts └── trianglesIntersect.test.ts ├── demo ├── index.html └── intersect.ts ├── .github └── workflows │ ├── publish.yml │ ├── build.yml │ └── build-demo.yml ├── tsconfig.json ├── .eslintrc.cjs ├── CHANGELOG.md ├── LICENSE ├── rollup.config.js ├── package.json ├── webpack.config.cjs-unused └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | node_modules 3 | build -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "preset": 'ts-jest/presets/js-with-ts-esm', 3 | "testEnvironment": 'node', 4 | 'verbose': true, 5 | "roots": [ 6 | "src", 7 | ], 8 | "globals": { 9 | "ts-jest": { 10 | "useESM": true 11 | } 12 | }, 13 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Author: Axel Antoine 2 | // mail: ax.antoine@gmail.com 3 | // website: https://axantoine.com 4 | // 24/02/2022 5 | 6 | // Loki, Inria project-team with Université de Lille 7 | // within the Joint Research Unit UMR 9189 CNRS-Centrale 8 | // Lille-Université de Lille, CRIStAL. 9 | // https://loki.lille.inria.fr 10 | 11 | // LICENCE: Licence.md 12 | 13 | export {trianglesIntersect, Intersection} from './trianglesIntersect'; 14 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Triangle-Triangle Intersection Demo 5 | 6 | 7 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | jobs: 7 | publish-npm: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | # Setup .npmrc file to publish to npm 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: '16.x' 15 | registry-url: 'https://registry.npmjs.org' 16 | - run: npm ci 17 | - run: npm run build 18 | - run: npm run lint 19 | - run: npm test 20 | - run: npm publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "noEmitOnError": true, 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "removeComments": true, 10 | "strictNullChecks":true, 11 | "strict": true, 12 | "target": "esnext", 13 | "module": "esnext", 14 | "outDir": "build", 15 | "moduleResolution": "node", 16 | "esModuleInterop": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "lib": [ 19 | "es2021", 20 | "dom", 21 | ] 22 | }, 23 | "include": [ 24 | "./src", 25 | "./demo" 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "./src/**/*.test.ts", 30 | "./src/testUtils" 31 | ] 32 | } -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | "jest", 6 | '@typescript-eslint', 7 | ], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | ], 12 | rules: { 13 | "jest/no-disabled-tests": "warn", 14 | "jest/no-focused-tests": "error", 15 | "jest/no-identical-title": "error", 16 | "jest/prefer-to-have-length": "warn", 17 | "jest/valid-expect": "error", 18 | 'array-bracket-spacing':["error"], 19 | 'space-in-parens':["error"], 20 | "@typescript-eslint/no-namespace": "off", 21 | "indent": ["error", 2, { 22 | "FunctionDeclaration": {"parameters": 2}, 23 | "FunctionExpression": {"parameters": 2} 24 | }] 25 | } 26 | }; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.7] - 2022-09-01 2 | 3 | - Fixed wrong location for the ts types definitions 4 | 5 | ## [1.0.6] - 2022-08-01 6 | 7 | - Fixed issue #3 8 | - Added tests 9 | 10 | ## [1.0.5] - 2022-05-17 11 | 12 | - Added more tests 13 | - Fixed issue #1 14 | - Moved three to peerDependencies 15 | 16 | ## [1.0.4] - 2022-05-17 17 | 18 | - Moved three to devDependencies 19 | 20 | ## [1.0.3] - 2022-03-04 21 | 22 | - Added a triangles intersection demo 23 | - Fix an issue where an intersection is found while the first three intersection 24 | tests are all positive or negative 25 | 26 | ## [1.0.2] - 2022-03-02 27 | 28 | - Added rollup config to generate esm and cjs 29 | 30 | ## [1.0.1] - 2022-03-01 31 | 32 | - Updated documentation in Readme 33 | - Updated package info 34 | 35 | ## [1.0.0] - 2022-02-21 36 | 37 | - Added base code for `trianglesIntersect function and associated tests. 38 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | - run: npm ci 29 | - run: npm run build 30 | - run: npm run lint 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /.github/workflows/build-demo.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build-demo 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | - run: npm ci 29 | - run: npm run build-demo 30 | 31 | - name: Commit Demo 32 | uses: EndBug/add-and-commit@v7 33 | with: 34 | add: 'build/demo --force' 35 | message: 'update demo' 36 | push: 'origin HEAD:demo --force' 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 minitoine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import htmlTemplate from 'rollup-plugin-generate-html-template'; 4 | 5 | const lib_cfg = { 6 | input: 'src/index.ts', 7 | external: ['three'], 8 | output: [ 9 | { 10 | name: "FastTriangleTriangleIntersection", 11 | format: 'umd', 12 | file: 'build/index.umd.js', 13 | sourcemap: true, 14 | globals: { 15 | 'three':'three' 16 | } 17 | }, 18 | { 19 | format: 'esm', 20 | file: 'build/index.esm.js', 21 | sourcemap: true, 22 | } 23 | ], 24 | plugins: [typescript({ 25 | tsconfig: './tsconfig.json', 26 | compilerOptions: { 27 | "sourceMap": true, 28 | "declaration": true, 29 | "declarationMap": true, 30 | "declarationDir": "types", 31 | }, 32 | exclude: ["demo/*"] 33 | })] 34 | }; 35 | 36 | const demo_cfg = { 37 | input: 'demo/intersect.ts', 38 | output: { 39 | file: 'build/demo/intersect.js', 40 | }, 41 | plugins: [ 42 | typescript({ 43 | tsconfig: './tsconfig.json' 44 | }), 45 | nodeResolve(), 46 | htmlTemplate({ 47 | template: 'demo/index.html', 48 | target: 'index.html', 49 | }), 50 | ] 51 | }; 52 | 53 | 54 | let exported; 55 | if (process.env.demo) { 56 | exported = demo_cfg; 57 | } else { 58 | exported = lib_cfg; 59 | } 60 | 61 | export default exported; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-triangle-triangle-intersection", 3 | "description": "Fast and robust triangle-triangle intersection test with high precision for cross and coplanar triangles based on the algorithm by Devillers & Guigue.", 4 | "keywords": [ 5 | "triangle", 6 | "intersection", 7 | "crossing", 8 | "coplanar", 9 | "Devillers", 10 | "Muller", 11 | "3D", 12 | "2D", 13 | "three.js", 14 | "precision" 15 | ], 16 | "version": "1.0.7", 17 | "author": { 18 | "name": "Axel Antoine", 19 | "email": "ax.antoine@gmail.com", 20 | "url": "https://axantoine.com" 21 | }, 22 | "license": "MIT", 23 | "branch": "master", 24 | "type": "module", 25 | "main": "build/index.umd.js", 26 | "module": "build/index.esm.js", 27 | "types": "build/types/index.d.ts", 28 | "files": [ 29 | "build" 30 | ], 31 | "peerDependencies": { 32 | "three": ">= 0.123.0" 33 | }, 34 | "devDependencies": { 35 | "@rollup/plugin-node-resolve": "^13.1.3", 36 | "@rollup/plugin-typescript": "^8.3.1", 37 | "@types/dat.gui": "^0.7.7", 38 | "@types/jest": "^27.4.0", 39 | "@types/three": "^0.140.0", 40 | "@typescript-eslint/eslint-plugin": "^5.12.0", 41 | "@typescript-eslint/parser": "^5.13.0", 42 | "dat.gui": "^0.7.9", 43 | "eslint": "^8.10.0", 44 | "eslint-plugin-jest": "^26.1.1", 45 | "jest": "^27.5.1", 46 | "rollup": "^2.69.1", 47 | "rollup-plugin-generate-html-template": "^1.7.0", 48 | "ts-jest": "^27.1.3", 49 | "tslib": "^2.3.1", 50 | "typescript": "^4.6.2" 51 | }, 52 | "scripts": { 53 | "clean": "rm -rf build", 54 | "tsc": "tsc", 55 | "test": "jest", 56 | "build": "rollup -c", 57 | "build-demo": "rollup -c --environment demo", 58 | "lint": "eslint src demo", 59 | "prepublishOnly": "npm run build" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /webpack.config.cjs-unused: -------------------------------------------------------------------------------- 1 | // Author: Axel Antoine 2 | // mail: ax.antoine@gmail.com 3 | // website: https://axantoine.com 4 | // 2022/03/03 5 | 6 | // Loki, Inria project-team with Université de Lille 7 | // within the Joint Research Unit UMR 9189 CNRS-Centrale 8 | // Lille-Université de Lille, CRIStAL. 9 | // https://loki.lille.inria.fr 10 | 11 | // LICENCE: Licence.md 12 | 13 | const pkg = require('./package.json'); 14 | const webpack = require('webpack'); 15 | const {merge} = require('webpack-merge'); 16 | const path = require('path'); 17 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 18 | const nodeExternals = require('webpack-node-externals'); 19 | 20 | const libConfig = { 21 | target: "web", 22 | entry: ["./src/index.ts"], 23 | devtool: "source-map", 24 | mode: "production", 25 | externalsPresets: {node: true}, 26 | externals: [nodeExternals()], 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.tsx?$/, 31 | loader: 'ts-loader', 32 | } 33 | ] 34 | }, 35 | resolve: { 36 | extensions: ['.ts'], 37 | }, 38 | }; 39 | 40 | const esmConfig = { 41 | name: "esm", 42 | experiments: { 43 | outputModule: true, 44 | }, 45 | output: { 46 | filename: "index.js", 47 | path: path.resolve(__dirname, "build"), 48 | library: { 49 | type: "module", 50 | } 51 | } 52 | }; 53 | 54 | const cjsConfig = { 55 | name: "cjs", 56 | output: { 57 | filename: "index.cjs", 58 | path: path.resolve(__dirname, "build"), 59 | library: { 60 | type: "commonjs", 61 | }, 62 | }, 63 | }; 64 | 65 | 66 | const demoConfig = { 67 | name: "demo", 68 | target: "web", 69 | mode: "development", 70 | entry: ["./demo/intersect.ts"], 71 | module: { 72 | rules: [ 73 | { 74 | test: /\.tsx?$/, 75 | loader: 'ts-loader', 76 | options: { 77 | configFile: "tsconfig-demo.json" 78 | } 79 | } 80 | ] 81 | }, 82 | plugins: [ 83 | new HtmlWebpackPlugin({ 84 | template: path.resolve(__dirname, "demo/index.html") 85 | }) 86 | ], 87 | resolve: { 88 | extensions: ['.ts'], 89 | }, 90 | output: { 91 | filename: "intersect.js", 92 | path: path.resolve(__dirname, "build/demo"), 93 | }, 94 | } 95 | 96 | module.exports = env => { 97 | 98 | esm = merge(esmConfig, libConfig) 99 | cjs = merge(cjsConfig, libConfig) 100 | 101 | if (env.demo) { 102 | return [demoConfig]; 103 | } else { 104 | return [esm, cjs]; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/trianglesIntersect.ts: -------------------------------------------------------------------------------- 1 | // Author: Axel Antoine 2 | // mail: ax.antoine@gmail.com 3 | // website: https://axantoine.com 4 | // 24/02/2022 5 | 6 | // Loki, Inria project-team with Université de Lille 7 | // within the Joint Research Unit UMR 9189 CNRS-Centrale 8 | // Lille-Université de Lille, CRIStAL. 9 | // https://loki.lille.inria.fr 10 | 11 | // LICENCE: Licence.md 12 | 13 | // From research papers https://hal.inria.fr/inria-00072100/document 14 | // Adapted from https://github.com/risgpta/Triangle-Triangle-Intersection-Algorithm-Optimised_Approach- 15 | // and 16 | // https://raw.githubusercontent.com/erich666/jgt-code/master/Volume_08/Number_1/Guigue2003/tri_tri_intersect.c 17 | 18 | import {Triangle, Vector3} from 'three'; 19 | import {orient3D, isTriDegenerated} from './utils'; 20 | import {crossIntersect} from './crossIntersect'; 21 | import {coplanarIntersect} from './coplanarIntersect'; 22 | 23 | const _t1 = new Triangle(); 24 | const _t2 = new Triangle(); 25 | 26 | export enum Intersection { 27 | Cross = "Cross", 28 | Coplanar = "Coplanar" 29 | } 30 | 31 | /** 32 | * Return wether triangle t1 and t2 are cross-intersecting or coplanar-intersecting, otherwise returns null. 33 | * If target array is given, it is *emptied*, intersection points are then computed and put in the array. 34 | * 35 | * @param {Triangle} t1 The t 1 36 | * @param {Triangle} t2 The t 2 37 | * @param {Vector3[]} target The target 38 | * @return {(Intersection|null)} { description_of_the_return_value } 39 | */ 40 | export function trianglesIntersect( 41 | t1: Triangle, t2: Triangle, target?: Vector3[]): Intersection | null { 42 | 43 | // Clear target 44 | if (target) { 45 | target.splice(0, target.length); 46 | } 47 | 48 | // Check wether t1 or t2 is degenerated (flat) 49 | if (isTriDegenerated(t1) || isTriDegenerated(t2)) { 50 | console.warn("Degenerated triangles provided, skipping."); 51 | // TODO: check wether degenerated triangle is a line or a point, and compute 52 | // intersection with these new shapes 53 | return null; 54 | } 55 | 56 | _t1.copy(t1); 57 | _t2.copy(t2); 58 | t1 = _t1; 59 | t2 = _t2; 60 | 61 | // Check relative position of t1's vertices againt t2 62 | const o1a = orient3D(t2.a, t2.b, t2.c, t1.a); 63 | const o1b = orient3D(t2.a, t2.b, t2.c, t1.b); 64 | const o1c = orient3D(t2.a, t2.b, t2.c, t1.c); 65 | 66 | if (o1a === o1b && o1a === o1c) { 67 | 68 | if (o1a === 0 && coplanarIntersect(t1, t2, target)) { 69 | return Intersection.Coplanar; 70 | } 71 | return null; 72 | } 73 | 74 | if (crossIntersect(t1, t2, o1a, o1b, o1c, target)) { 75 | return Intersection.Cross; 76 | } 77 | 78 | return null; 79 | } -------------------------------------------------------------------------------- /src/testUtils/matchers.ts: -------------------------------------------------------------------------------- 1 | // Author: Axel Antoine 2 | // mail: ax.antoine@gmail.com 3 | // website: https://axantoine.com 4 | // 24/02/2022 5 | 6 | // Loki, Inria project-team with Université de Lille 7 | // within the Joint Research Unit UMR 9189 CNRS-Centrale 8 | // Lille-Université de Lille, CRIStAL. 9 | // https://loki.lille.inria.fr 10 | 11 | // LICENCE: Licence.md 12 | 13 | import {Vector3} from 'three'; 14 | 15 | declare global { 16 | namespace jest { 17 | interface Matchers { 18 | toEqualVector(expected: Vector3, precision?: number): CustomMatcherResult; 19 | toEqualVectors(expected: Vector3[], precision?: number): CustomMatcherResult; 20 | } 21 | } 22 | } 23 | 24 | function strVector3(a: Vector3) { 25 | return `(${a.x}, ${a.y}, ${a.z})`; 26 | } 27 | 28 | function strPoly3(p: Vector3[]) { 29 | let s = ""; 30 | p.map((v,idx) => s += '\t'+idx+": "+strVector3(v)+'\n'); 31 | return s; 32 | } 33 | 34 | function compareVector3(a: Vector3, b: Vector3) { 35 | if (Math.abs(a.x - b.x) > 1e-10) { 36 | return a.x - b.x; 37 | } else if (Math.abs(a.y - b.y) > 1e-10) { 38 | return a.y - b.y; 39 | } else { 40 | return a.z - b.z; 41 | } 42 | } 43 | 44 | expect.extend({ 45 | 46 | toEqualVector(received: Vector3, expected: Vector3, precision = 1e-10) { 47 | 48 | const pass = received.distanceTo(expected) <= precision; 49 | return { 50 | message: () => 51 | `Expected vectors ${pass? 'not': ''} to be equal`+ 52 | '\nReceived: '+strVector3(received)+ 53 | '\nExpected: '+strVector3(expected), 54 | pass: pass, 55 | }; 56 | }, 57 | 58 | toEqualVectors(received: Vector3[], expected: Vector3[], precision = 1e-10) { 59 | 60 | if (received.length !== expected.length) { 61 | return { 62 | message: () => 63 | `Expected polygons to have the same size`+ 64 | `\nReceived [ size = ${received.length} ]: `+strPoly3(received)+ 65 | `\nExpected [ size = ${expected.length} ]: `+strPoly3(expected), 66 | pass: false 67 | } 68 | } 69 | 70 | received.sort(compareVector3); 71 | expected.sort(compareVector3); 72 | 73 | let pass = true; 74 | let i = 0; 75 | while (pass && i 83 | `Expected polygons not to be equal`+ 84 | '\nReceived: '+strPoly3(received)+ 85 | '\nExpected: '+strPoly3(expected), 86 | pass: true, 87 | }; 88 | } else { 89 | return { 90 | message: () => 91 | `Expected polygons to be equal [failed at index ${i-1}]`+ 92 | '\nReceived: '+strPoly3(received)+ 93 | '\nExpected: '+strPoly3(expected), 94 | pass: false, 95 | }; 96 | } 97 | } 98 | }); 99 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | // Author: Axel Antoine 2 | // mail: ax.antoine@gmail.com 3 | // website: https://axantoine.com 4 | // 24/02/2022 5 | 6 | // Loki, Inria project-team with Université de Lille 7 | // within the Joint Research Unit UMR 9189 CNRS-Centrale 8 | // Lille-Université de Lille, CRIStAL. 9 | // https://loki.lille.inria.fr 10 | 11 | // LICENCE: Licence.md 12 | 13 | import {Triangle, Vector3, Matrix3, Matrix4} from 'three'; 14 | 15 | const EPSILON = 1e-10; 16 | const _tmp1 = new Vector3(); 17 | const _tmp2 = new Vector3(); 18 | const _matrix4 = new Matrix4(); 19 | const _matrix3 = new Matrix3(); 20 | 21 | export function isTriDegenerated(tri: Triangle) { 22 | 23 | _tmp1.subVectors(tri.a, tri.b); 24 | _tmp2.subVectors(tri.a, tri.c); 25 | _tmp1.cross(_tmp2); 26 | 27 | return _tmp1.x > -EPSILON && _tmp1.x < EPSILON && 28 | _tmp1.y > -EPSILON && _tmp1.y < EPSILON && 29 | _tmp1.z > -EPSILON && _tmp1.z < EPSILON; 30 | } 31 | 32 | export function orient3D(a: Vector3, b: Vector3, c: Vector3, d: Vector3) { 33 | 34 | _matrix4.set( 35 | a.x, a.y, a.z, 1, 36 | b.x, b.y, b.z, 1, 37 | c.x, c.y, c.z, 1, 38 | d.x, d.y, d.z, 1 39 | ); 40 | const det = _matrix4.determinant(); 41 | 42 | if (det < -EPSILON) 43 | return -1; 44 | else if (det > EPSILON) 45 | return 1; 46 | else 47 | return 0; 48 | } 49 | 50 | export function orient2D(a: Vector3, b: Vector3, c: Vector3) { 51 | 52 | _matrix3.set( 53 | a.x, a.y, 1, 54 | b.x, b.y, 1, 55 | c.x, c.y, 1 56 | ); 57 | const det = _matrix3.determinant(); 58 | 59 | if (det < -EPSILON) 60 | return -1; 61 | else if (det > EPSILON) 62 | return 1; 63 | else 64 | return 0; 65 | } 66 | 67 | export function permuteTriLeft(tri: Triangle) { 68 | const tmp = tri.a; 69 | tri.a = tri.b; 70 | tri.b = tri.c; 71 | tri.c = tmp; 72 | } 73 | 74 | export function permuteTriRight(tri: Triangle) { 75 | const tmp = tri.c; 76 | tri.c = tri.b; 77 | tri.b = tri.a; 78 | tri.a = tmp; 79 | } 80 | 81 | export function makeTriCounterClockwise(tri: Triangle) { 82 | 83 | if (orient2D(tri.a, tri.b, tri.c) < 0) { 84 | const tmp = tri.c; 85 | tri.c = tri.b; 86 | tri.b = tmp; 87 | } 88 | } 89 | 90 | export function linesIntersect2d( 91 | a1: Vector3, b1: Vector3, 92 | a2: Vector3, b2: Vector3, 93 | target: Vector3) { 94 | 95 | const dx1 = (a1.x-b1.x); 96 | const dx2 = (a2.x-b2.x); 97 | const dy1 = (a1.y-b1.y); 98 | const dy2 = (a2.y-b2.y); 99 | 100 | const D = dx1*dy2 - dx2*dy1; 101 | 102 | // if (D > -EPSILON && D < EPSILON) { 103 | // return false; 104 | // } 105 | 106 | const n1 = a1.x*b1.y - a1.y*b1.x; 107 | const n2 = a2.x*b2.y - a2.y*b2.x; 108 | 109 | target.set((n1*dx2 - n2*dx1)/D, (n1*dy2 - n2*dy1)/D, 0); 110 | 111 | // return true; 112 | } 113 | 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast-triangle-triangle-intersection 2 | 3 | [![build](https://github.com/LokiResearch/fast-triangle-triangle-intersection/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/LokiResearch/fast-triangle-triangle-intersection/actions/workflows/build.yml) 4 | [![npm version](https://badge.fury.io/js/fast-triangle-triangle-intersection.svg)](https://badge.fury.io/js/fast-triangle-triangle-intersection) 5 | [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/LokiResearch/fast-triangle-triangle-intersection.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/LokiResearch/fast-triangle-triangle-intersection/context:javascript) 6 | 7 | Fast and robust triangle-triangle intersection test with high precision for cross and coplanar triangles based on the algorithm by Devillers & Guigue [[1]](https://hal.inria.fr/inria-00072100/document). 8 | 9 | - Uses Three.js 10 | - Computes the intersection shape (point, line or polygon). 11 | - Typescript definitions included. 12 | 13 | ## Demo 14 | 15 | [Online Triangles Intersection demo](https://lokiresearch.github.io/fast-triangle-triangle-intersection/build/demo/) 16 | 17 | ## Install 18 | 19 | `npm i fast-triangle-triangle-intersection` 20 | 21 | ## Documentation 22 | 23 | ```ts 24 | trianglesIntersect(t1: Triangle, t2: Triangle, target?: Array): Intersection 25 | ``` 26 | 27 | Computes wether triangle `t1` and `t2` are intersecting and returns `Intersection.Cross` if triangles are *cross-intersecting*, `Intersection.Coplanar` if triangles are *coplanar-intersecting*, otherwise returns `null`. 28 | If `target` array is given, **it is emptied** and intersection points are then computed and put in the array. 29 | 30 | ## Example 31 | 32 | Check if triangles are simply intersecting. 33 | 34 | ```ts 35 | import {Triangle} from 'three'; 36 | import {trianglesIntersect, Intersection} from 'fast-triangle-triangle-intersection'; 37 | 38 | const t1 = new Triangle(); 39 | t1.a.set(-1, 0, 0); 40 | t1.b.set(2, 0, -2); 41 | t1.c.set(2, 0, 2); 42 | 43 | const t2 = new Triangle(); 44 | t2.a.set(1, 0, 0); 45 | t2.b.set(-2, -2, 0); 46 | t2.c.set(-2, 2, 0); 47 | 48 | const intersection = trianglesIntersect(t1, t2); 49 | if (intersection === Intersection.Cross) { 50 | console.log("Triangles are cross-intersecting."); 51 | } else if (intersection === Intersection.Coplanar) { 52 | console.log("Triangles are coplanar-intersecting."); 53 | } else { 54 | console.log("Triangles are not intersecting."); 55 | } 56 | ``` 57 | 58 | Obtening the intersection points. 59 | 60 | ```ts 61 | const points = new Array(); 62 | if (trianglesIntersect(t1, t2, points)) { 63 | console.log("Intersection points: ", points); // [Vector3(1, 0, 0), Vector3(-1, 0, 0)] 64 | } 65 | ``` 66 | 67 | ## Info 68 | 69 | This algorithm is based on the publication by Devillers & Guigue [[1]](https://hal.inria.fr/inria-00072100/document). 70 | 71 | ``` 72 | @techreport{devillers:inria-00072100, 73 | TITLE = {{Faster Triangle-Triangle Intersection Tests}}, 74 | AUTHOR = {Devillers, Olivier and Guigue, Philippe}, 75 | URL = {https://hal.inria.fr/inria-00072100}, 76 | NUMBER = {RR-4488}, 77 | INSTITUTION = {{INRIA}}, 78 | YEAR = {2002}, 79 | MONTH = Jun, 80 | KEYWORDS = {LOW DEGREE PREDICATE ; COLLISION DETECTION ; GEOMETRIC PREDICATES}, 81 | PDF = {https://hal.inria.fr/inria-00072100/file/RR-4488.pdf}, 82 | HAL_ID = {inria-00072100}, 83 | HAL_VERSION = {v1}, 84 | } 85 | ``` 86 | -------------------------------------------------------------------------------- /src/utils.test.ts: -------------------------------------------------------------------------------- 1 | // Author: Axel Antoine 2 | // mail: ax.antoine@gmail.com 3 | // website: https://axantoine.com 4 | // 24/02/2022 5 | 6 | // Loki, Inria project-team with Université de Lille 7 | // within the Joint Research Unit UMR 9189 CNRS-Centrale 8 | // Lille-Université de Lille, CRIStAL. 9 | // https://loki.lille.inria.fr 10 | 11 | // LICENCE: Licence.md 12 | 13 | import {Triangle, Vector3} from 'three'; 14 | import './testUtils/matchers'; 15 | import * as utils from './utils'; 16 | 17 | const t1 = new Triangle(); 18 | 19 | describe("isTriDegenerated", () => { 20 | 21 | test ("Degenerated triangles", () => { 22 | t1.a.set(1, 1, 0); 23 | t1.b.set(2, 2, 0); 24 | t1.c.set(3, 3, 0); 25 | expect(utils.isTriDegenerated(t1)).toBeTruthy(); 26 | 27 | // Under 1e-10, triangle should be considered as degenerated 28 | t1.a.set(1, 1, 0); 29 | t1.b.set(2-1e-11, 2+1e-11, 0); 30 | t1.c.set(3, 3, 0); 31 | expect(utils.isTriDegenerated(t1)).toBeTruthy(); 32 | }); 33 | 34 | test ("Non Degenerated triangles", () => { 35 | t1.a.set(1, 1, 0); 36 | t1.b.set(3, 3, 0); 37 | t1.c.set(3, 1, 0); 38 | expect(utils.isTriDegenerated(t1)).not.toBeTruthy(); 39 | 40 | // Above 1e-10, triangle should not be considered as degenerated 41 | t1.a.set(1, 1, 0); 42 | t1.b.set(2-1e-9, 2, 0); 43 | t1.c.set(3, 3, 0); 44 | expect(utils.isTriDegenerated(t1)).not.toBeTruthy(); 45 | }); 46 | 47 | }); 48 | 49 | test("orient2D", () => { 50 | 51 | const a = new Vector3(0,0,0); 52 | const b = new Vector3(0,3,0); 53 | const c = new Vector3(); 54 | 55 | c.set(-1, -1, 0); 56 | expect(utils.orient2D(a,b,c)).toBe(1); 57 | 58 | c.set(-1, 2, 0); 59 | expect(utils.orient2D(a,b,c)).toBe(1); 60 | 61 | c.set(-1, 4, 0); 62 | expect(utils.orient2D(a,b,c)).toBe(1); 63 | 64 | c.set(1, -1, 0); 65 | expect(utils.orient2D(a,b,c)).toBe(-1); 66 | 67 | c.set(1, 2, 0); 68 | expect(utils.orient2D(a,b,c)).toBe(-1); 69 | 70 | c.set(1, 4, 0); 71 | expect(utils.orient2D(a,b,c)).toBe(-1); 72 | 73 | c.set(0, 2, 0); 74 | expect(utils.orient2D(a,b,c)).toBe(0); 75 | 76 | c.set(0+1e-11, 2, 0); 77 | expect(utils.orient2D(a,b,c)).toBe(0); 78 | 79 | c.set(0-1e-9, 2, 0); 80 | expect(utils.orient2D(a,b,c)).toBe(1); 81 | 82 | c.set(0+1e-9, 2, 0); 83 | expect(utils.orient2D(a,b,c)).toBe(-1); 84 | 85 | }); 86 | 87 | 88 | describe("permuteTri", () => { 89 | 90 | const a = new Vector3(); 91 | const b = new Vector3(); 92 | const c = new Vector3(); 93 | 94 | beforeEach(() => { 95 | t1.a.set(1, 1, 1); 96 | t1.b.set(2, 2, 2); 97 | t1.c.set(3, 3, 3); 98 | a.copy(t1.a); 99 | b.copy(t1.b); 100 | c.copy(t1.c); 101 | }); 102 | 103 | test("permute right", () => { 104 | utils.permuteTriRight(t1); 105 | expect(t1.a).toEqualVector(c); 106 | expect(t1.b).toEqualVector(a); 107 | expect(t1.c).toEqualVector(b); 108 | }); 109 | 110 | test("permute left", () => { 111 | utils.permuteTriLeft(t1); 112 | expect(t1.a).toEqualVector(b); 113 | expect(t1.b).toEqualVector(c); 114 | expect(t1.c).toEqualVector(a); 115 | }); 116 | 117 | }); 118 | 119 | test("makeTriCounterClockwise", () => { 120 | 121 | const a = new Vector3(1,1,0); 122 | const b = new Vector3(3,1,0); 123 | const c = new Vector3(3,3,0); 124 | 125 | // t1 already CCW 126 | t1.a.copy(a); 127 | t1.b.copy(b); 128 | t1.c.copy(c); 129 | utils.makeTriCounterClockwise(t1); 130 | expect(t1.a).toEqualVector(a); 131 | expect(t1.b).toEqualVector(b); 132 | expect(t1.c).toEqualVector(c); 133 | 134 | // should invert b and c 135 | t1.a.copy(a); 136 | t1.b.copy(c); 137 | t1.c.copy(b); 138 | utils.makeTriCounterClockwise(t1); 139 | expect(t1.a).toEqualVector(a); 140 | expect(t1.b).toEqualVector(b); 141 | expect(t1.c).toEqualVector(c); 142 | 143 | }); 144 | -------------------------------------------------------------------------------- /src/crossIntersect.ts: -------------------------------------------------------------------------------- 1 | // Author: Axel Antoine 2 | // mail: ax.antoine@gmail.com 3 | // website: https://axantoine.com 4 | // 24/02/2022 5 | 6 | // Loki, Inria project-team with Université de Lille 7 | // within the Joint Research Unit UMR 9189 CNRS-Centrale 8 | // Lille-Université de Lille, CRIStAL. 9 | // https://loki.lille.inria.fr 10 | 11 | // LICENCE: Licence.md 12 | 13 | import {Triangle, Vector3} from 'three'; 14 | import {orient3D, permuteTriLeft, permuteTriRight} from './utils'; 15 | 16 | const EPSILON = 1e-10; 17 | const _u = new Vector3(); 18 | const _v = new Vector3(); 19 | const _n1 = new Vector3(); 20 | const _n2 = new Vector3(); 21 | const _i1 = new Vector3(); 22 | const _i2 = new Vector3(); 23 | 24 | export function crossIntersect( 25 | t1: Triangle, t2: Triangle, 26 | o1a: number, o1b: number, o1c: number, 27 | target?: Vector3[]) { 28 | 29 | // Check relative position of t2's vertices againt t1 30 | const o2a = orient3D(t1.a, t1.b, t1.c, t2.a); 31 | const o2b = orient3D(t1.a, t1.b, t1.c, t2.b); 32 | const o2c = orient3D(t1.a, t1.b, t1.c, t2.c); 33 | 34 | if (o2a === o2b && o2a === o2c) { 35 | return false; 36 | } 37 | 38 | makeTriAVertexAlone(t1, o1a, o1b, o1c); 39 | makeTriAVertexAlone(t2, o2a, o2b, o2c); 40 | 41 | makeTriAVertexPositive(t2, t1); 42 | makeTriAVertexPositive(t1, t2); 43 | 44 | const o1 = orient3D(t1.a, t1.b, t2.a, t2.b); 45 | const o2 = orient3D(t1.a, t1.c, t2.c, t2.a); 46 | 47 | if (o1 <= 0 && o2 <=0) { 48 | if (target) { 49 | computeLineIntersection(t1, t2, target); 50 | } 51 | return true; 52 | } 53 | 54 | return false; 55 | } 56 | 57 | function makeTriAVertexAlone(tri: Triangle, oa: number, ob: number, oc: number) { 58 | 59 | // Permute a, b, c so that a is alone on its side 60 | if (oa === ob) { 61 | // c is alone, permute right so c becomes a 62 | permuteTriRight(tri); 63 | 64 | } else if (oa === oc) { 65 | // b is alone, permute so b becomes a 66 | permuteTriLeft(tri); 67 | } else if (ob !== oc) { 68 | 69 | // In case a, b, c have different orientation, put a on positive side 70 | if (ob > 0) { 71 | permuteTriLeft(tri); 72 | } else if (oc > 0) { 73 | permuteTriRight(tri); 74 | } 75 | } 76 | 77 | } 78 | 79 | function makeTriAVertexPositive(tri: Triangle, other: Triangle) { 80 | const o = orient3D(other.a, other.b, other.c, tri.a); 81 | if (o < 0) { 82 | const tmp = other.c; 83 | other.c = other.b; 84 | other.b = tmp; 85 | } 86 | } 87 | 88 | function intersectPlane( 89 | a: Vector3, b: Vector3, 90 | p: Vector3, n: Vector3, 91 | target: Vector3) { 92 | 93 | _u.subVectors(b, a); 94 | _v.subVectors(a, p); 95 | const dot1 = n.dot(_u); 96 | const dot2 = n.dot(_v); 97 | _u.multiplyScalar(-dot2/dot1); 98 | target.addVectors(a, _u); 99 | 100 | } 101 | 102 | function computeLineIntersection(t1: Triangle, t2: Triangle, target: Vector3[]) { 103 | 104 | t1.getNormal(_n1); 105 | t2.getNormal(_n2); 106 | 107 | const o1 = orient3D(t1.a, t1.c, t2.b, t2.a); 108 | const o2 = orient3D(t1.a, t1.b, t2.c, t2.a); 109 | 110 | if (o1 > 0) { 111 | if (o2 > 0) { 112 | 113 | // Intersection: k i l j 114 | intersectPlane(t1.a, t1.c, t2.a, _n2, _i1); // i 115 | intersectPlane(t2.a, t2.c, t1.a, _n1, _i2); // l 116 | 117 | } else { 118 | 119 | // Intersection: k i j l 120 | intersectPlane(t1.a, t1.c, t2.a, _n2, _i1); // i 121 | intersectPlane(t1.a, t1.b, t2.a, _n2, _i2); // j 122 | 123 | } 124 | 125 | } else { 126 | if (o2 > 0) { 127 | 128 | // Intersection: i k l j 129 | intersectPlane(t2.a, t2.b, t1.a, _n1, _i1); // k 130 | intersectPlane(t2.a, t2.c, t1.a, _n1, _i2); // l 131 | 132 | } else { 133 | 134 | // Intersection: i k j l 135 | intersectPlane(t2.a, t2.b, t1.a, _n1, _i1); // i 136 | intersectPlane(t1.a, t1.b, t2.a, _n2, _i2); // k 137 | 138 | } 139 | } 140 | 141 | target.push(_i1.clone()); 142 | if (_i1.distanceTo(_i2) >= EPSILON) { 143 | target.push(_i2.clone()); 144 | } 145 | } -------------------------------------------------------------------------------- /src/coplanarIntersect.ts: -------------------------------------------------------------------------------- 1 | // Author: Axel Antoine 2 | // mail: ax.antoine@gmail.com 3 | // website: https://axantoine.com 4 | // 24/02/2022 5 | 6 | // Loki, Inria project-team with Université de Lille 7 | // within the Joint Research Unit UMR 9189 CNRS-Centrale 8 | // Lille-Université de Lille, CRIStAL. 9 | // https://loki.lille.inria.fr 10 | 11 | // LICENCE: Licence.md 12 | 13 | import {Triangle, Matrix4, Vector3} from 'three'; 14 | import {orient2D, permuteTriLeft, permuteTriRight, 15 | makeTriCounterClockwise, linesIntersect2d} from './utils'; 16 | 17 | const _matrix = new Matrix4(); 18 | const _n = new Vector3(); 19 | const _u = new Vector3(); 20 | const _v = new Vector3(); 21 | const _affineMatrix = new Matrix4().set( 22 | 0,1,0,0, 23 | 0,0,1,0, 24 | 0,0,0,1, 25 | 1,1,1,1 26 | ); 27 | 28 | export function coplanarIntersect(t1: Triangle, t2: Triangle, target?: Vector3[]) { 29 | 30 | // Convert 3D coordinates into coplanar plane coordinates (so that z=0) 31 | 32 | // Get the unit vectors of the plane basis 33 | t1.getNormal(_n); 34 | _u.subVectors(t1.a, t1.b).normalize(); 35 | _v.crossVectors(_n, _u); 36 | 37 | // Move basis to t1.a 38 | _u.add(t1.a); 39 | _v.add(t1.a); 40 | _n.add(t1.a); 41 | 42 | _matrix.set( 43 | t1.a.x, _u.x, _v.x, _n.x, 44 | t1.a.y, _u.y, _v.y, _n.y, 45 | t1.a.z, _u.z, _v.z, _n.z, 46 | 1, 1, 1, 1 47 | ); 48 | 49 | _matrix.invert(); 50 | _matrix.premultiply(_affineMatrix); 51 | 52 | t1.a.applyMatrix4(_matrix); 53 | t1.b.applyMatrix4(_matrix); 54 | t1.c.applyMatrix4(_matrix); 55 | t2.a.applyMatrix4(_matrix); 56 | t2.b.applyMatrix4(_matrix); 57 | t2.c.applyMatrix4(_matrix); 58 | 59 | makeTriCounterClockwise(t1); 60 | makeTriCounterClockwise(t2); 61 | 62 | const p1 = t1.a; 63 | const p2 = t2.a; 64 | const q2 = t2.b; 65 | const r2 = t2.c; 66 | 67 | const o_p2q2 = orient2D(p2, q2, p1); 68 | const o_q2r2 = orient2D(q2, r2, p1); 69 | const o_r2p2 = orient2D(r2, p2, p1); 70 | 71 | // See paper Figure 6 to a better understanding of the decision tree. 72 | 73 | let intersecting = false; 74 | if (o_p2q2 >= 0) { 75 | if (o_q2r2 >= 0) { 76 | if (o_r2p2 >= 0) { 77 | // + + + 78 | intersecting = true; 79 | } else { 80 | // + + - 81 | intersecting = intersectionTypeR1(t1, t2); 82 | } 83 | } else { 84 | if (o_r2p2 >= 0) { 85 | // + - + 86 | permuteTriRight(t2); 87 | intersecting = intersectionTypeR1(t1, t2); 88 | } else { 89 | // + - - 90 | intersecting = intersectionTypeR2(t1, t2); 91 | } 92 | } 93 | } else { 94 | if (o_q2r2 >= 0) { 95 | if (o_r2p2 >= 0) { 96 | // - + + 97 | permuteTriLeft(t2); 98 | intersecting = intersectionTypeR1(t1, t2); 99 | } else { 100 | // - + - 101 | permuteTriLeft(t2); 102 | intersecting = intersectionTypeR2(t1, t2); 103 | } 104 | } else { 105 | if (o_r2p2 >= 0) { 106 | // - - + 107 | permuteTriRight(t2); 108 | intersecting = intersectionTypeR2(t1, t2); 109 | } else { 110 | // - - - 111 | console.error("Triangles should not be flat.", t1, t2, _v); 112 | return false; 113 | } 114 | } 115 | } 116 | 117 | if (intersecting && target) { 118 | clipTriangle(t1, t2, target); 119 | 120 | _matrix.invert(); 121 | for (const p of target) { 122 | p.applyMatrix4(_matrix); 123 | } 124 | } 125 | 126 | return intersecting; 127 | } 128 | 129 | 130 | function intersectionTypeR1(t1: Triangle, t2: Triangle) { 131 | 132 | // Follow paper's convention to ease debug 133 | const p1 = t1.a; 134 | const q1 = t1.b; 135 | const r1 = t1.c; 136 | const p2 = t2.a; 137 | const r2 = t2.c; 138 | 139 | // See paper Figure 9 for a better understanding of the decision tree. 140 | 141 | if (orient2D(r2, p2, q1) >= 0) { // I 142 | if (orient2D(r2, p1, q1) >= 0) { // II.a 143 | if (orient2D(p1, p2, q1) >= 0) { // III.a 144 | return true; 145 | } else { 146 | if (orient2D(p1, p2, r1) >= 0) { // IV.a 147 | if (orient2D(q1, r1, p2) >= 0) { // V 148 | return true; 149 | } 150 | } 151 | } 152 | } 153 | } else { 154 | if (orient2D(r2, p2, r1) >= 0) { // II.b 155 | if (orient2D(q1, r1, r2) >= 0) { // III.b 156 | if (orient2D(p1, p2, r1) >= 0) { // IV.b Diverge from paper 157 | return true; 158 | } 159 | } 160 | } 161 | } 162 | 163 | return false; 164 | } 165 | 166 | function intersectionTypeR2(t1: Triangle, t2: Triangle) { 167 | 168 | // Follow paper's convention to ease debug 169 | const p1 = t1.a; 170 | const q1 = t1.b; 171 | const r1 = t1.c; 172 | const p2 = t2.a; 173 | const q2 = t2.b; 174 | const r2 = t2.c; 175 | 176 | // See paper Figure 10 for a better understanding of the decision tree. 177 | 178 | if (orient2D(r2, p2, q1) >= 0) { // I 179 | if (orient2D(q2, r2, q1) >= 0) { // II.a 180 | if (orient2D(p1, p2, q1) >= 0) { // III.a 181 | if (orient2D(p1, q2, q1) <= 0) { // IV.a 182 | return true; 183 | } 184 | } else { 185 | if (orient2D(p1, p2, r1) >= 0) { // IV.b 186 | if (orient2D(r2, p2, r1) <= 0) { // V.a 187 | return true; 188 | } 189 | } 190 | } 191 | } else { 192 | if (orient2D(p1, q2, q1) <= 0) { // III.b 193 | if (orient2D(q2, r2, r1) >= 0) { // IV.c 194 | if (orient2D(q1, r1, q2) >= 0) { // V.b 195 | return true; 196 | } 197 | } 198 | } 199 | } 200 | } else { 201 | if (orient2D(r2, p2, r1) >= 0) { // II.b 202 | if (orient2D(q1, r1, r2) >= 0) { // III.c 203 | if (orient2D(r1, p1, p2) >= 0) { // IV.d 204 | return true; 205 | } 206 | } else { 207 | if (orient2D(q1, r1, q2) >= 0) { // IV.e 208 | if (orient2D(q2, r2, r1) >= 0) { // V.c 209 | return true; 210 | } 211 | } 212 | } 213 | } 214 | } 215 | 216 | return false; 217 | } 218 | 219 | const _tmp = new Vector3(); 220 | const _clip = new Array(3).fill(_tmp); 221 | const _output = new Array(); 222 | const _inter = new Vector3(); 223 | const _orients = new Array(9).fill(0); 224 | 225 | export function clipTriangle(t1: Triangle, t2: Triangle, target: Vector3[]) { 226 | // https://en.wikipedia.org/wiki/Sutherland–Hodgman_algorithm 227 | 228 | _clip[0] = t1.a; 229 | _clip[1] = t1.b; 230 | _clip[2] = t1.c; 231 | 232 | _output.splice(0, _output.length); 233 | _output.push(t2.a); 234 | _output.push(t2.b); 235 | _output.push(t2.c); 236 | 237 | for (let i=0; i<3; i++) { 238 | 239 | const input = [..._output]; 240 | _output.splice(0, _output.length); 241 | const i_prev = (i+2)%3; 242 | 243 | // Compute orientation for the input regarding the current clip edge 244 | for (let j=0; j= 0) { 252 | if (_orients[j_prev] < 0) { 253 | linesIntersect2d(_clip[i_prev], _clip[i], input[j_prev], input[j], _inter); 254 | _output.push(_inter.clone()); 255 | } 256 | _output.push(input[j].clone()); 257 | } else if (_orients[j_prev] >= 0){ 258 | linesIntersect2d(_clip[i_prev], _clip[i], input[j_prev], input[j], _inter); 259 | _output.push(_inter.clone()); 260 | } 261 | } 262 | } 263 | 264 | // Clear duplicated points 265 | for (const point of _output) { 266 | 267 | let j = 0; 268 | let sameFound = false; 269 | while (!sameFound && j < target.length) { 270 | sameFound = point.distanceTo(target[j]) <= 1e-10; 271 | j++; 272 | } 273 | 274 | if (!sameFound) { 275 | target.push(point); 276 | } 277 | } 278 | 279 | } 280 | -------------------------------------------------------------------------------- /demo/intersect.ts: -------------------------------------------------------------------------------- 1 | import {GUI, GUIController} from 'dat.gui'; 2 | import * as THREE from 'three'; 3 | import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import {trianglesIntersect} from '../src/trianglesIntersect'; 5 | 6 | 7 | const t1 = new THREE.Triangle(); 8 | const t2 = new THREE.Triangle(); 9 | 10 | const params = { 11 | scale: 0.05, 12 | t1Text: "", 13 | t2Text: "", 14 | }; 15 | 16 | t1.a.set(-1, 0, 0); 17 | t1.b.set(2, 0, -2); 18 | t1.c.set(2, 0, 2); 19 | t2.a.set(1, 0, 0); 20 | t2.b.set(-2, -2, 0); 21 | t2.c.set(-2, 2, 0); 22 | 23 | const bgColor = 0x555555; 24 | 25 | // Init renderer 26 | const renderer = new THREE.WebGLRenderer({ antialias: true }); 27 | renderer.setPixelRatio(window.devicePixelRatio); 28 | renderer.setSize(window.innerWidth, window.innerHeight); 29 | renderer.setClearColor(bgColor, 1); 30 | document.body.appendChild(renderer.domElement); 31 | 32 | // Init scene 33 | const scene = new THREE.Scene(); 34 | scene.add(new THREE.AmbientLight(0xffffff, 0.8)); 35 | 36 | // Init camera 37 | const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 50); 38 | camera.position.set(10, 10, 10); 39 | camera.far = 100; 40 | camera.updateProjectionMatrix(); 41 | 42 | // Init material 43 | const interMaterial = new THREE.MeshPhongMaterial({ color: 0xff0000, side: THREE.DoubleSide }); 44 | const t1Material = new THREE.MeshPhongMaterial({ color: 0x0000ff, side: THREE.DoubleSide }); 45 | const t2Material = new THREE.MeshPhongMaterial({ color: 0x00ff00, side: THREE.DoubleSide }); 46 | 47 | // Init tri meshes 48 | const triGeometry = new THREE.BufferGeometry(); 49 | triGeometry.setAttribute('position', new THREE.BufferAttribute( 50 | new Float32Array([1, 1, 1, 2, 2, 2, 3, 3, 3]), 3)); // Only need 9 floats to be updated later on 51 | 52 | const t1Mesh = new THREE.Mesh(triGeometry.clone(), t1Material); 53 | scene.add(t1Mesh); 54 | const t2Mesh = new THREE.Mesh(triGeometry.clone(), t2Material); 55 | scene.add(t2Mesh); 56 | 57 | 58 | // Init controls 59 | const orbitControls = new OrbitControls(camera, renderer.domElement); 60 | 61 | orbitControls.addEventListener('change', function () { 62 | render(false); 63 | }); 64 | 65 | 66 | window.addEventListener('resize', function () { 67 | 68 | camera.aspect = window.innerWidth / window.innerHeight; 69 | camera.updateProjectionMatrix(); 70 | 71 | renderer.setSize(window.innerWidth, window.innerHeight); 72 | render(false); 73 | 74 | }, false); 75 | 76 | 77 | // Intersection 78 | const interInfo = { 79 | type: "", 80 | p1: "", 81 | p2: "", 82 | p3: "", 83 | p4: "", 84 | p5: "", 85 | p6: "", 86 | }; 87 | const interPoints = new Array(); 88 | const interMesh = new THREE.Mesh(new THREE.BufferGeometry(), interMaterial); 89 | scene.add(interMesh); 90 | 91 | 92 | // Init gui 93 | const gui = new GUI(); 94 | 95 | gui.add(params, 'scale', 0.0001, 1, 0.0001).onChange(render); 96 | gui.add(params, 't1Text').onFinishChange(updateTriFromTextFields); 97 | gui.add(params, 't2Text').onFinishChange(updateTriFromTextFields); 98 | 99 | // TriSphere positions 100 | const tris = {'t1': t1, 't2': t2}; 101 | for (const triName of (['t1', 't2'] as const)) { 102 | 103 | const triFolder = gui.addFolder(triName); 104 | 105 | for (const pName of (['a', 'b', 'c'] as const)) { 106 | 107 | const pFolder = triFolder.addFolder(pName); 108 | 109 | for (const coord of ['x', 'y', 'z']) { 110 | 111 | pFolder.add(tris[triName][pName], coord) 112 | .min(-10).max(10).step(0.001).onChange(updateTrianglesFromGui); 113 | } 114 | } 115 | } 116 | 117 | // Intersection gui 118 | const interFolderGui = gui.addFolder("Intersection"); 119 | const interPointsGui = new Array(); 120 | interFolderGui.add(interInfo, 'type'); 121 | interFolderGui.open(); 122 | 123 | gui.open(); 124 | 125 | 126 | function updateTriFromTextFields() { 127 | let text1 = params.t1Text; 128 | let text2 = params.t2Text; 129 | 130 | text1 = text1.replaceAll('),(', ');(',); 131 | text2 = text2.replaceAll('),(', ');('); 132 | text1 = text1.replaceAll('(', '').replaceAll(')', ''); 133 | text2 = text2.replaceAll('(', '').replaceAll(')', ''); 134 | 135 | const vectors1 = text1.split(';'); 136 | const vectors2 = text2.split(';'); 137 | if (vectors1.length !==3 || vectors2.length !== 3){ 138 | return; 139 | } 140 | 141 | const triPos = [t1.a, t1.b, t1.c, t2.a, t2.b, t2.c]; 142 | for (let i=0; i<3; i++) { 143 | const coords1 = vectors1[i].split(','); 144 | const coords2 = vectors2[i].split(','); 145 | if (coords1.length !== 3 || coords2.length !== 3) { 146 | return; 147 | } 148 | 149 | triPos[i].x = Number.parseFloat(coords1[0]) 150 | triPos[i].y = Number.parseFloat(coords1[1]) 151 | triPos[i].z = Number.parseFloat(coords1[2]) 152 | 153 | triPos[i+3].x = Number.parseFloat(coords2[0]) 154 | triPos[i+3].y = Number.parseFloat(coords2[1]) 155 | triPos[i+3].z = Number.parseFloat(coords2[2]) 156 | 157 | } 158 | 159 | updateTriangles(); 160 | } 161 | 162 | function updateTrianglesFromGui() { 163 | updateTextFields(); 164 | updateTriangles(); 165 | } 166 | 167 | function vector3toStr(v: THREE.Vector3) { 168 | return `(${v.x.toFixed(3)},${v.y.toFixed(3)},${v.z.toFixed(3)})`; 169 | } 170 | 171 | function triToStr(tri: THREE.Triangle) { 172 | return vector3toStr(tri.a)+','+vector3toStr(tri.b)+','+vector3toStr(tri.c); 173 | } 174 | 175 | function updateTextFields() { 176 | params.t1Text = triToStr(t1); 177 | params.t2Text = triToStr(t2); 178 | params.t1Text.replace(' ', ''); 179 | params.t2Text.replace(' ', ''); 180 | } 181 | 182 | function updateTrianglesGeometry() { 183 | 184 | const buff1 = t1Mesh.geometry.getAttribute('position'); 185 | buff1.setXYZ(0, t1.a.x, t1.a.y, t1.a.z); 186 | buff1.setXYZ(1, t1.b.x, t1.b.y, t1.b.z); 187 | buff1.setXYZ(2, t1.c.x, t1.c.y, t1.c.z); 188 | buff1.needsUpdate = true; 189 | t1Mesh.geometry.computeVertexNormals(); 190 | 191 | const buff2 = t2Mesh.geometry.getAttribute('position'); 192 | buff2.setXYZ(0, t2.a.x, t2.a.y, t2.a.z); 193 | buff2.setXYZ(1, t2.b.x, t2.b.y, t2.b.z); 194 | buff2.setXYZ(2, t2.c.x, t2.c.y, t2.c.z); 195 | buff2.needsUpdate = true; 196 | t2Mesh.geometry.computeVertexNormals(); 197 | 198 | } 199 | 200 | function updateInterInfoGui() { 201 | 202 | for(let i=0; i(); 20 | let target = new Array(); 21 | 22 | describe("Special cases", () => { 23 | 24 | test ("Parallel triangles", () => { 25 | t1.a.set(1, 0, 0); 26 | t1.b.set(0, 0, 1); 27 | t1.c.set(0, 1, 0); 28 | 29 | t2.a.set(2, 0, 0); 30 | t2.b.set(0, 0, 2); 31 | t2.c.set(0, 2, 0); 32 | 33 | expect(trianglesIntersect(t1, t2)).toBeNull(); 34 | expect(trianglesIntersect(t2, t1)).toBeNull(); 35 | }); 36 | 37 | test ("One triangle degenerated", () => { 38 | t1.a.set(1, 0, 0); 39 | t1.b.set(1, 1, 2); 40 | t1.c.set(1, 0.5, 1); 41 | 42 | t2.a.set(2, 0, 0); 43 | t2.b.set(0, 0, 2); 44 | t2.c.set(0, 2, 0); 45 | 46 | const warn = jest.spyOn(console, 'warn').mockImplementation(); 47 | expect(trianglesIntersect(t1, t2)).toBeNull(); 48 | expect(warn).toHaveBeenCalledWith("Degenerated triangles provided, skipping."); 49 | expect(trianglesIntersect(t2, t1)).toBeNull(); 50 | expect(warn).toHaveBeenCalledWith("Degenerated triangles provided, skipping."); 51 | warn.mockRestore(); 52 | 53 | }); 54 | 55 | }); 56 | 57 | describe("Testing epsilon precision", () => { 58 | 59 | test ("One point close with 1e-10", () => { 60 | t1.a.set(0, 0, 0); 61 | t1.b.set(1, 1, 1); 62 | t1.c.set(1, 1, -1); 63 | 64 | t2.a.set(-1e-10, 0, 0); 65 | t2.b.set(-1, 1, 1); 66 | t2.c.set(-1, 1, -1); 67 | 68 | expect(trianglesIntersect(t1, t2)).toBeNull(); 69 | expect(trianglesIntersect(t2, t1)).toBeNull(); 70 | }); 71 | 72 | test ("One point close with 1e-11", () => { 73 | t1.a.set(0, 0, 0); 74 | t1.b.set(1, 1, 1); 75 | t1.c.set(1, 1, -1); 76 | 77 | t2.a.set(-1e-11, 0, 0); 78 | t2.b.set(-1, 1, 1); 79 | t2.c.set(-1, 1, -1); 80 | 81 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Cross); 82 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Cross); 83 | }); 84 | 85 | test ("Two points close with 1e-10", () => { 86 | t1.a.set(1,0,0); 87 | t1.b.set(-1,0,0); 88 | t1.c.set(0,1,1); 89 | 90 | t2.a.set(1, 0, -1e-10); 91 | t2.b.set(-1, 0, -1e-10); 92 | t2.c.set(0,1,-1); 93 | 94 | expect(trianglesIntersect(t1, t2)).toBeNull(); 95 | expect(trianglesIntersect(t2, t1)).toBeNull(); 96 | }); 97 | 98 | test ("Two points close with 1e-11", () => { 99 | t1.a.set(1,0,0); 100 | t1.b.set(-1,0,0); 101 | t1.c.set(0,1,1); 102 | 103 | t2.a.set(1, 0, -1e-11); 104 | t2.b.set(-1, 0, -1e-11); 105 | t2.c.set(0,1,-1); 106 | 107 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Cross); 108 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Cross); 109 | }); 110 | 111 | test ("Three points close with 1e-10", () => { 112 | t1.a.set(1, 0, 0); 113 | t1.b.set(0, 0, 1); 114 | t1.c.set(0, 1, 0); 115 | 116 | t2.a.set(1+1e-10, 0, 0); 117 | t2.b.set(0, 0, 1+1e-10); 118 | t2.c.set(0, 1+1e-10, 0); 119 | 120 | expect(trianglesIntersect(t1, t2)).toBeNull(); 121 | expect(trianglesIntersect(t2, t1)).toBeNull(); 122 | 123 | }); 124 | 125 | test ("Three points close with 1e-11", () => { 126 | t1.a.set(1, 0, 0); 127 | t1.b.set(0, 0, 1); 128 | t1.c.set(0, 1, 0); 129 | 130 | t2.a.set(1+1e-11, 0, 0); 131 | t2.b.set(0, 0, 1+1e-11); 132 | t2.c.set(0, 1+1e-11, 0); 133 | 134 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 135 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 136 | }); 137 | 138 | }); 139 | 140 | describe("Cross triangles", () => { 141 | 142 | test ("Normal intersection", () => { 143 | t1.a.set(0, 0, 0); 144 | t1.b.set(0, 0, 5); 145 | t1.c.set(5, 0, 0); 146 | 147 | t2.a.set(1, -1, 1); 148 | t2.b.set(1, -1, -1); 149 | t2.c.set(1, 1, 1); 150 | 151 | expected = [ 152 | new Vector3(1,0,0), 153 | new Vector3(1,0,1), 154 | ]; 155 | 156 | target = new Array(); 157 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Cross); 158 | expect(target).toEqualVectors(expected); 159 | 160 | target = new Array(); 161 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Cross); 162 | expect(target).toEqualVectors(expected); 163 | }); 164 | 165 | test("One tri point on the plane of the other tri", () => { 166 | t1.a.set(-1, 0, 0); 167 | t1.b.set(2, 0, -2); 168 | t1.c.set(2, 0, 2); 169 | 170 | t2.a.set(1, 0, 0); 171 | t2.b.set(-2, -2, 0); 172 | t2.c.set(-2, 2, 0); 173 | 174 | expected = [ 175 | new Vector3(1,0,0), 176 | new Vector3(-1,0,0), 177 | ]; 178 | 179 | target = new Array(); 180 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Cross); 181 | expect(target).toEqualVectors(expected); 182 | 183 | target = new Array(); 184 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Cross); 185 | expect(target).toEqualVectors(expected); 186 | }); 187 | 188 | test("One point of intersection", () => { 189 | t1.a.set(0,0,0); 190 | t1.b.set(0,0,2); 191 | t1.c.set(2,0,0); 192 | 193 | t2.a.set(1, -1, 0); 194 | t2.b.set(1, 1, 0); 195 | t2.c.set(1, 0, -1); 196 | 197 | expected = [ 198 | new Vector3(1,0,0), 199 | ]; 200 | 201 | target = new Array(); 202 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Cross); 203 | expect(target).toEqualVectors(expected); 204 | 205 | target = new Array(); 206 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Cross); 207 | expect(target).toEqualVectors(expected); 208 | }); 209 | 210 | test("One point in common", () => { 211 | t1.a.set(1, 0, 0); 212 | t1.b.set(2, 0, -2); 213 | t1.c.set(2, 0, 2); 214 | 215 | t2.a.set(1, 0, 0); 216 | t2.b.set(0, -2, 0); 217 | t2.c.set(0, 2, 0); 218 | 219 | expected = [ 220 | new Vector3(1,0,0), 221 | ]; 222 | 223 | target = new Array(); 224 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Cross); 225 | expect(target).toEqualVectors(expected); 226 | 227 | target = new Array(); 228 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Cross); 229 | expect(target).toEqualVectors(expected); 230 | }); 231 | 232 | test("One side in common", () => { 233 | t1.a.set(0,0,0); 234 | t1.b.set(3,0,0); 235 | t1.c.set(0,1,2); 236 | 237 | t2.a.set(0,0,0); 238 | t2.b.set(3,0,0); 239 | t2.c.set(0,1,-2); 240 | 241 | expected = [ 242 | new Vector3(0,0,0), 243 | new Vector3(3,0,0), 244 | ]; 245 | 246 | target = new Array(); 247 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Cross); 248 | expect(target).toEqualVectors(expected); 249 | 250 | target = new Array(); 251 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Cross); 252 | expect(target).toEqualVectors(expected); 253 | }); 254 | 255 | test("A part of a side in common", () => { 256 | t1.a.set(0,0,0); 257 | t1.b.set(3,0,0); 258 | t1.c.set(0,1,2); 259 | 260 | t2.a.set(1,0,0); 261 | t2.b.set(2,0,0); 262 | t2.c.set(0,1,-2); 263 | 264 | expected = [ 265 | new Vector3(1,0,0), 266 | new Vector3(2,0,0), 267 | ]; 268 | 269 | target = new Array(); 270 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Cross); 271 | expect(target).toEqualVectors(expected); 272 | 273 | target = new Array(); 274 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Cross); 275 | expect(target).toEqualVectors(expected); 276 | }); 277 | 278 | test("Almost coplanar and common point", () => { 279 | t1.a.set(0.0720, 0.2096, 0.3220); 280 | t1.b.set(0.0751, 0.2148, 0.3234); 281 | t1.c.set(0.0693, 0.2129, 0.3209); 282 | 283 | t2.a.set(0.0677, 0.2170, 0.3196); 284 | t2.b.set(0.0607, 0.2135, 0.3165); 285 | t2.c.set(0.0693, 0.2129, 0.3209); 286 | 287 | expected = [ 288 | new Vector3(0.0693, 0.2129, 0.3209), 289 | ]; 290 | 291 | target = new Array(); 292 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Cross); 293 | expect(target).toEqualVectors(expected); 294 | 295 | target = new Array(); 296 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Cross); 297 | expect(target).toEqualVectors(expected); 298 | }); 299 | 300 | }); 301 | 302 | describe("Coplanar triangles", () => { 303 | 304 | test("Same triangles", () => { 305 | t1.a.set(0,0,0); 306 | t1.b.set(2,2,0); 307 | t1.c.set(0,4,0); 308 | 309 | t2.a.set(0,0,0); 310 | t2.b.set(2,2,0); 311 | t2.c.set(0,4,0); 312 | 313 | expected = [ 314 | new Vector3(0,0,0), 315 | new Vector3(0,4,0), 316 | new Vector3(2,2,0), 317 | ] 318 | 319 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Coplanar); 320 | expect(target).toEqualVectors(expected); 321 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Coplanar); 322 | expect(target).toEqualVectors(expected); 323 | }); 324 | 325 | test("One point in common", () => { 326 | t1.a.set(0,0,0); 327 | t1.b.set(1,2,0); 328 | t1.c.set(0,4,0); 329 | 330 | t2.a.set(1,2,0); 331 | t2.b.set(3,0,0); 332 | t2.c.set(3,4,0); 333 | 334 | expected = [ 335 | new Vector3(1,2,0), 336 | ] 337 | 338 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Coplanar); 339 | expect(target).toEqualVectors(expected); 340 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Coplanar); 341 | expect(target).toEqualVectors(expected); 342 | }); 343 | 344 | test("One point inside other triangle", () => { 345 | t1.a.set(0,0,0); 346 | t1.b.set(2,2,0); 347 | t1.c.set(0,4,0); 348 | 349 | t2.a.set(1,2,0); 350 | t2.b.set(3,0,0); 351 | t2.c.set(3,4,0); 352 | 353 | expected = [ 354 | new Vector3(1,2,0), 355 | new Vector3(1.5,1.5,0), 356 | new Vector3(2,2,0), 357 | new Vector3(1.5,2.5,0), 358 | ] 359 | 360 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Coplanar); 361 | expect(target).toEqualVectors(expected); 362 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Coplanar); 363 | expect(target).toEqualVectors(expected); 364 | }); 365 | 366 | test("Two points in common", () => { 367 | t1.a.set(0,0,0); 368 | t1.b.set(3,3,0); 369 | t1.c.set(0,6,0); 370 | 371 | t2.a.set(0,0,0); 372 | t2.b.set(-3,3,0); 373 | t2.c.set(0,6,0); 374 | 375 | expected = [ 376 | new Vector3(0,0,0), 377 | new Vector3(0,6,0), 378 | ] 379 | 380 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Coplanar); 381 | expect(target).toEqualVectors(expected); 382 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Coplanar); 383 | expect(target).toEqualVectors(expected); 384 | }); 385 | 386 | test("Two points inside other triangle", () => { 387 | t1.a.set(0,0,0); 388 | t1.b.set(3,3,0); 389 | t1.c.set(0,6,0); 390 | 391 | t2.a.set(1,2,0); 392 | t2.b.set(2,1,0); 393 | t2.c.set(2,3,0); 394 | 395 | expected = [ 396 | new Vector3(1,2,0), 397 | new Vector3(1.5,1.5,0), 398 | new Vector3(2,2,0), 399 | new Vector3(2,3,0) 400 | ] 401 | 402 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Coplanar); 403 | expect(target).toEqualVectors(expected); 404 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Coplanar); 405 | expect(target).toEqualVectors(expected); 406 | }); 407 | 408 | test("Triangle inside other triangle in 3D", () => { 409 | t1.a.set(0,0,0); 410 | t1.b.set(4,0,0); 411 | t1.c.set(2,4,4); 412 | 413 | t2.a.set(2,3,3); 414 | t2.b.set(1,1,1); 415 | t2.c.set(3,1,1); 416 | 417 | expected = [ 418 | new Vector3(2,3,3), 419 | new Vector3(1,1,1), 420 | new Vector3(3,1,1), 421 | ] 422 | 423 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Coplanar); 424 | expect(target).toEqualVectors(expected); 425 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Coplanar); 426 | expect(target).toEqualVectors(expected); 427 | }); 428 | 429 | test("One point inside each other 3D", () => { 430 | t1.a.set(0,0,0); 431 | t1.b.set(2,2,2); 432 | t1.c.set(0,4,4); 433 | 434 | t2.a.set(0,2,2); 435 | t2.b.set(2,4,4); 436 | t2.c.set(2,0,0); 437 | 438 | expected = [ 439 | new Vector3(1,1,1), 440 | new Vector3(0,2,2), 441 | new Vector3(1,3,3), 442 | new Vector3(2,2,2), 443 | ] 444 | 445 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Coplanar); 446 | expect(target).toEqualVectors(expected); 447 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Coplanar); 448 | expect(target).toEqualVectors(expected); 449 | }); 450 | 451 | test("All edges crossing 3D", () => { 452 | t1.a.set(0,2,2); 453 | t1.b.set(4,0,0); 454 | t1.c.set(4,4,4); 455 | 456 | t2.a.set(2,0,0); 457 | t2.b.set(6,2,2); 458 | t2.c.set(2,4,4); 459 | 460 | expected = [ 461 | new Vector3(2,1,1), 462 | new Vector3(3,0.5,0.5), 463 | new Vector3(4,1,1), 464 | new Vector3(4,3,3), 465 | new Vector3(3,3.5,3.5), 466 | new Vector3(2,3,3), 467 | ] 468 | 469 | expect(trianglesIntersect(t2, t1, target)).toBe(Intersection.Coplanar); 470 | expect(target).toEqualVectors(expected); 471 | expect(trianglesIntersect(t1, t2, target)).toBe(Intersection.Coplanar); 472 | expect(target).toEqualVectors(expected); 473 | }); 474 | 475 | }); 476 | 477 | 478 | 479 | describe("More coplanar triangles", () => { 480 | 481 | beforeEach(() => { 482 | t2.a.set(3,0,0); 483 | t2.b.set(0,3,0); 484 | t2.c.set(0,0,0); 485 | }); 486 | 487 | test("p orientation: + - -", () => { 488 | t1.a.set(-1,-1,0); 489 | t1.b.set(1,1,0); 490 | t1.c.set(1,-1,0); 491 | 492 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 493 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 494 | }); 495 | 496 | test("p orientation: + + -", () => { 497 | t1.a.set(1,-1,0); 498 | t1.b.set(1,1,0); 499 | t1.c.set(5,-1,0); 500 | 501 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 502 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 503 | }); 504 | 505 | test("p orientation: - + -", () => { 506 | t1.a.set(5,-1,0); 507 | t1.b.set(1,1,0); 508 | t1.c.set(2,2,0); 509 | 510 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 511 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 512 | }); 513 | 514 | test("p orientation: - + +", () => { 515 | t1.a.set(2,2,0); 516 | t1.b.set(1,1,0); 517 | t1.c.set(-1,5,0); 518 | 519 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 520 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 521 | }); 522 | 523 | test("p orientation: - - +", () => { 524 | t1.a.set(-1,5,0); 525 | t1.b.set(1,1,0); 526 | t1.c.set(-1,2,0); 527 | 528 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 529 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 530 | }); 531 | 532 | test("p orientation: + - +", () => { 533 | t1.a.set(-1,2,0); 534 | t1.b.set(1,1,0); 535 | t1.c.set(-1,-1,0); 536 | 537 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 538 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 539 | }); 540 | 541 | test("p orientation: + + +", () => { 542 | t1.a.set(1,1,0); 543 | t1.b.set(-1,5,0); 544 | t1.c.set(-1,2,0); 545 | 546 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 547 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 548 | }); 549 | 550 | 551 | }); 552 | 553 | describe("Even more coplanar triangles", () => { 554 | 555 | beforeEach(() => { 556 | t2.a.set(3,0,0); 557 | t2.b.set(0,3,0); 558 | t2.c.set(0,0,0); 559 | }); 560 | 561 | test("p orientation: + - -", () => { 562 | t1.a.set(-1,-1,0); 563 | t1.b.set(1,1,0); 564 | t1.c.set(t1.a.x+0.2, t1.a.y-0.3, 0); 565 | 566 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 567 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 568 | }); 569 | 570 | test("p orientation: + + -", () => { 571 | t1.a.set(1,-1,0); 572 | t1.b.set(1,1,0); 573 | t1.c.set(t1.a.x+0.2, t1.a.y-0.3, 0); 574 | 575 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 576 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 577 | }); 578 | 579 | test("p orientation: - + -", () => { 580 | t1.a.set(5,-1,0); 581 | t1.b.set(1,1,0); 582 | t1.c.set(t1.a.x+0.2, t1.a.y-0.3, 0); 583 | 584 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 585 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 586 | }); 587 | 588 | test("p orientation: - + +", () => { 589 | t1.a.set(2,2,0); 590 | t1.b.set(1,1,0); 591 | t1.c.set(t1.a.x+0.2, t1.a.y-0.3, 0); 592 | 593 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 594 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 595 | }); 596 | 597 | test("p orientation: - - +", () => { 598 | t1.a.set(-1,5,0); 599 | t1.b.set(1,1,0); 600 | t1.c.set(t1.a.x+0.2, t1.a.y-0.3, 0); 601 | 602 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 603 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 604 | }); 605 | 606 | test("p orientation: + - +", () => { 607 | t1.a.set(-1,2,0); 608 | t1.b.set(1,1,0); 609 | t1.c.set(t1.a.x+0.2, t1.a.y-0.3, 0); 610 | 611 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 612 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 613 | }); 614 | 615 | test("p orientation: + + +", () => { 616 | t1.a.set(1,1,0); 617 | t1.b.set(-1,5,0); 618 | t1.c.set(t1.a.x+0.2, t1.a.y-0.3, 0); 619 | 620 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 621 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 622 | }); 623 | 624 | 625 | }); 626 | 627 | describe("Github Issues triangles", () => { 628 | 629 | test("issue #1", () => { 630 | t1.a.set(-2,-2,0); 631 | t1.b.set(2,-2,0); 632 | t1.c.set(0,2,0); 633 | 634 | t2.a.set(0,3,0); 635 | t2.b.set(-3,-1,0); 636 | t2.c.set(3,-1,0); 637 | 638 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 639 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 640 | }); 641 | 642 | test("issue #3", () => { 643 | t1.a.set(-1,0,0); 644 | t1.b.set(2,-2,0); 645 | t1.c.set(2,2,0); 646 | 647 | t2.a.set(0.551,-0.796,0); 648 | t2.b.set(1.224,0.326,0); 649 | t2.c.set(3.469,1,0); 650 | 651 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 652 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 653 | 654 | t1.a.set(-1,0,0); 655 | t1.b.set(2,0,-2); 656 | t1.c.set(2,0,2); 657 | 658 | t2.a.set(0.551,0,-0.796); 659 | t2.b.set(1.224,0,0.326); 660 | t2.c.set(3.469,0,1); 661 | 662 | expect(trianglesIntersect(t1, t2)).toBe(Intersection.Coplanar); 663 | expect(trianglesIntersect(t2, t1)).toBe(Intersection.Coplanar); 664 | }); 665 | 666 | 667 | }); 668 | 669 | --------------------------------------------------------------------------------