├── .github ├── FUNDING.yml ├── codecov.yml └── workflows │ └── ci.yml ├── .gitignore ├── renovate.json ├── tsconfig.json ├── lib ├── ConvexHull.d.ts └── ConvexHull.js ├── CHANGELOG.md ├── .eslintrc.json ├── README.md ├── package.json ├── test └── index.test.ts └── src ├── utils.ts └── index.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [donmccurdy] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Builds 5 | dist 6 | 7 | # Coverage 8 | coverage 9 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>donmccurdy/renovate-config"], 3 | "packageRules": [ 4 | { 5 | "description": "three.js >140 prevents testing legacy Geometry support", 6 | "matchPackageNames": ["three", "@types/three"], 7 | "enabled": false 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "paths": { 5 | "three-to-cannon": ["./"] 6 | }, 7 | "module": "es2020", 8 | "lib": ["es2020", "dom"], 9 | "target": "es2020", 10 | "declaration": true, 11 | "typeRoots": ["node_modules/@types"], 12 | "strict": true, 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /lib/ConvexHull.d.ts: -------------------------------------------------------------------------------- 1 | import { Mesh, Vector3 } from 'three'; 2 | 3 | declare class HalfEdge { 4 | next: HalfEdge; 5 | head: () => {point: Vector3}; 6 | } 7 | 8 | declare class Face { 9 | edge: HalfEdge; 10 | normal: Vector3; 11 | } 12 | 13 | declare class ConvexHull { 14 | public faces: Face[]; 15 | setFromObject(mesh: Mesh): this; 16 | toJSON(): [ positions: number[], cells: [number, number, number][] ]; 17 | } 18 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | # Based on https://docs.codecov.com/docs/codecov-yaml#default-yaml 2 | codecov: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "50...95" 9 | status: 10 | patch: off 11 | project: 12 | default: 13 | target: 80% 14 | threshold: 1% 15 | 16 | parsers: 17 | gcov: 18 | branch_detection: 19 | conditional: yes 20 | loop: yes 21 | method: no 22 | macro: no 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v5 4 | 5 | - Removed support for deprecated THREE.Geometry, use THREE.BufferGeometry instead. 6 | 7 | ## v4 8 | 9 | - Removed dependency on deprecated THREE.Geometry, which was removed in three.js >r125. 10 | - THREE.Geometry inputs are still supported, and converted automatically to THREE.BufferGeometry. 11 | - Converted the project to TypeScript. 12 | - Moved type enum from `threeToCannon.Type` to top-level `ShapeType` export. 13 | - Optional `.offset` and `.quaternion` properties are returned with the Shape, not attached to it: 14 | 15 | ```js 16 | // Before: 17 | const shape = threeToCannon(object); 18 | shape.offset; // → CANNON.Vec3 | undefined 19 | shape.quaternion; // → CANNON.Quaternion | undefined 20 | 21 | // After: 22 | const {shape, offset, quaternion} = threeToCannon(object); 23 | ``` 24 | 25 | ## v3 26 | 27 | ... 28 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": [ 4 | "@typescript-eslint" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "es6": true, 9 | "node": true 10 | }, 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/eslint-recommended", 14 | "plugin:@typescript-eslint/recommended" 15 | ], 16 | "parserOptions": { 17 | "ecmaVersion": 2018, 18 | "sourceType": "module" 19 | }, 20 | "rules": { 21 | "@typescript-eslint/no-use-before-define": "off", 22 | "@typescript-eslint/no-unused-vars": ["warn", {"argsIgnorePattern": "^_"}], 23 | "@typescript-eslint/no-non-null-assertion": "off", 24 | "quotes": ["warn", "single"], 25 | "max-len": ["warn", {"code": 100, "tabWidth": 4, "ignoreUrls": true, "ignorePattern": "^import|^export"}], 26 | "newline-per-chained-call": [2, {"ignoreChainWithDepth": 3}] 27 | }, 28 | "ignorePatterns": ["**/*.js"] 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x] 16 | 17 | env: 18 | CI: true 19 | COVERAGE: ${{ matrix.node-version == '18.x' && true || false }} 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: yarn install 28 | - run: yarn dist 29 | - run: yarn test 30 | 31 | # Coverage. 32 | - name: Run coverage 33 | if: ${{ env.COVERAGE == 'true' }} 34 | run: | 35 | yarn coverage 36 | yarn coverage:report 37 | - name: Report coverage 38 | if: ${{ env.COVERAGE == 'true' }} 39 | uses: codecov/codecov-action@v5 40 | with: 41 | files: coverage/coverage.lcov 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # three-to-cannon 2 | 3 | [![Latest NPM release](https://img.shields.io/npm/v/three-to-cannon.svg)](https://www.npmjs.com/package/three-to-cannon) 4 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/three-to-cannon)](https://bundlephobia.com/package/three-to-cannon) 5 | [![License](https://img.shields.io/badge/license-MIT-007ec6.svg)](https://github.com/donmccurdy/three-to-cannon/blob/master/LICENSE) 6 | [![Build Status](https://github.com/donmccurdy/three-to-cannon/workflows/build/badge.svg?branch=main&event=push)](https://github.com/donmccurdy/three-to-cannon/actions?query=workflow%3Abuild) 7 | [![Coverage](https://codecov.io/gh/donmccurdy/three-to-cannon/branch/main/graph/badge.svg?token=S30LCC3L04)](https://codecov.io/gh/donmccurdy/three-to-cannon) 8 | 9 | Convert a [THREE.Mesh](https://threejs.org/docs/?q=mesh#api/en/objects/Mesh) or [THREE.Object3D](https://threejs.org/docs/?q=object3d#api/en/core/Object3D) to a [CANNON.Shape](https://pmndrs.github.io/cannon-es/docs/classes/Shape.html), with optimizations to simplified bounding shapes (AABB, sphere, etc.). 10 | 11 | ## API 12 | 13 | Installation: 14 | 15 | ```js 16 | npm install --save three-to-cannon 17 | ``` 18 | 19 | Use: 20 | 21 | ```js 22 | /**************************************** 23 | * Import: 24 | */ 25 | 26 | // ES6 27 | import { threeToCannon, ShapeType } from 'three-to-cannon'; 28 | 29 | // CommonJS 30 | const { threeToCannon, ShapeType } = require('three-to-cannon'); 31 | 32 | /**************************************** 33 | * Generate a CANNON.Shape: 34 | */ 35 | 36 | // Automatic (Usually an AABB, except obvious cases like THREE.SphereGeometry). 37 | const result = threeToCannon(object3D); 38 | 39 | // Bounding box (AABB). 40 | const result = threeToCannon(object3D, {type: ShapeType.BOX}); 41 | 42 | // Bounding sphere. 43 | const result = threeToCannon(object3D, {type: ShapeType.SPHERE}); 44 | 45 | // Cylinder. 46 | const result = threeToCannon(object3D, {type: ShapeType.CYLINDER}); 47 | 48 | // Convex hull. 49 | const result = threeToCannon(object3D, {type: ShapeType.HULL}); 50 | 51 | // Mesh (Not recommended — limitations: https://github.com/pmndrs/cannon-es/issues/21). 52 | const result = threeToCannon(object3D, {type: ShapeType.MESH}); 53 | 54 | /**************************************** 55 | * Using the result: 56 | */ 57 | 58 | // Result object includes a CANNON.Shape instance, and (optional) 59 | // an offset or quaternion for that shape. 60 | const {shape, offset, orientation} = result; 61 | 62 | // Add the shape to a CANNON.Body. 63 | body.addShape(shape, offset, orientation); 64 | ``` 65 | 66 | See further documentation on the [CANNON.Shape](https://pmndrs.github.io/cannon-es/docs/classes/Shape.html) class. 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-to-cannon", 3 | "version": "5.0.2", 4 | "description": "Convert a THREE.Mesh to a CANNON.Shape.", 5 | "type": "module", 6 | "sideEffects": false, 7 | "source": "./src/index.ts", 8 | "types": "./dist/src/index.d.ts", 9 | "main": "./dist/three-to-cannon.cjs", 10 | "module": "./dist/three-to-cannon.esm.js", 11 | "exports": { 12 | "types": "./dist/src/index.d.ts", 13 | "require": "./dist/three-to-cannon.cjs", 14 | "default": "./dist/three-to-cannon.modern.js" 15 | }, 16 | "scripts": { 17 | "dist": "microbundle --format cjs,esm,modern,umd --no-compress --globals three=THREE --external three", 18 | "watch": "microbundle watch --format cjs,esm,modern,umd --no-compress --globals three=THREE --external three", 19 | "test": "ava --no-worker-threads test/*.test.ts", 20 | "coverage": "c8 --reporter=lcov --reporter=text ava --no-worker-threads test/*.ts --tap", 21 | "coverage:report": "c8 report --reporter=text-lcov > coverage/coverage.lcov", 22 | "preversion": "rimraf dist/* && npm run dist && npm test", 23 | "postversion": "git push && git push --tags && npm publish && yarn coverage:report", 24 | "lint": "eslint src/* test/* --fix" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/donmccurdy/three-to-cannon.git" 29 | }, 30 | "keywords": [ 31 | "threejs", 32 | "three", 33 | "cannonjs", 34 | "cannon", 35 | "physics", 36 | "simulation" 37 | ], 38 | "author": "Don McCurdy ", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/donmccurdy/three-to-cannon/issues" 42 | }, 43 | "homepage": "https://github.com/donmccurdy/three-to-cannon#readme", 44 | "peerDependencies": { 45 | "cannon-es": "0.x.x", 46 | "three": ">=0.125.x" 47 | }, 48 | "dependencies": { 49 | "@types/three": ">=0.160.0" 50 | }, 51 | "devDependencies": { 52 | "@typescript-eslint/eslint-plugin": "8.29.0", 53 | "@typescript-eslint/parser": "8.29.0", 54 | "ava": "6.2.0", 55 | "c8": "10.1.3", 56 | "cannon-es": "0.20.0", 57 | "eslint": "9.1.1", 58 | "microbundle": "0.15.1", 59 | "rimraf": "5.0.10", 60 | "three": "0.160.0", 61 | "tsx": "^4.7.0", 62 | "typescript": "5.8.2" 63 | }, 64 | "files": [ 65 | "dist/", 66 | "lib/", 67 | "index.js", 68 | "README.md", 69 | "LICENSE", 70 | "package.json", 71 | "package-lock.json" 72 | ], 73 | "browserslist": [ 74 | "defaults", 75 | "not IE 11", 76 | "node >= 14" 77 | ], 78 | "ava": { 79 | "extensions": { 80 | "ts": "module" 81 | }, 82 | "nodeArguments": [ 83 | "--import=tsx" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Box, ConvexPolyhedron, Cylinder, Shape, Sphere, Trimesh } from 'cannon-es'; 2 | import test from 'ava'; 3 | import { BoxGeometry, Group, Matrix4, Mesh } from 'three'; 4 | import { getShapeParameters, ShapeParameters, ShapeResult, ShapeType, threeToCannon } from 'three-to-cannon'; 5 | 6 | const object = new Mesh(new BoxGeometry(10, 10, 10)); 7 | 8 | function equalsApprox (a: number, b: number) { 9 | return Math.abs( a - b ) < 0.0001; 10 | } 11 | 12 | test('getShapeParameters - shape - box', function (t) { 13 | const {type, params} = getShapeParameters(object, { 14 | type: ShapeType.BOX, 15 | }) as ShapeParameters; 16 | 17 | t.is( type, ShapeType.BOX, 'type' ); 18 | t.is( params.x, 5, 'params.x (half extent x)' ); 19 | t.is( params.y, 5, 'params.y (half extent y)' ); 20 | t.is( params.z, 5, 'params.z (half extent z)' ); 21 | }); 22 | 23 | test('threeToCannon - shape - box', function (t) { 24 | const {shape: box} = threeToCannon(object, { 25 | type: ShapeType.BOX, 26 | }) as ShapeResult; 27 | 28 | t.is( box.type, Shape.types.BOX, 'box.type' ); 29 | t.is( box.halfExtents.x, 5, 'box.halfExtents.x' ); 30 | t.is( box.halfExtents.y, 5, 'box.halfExtents.y' ); 31 | t.is( box.halfExtents.z, 5, 'box.halfExtents.z' ); 32 | }); 33 | 34 | test('getShapeParameters - shape - sphere', function (t) { 35 | const {type, params} = getShapeParameters(object, { 36 | type: ShapeType.SPHERE, 37 | }) as ShapeParameters; 38 | 39 | t.is( type, ShapeType.SPHERE, 'type' ); 40 | t.true( equalsApprox( params.radius, 8.660254 ), 'params.radius' ); 41 | }); 42 | 43 | test('threeToCannon - shape - sphere', function (t) { 44 | const {shape: sphere} = threeToCannon(object, { 45 | type: ShapeType.SPHERE, 46 | }) as ShapeResult; 47 | 48 | t.is( sphere.type, Shape.types.SPHERE, 'sphere.type' ); 49 | t.true( equalsApprox( sphere.radius, 8.660254 ), 'sphere.radius' ); 50 | }); 51 | 52 | test('getShapeParameters - shape - cylinder', function (t) { 53 | const {type, params} = getShapeParameters(object, { 54 | type: ShapeType.CYLINDER, 55 | }) as ShapeParameters; 56 | 57 | t.is( type, ShapeType.CYLINDER, 'type' ); 58 | t.is( params.radiusTop, 5, 'params.radiusTop' ); 59 | t.is( params.radiusBottom, 5, 'params.radiusBottom' ); 60 | t.is( params.height, 10, 'params.height' ); 61 | }); 62 | 63 | test('threeToCannon - shape - cylinder', function (t) { 64 | const { 65 | shape: cylinder, 66 | orientation 67 | } = threeToCannon(object, {type: ShapeType.CYLINDER}) as ShapeResult; 68 | 69 | t.is( cylinder.type, Shape.types.CYLINDER, 'cylinder.type' ); 70 | t.is( cylinder.radiusTop, 5, 'cylinder.radiusTop' ); 71 | t.is( cylinder.radiusBottom, 5, 'cylinder.radiusBottom' ); 72 | t.is( cylinder.height, 10, 'cylinder.height' ); 73 | 74 | t.true( equalsApprox( orientation!.x, 0.707106 ), 'cylinder.orientation.x' ); 75 | t.true( equalsApprox( orientation!.y, 0 ), 'cylinder.orientation.y' ); 76 | t.true( equalsApprox( orientation!.z, 0 ), 'cylinder.orientation.z' ); 77 | t.true( equalsApprox( orientation!.w, 0.707106 ), 'cylinder.orientation.w' ); 78 | }); 79 | 80 | test('getShapeParameters - shape - hull', function (t) { 81 | const {type, params} = getShapeParameters(object, { 82 | type: ShapeType.HULL, 83 | }) as ShapeParameters; 84 | 85 | t.is( type, ShapeType.HULL, 'type' ); 86 | t.is( params.vertices.every((v) => typeof v === 'number'), true, 'params.vertices' ) 87 | t.is( params.faces.every((f) => f.length === 3), true, 'params.faces' ) 88 | }); 89 | 90 | test('threeToCannon - shape - hull', function (t) { 91 | const {shape: hull} 92 | = threeToCannon(object, {type: ShapeType.HULL}) as ShapeResult; 93 | 94 | t.is( hull.type, Shape.types.CONVEXPOLYHEDRON, 'hull.type' ); 95 | t.is( hull.boundingSphereRadius.toFixed( 3 ), '8.660', 'hull.boundingSphereRadius' ); 96 | }); 97 | 98 | test('getShapeParameters - shape - mesh', function (t) { 99 | const {type, params} = getShapeParameters(object, { 100 | type: ShapeType.MESH 101 | }) as ShapeParameters; 102 | 103 | t.is( type, ShapeType.MESH, 'type' ); 104 | t.is( params.vertices.every((v) => typeof v === 'number'), true, 'params.vertices' ); 105 | t.is( params.indices.every((i) => typeof i === 'number'), true, 'params.indices' ); 106 | }); 107 | 108 | 109 | test('threeToCannon - shape - mesh', function (t) { 110 | const {shape: mesh} = threeToCannon(object, {type: ShapeType.MESH}) as ShapeResult; 111 | 112 | t.is( mesh.type, Shape.types.TRIMESH, 'mesh.type' ); 113 | t.is( mesh.boundingSphereRadius.toFixed( 3 ), '8.660', 'mesh.boundingSphereRadius' ); 114 | }); 115 | 116 | test('threeToCannon - transform - position', function (t) { 117 | const group = new Group(); 118 | const object = new Mesh(new BoxGeometry(10, 10, 10)); 119 | const matrix = new Matrix4().makeTranslation(0, 50, 0); 120 | object.geometry.applyMatrix4(matrix); 121 | group.position.set(100, 0, 0); 122 | group.add(object); 123 | group.updateMatrixWorld(); 124 | 125 | const {shape: box, offset, orientation} = threeToCannon( 126 | object, {type: ShapeType.BOX} 127 | ) as ShapeResult; 128 | 129 | t.is( box.type, Shape.types.BOX, 'box.type' ); 130 | t.is( box.halfExtents.x, 5, 'box.halfExtents.x' ); 131 | t.is( box.halfExtents.y, 5, 'box.halfExtents.y' ); 132 | t.is( box.halfExtents.z, 5, 'box.halfExtents.z' ); 133 | 134 | t.is( offset?.x, 0, 'box.offset.x' ); 135 | t.is( offset?.y, 50, 'box.offset.y' ); 136 | t.is( offset?.z, 0, 'box.offset.z' ); 137 | t.is( orientation, undefined, 'box.orientation' ); 138 | }); 139 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { BufferAttribute, BufferGeometry, Mesh, Object3D, Quaternion, Vector3 } from 'three'; 2 | 3 | const _v1 = new Vector3(); 4 | const _v2 = new Vector3(); 5 | const _q1 = new Quaternion(); 6 | 7 | /** 8 | * Returns a single geometry for the given object. If the object is compound, 9 | * its geometries are automatically merged. Bake world scale into each 10 | * geometry, because we can't easily apply that to the cannonjs shapes later. 11 | */ 12 | export function getGeometry (object: Object3D): BufferGeometry | null { 13 | const meshes = getMeshes(object); 14 | if (meshes.length === 0) return null; 15 | 16 | // Single mesh. Return, preserving original type. 17 | if (meshes.length === 1) { 18 | return normalizeGeometry(meshes[0]); 19 | } 20 | 21 | // Multiple meshes. Merge and return. 22 | let mesh: Mesh | undefined; 23 | const geometries: BufferGeometry[] = []; 24 | while ((mesh = meshes.pop())) { 25 | geometries.push(simplifyGeometry(normalizeGeometry(mesh))); 26 | } 27 | 28 | return mergeBufferGeometries(geometries); 29 | } 30 | 31 | function normalizeGeometry (mesh: Mesh): BufferGeometry { 32 | // Preserve original type, e.g. CylinderBufferGeometry. 33 | const geometry: BufferGeometry = mesh.geometry.clone(); 34 | 35 | mesh.updateMatrixWorld(); 36 | mesh.matrixWorld.decompose(_v1, _q1, _v2); 37 | geometry.scale(_v2.x, _v2.y, _v2.z); 38 | return geometry; 39 | } 40 | 41 | /** 42 | * Greatly simplified version of BufferGeometryUtils.mergeBufferGeometries. 43 | * Because we only care about the vertex positions, and not the indices or 44 | * other attributes, we throw everything else away. 45 | */ 46 | function mergeBufferGeometries (geometries: BufferGeometry[]): BufferGeometry { 47 | let vertexCount = 0; 48 | for (let i = 0; i < geometries.length; i++) { 49 | const position = geometries[i].attributes.position; 50 | if (position && position.itemSize === 3) { 51 | vertexCount += position.count; 52 | } 53 | } 54 | 55 | const positionArray = new Float32Array(vertexCount * 3); 56 | 57 | let positionOffset = 0; 58 | for (let i = 0; i < geometries.length; i++) { 59 | const position = geometries[i].attributes.position; 60 | if (position && position.itemSize === 3) { 61 | for (let j = 0; j < position.count; j++) { 62 | positionArray[positionOffset++] = position.getX(j); 63 | positionArray[positionOffset++] = position.getY(j); 64 | positionArray[positionOffset++] = position.getZ(j); 65 | } 66 | } 67 | } 68 | 69 | return new BufferGeometry().setAttribute('position', new BufferAttribute(positionArray, 3)); 70 | } 71 | 72 | export function getVertices (geometry: BufferGeometry): Float32Array { 73 | const position = geometry.attributes.position; 74 | const vertices = new Float32Array(position.count * 3); 75 | for (let i = 0; i < position.count; i++) { 76 | vertices[i * 3] = position.getX(i); 77 | vertices[i * 3 + 1] = position.getY(i); 78 | vertices[i * 3 + 2] = position.getZ(i); 79 | } 80 | return vertices; 81 | } 82 | 83 | /** 84 | * Returns a flat array of THREE.Mesh instances from the given object. If 85 | * nested transformations are found, they are applied to child meshes 86 | * as mesh.userData.matrix, so that each mesh has its position/rotation/scale 87 | * independently of all of its parents except the top-level object. 88 | */ 89 | function getMeshes (object: Object3D): Mesh[] { 90 | const meshes: Mesh[] = []; 91 | object.traverse(function (o) { 92 | if ((o as Mesh).isMesh) { 93 | meshes.push(o as Mesh); 94 | } 95 | }); 96 | return meshes; 97 | } 98 | 99 | export function getComponent(v: Vector3, component: string): number { 100 | switch(component) { 101 | case 'x': return v.x; 102 | case 'y': return v.y; 103 | case 'z': return v.z; 104 | } 105 | throw new Error(`Unexpected component ${component}`); 106 | } 107 | 108 | /** 109 | * Modified version of BufferGeometryUtils.mergeVertices, ignoring vertex 110 | * attributes other than position. 111 | * 112 | * @param {THREE.BufferGeometry} geometry 113 | * @param {number} tolerance 114 | * @return {THREE.BufferGeometry>} 115 | */ 116 | function simplifyGeometry (geometry: BufferGeometry, tolerance = 1e-4): BufferGeometry { 117 | 118 | tolerance = Math.max( tolerance, Number.EPSILON ); 119 | 120 | // Generate an index buffer if the geometry doesn't have one, or optimize it 121 | // if it's already available. 122 | const hashToIndex: {[key: string]: number} = {}; 123 | const indices = geometry.getIndex(); 124 | const positions = geometry.getAttribute( 'position' ); 125 | const vertexCount = indices ? indices.count : positions.count; 126 | 127 | // Next value for triangle indices. 128 | let nextIndex = 0; 129 | 130 | const newIndices = []; 131 | const newPositions = []; 132 | 133 | // Convert the error tolerance to an amount of decimal places to truncate to. 134 | const decimalShift = Math.log10( 1 / tolerance ); 135 | const shiftMultiplier = Math.pow( 10, decimalShift ); 136 | 137 | for ( let i = 0; i < vertexCount; i ++ ) { 138 | 139 | const index = indices ? indices.getX( i ) : i; 140 | 141 | // Generate a hash for the vertex attributes at the current index 'i'. 142 | let hash = ''; 143 | 144 | // Double tilde truncates the decimal value. 145 | hash += `${ ~ ~ ( positions.getX( index ) * shiftMultiplier ) },`; 146 | hash += `${ ~ ~ ( positions.getY( index ) * shiftMultiplier ) },`; 147 | hash += `${ ~ ~ ( positions.getZ( index ) * shiftMultiplier ) },`; 148 | 149 | // Add another reference to the vertex if it's already 150 | // used by another index. 151 | if ( hash in hashToIndex ) { 152 | 153 | newIndices.push( hashToIndex[ hash ] ); 154 | 155 | } else { 156 | 157 | newPositions.push( positions.getX( index ) ); 158 | newPositions.push( positions.getY( index ) ); 159 | newPositions.push( positions.getZ( index ) ); 160 | 161 | hashToIndex[ hash ] = nextIndex; 162 | newIndices.push( nextIndex ); 163 | nextIndex ++; 164 | 165 | } 166 | 167 | } 168 | 169 | // Construct merged BufferGeometry. 170 | 171 | const positionAttribute = new BufferAttribute( 172 | new Float32Array( newPositions ), 173 | positions.itemSize, 174 | positions.normalized 175 | ); 176 | 177 | const result = new BufferGeometry(); 178 | result.setAttribute( 'position', positionAttribute ); 179 | result.setIndex( newIndices ); 180 | 181 | return result; 182 | 183 | } 184 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Box, ConvexPolyhedron, Cylinder, Quaternion as CQuaternion, Shape, Sphere, Trimesh, Vec3 } from 'cannon-es'; 2 | import { Box3, BufferGeometry, CylinderGeometry, MathUtils, Mesh, Object3D, SphereGeometry, Vector3 } from 'three'; 3 | import { ConvexHull } from '../lib/ConvexHull'; 4 | import { getComponent, getGeometry, getVertices } from './utils'; 5 | 6 | const PI_2 = Math.PI / 2; 7 | 8 | export type BoxParameters = { x: number, y: number, z: number }; 9 | 10 | export type CylinderParameters = { radiusTop: number, radiusBottom: number, height: number, segments: number }; 11 | 12 | export type SphereParameters = { radius: number }; 13 | 14 | export type ConvexPolyhedronParameters = { vertices: Float32Array, faces: number[][] }; 15 | 16 | export type TrimeshParameters = { vertices: Float32Array, indices: Uint32Array }; 17 | 18 | type ShapeTypeToShapeParameters = { 19 | Box: BoxParameters, 20 | Cylinder: CylinderParameters, 21 | Sphere: SphereParameters, 22 | ConvexPolyhedron: ConvexPolyhedronParameters, 23 | Trimesh: TrimeshParameters, 24 | }; 25 | 26 | export enum ShapeType { 27 | BOX = 'Box', 28 | CYLINDER = 'Cylinder', 29 | SPHERE = 'Sphere', 30 | HULL = 'ConvexPolyhedron', 31 | MESH = 'Trimesh', 32 | } 33 | 34 | export interface ShapeOptions { 35 | type?: ShapeType, 36 | cylinderAxis?: 'x' | 'y' | 'z', 37 | sphereRadius?: number, 38 | } 39 | 40 | export interface ShapeParameters { 41 | type: T, 42 | params: ShapeTypeToShapeParameters[T], 43 | offset?: Vec3, 44 | orientation?: CQuaternion, 45 | } 46 | 47 | export interface ShapeResult { 48 | shape: T, 49 | offset?: Vec3, 50 | orientation?: CQuaternion, 51 | } 52 | 53 | /** 54 | * Given a THREE.Object3D instance, creates parameters for a CANNON shape. 55 | */ 56 | export const getShapeParameters = function (object: Object3D, options: ShapeOptions = {}): ShapeParameters | null { 57 | let geometry: BufferGeometry | null; 58 | 59 | if (options.type === ShapeType.BOX) { 60 | return getBoundingBoxParameters(object); 61 | } else if (options.type === ShapeType.CYLINDER) { 62 | return getBoundingCylinderParameters(object, options); 63 | } else if (options.type === ShapeType.SPHERE) { 64 | return getBoundingSphereParameters(object, options); 65 | } else if (options.type === ShapeType.HULL) { 66 | return getConvexPolyhedronParameters(object); 67 | } else if (options.type === ShapeType.MESH) { 68 | geometry = getGeometry(object); 69 | return geometry ? getTrimeshParameters(geometry) : null; 70 | } else if (options.type) { 71 | throw new Error(`[CANNON.getShapeParameters] Invalid type "${options.type}".`); 72 | } 73 | 74 | geometry = getGeometry(object); 75 | if (!geometry) return null; 76 | 77 | switch (geometry.type) { 78 | case 'BoxGeometry': 79 | case 'BoxBufferGeometry': 80 | return getBoxParameters(geometry); 81 | case 'CylinderGeometry': 82 | case 'CylinderBufferGeometry': 83 | return getCylinderParameters(geometry as CylinderGeometry); 84 | case 'PlaneGeometry': 85 | case 'PlaneBufferGeometry': 86 | return getPlaneParameters(geometry); 87 | case 'SphereGeometry': 88 | case 'SphereBufferGeometry': 89 | return getSphereParameters(geometry as SphereGeometry); 90 | case 'TubeGeometry': 91 | case 'BufferGeometry': 92 | return getBoundingBoxParameters(object); 93 | default: 94 | console.warn( 95 | 'Unrecognized geometry: "%s". Using bounding box as shape.', geometry.type 96 | ); 97 | return getBoxParameters(geometry); 98 | } 99 | }; 100 | 101 | /** 102 | * Given a THREE.Object3D instance, creates a corresponding CANNON shape. 103 | */ 104 | export const threeToCannon = function (object: Object3D, options: ShapeOptions = {}): ShapeResult | null { 105 | const shapeParameters = getShapeParameters(object, options); 106 | if (!shapeParameters) { 107 | return null; 108 | } 109 | 110 | const { type, params, offset, orientation } = shapeParameters; 111 | 112 | let shape: Shape; 113 | if (type === ShapeType.BOX) { 114 | shape = createBox(params as BoxParameters); 115 | } else if (type === ShapeType.CYLINDER) { 116 | shape = createCylinder(params as CylinderParameters); 117 | } else if (type === ShapeType.SPHERE) { 118 | shape = createSphere(params as SphereParameters); 119 | } else if (type === ShapeType.HULL) { 120 | shape = createConvexPolyhedron(params as ConvexPolyhedronParameters); 121 | } else { 122 | shape = createTrimesh(params as TrimeshParameters); 123 | } 124 | 125 | return { 126 | shape, 127 | offset, 128 | orientation, 129 | }; 130 | }; 131 | 132 | /****************************************************************************** 133 | * Shape construction 134 | */ 135 | 136 | function createBox (params: BoxParameters): Box { 137 | const { x, y, z } = params; 138 | const shape = new Box(new Vec3(x, y, z)); 139 | return shape; 140 | } 141 | 142 | function createCylinder (params: CylinderParameters): Cylinder { 143 | const { radiusTop, radiusBottom, height, segments } = params; 144 | 145 | const shape = new Cylinder(radiusTop, radiusBottom, height, segments); 146 | 147 | // Include metadata for serialization. 148 | // TODO(cleanup): Is this still necessary? 149 | shape.radiusTop = radiusBottom; 150 | shape.radiusBottom = radiusBottom; 151 | shape.height = height; 152 | shape.numSegments = segments; 153 | 154 | return shape; 155 | } 156 | 157 | function createSphere (params: SphereParameters): Sphere { 158 | const shape = new Sphere(params.radius); 159 | 160 | return shape; 161 | } 162 | 163 | function createConvexPolyhedron (params: ConvexPolyhedronParameters): ConvexPolyhedron { 164 | const { faces, vertices: verticesArray } = params; 165 | 166 | const vertices: Vec3[] = []; 167 | for (let i = 0; i < verticesArray.length; i += 3) { 168 | vertices.push(new Vec3( 169 | verticesArray[i], 170 | verticesArray[i + 1], 171 | verticesArray[i + 2] 172 | )); 173 | } 174 | 175 | const shape = new ConvexPolyhedron({ 176 | faces, 177 | vertices 178 | }); 179 | 180 | return shape; 181 | } 182 | 183 | function createTrimesh (params: TrimeshParameters): Trimesh { 184 | const { vertices, indices } = params 185 | const shape = new Trimesh( 186 | vertices as unknown as number[], 187 | indices as unknown as number[], 188 | ); 189 | 190 | return shape; 191 | } 192 | 193 | /****************************************************************************** 194 | * Shape parameters 195 | */ 196 | 197 | function getBoxParameters (geometry: BufferGeometry): ShapeParameters | null { 198 | const vertices = getVertices(geometry); 199 | 200 | if (!vertices.length) return null; 201 | 202 | geometry.computeBoundingBox(); 203 | const box = geometry.boundingBox!; 204 | 205 | return { 206 | type: ShapeType.BOX, 207 | params: { 208 | x: (box.max.x - box.min.x) / 2, 209 | y: (box.max.y - box.min.y) / 2, 210 | z: (box.max.z - box.min.z) / 2, 211 | }, 212 | }; 213 | } 214 | 215 | /** Bounding box needs to be computed with the entire subtree, not just geometry. */ 216 | function getBoundingBoxParameters (object: Object3D): ShapeParameters | null { 217 | const clone = object.clone(); 218 | clone.quaternion.set(0, 0, 0, 1); 219 | clone.updateMatrixWorld(); 220 | 221 | const box = new Box3().setFromObject(clone); 222 | 223 | if (!isFinite(box.min.lengthSq())) return null; 224 | 225 | const localPosition = box.translate(clone.position.negate()).getCenter(new Vector3()); 226 | 227 | return { 228 | type: ShapeType.BOX, 229 | params: { 230 | x: (box.max.x - box.min.x) / 2, 231 | y: (box.max.y - box.min.y) / 2, 232 | z: (box.max.z - box.min.z) / 2, 233 | }, 234 | offset: localPosition.lengthSq() 235 | ? new Vec3(localPosition.x, localPosition.y, localPosition.z) 236 | : undefined, 237 | }; 238 | } 239 | 240 | /** Computes 3D convex hull as a CANNON.ConvexPolyhedron. */ 241 | function getConvexPolyhedronParameters (object: Object3D): ShapeParameters | null { 242 | const geometry = getGeometry(object); 243 | 244 | if (!geometry) return null; 245 | 246 | // Perturb. 247 | const eps = 1e-4; 248 | for (let i = 0; i < geometry.attributes.position.count; i++) { 249 | geometry.attributes.position.setXYZ( 250 | i, 251 | geometry.attributes.position.getX(i) + (Math.random() - 0.5) * eps, 252 | geometry.attributes.position.getY(i) + (Math.random() - 0.5) * eps, 253 | geometry.attributes.position.getZ(i) + (Math.random() - 0.5) * eps, 254 | ); 255 | } 256 | 257 | // Compute the 3D convex hull and collect convex hull vertices and faces. 258 | const [ positions, indices ] = new ConvexHull() 259 | .setFromObject(new Mesh(geometry)) 260 | .toJSON(); 261 | 262 | return { 263 | type: ShapeType.HULL, 264 | params: { 265 | vertices: new Float32Array(positions), 266 | faces: indices, 267 | }, 268 | }; 269 | } 270 | 271 | function getCylinderParameters ( 272 | geometry: CylinderGeometry 273 | ): ShapeParameters | null { 274 | const params = geometry.parameters; 275 | 276 | return { 277 | type: ShapeType.CYLINDER, 278 | params: { 279 | radiusTop: params.radiusTop, 280 | radiusBottom: params.radiusBottom, 281 | height: params.height, 282 | segments: params.radialSegments, 283 | }, 284 | orientation: new CQuaternion() 285 | .setFromEuler(MathUtils.degToRad(-90), 0, 0, 'XYZ') 286 | .normalize(), 287 | } 288 | } 289 | 290 | function getBoundingCylinderParameters ( 291 | object: Object3D, 292 | options: ShapeOptions 293 | ): ShapeParameters | null { 294 | const axes = ['x', 'y', 'z']; 295 | const majorAxis = options.cylinderAxis || 'y'; 296 | const minorAxes = axes.splice(axes.indexOf(majorAxis), 1) && axes; 297 | const box = new Box3().setFromObject(object); 298 | 299 | if (!isFinite(box.min.lengthSq())) return null; 300 | 301 | // Compute cylinder dimensions. 302 | const height = box.max[majorAxis] - box.min[majorAxis]; 303 | const radius = 0.5 * Math.max( 304 | getComponent(box.max, minorAxes[0]) - getComponent(box.min, minorAxes[0]), 305 | getComponent(box.max, minorAxes[1]) - getComponent(box.min, minorAxes[1]), 306 | ); 307 | 308 | const eulerX = majorAxis === 'y' ? PI_2 : 0; 309 | const eulerY = majorAxis === 'z' ? PI_2 : 0; 310 | 311 | return { 312 | type: ShapeType.CYLINDER, 313 | params: { 314 | radiusTop: radius, 315 | radiusBottom: radius, 316 | height, 317 | segments: 12, 318 | }, 319 | orientation: new CQuaternion() 320 | .setFromEuler(eulerX, eulerY, 0, 'XYZ') 321 | .normalize(), 322 | }; 323 | } 324 | 325 | function getPlaneParameters (geometry: BufferGeometry): ShapeParameters | null { 326 | geometry.computeBoundingBox(); 327 | const box = geometry.boundingBox!; 328 | 329 | return { 330 | type: ShapeType.BOX, 331 | params: { 332 | x: (box.max.x - box.min.x) / 2 || 0.1, 333 | y: (box.max.y - box.min.y) / 2 || 0.1, 334 | z: (box.max.z - box.min.z) / 2 || 0.1, 335 | }, 336 | }; 337 | } 338 | 339 | function getSphereParameters (geometry: SphereGeometry): ShapeParameters | null { 340 | return { 341 | type: ShapeType.SPHERE, 342 | params: { radius: geometry.parameters.radius }, 343 | }; 344 | } 345 | 346 | function getBoundingSphereParameters ( 347 | object: Object3D, 348 | options: ShapeOptions 349 | ): ShapeParameters | null { 350 | if (options.sphereRadius) { 351 | return { 352 | type: ShapeType.SPHERE, 353 | params: { radius: options.sphereRadius }, 354 | }; 355 | } 356 | const geometry = getGeometry(object); 357 | if (!geometry) return null; 358 | geometry.computeBoundingSphere(); 359 | 360 | return { 361 | type: ShapeType.SPHERE, 362 | params: { radius: geometry.boundingSphere!.radius }, 363 | }; 364 | } 365 | 366 | function getTrimeshParameters (geometry: BufferGeometry): ShapeParameters | null { 367 | const vertices = getVertices(geometry); 368 | 369 | if (!vertices.length) return null; 370 | 371 | const indices = new Uint32Array(vertices.length); 372 | for (let i = 0; i < vertices.length; i++) { 373 | indices[i] = i; 374 | } 375 | 376 | return { 377 | type: ShapeType.MESH, 378 | params: { 379 | vertices, 380 | indices, 381 | }, 382 | }; 383 | } 384 | -------------------------------------------------------------------------------- /lib/ConvexHull.js: -------------------------------------------------------------------------------- 1 | import { 2 | Line3, 3 | Plane, 4 | Triangle, 5 | Vector3 6 | } from 'three'; 7 | /** 8 | * Ported from: https://github.com/maurizzzio/quickhull3d/ by Mauricio Poppe (https://github.com/maurizzzio) 9 | */ 10 | 11 | var ConvexHull = ( function () { 12 | 13 | var Visible = 0; 14 | var Deleted = 1; 15 | 16 | var v1 = new Vector3(); 17 | 18 | function ConvexHull() { 19 | 20 | this.tolerance = - 1; 21 | 22 | this.faces = []; // the generated faces of the convex hull 23 | this.newFaces = []; // this array holds the faces that are generated within a single iteration 24 | 25 | // the vertex lists work as follows: 26 | // 27 | // let 'a' and 'b' be 'Face' instances 28 | // let 'v' be points wrapped as instance of 'Vertex' 29 | // 30 | // [v, v, ..., v, v, v, ...] 31 | // ^ ^ 32 | // | | 33 | // a.outside b.outside 34 | // 35 | this.assigned = new VertexList(); 36 | this.unassigned = new VertexList(); 37 | 38 | this.vertices = []; // vertices of the hull (internal representation of given geometry data) 39 | 40 | } 41 | 42 | Object.assign( ConvexHull.prototype, { 43 | 44 | toJSON: function () { 45 | // Original ('src') indices do not include interior vertices, 46 | // but 'this.vertices' (the list they index) does. Output ('dst') 47 | // arrays have interior vertices omitted. 48 | 49 | const srcIndices = this.faces.map((f) => f.toArray()); 50 | const uniqueSrcIndices = Array.from(new Set(srcIndices.flat())).sort(); 51 | 52 | // Output vertex positions, omitting interior vertices. 53 | const dstPositions = []; 54 | for (let i = 0; i < uniqueSrcIndices.length; i++) { 55 | dstPositions.push( 56 | this.vertices[uniqueSrcIndices[i]].point.x, 57 | this.vertices[uniqueSrcIndices[i]].point.y, 58 | this.vertices[uniqueSrcIndices[i]].point.z, 59 | ); 60 | } 61 | 62 | // Mapping from 'src' (this.vertices) to 'dst' (dstPositions) indices. 63 | const srcToDstIndexMap = new Map(); 64 | for (let i = 0; i < uniqueSrcIndices.length; i++) { 65 | srcToDstIndexMap.set(uniqueSrcIndices[i], i); 66 | } 67 | 68 | // Output triangles, as indices on dstPositions. 69 | const dstIndices = []; 70 | for (let i = 0; i < srcIndices.length; i++) { 71 | dstIndices.push([ 72 | srcToDstIndexMap.get(srcIndices[i][0]), 73 | srcToDstIndexMap.get(srcIndices[i][1]), 74 | srcToDstIndexMap.get(srcIndices[i][2]), 75 | ]); 76 | } 77 | 78 | return [dstPositions, dstIndices]; 79 | }, 80 | 81 | setFromPoints: function ( points ) { 82 | 83 | if ( Array.isArray( points ) !== true ) { 84 | 85 | console.error( 'THREE.ConvexHull: Points parameter is not an array.' ); 86 | 87 | } 88 | 89 | if ( points.length < 4 ) { 90 | 91 | console.error( 'THREE.ConvexHull: The algorithm needs at least four points.' ); 92 | 93 | } 94 | 95 | this.makeEmpty(); 96 | 97 | for ( var i = 0, l = points.length; i < l; i ++ ) { 98 | 99 | this.vertices.push( new VertexNode( points[ i ], i ) ); 100 | 101 | } 102 | 103 | this.compute(); 104 | 105 | return this; 106 | 107 | }, 108 | 109 | setFromObject: function ( object ) { 110 | 111 | var points = []; 112 | 113 | object.updateMatrixWorld( true ); 114 | 115 | object.traverse( function ( node ) { 116 | 117 | var i, l, point; 118 | 119 | var geometry = node.geometry; 120 | 121 | if ( geometry === undefined ) return; 122 | 123 | if ( geometry.isGeometry ) { 124 | 125 | geometry = geometry.toBufferGeometry 126 | ? geometry.toBufferGeometry() 127 | : new BufferGeometry().fromGeometry( geometry ); 128 | 129 | } 130 | 131 | if ( geometry.isBufferGeometry ) { 132 | 133 | var attribute = geometry.attributes.position; 134 | 135 | if ( attribute !== undefined ) { 136 | 137 | for ( i = 0, l = attribute.count; i < l; i ++ ) { 138 | 139 | point = new Vector3(); 140 | 141 | point.fromBufferAttribute( attribute, i ).applyMatrix4( node.matrixWorld ); 142 | 143 | points.push( point ); 144 | 145 | } 146 | 147 | } 148 | 149 | } 150 | 151 | } ); 152 | 153 | return this.setFromPoints( points ); 154 | 155 | }, 156 | 157 | containsPoint: function ( point ) { 158 | 159 | var faces = this.faces; 160 | 161 | for ( var i = 0, l = faces.length; i < l; i ++ ) { 162 | 163 | var face = faces[ i ]; 164 | 165 | // compute signed distance and check on what half space the point lies 166 | 167 | if ( face.distanceToPoint( point ) > this.tolerance ) return false; 168 | 169 | } 170 | 171 | return true; 172 | 173 | }, 174 | 175 | intersectRay: function ( ray, target ) { 176 | 177 | // based on "Fast Ray-Convex Polyhedron Intersection" by Eric Haines, GRAPHICS GEMS II 178 | 179 | var faces = this.faces; 180 | 181 | var tNear = - Infinity; 182 | var tFar = Infinity; 183 | 184 | for ( var i = 0, l = faces.length; i < l; i ++ ) { 185 | 186 | var face = faces[ i ]; 187 | 188 | // interpret faces as planes for the further computation 189 | 190 | var vN = face.distanceToPoint( ray.origin ); 191 | var vD = face.normal.dot( ray.direction ); 192 | 193 | // if the origin is on the positive side of a plane (so the plane can "see" the origin) and 194 | // the ray is turned away or parallel to the plane, there is no intersection 195 | 196 | if ( vN > 0 && vD >= 0 ) return null; 197 | 198 | // compute the distance from the ray’s origin to the intersection with the plane 199 | 200 | var t = ( vD !== 0 ) ? ( - vN / vD ) : 0; 201 | 202 | // only proceed if the distance is positive. a negative distance means the intersection point 203 | // lies "behind" the origin 204 | 205 | if ( t <= 0 ) continue; 206 | 207 | // now categorized plane as front-facing or back-facing 208 | 209 | if ( vD > 0 ) { 210 | 211 | // plane faces away from the ray, so this plane is a back-face 212 | 213 | tFar = Math.min( t, tFar ); 214 | 215 | } else { 216 | 217 | // front-face 218 | 219 | tNear = Math.max( t, tNear ); 220 | 221 | } 222 | 223 | if ( tNear > tFar ) { 224 | 225 | // if tNear ever is greater than tFar, the ray must miss the convex hull 226 | 227 | return null; 228 | 229 | } 230 | 231 | } 232 | 233 | // evaluate intersection point 234 | 235 | // always try tNear first since its the closer intersection point 236 | 237 | if ( tNear !== - Infinity ) { 238 | 239 | ray.at( tNear, target ); 240 | 241 | } else { 242 | 243 | ray.at( tFar, target ); 244 | 245 | } 246 | 247 | return target; 248 | 249 | }, 250 | 251 | intersectsRay: function ( ray ) { 252 | 253 | return this.intersectRay( ray, v1 ) !== null; 254 | 255 | }, 256 | 257 | makeEmpty: function () { 258 | 259 | this.faces = []; 260 | this.vertices = []; 261 | 262 | return this; 263 | 264 | }, 265 | 266 | // Adds a vertex to the 'assigned' list of vertices and assigns it to the given face 267 | 268 | addVertexToFace: function ( vertex, face ) { 269 | 270 | vertex.face = face; 271 | 272 | if ( face.outside === null ) { 273 | 274 | this.assigned.append( vertex ); 275 | 276 | } else { 277 | 278 | this.assigned.insertBefore( face.outside, vertex ); 279 | 280 | } 281 | 282 | face.outside = vertex; 283 | 284 | return this; 285 | 286 | }, 287 | 288 | // Removes a vertex from the 'assigned' list of vertices and from the given face 289 | 290 | removeVertexFromFace: function ( vertex, face ) { 291 | 292 | if ( vertex === face.outside ) { 293 | 294 | // fix face.outside link 295 | 296 | if ( vertex.next !== null && vertex.next.face === face ) { 297 | 298 | // face has at least 2 outside vertices, move the 'outside' reference 299 | 300 | face.outside = vertex.next; 301 | 302 | } else { 303 | 304 | // vertex was the only outside vertex that face had 305 | 306 | face.outside = null; 307 | 308 | } 309 | 310 | } 311 | 312 | this.assigned.remove( vertex ); 313 | 314 | return this; 315 | 316 | }, 317 | 318 | // Removes all the visible vertices that a given face is able to see which are stored in the 'assigned' vertext list 319 | 320 | removeAllVerticesFromFace: function ( face ) { 321 | 322 | if ( face.outside !== null ) { 323 | 324 | // reference to the first and last vertex of this face 325 | 326 | var start = face.outside; 327 | var end = face.outside; 328 | 329 | while ( end.next !== null && end.next.face === face ) { 330 | 331 | end = end.next; 332 | 333 | } 334 | 335 | this.assigned.removeSubList( start, end ); 336 | 337 | // fix references 338 | 339 | start.prev = end.next = null; 340 | face.outside = null; 341 | 342 | return start; 343 | 344 | } 345 | 346 | }, 347 | 348 | // Removes all the visible vertices that 'face' is able to see 349 | 350 | deleteFaceVertices: function ( face, absorbingFace ) { 351 | 352 | var faceVertices = this.removeAllVerticesFromFace( face ); 353 | 354 | if ( faceVertices !== undefined ) { 355 | 356 | if ( absorbingFace === undefined ) { 357 | 358 | // mark the vertices to be reassigned to some other face 359 | 360 | this.unassigned.appendChain( faceVertices ); 361 | 362 | 363 | } else { 364 | 365 | // if there's an absorbing face try to assign as many vertices as possible to it 366 | 367 | var vertex = faceVertices; 368 | 369 | do { 370 | 371 | // we need to buffer the subsequent vertex at this point because the 'vertex.next' reference 372 | // will be changed by upcoming method calls 373 | 374 | var nextVertex = vertex.next; 375 | 376 | var distance = absorbingFace.distanceToPoint( vertex.point ); 377 | 378 | // check if 'vertex' is able to see 'absorbingFace' 379 | 380 | if ( distance > this.tolerance ) { 381 | 382 | this.addVertexToFace( vertex, absorbingFace ); 383 | 384 | } else { 385 | 386 | this.unassigned.append( vertex ); 387 | 388 | } 389 | 390 | // now assign next vertex 391 | 392 | vertex = nextVertex; 393 | 394 | } while ( vertex !== null ); 395 | 396 | } 397 | 398 | } 399 | 400 | return this; 401 | 402 | }, 403 | 404 | // Reassigns as many vertices as possible from the unassigned list to the new faces 405 | 406 | resolveUnassignedPoints: function ( newFaces ) { 407 | 408 | if ( this.unassigned.isEmpty() === false ) { 409 | 410 | var vertex = this.unassigned.first(); 411 | 412 | do { 413 | 414 | // buffer 'next' reference, see .deleteFaceVertices() 415 | 416 | var nextVertex = vertex.next; 417 | 418 | var maxDistance = this.tolerance; 419 | 420 | var maxFace = null; 421 | 422 | for ( var i = 0; i < newFaces.length; i ++ ) { 423 | 424 | var face = newFaces[ i ]; 425 | 426 | if ( face.mark === Visible ) { 427 | 428 | var distance = face.distanceToPoint( vertex.point ); 429 | 430 | if ( distance > maxDistance ) { 431 | 432 | maxDistance = distance; 433 | maxFace = face; 434 | 435 | } 436 | 437 | if ( maxDistance > 1000 * this.tolerance ) break; 438 | 439 | } 440 | 441 | } 442 | 443 | // 'maxFace' can be null e.g. if there are identical vertices 444 | 445 | if ( maxFace !== null ) { 446 | 447 | this.addVertexToFace( vertex, maxFace ); 448 | 449 | } 450 | 451 | vertex = nextVertex; 452 | 453 | } while ( vertex !== null ); 454 | 455 | } 456 | 457 | return this; 458 | 459 | }, 460 | 461 | // Computes the extremes of a simplex which will be the initial hull 462 | 463 | computeExtremes: function () { 464 | 465 | var min = new Vector3(); 466 | var max = new Vector3(); 467 | 468 | var minVertices = []; 469 | var maxVertices = []; 470 | 471 | var i, l, j; 472 | 473 | // initially assume that the first vertex is the min/max 474 | 475 | for ( i = 0; i < 3; i ++ ) { 476 | 477 | minVertices[ i ] = maxVertices[ i ] = this.vertices[ 0 ]; 478 | 479 | } 480 | 481 | min.copy( this.vertices[ 0 ].point ); 482 | max.copy( this.vertices[ 0 ].point ); 483 | 484 | // compute the min/max vertex on all six directions 485 | 486 | for ( i = 0, l = this.vertices.length; i < l; i ++ ) { 487 | 488 | var vertex = this.vertices[ i ]; 489 | var point = vertex.point; 490 | 491 | // update the min coordinates 492 | 493 | for ( j = 0; j < 3; j ++ ) { 494 | 495 | if ( point.getComponent( j ) < min.getComponent( j ) ) { 496 | 497 | min.setComponent( j, point.getComponent( j ) ); 498 | minVertices[ j ] = vertex; 499 | 500 | } 501 | 502 | } 503 | 504 | // update the max coordinates 505 | 506 | for ( j = 0; j < 3; j ++ ) { 507 | 508 | if ( point.getComponent( j ) > max.getComponent( j ) ) { 509 | 510 | max.setComponent( j, point.getComponent( j ) ); 511 | maxVertices[ j ] = vertex; 512 | 513 | } 514 | 515 | } 516 | 517 | } 518 | 519 | // use min/max vectors to compute an optimal epsilon 520 | 521 | this.tolerance = 3 * Number.EPSILON * ( 522 | Math.max( Math.abs( min.x ), Math.abs( max.x ) ) + 523 | Math.max( Math.abs( min.y ), Math.abs( max.y ) ) + 524 | Math.max( Math.abs( min.z ), Math.abs( max.z ) ) 525 | ); 526 | 527 | return { min: minVertices, max: maxVertices }; 528 | 529 | }, 530 | 531 | // Computes the initial simplex assigning to its faces all the points 532 | // that are candidates to form part of the hull 533 | 534 | computeInitialHull: function () { 535 | 536 | var line3, plane, closestPoint; 537 | 538 | return function computeInitialHull() { 539 | 540 | if ( line3 === undefined ) { 541 | 542 | line3 = new Line3(); 543 | plane = new Plane(); 544 | closestPoint = new Vector3(); 545 | 546 | } 547 | 548 | var vertex, vertices = this.vertices; 549 | var extremes = this.computeExtremes(); 550 | var min = extremes.min; 551 | var max = extremes.max; 552 | 553 | var v0, v1, v2, v3; 554 | var i, l, j; 555 | 556 | // 1. Find the two vertices 'v0' and 'v1' with the greatest 1d separation 557 | // (max.x - min.x) 558 | // (max.y - min.y) 559 | // (max.z - min.z) 560 | 561 | var distance, maxDistance = 0; 562 | var index = 0; 563 | 564 | for ( i = 0; i < 3; i ++ ) { 565 | 566 | distance = max[ i ].point.getComponent( i ) - min[ i ].point.getComponent( i ); 567 | 568 | if ( distance > maxDistance ) { 569 | 570 | maxDistance = distance; 571 | index = i; 572 | 573 | } 574 | 575 | } 576 | 577 | v0 = min[ index ]; 578 | v1 = max[ index ]; 579 | 580 | // 2. The next vertex 'v2' is the one farthest to the line formed by 'v0' and 'v1' 581 | 582 | maxDistance = 0; 583 | line3.set( v0.point, v1.point ); 584 | 585 | for ( i = 0, l = this.vertices.length; i < l; i ++ ) { 586 | 587 | vertex = vertices[ i ]; 588 | 589 | if ( vertex !== v0 && vertex !== v1 ) { 590 | 591 | line3.closestPointToPoint( vertex.point, true, closestPoint ); 592 | 593 | distance = closestPoint.distanceToSquared( vertex.point ); 594 | 595 | if ( distance > maxDistance ) { 596 | 597 | maxDistance = distance; 598 | v2 = vertex; 599 | 600 | } 601 | 602 | } 603 | 604 | } 605 | 606 | // 3. The next vertex 'v3' is the one farthest to the plane 'v0', 'v1', 'v2' 607 | 608 | maxDistance = - 1; 609 | plane.setFromCoplanarPoints( v0.point, v1.point, v2.point ); 610 | 611 | for ( i = 0, l = this.vertices.length; i < l; i ++ ) { 612 | 613 | vertex = vertices[ i ]; 614 | 615 | if ( vertex !== v0 && vertex !== v1 && vertex !== v2 ) { 616 | 617 | distance = Math.abs( plane.distanceToPoint( vertex.point ) ); 618 | 619 | if ( distance > maxDistance ) { 620 | 621 | maxDistance = distance; 622 | v3 = vertex; 623 | 624 | } 625 | 626 | } 627 | 628 | } 629 | 630 | var faces = []; 631 | 632 | if ( plane.distanceToPoint( v3.point ) < 0 ) { 633 | 634 | // the face is not able to see the point so 'plane.normal' is pointing outside the tetrahedron 635 | 636 | faces.push( 637 | Face.create( v0, v1, v2 ), 638 | Face.create( v3, v1, v0 ), 639 | Face.create( v3, v2, v1 ), 640 | Face.create( v3, v0, v2 ) 641 | ); 642 | 643 | // set the twin edge 644 | 645 | for ( i = 0; i < 3; i ++ ) { 646 | 647 | j = ( i + 1 ) % 3; 648 | 649 | // join face[ i ] i > 0, with the first face 650 | 651 | faces[ i + 1 ].getEdge( 2 ).setTwin( faces[ 0 ].getEdge( j ) ); 652 | 653 | // join face[ i ] with face[ i + 1 ], 1 <= i <= 3 654 | 655 | faces[ i + 1 ].getEdge( 1 ).setTwin( faces[ j + 1 ].getEdge( 0 ) ); 656 | 657 | } 658 | 659 | } else { 660 | 661 | // the face is able to see the point so 'plane.normal' is pointing inside the tetrahedron 662 | 663 | faces.push( 664 | Face.create( v0, v2, v1 ), 665 | Face.create( v3, v0, v1 ), 666 | Face.create( v3, v1, v2 ), 667 | Face.create( v3, v2, v0 ) 668 | ); 669 | 670 | // set the twin edge 671 | 672 | for ( i = 0; i < 3; i ++ ) { 673 | 674 | j = ( i + 1 ) % 3; 675 | 676 | // join face[ i ] i > 0, with the first face 677 | 678 | faces[ i + 1 ].getEdge( 2 ).setTwin( faces[ 0 ].getEdge( ( 3 - i ) % 3 ) ); 679 | 680 | // join face[ i ] with face[ i + 1 ] 681 | 682 | faces[ i + 1 ].getEdge( 0 ).setTwin( faces[ j + 1 ].getEdge( 1 ) ); 683 | 684 | } 685 | 686 | } 687 | 688 | // the initial hull is the tetrahedron 689 | 690 | for ( i = 0; i < 4; i ++ ) { 691 | 692 | this.faces.push( faces[ i ] ); 693 | 694 | } 695 | 696 | // initial assignment of vertices to the faces of the tetrahedron 697 | 698 | for ( i = 0, l = vertices.length; i < l; i ++ ) { 699 | 700 | vertex = vertices[ i ]; 701 | 702 | if ( vertex !== v0 && vertex !== v1 && vertex !== v2 && vertex !== v3 ) { 703 | 704 | maxDistance = this.tolerance; 705 | var maxFace = null; 706 | 707 | for ( j = 0; j < 4; j ++ ) { 708 | 709 | distance = this.faces[ j ].distanceToPoint( vertex.point ); 710 | 711 | if ( distance > maxDistance ) { 712 | 713 | maxDistance = distance; 714 | maxFace = this.faces[ j ]; 715 | 716 | } 717 | 718 | } 719 | 720 | if ( maxFace !== null ) { 721 | 722 | this.addVertexToFace( vertex, maxFace ); 723 | 724 | } 725 | 726 | } 727 | 728 | } 729 | 730 | return this; 731 | 732 | }; 733 | 734 | }(), 735 | 736 | // Removes inactive faces 737 | 738 | reindexFaces: function () { 739 | 740 | var activeFaces = []; 741 | 742 | for ( var i = 0; i < this.faces.length; i ++ ) { 743 | 744 | var face = this.faces[ i ]; 745 | 746 | if ( face.mark === Visible ) { 747 | 748 | activeFaces.push( face ); 749 | 750 | } 751 | 752 | } 753 | 754 | this.faces = activeFaces; 755 | 756 | return this; 757 | 758 | }, 759 | 760 | // Finds the next vertex to create faces with the current hull 761 | 762 | nextVertexToAdd: function () { 763 | 764 | // if the 'assigned' list of vertices is empty, no vertices are left. return with 'undefined' 765 | 766 | if ( this.assigned.isEmpty() === false ) { 767 | 768 | var eyeVertex, maxDistance = 0; 769 | 770 | // grap the first available face and start with the first visible vertex of that face 771 | 772 | var eyeFace = this.assigned.first().face; 773 | var vertex = eyeFace.outside; 774 | 775 | // now calculate the farthest vertex that face can see 776 | 777 | do { 778 | 779 | var distance = eyeFace.distanceToPoint( vertex.point ); 780 | 781 | if ( distance > maxDistance ) { 782 | 783 | maxDistance = distance; 784 | eyeVertex = vertex; 785 | 786 | } 787 | 788 | vertex = vertex.next; 789 | 790 | } while ( vertex !== null && vertex.face === eyeFace ); 791 | 792 | return eyeVertex; 793 | 794 | } 795 | 796 | }, 797 | 798 | // Computes a chain of half edges in CCW order called the 'horizon'. 799 | // For an edge to be part of the horizon it must join a face that can see 800 | // 'eyePoint' and a face that cannot see 'eyePoint'. 801 | 802 | computeHorizon: function ( eyePoint, crossEdge, face, horizon ) { 803 | 804 | // moves face's vertices to the 'unassigned' vertex list 805 | 806 | this.deleteFaceVertices( face ); 807 | 808 | face.mark = Deleted; 809 | 810 | var edge; 811 | 812 | if ( crossEdge === null ) { 813 | 814 | edge = crossEdge = face.getEdge( 0 ); 815 | 816 | } else { 817 | 818 | // start from the next edge since 'crossEdge' was already analyzed 819 | // (actually 'crossEdge.twin' was the edge who called this method recursively) 820 | 821 | edge = crossEdge.next; 822 | 823 | } 824 | 825 | do { 826 | 827 | var twinEdge = edge.twin; 828 | var oppositeFace = twinEdge.face; 829 | 830 | if ( oppositeFace.mark === Visible ) { 831 | 832 | if ( oppositeFace.distanceToPoint( eyePoint ) > this.tolerance ) { 833 | 834 | // the opposite face can see the vertex, so proceed with next edge 835 | 836 | this.computeHorizon( eyePoint, twinEdge, oppositeFace, horizon ); 837 | 838 | } else { 839 | 840 | // the opposite face can't see the vertex, so this edge is part of the horizon 841 | 842 | horizon.push( edge ); 843 | 844 | } 845 | 846 | } 847 | 848 | edge = edge.next; 849 | 850 | } while ( edge !== crossEdge ); 851 | 852 | return this; 853 | 854 | }, 855 | 856 | // Creates a face with the vertices 'eyeVertex.point', 'horizonEdge.tail' and 'horizonEdge.head' in CCW order 857 | 858 | addAdjoiningFace: function ( eyeVertex, horizonEdge ) { 859 | 860 | // all the half edges are created in ccw order thus the face is always pointing outside the hull 861 | 862 | var face = Face.create( eyeVertex, horizonEdge.tail(), horizonEdge.head() ); 863 | 864 | this.faces.push( face ); 865 | 866 | // join face.getEdge( - 1 ) with the horizon's opposite edge face.getEdge( - 1 ) = face.getEdge( 2 ) 867 | 868 | face.getEdge( - 1 ).setTwin( horizonEdge.twin ); 869 | 870 | return face.getEdge( 0 ); // the half edge whose vertex is the eyeVertex 871 | 872 | 873 | }, 874 | 875 | // Adds 'horizon.length' faces to the hull, each face will be linked with the 876 | // horizon opposite face and the face on the left/right 877 | 878 | addNewFaces: function ( eyeVertex, horizon ) { 879 | 880 | this.newFaces = []; 881 | 882 | var firstSideEdge = null; 883 | var previousSideEdge = null; 884 | 885 | for ( var i = 0; i < horizon.length; i ++ ) { 886 | 887 | var horizonEdge = horizon[ i ]; 888 | 889 | // returns the right side edge 890 | 891 | var sideEdge = this.addAdjoiningFace( eyeVertex, horizonEdge ); 892 | 893 | if ( firstSideEdge === null ) { 894 | 895 | firstSideEdge = sideEdge; 896 | 897 | } else { 898 | 899 | // joins face.getEdge( 1 ) with previousFace.getEdge( 0 ) 900 | 901 | sideEdge.next.setTwin( previousSideEdge ); 902 | 903 | } 904 | 905 | this.newFaces.push( sideEdge.face ); 906 | previousSideEdge = sideEdge; 907 | 908 | } 909 | 910 | // perform final join of new faces 911 | 912 | firstSideEdge.next.setTwin( previousSideEdge ); 913 | 914 | return this; 915 | 916 | }, 917 | 918 | // Adds a vertex to the hull 919 | 920 | addVertexToHull: function ( eyeVertex ) { 921 | 922 | var horizon = []; 923 | 924 | this.unassigned.clear(); 925 | 926 | // remove 'eyeVertex' from 'eyeVertex.face' so that it can't be added to the 'unassigned' vertex list 927 | 928 | this.removeVertexFromFace( eyeVertex, eyeVertex.face ); 929 | 930 | this.computeHorizon( eyeVertex.point, null, eyeVertex.face, horizon ); 931 | 932 | this.addNewFaces( eyeVertex, horizon ); 933 | 934 | // reassign 'unassigned' vertices to the new faces 935 | 936 | this.resolveUnassignedPoints( this.newFaces ); 937 | 938 | return this; 939 | 940 | }, 941 | 942 | cleanup: function () { 943 | 944 | this.assigned.clear(); 945 | this.unassigned.clear(); 946 | this.newFaces = []; 947 | 948 | return this; 949 | 950 | }, 951 | 952 | compute: function () { 953 | 954 | var vertex; 955 | 956 | this.computeInitialHull(); 957 | 958 | // add all available vertices gradually to the hull 959 | 960 | while ( ( vertex = this.nextVertexToAdd() ) !== undefined ) { 961 | 962 | this.addVertexToHull( vertex ); 963 | 964 | } 965 | 966 | this.reindexFaces(); 967 | 968 | this.cleanup(); 969 | 970 | return this; 971 | 972 | } 973 | 974 | } ); 975 | 976 | // 977 | 978 | function Face() { 979 | 980 | this.normal = new Vector3(); 981 | this.midpoint = new Vector3(); 982 | this.area = 0; 983 | 984 | this.constant = 0; // signed distance from face to the origin 985 | this.outside = null; // reference to a vertex in a vertex list this face can see 986 | this.mark = Visible; 987 | this.edge = null; 988 | 989 | } 990 | 991 | Object.assign( Face, { 992 | 993 | create: function ( a, b, c ) { 994 | 995 | var face = new Face(); 996 | 997 | var e0 = new HalfEdge( a, face ); 998 | var e1 = new HalfEdge( b, face ); 999 | var e2 = new HalfEdge( c, face ); 1000 | 1001 | // join edges 1002 | 1003 | e0.next = e2.prev = e1; 1004 | e1.next = e0.prev = e2; 1005 | e2.next = e1.prev = e0; 1006 | 1007 | // main half edge reference 1008 | 1009 | face.edge = e0; 1010 | 1011 | return face.compute(); 1012 | 1013 | } 1014 | 1015 | } ); 1016 | 1017 | Object.assign( Face.prototype, { 1018 | 1019 | toArray: function () { 1020 | const indices = []; 1021 | let edge = this.edge; 1022 | do { 1023 | indices.push(edge.head().index); 1024 | edge = edge.next; 1025 | } while (edge !== this.edge); 1026 | return indices; 1027 | }, 1028 | 1029 | getEdge: function ( i ) { 1030 | 1031 | var edge = this.edge; 1032 | 1033 | while ( i > 0 ) { 1034 | 1035 | edge = edge.next; 1036 | i --; 1037 | 1038 | } 1039 | 1040 | while ( i < 0 ) { 1041 | 1042 | edge = edge.prev; 1043 | i ++; 1044 | 1045 | } 1046 | 1047 | return edge; 1048 | 1049 | }, 1050 | 1051 | compute: function () { 1052 | 1053 | var triangle; 1054 | 1055 | return function compute() { 1056 | 1057 | if ( triangle === undefined ) triangle = new Triangle(); 1058 | 1059 | var a = this.edge.tail(); 1060 | var b = this.edge.head(); 1061 | var c = this.edge.next.head(); 1062 | 1063 | triangle.set( a.point, b.point, c.point ); 1064 | 1065 | triangle.getNormal( this.normal ); 1066 | triangle.getMidpoint( this.midpoint ); 1067 | this.area = triangle.getArea(); 1068 | 1069 | this.constant = this.normal.dot( this.midpoint ); 1070 | 1071 | return this; 1072 | 1073 | }; 1074 | 1075 | }(), 1076 | 1077 | distanceToPoint: function ( point ) { 1078 | 1079 | return this.normal.dot( point ) - this.constant; 1080 | 1081 | } 1082 | 1083 | } ); 1084 | 1085 | // Entity for a Doubly-Connected Edge List (DCEL). 1086 | 1087 | function HalfEdge( vertex, face ) { 1088 | 1089 | this.vertex = vertex; 1090 | this.prev = null; 1091 | this.next = null; 1092 | this.twin = null; 1093 | this.face = face; 1094 | 1095 | } 1096 | 1097 | Object.assign( HalfEdge.prototype, { 1098 | 1099 | head: function () { 1100 | 1101 | return this.vertex; 1102 | 1103 | }, 1104 | 1105 | tail: function () { 1106 | 1107 | return this.prev ? this.prev.vertex : null; 1108 | 1109 | }, 1110 | 1111 | length: function () { 1112 | 1113 | var head = this.head(); 1114 | var tail = this.tail(); 1115 | 1116 | if ( tail !== null ) { 1117 | 1118 | return tail.point.distanceTo( head.point ); 1119 | 1120 | } 1121 | 1122 | return - 1; 1123 | 1124 | }, 1125 | 1126 | lengthSquared: function () { 1127 | 1128 | var head = this.head(); 1129 | var tail = this.tail(); 1130 | 1131 | if ( tail !== null ) { 1132 | 1133 | return tail.point.distanceToSquared( head.point ); 1134 | 1135 | } 1136 | 1137 | return - 1; 1138 | 1139 | }, 1140 | 1141 | setTwin: function ( edge ) { 1142 | 1143 | this.twin = edge; 1144 | edge.twin = this; 1145 | 1146 | return this; 1147 | 1148 | } 1149 | 1150 | } ); 1151 | 1152 | // A vertex as a double linked list node. 1153 | 1154 | function VertexNode( point, index ) { 1155 | 1156 | this.point = point; 1157 | // index in the input array 1158 | this.index = index; 1159 | this.prev = null; 1160 | this.next = null; 1161 | // the face that is able to see this vertex 1162 | this.face = null; 1163 | 1164 | } 1165 | 1166 | // A double linked list that contains vertex nodes. 1167 | 1168 | function VertexList() { 1169 | 1170 | this.head = null; 1171 | this.tail = null; 1172 | 1173 | } 1174 | 1175 | Object.assign( VertexList.prototype, { 1176 | 1177 | first: function () { 1178 | 1179 | return this.head; 1180 | 1181 | }, 1182 | 1183 | last: function () { 1184 | 1185 | return this.tail; 1186 | 1187 | }, 1188 | 1189 | clear: function () { 1190 | 1191 | this.head = this.tail = null; 1192 | 1193 | return this; 1194 | 1195 | }, 1196 | 1197 | // Inserts a vertex before the target vertex 1198 | 1199 | insertBefore: function ( target, vertex ) { 1200 | 1201 | vertex.prev = target.prev; 1202 | vertex.next = target; 1203 | 1204 | if ( vertex.prev === null ) { 1205 | 1206 | this.head = vertex; 1207 | 1208 | } else { 1209 | 1210 | vertex.prev.next = vertex; 1211 | 1212 | } 1213 | 1214 | target.prev = vertex; 1215 | 1216 | return this; 1217 | 1218 | }, 1219 | 1220 | // Inserts a vertex after the target vertex 1221 | 1222 | insertAfter: function ( target, vertex ) { 1223 | 1224 | vertex.prev = target; 1225 | vertex.next = target.next; 1226 | 1227 | if ( vertex.next === null ) { 1228 | 1229 | this.tail = vertex; 1230 | 1231 | } else { 1232 | 1233 | vertex.next.prev = vertex; 1234 | 1235 | } 1236 | 1237 | target.next = vertex; 1238 | 1239 | return this; 1240 | 1241 | }, 1242 | 1243 | // Appends a vertex to the end of the linked list 1244 | 1245 | append: function ( vertex ) { 1246 | 1247 | if ( this.head === null ) { 1248 | 1249 | this.head = vertex; 1250 | 1251 | } else { 1252 | 1253 | this.tail.next = vertex; 1254 | 1255 | } 1256 | 1257 | vertex.prev = this.tail; 1258 | vertex.next = null; // the tail has no subsequent vertex 1259 | 1260 | this.tail = vertex; 1261 | 1262 | return this; 1263 | 1264 | }, 1265 | 1266 | // Appends a chain of vertices where 'vertex' is the head. 1267 | 1268 | appendChain: function ( vertex ) { 1269 | 1270 | if ( this.head === null ) { 1271 | 1272 | this.head = vertex; 1273 | 1274 | } else { 1275 | 1276 | this.tail.next = vertex; 1277 | 1278 | } 1279 | 1280 | vertex.prev = this.tail; 1281 | 1282 | // ensure that the 'tail' reference points to the last vertex of the chain 1283 | 1284 | while ( vertex.next !== null ) { 1285 | 1286 | vertex = vertex.next; 1287 | 1288 | } 1289 | 1290 | this.tail = vertex; 1291 | 1292 | return this; 1293 | 1294 | }, 1295 | 1296 | // Removes a vertex from the linked list 1297 | 1298 | remove: function ( vertex ) { 1299 | 1300 | if ( vertex.prev === null ) { 1301 | 1302 | this.head = vertex.next; 1303 | 1304 | } else { 1305 | 1306 | vertex.prev.next = vertex.next; 1307 | 1308 | } 1309 | 1310 | if ( vertex.next === null ) { 1311 | 1312 | this.tail = vertex.prev; 1313 | 1314 | } else { 1315 | 1316 | vertex.next.prev = vertex.prev; 1317 | 1318 | } 1319 | 1320 | return this; 1321 | 1322 | }, 1323 | 1324 | // Removes a list of vertices whose 'head' is 'a' and whose 'tail' is b 1325 | 1326 | removeSubList: function ( a, b ) { 1327 | 1328 | if ( a.prev === null ) { 1329 | 1330 | this.head = b.next; 1331 | 1332 | } else { 1333 | 1334 | a.prev.next = b.next; 1335 | 1336 | } 1337 | 1338 | if ( b.next === null ) { 1339 | 1340 | this.tail = a.prev; 1341 | 1342 | } else { 1343 | 1344 | b.next.prev = a.prev; 1345 | 1346 | } 1347 | 1348 | return this; 1349 | 1350 | }, 1351 | 1352 | isEmpty: function () { 1353 | 1354 | return this.head === null; 1355 | 1356 | } 1357 | 1358 | } ); 1359 | 1360 | return ConvexHull; 1361 | 1362 | } )(); 1363 | 1364 | export { ConvexHull }; 1365 | --------------------------------------------------------------------------------