├── .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 | [](https://github.com/LokiResearch/fast-triangle-triangle-intersection/actions/workflows/build.yml)
4 | [](https://badge.fury.io/js/fast-triangle-triangle-intersection)
5 | [](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 |
--------------------------------------------------------------------------------