├── .eslintrc.cjs ├── .github └── workflows │ └── build-and-test.yml ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── babel.config.cjs ├── benchmark ├── data │ ├── data-v2.json │ ├── data-v3.json │ └── data.json ├── pointGenerator.js └── runner.js ├── deploy.cjs ├── docs ├── index.html ├── index.js └── quickhull3d.png ├── jest.config.cjs ├── package-lock.json ├── package.json ├── src ├── Face.ts ├── HalfEdge.ts ├── QuickHull.ts ├── Vertex.ts ├── VertexList.ts ├── debug.ts ├── index.ts ├── types.ts └── types │ └── index.d.ts ├── test ├── index.test.ts ├── issue3.json ├── issue38.json └── issue5.json ├── tsconfig.json └── webpack.config.cjs /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['standard', 'react-app'], 4 | rules: { 5 | 'space-before-function-paren': [ 6 | 'error', 7 | { 8 | anonymous: 'ignore', 9 | named: 'ignore', 10 | asyncArrow: 'ignore' 11 | } 12 | ], 13 | 'no-unused-vars': 'off', 14 | 'no-use-before-define': 'off', 15 | '@typescript-eslint/no-unused-vars': 'error' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | fetch-depth: 0 13 | 14 | - uses: actions/setup-node@v3.1.1 15 | - run: npm install 16 | - run: npm run test 17 | - run: npm run lint 18 | - run: npm run coverage 19 | - uses: codecov/codecov-action@v3 20 | with: 21 | token: ${{ secrets.CODECOV_TOKEN }} 22 | 23 | build: 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - uses: actions/checkout@v2 28 | with: 29 | fetch-depth: 0 30 | 31 | - uses: actions/setup-node@v3.1.1 32 | - run: npm install 33 | - run: npm run build 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | benchmark 5 | .idea 6 | 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "none" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) YYYY Mauricio Poppe 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quickhull3d 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Codecov Status][codecov-image]][codecov-url] 5 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fmauriciopoppe%2Fquickhull3d.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fmauriciopoppe%2Fquickhull3d?ref=badge_shield) 6 | 7 | A robust quickhull implementation to find the convex hull of a set of 3d points in `O(n log n)` ported from [John Lloyd implementation](http://www.cs.ubc.ca/~lloyd/java/quickhull3d.html) 8 | 9 | Additional implementation material: 10 | 11 | - Dirk Gregorius presentation: https://archive.org/details/GDC2014Gregorius 12 | - Convex Hull Generation with Quick Hull by Randy Gaul (lost link) 13 | 14 | [This library was incorporated into ThreeJS!](https://github.com/mrdoob/three.js/pull/10987). Thanks to https://github.com/Mugen87 for his work to move the primitives to ThreeJS primitives, the quickhull3d library will always be library agnostic and will operate with raw arrays. 15 | 16 | ## Features 17 | 18 | - Key functions are well documented (including ascii graphics) 19 | - [Faster](https://plot.ly/~maurizzzio/36/quickhull3d-vs-convexhull/) than other JavaScript implementations of convex hull 20 | 21 | ## Demo 22 | 23 | Click on the image to see a demo! 24 | 25 | [![demo](./docs/quickhull3d.png)](http://mauriciopoppe.github.io/quickhull3d/) 26 | 27 | ## Minimal browser demo (using v3 or above) 28 | 29 | ```html 30 | 50 | ``` 51 | 52 | ## Installation 53 | 54 | ```bash 55 | $ npm install --save quickhull3d 56 | ``` 57 | 58 | ## Usage 59 | 60 | ```javascript 61 | import qh from 'quickhull3d' 62 | ``` 63 | 64 | ### `qh(points, options)` 65 | 66 | **params** 67 | * `points` {Array>} an array of 3d points whose convex hull needs to be computed 68 | * `options` {Object} (optional) 69 | * `options.skipTriangulation` {Boolean} True to skip the triangulation of the faces 70 | (returning n-vertex faces) 71 | 72 | **returns** An array of 3 element arrays, each subarray has the indices of 3 points which form a face whose normal points outside the polyhedra 73 | 74 | ### `isPointInsideHull(point, points, faces)` 75 | 76 | **params** 77 | * `point` {Array} The point that we want to check that it's a convex hull. 78 | * `points` {Array>} The array of 3d points whose convex hull was computed 79 | * `faces` {Array>} An array of 3 element arrays, each subarray has the indices of 3 points which form a face whose normal points outside the polyhedra 80 | 81 | **returns** `true` if the point `point` is inside the convex hull 82 | 83 | **example** 84 | 85 | ```javascript 86 | import qh, { isPointInsideHull } from 'quickhull3d' 87 | 88 | const points = [ 89 | [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1], 90 | [1, 1, 0], [1, 0, 1], [0, 1, 1], [1, 1, 1] 91 | ] 92 | const faces = qh(points) 93 | expect(isPointInsideHull([0.5, 0.5, 0.5], points, faces)).toBe(true) 94 | expect(isPointInsideHull([0, 0, -0.1], points, faces)).toBe(false) 95 | ``` 96 | 97 | ### Constructor 98 | 99 | ```javascript 100 | import QuickHull from 'quickhull3d/dist/QuickHull' 101 | ``` 102 | 103 | #### `instance = new QuickHull(points)` 104 | 105 | **params** 106 | * `points` {Array} an array of 3d points whose convex hull needs to be computed 107 | 108 | #### `instance.build()` 109 | 110 | Computes the quickhull of all the points stored in the instance 111 | 112 | **time complexity** `O(n log n)` 113 | 114 | #### `instance.collectFaces(skipTriangulation)` 115 | 116 | **params** 117 | * `skipTriangulation` {Boolean} (default: false) True to skip the triangulation 118 | and return n-vertices faces 119 | 120 | **returns** 121 | 122 | An array of 3-element arrays (or n-element arrays if `skipTriangulation = true`) 123 | which are the faces of the convex hull 124 | 125 | ## Example 126 | 127 | ```javascript 128 | import qh from 'quickhull3d' 129 | const points = [ 130 | [0, 1, 0], 131 | [1, -1, 1], 132 | [-1, -1, 1], 133 | [0, -1, -1] 134 | ] 135 | 136 | qh(points) 137 | // output: 138 | // [ [ 2, 0, 3 ], [ 0, 1, 3 ], [ 2, 1, 0 ], [ 2, 3, 1 ] ] 139 | // 1st face: 140 | // points[2] = [-1, -1, 1] 141 | // points[0] = [0, 1, 0] 142 | // points[3] = [0, -1, -1] 143 | // normal = (points[0] - points[2]) x (points[3] - points[2]) 144 | ``` 145 | 146 | Using the constructor: 147 | 148 | ```javascript 149 | import { QuickHull } from 'quickhull3d' 150 | const points = [ 151 | [0, 1, 0], 152 | [1, -1, 1], 153 | [-1, -1, 1], 154 | [0, -1, -1] 155 | ]; 156 | const instance = new QuickHull(points) 157 | instance.build() 158 | instance.collectFaces() // returns an array of 3-element arrays 159 | ``` 160 | 161 | ## Benchmarks 162 | 163 | Specs: 164 | 165 | ``` 166 | MacBook Pro (Retina, Mid 2012) 167 | 2.3 GHz Intel Core i7 168 | 8 GB 1600 MHz DDR3 169 | NVIDIA GeForce GT 650M 1024 MB 170 | ``` 171 | 172 | Versus [`convex-hull`](https://www.npmjs.com/package/convex-hull) 173 | 174 | ``` 175 | // LEGEND: program:numberOfPoints 176 | quickhull3d:100 x 6,212 ops/sec 1.24% (92 runs sampled) 177 | convexhull:100 x 2,507 ops/sec 1.20% (89 runs sampled) 178 | quickhull3d:1000 x 1,171 ops/sec 0.93% (97 runs sampled) 179 | convexhull:1000 x 361 ops/sec 1.38% (88 runs sampled) 180 | quickhull3d:10000 x 190 ops/sec 1.33% (87 runs sampled) 181 | convexhull:10000 x 32.04 ops/sec 2.37% (56 runs sampled) 182 | quickhull3d:100000 x 11.90 ops/sec 6.34% (34 runs sampled) 183 | convexhull:100000 x 2.81 ops/sec 2.17% (11 runs sampled) 184 | quickhull3d:200000 x 5.11 ops/sec 10.05% (18 runs sampled) 185 | convexhull:200000 x 1.23 ops/sec 3.33% (8 runs sampled) 186 | ``` 187 | 188 | [![quickhull3d vs convexhull](https://cloud.githubusercontent.com/assets/1616682/11645526/97036bea-9d2b-11e5-8549-8ccba137f1b2.png)](https://plot.ly/~maurizzzio/36/quickhull3d-vs-convexhull/) 189 | 190 | ## License 191 | 192 | Mauricio Poppe. Licensed under the MIT license. 193 | 194 | [npm-url]: https://npmjs.org/package/quickhull3d 195 | [npm-image]: https://img.shields.io/npm/v/quickhull3d.svg?style=flat 196 | 197 | [codecov-url]: https://codecov.io/github/mauriciopoppe/quickhull3d 198 | [codecov-image]: https://img.shields.io/codecov/c/github/mauriciopoppe/quickhull3d.svg?style=flat 199 | 200 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fmauriciopoppe%2Fquickhull3d.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fmauriciopoppe%2Fquickhull3d?ref=badge_large) 201 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | // NOTE: babel is used for jest but not to build the project with webpack. 2 | module.exports = { 3 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'] 4 | } 5 | -------------------------------------------------------------------------------- /benchmark/data/data-v2.json: -------------------------------------------------------------------------------- 1 | {"y":[0.3045790538302475,1.0640485965169726,4.609690537254902,58.66617514893616,109.95577044444444],"x":[100,1000,10000,100000,200000],"rme":[0.9664762547736114,0.886042869790167,0.8877283285404575,1.5695238527314475,3.416947007775881],"hz":[3283.219865005342,939.806699875713,216.934302187562,17.045597355908992,9.094565896432455],"name":"face assignment to the first face","type":"scatter"} 2 | {"y":[0.3091439114371725,1.0631130683941992,4.472945863026821,53.164510420000006,102.85565035714286],"x":[100,1000,10000,100000,200000],"rme":[0.6471365427802306,0.5675159511271277,0.6550184501203145,1.4664068344181966,3.4254633816174405],"hz":[3234.7394304197073,940.6337197138122,223.56630968104426,18.80954027602235,9.722363297764657],"name":"replacing edge representation to halfEdges","type":"scatter"} 3 | {"y":[0.408121530461785,1.4496768358908352,6.384833375,71.24809199999999,154.6883756],"x":[100,1000,10000,100000,200000],"rme":[1.0413563209930354,0.8169166902406454,1.1692567523253496,2.1093023993228353,3.473655903257951],"hz":[2450.2505390208426,689.8089113671282,156.62115849655981,14.035463574238594,6.464609872081429],"name":"after face merge implementation","type":"scatter"} 4 | {"y":[0.4094013402805851,1.4346506299179367,6.309571334548609,69.25389879999996,152.24970814285714],"x":[100,1000,10000,100000,200000],"rme":[0.8782348555616357,0.6823036493800583,0.7790454798998948,1.7086387651171855,4.396402452533553],"hz":[2442.5909287806567,697.0338137705352,158.48937225329598,14.43962025716306,6.568157090072657],"name":"after face merge implementation without debug","type":"scatter"} 5 | {"y":[0.36757547435171956,1.3042536447916664,6.123228234270417,68.90619642499998,155.85534694999998],"x":[100,1000,10000,100000,200000],"rme":[0.7553024992722579,0.5343404420319167,0.97124885045789,2.2026694295207645,5.160315913715647],"hz":[2720.5297136966665,766.7220283365464,163.31254719580934,14.512482938866556,6.416205921512661],"name":"after face merge implementation face merge disabled","type":"scatter"} 6 | {"y":[0.36934338456604604,1.3575469557191542,6.34459395473251,74.78018202631579,162.39062745000004],"x":[100,1000,10000,100000,200000],"rme":[0.9825938189180788,0.904060705558841,1.0225952234958038,2.394014904801982,4.4912696467790205],"hz":[2707.507543894237,736.622770790462,157.61449938874148,13.372526957049816,6.157990862544695],"name":"after face merge implementation face merge disabled and other edge face fix disabled","type":"scatter"} 7 | {"y":[0.34398982758522983,1.1661260146641732,4.824286218495934,58.40349491304347,109.87739107692308],"x":[100,1000,10000,100000,200000],"rme":[1.071445194658082,0.9269779256283442,1.2099685575225918,1.7852560744626687,2.9274913806319285],"hz":[2907.062708859411,857.54025501951,207.28455044107432,17.12226299965255,9.101053366837942],"name":"face merge implementation disabled completely including center computation","type":"scatter"} 8 | {"y":[0.32183065533251026,1.0877854778255962,4.52679602228682,58.81895346666667,116.06007208],"x":[100,1000,10000,100000,200000],"rme":[0.6897512162468206,0.5601280497434826,0.6279946435722538,4.627588218102916,6.319920670850979],"hz":[3107.224198287811,919.298906250272,220.90679480071333,17.001322551015985,8.616227631762126],"name":"face merge implementation disabled completely including center computation v2","type":"scatter"} 9 | {"y":[0.3606593099337829,1.3788290286026526,5.967606429894182,65.85341117073169,156.04865220000002],"x":[100,1000,10000,100000,200000],"rme":[1.0658915541048544,0.6169766804262076,0.6646729520454794,1.5933719234239616,4.61554046811412],"hz":[2772.699809644731,725.2530801541294,167.57137250047035,15.185242225454319,6.408257847163898],"name":"half edge stores a single vertex, no merge count check","type":"scatter"} 10 | {"y":[0.3092640101332548,1.2401183496539874,5.6756909482931714,65.11584890476192,156.21962560000003],"x":[100,1000,10000,100000,200000],"rme":[0.9654634906595171,0.49557467661381255,0.5403286081610739,1.5303222951644486,4.977472984222286],"hz":[3233.483261014183,806.3746498703256,176.19000208260567,15.357244308718057,6.401244377326173],"name":"half edge stores a single vertex, no merge count check","type":"scatter"} 11 | {"y":[0.29998493169106055,1.1130207697618661,4.444219250946972,51.86598107142857,107.40315281481482],"x":[100,1000,10000,100000,200000],"rme":[0.798859953295112,0.5282379138919387,0.6572894475529462,2.0227564507236093,2.125848949895714],"hz":[3333.5007673980435,898.4558304459608,225.01140099848146,19.280460512697605,9.310713641006481],"name":"half edge stores a single vertex, no merge count check","type":"scatter"} 12 | {"y":[0.31664418818525714,1.185830869392853,4.5521040135658914,54.98767960294117,117.75695996153847],"x":[100,1000,10000,100000,200000],"rme":[0.777133621674227,1.7344424630822297,0.7474512687345205,4.766899830244279,6.0521810230961535],"hz":[3158.1189148967924,843.2905786235784,219.6786358615408,18.185891952904157,8.492067053417632],"name":"triangulation moved to Face","type":"scatter"} 13 | {"y":[0.28793666956755243,1.1034579531046462,4.56406861252703,52.91698962857142,111.80208446153846],"x":[100,1000,10000,100000,200000],"rme":[0.8533548020896348,0.834223324465589,1.661041136936976,1.5917806991821646,4.328988014725509],"hz":[3472.9859225706973,906.2420522562179,219.10275346327907,18.89752245959341,8.944377064311485],"name":"triangulation moved to index.js","type":"scatter"} 14 | {"y":[0.6340000793362346,0.40817199799830156,2.089350730299243,2.8159151364619874,6.376366829385969],"x":[100,1000,10000,100000,200000],"rme":[1.9422507884840963,0.7813877897031397,1.9081995334766442,0.7589859635252448,1.0349388274753022],"hz":[1577.2868688706608,2449.947583136659,478.6175846392133,355.12433846157535,156.829120211752],"type":"scatter"} 15 | -------------------------------------------------------------------------------- /benchmark/data/data-v3.json: -------------------------------------------------------------------------------- 1 | [{"y":[0.16096597212002992,0.8540175989607655,5.266183928735631,84.00326273529413,195.60293966666666],"x":["100","1000","10000","100000","200000"],"rme":[1.2414170992752513,0.9269769494748911,1.334543237704003,6.33597570531901,10.0492917618284],"hz":[],"type":"scatter","name":"quickhull3d"},{"y":[0.3988803594033895,2.7667912284090903,31.215709749999995,355.70476063636363,814.2694763750001],"x":["100","1000","10000","100000","200000"],"rme":[1.2012969970516527,1.3843690280265033,2.369998498961481,2.171233229756029,3.3256691841958492],"hz":[],"type":"scatter","name":"convexhull"}] 2 | -------------------------------------------------------------------------------- /benchmark/data/data.json: -------------------------------------------------------------------------------- 1 | {"x":[100,1000,10000,100000],"y":[1237.88988208848,321.6602385440846,48.16367304133673,1.1052224403366429],"relativeMarginOfError":[3.522514705628406,5.0732425013643345,1.4601505597238706,4.14491448990492],"type":"scatter"} 2 | {"x":[100,1000,10000,100000],"y":[1244.6493365699794,322.18374890838885,47.66285135259094,1.0912791782025244],"relativeMarginOfError":[1.2882677102606968,0.9699304754677519,1.472664914518228,6.994454854620207],"type":"scatter"} 3 | {"x":[100,1000,10000,100000],"y":[1319.6469458911342,417.2430202109589,122.61339626431312,12.361774544249938],"relativeMarginOfError":[0.9708517973573406,0.7281714292896581,1.055212077600842,2.0804355223219217],"type":"scatter"} 4 | {"x":[100,1000,10000,100000],"y":[3185.3461528132907,808.1354213157173,182.4946648307365,12.893855555089608],"relativeMarginOfError":[0.7879333809281146,0.5026875593408048,1.0328809167408903,1.304008952221176],"type":"scatter"} 5 | {"args":["face assignment to the first face"],"x":[100,1000,10000,100000],"y":[3108.5157292649087,910.7543823664613,217.65578567702633,17.063448556850254],"relativeMarginOfError":[1.4772208465591894,0.794916844737486,0.7480756994438252,1.2216752627290188],"type":"scatter"} 6 | {"args":["face assignment to the first face"],"x":[100,1000,10000,100000],"y":[3229.871755483392,923.6305473723861,215.85491740896714,17.055597652210512],"relativeMarginOfError":[1.0387927197214948,0.7805520183517096,0.7045494803459631,1.1770564841428708],"type":"scatter"} 7 | {"args":["face assignment to the first face v2"],"x":[100,1000,10000,100000],"y":[3535.7171229135733,993.3177619007794,226.3396396518187,17.560380506230715],"relativeMarginOfError":[1.0902589525480282,1.4014809479787438,0.8166120558652827,1.4865287296486036],"type":"scatter"} 8 | -------------------------------------------------------------------------------- /benchmark/pointGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mauricio on 3/14/15. 3 | */ 4 | var fs = require('fs') 5 | var limit = process.argv[2] 6 | 7 | var LIMIT = +limit 8 | function p () { 9 | return -LIMIT + Math.random() * 2 * LIMIT 10 | } 11 | 12 | function genP () { 13 | return [p(), p(), p()] 14 | } 15 | 16 | var points = [] 17 | for (var i = 0; i < +limit; i += 1) { 18 | points.push(genP()) 19 | } 20 | 21 | fs.writeFile('./points' + limit + '.json', JSON.stringify(points), function (err) { 22 | if (err) { 23 | throw err 24 | } 25 | console.log('saved!') 26 | }) 27 | -------------------------------------------------------------------------------- /benchmark/runner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mauricio on 3/14/15. 3 | * 4 | * usage: 5 | * 6 | * node runner.js && node upload.js 7 | * 8 | */ 9 | var Benchmark = require('benchmark') 10 | var suite = new Benchmark.Suite() 11 | 12 | var qh = require('../dist/') 13 | var ch = require('convex-hull') 14 | var fs = require('fs') 15 | 16 | var arr = ['100', '1000', '10000', '100000', '200000'] 17 | var m = {quickhull3d: 0, convexhull: 1} 18 | var data = Object.keys(m).map(function (v) { 19 | return { 20 | y: [], // in ms 21 | x: [], 22 | rme: [], 23 | hz: [], 24 | type: 'scatter', 25 | name: v 26 | } 27 | }) 28 | 29 | arr.forEach(function (n) { 30 | var data = fs.readFileSync('./points' + n + '.json') 31 | data = JSON.parse(data) 32 | suite 33 | .add('quickhull3d:' + n, function () { 34 | qh(data) 35 | }) 36 | .add('convexhull:' + n, function () { 37 | ch(data) 38 | }) 39 | }) 40 | 41 | suite 42 | .on('cycle', function (event) { 43 | // console.log(event) 44 | console.log(String(event.target)) 45 | var results = event.target 46 | var suiteName = event.target.name 47 | var x = suiteName.split(':')[1] 48 | var i = m[suiteName.split(':')[0]] 49 | var datum = data[i] 50 | datum.x.push(x) 51 | // https://github.com/bestiejs/benchmark.js/blob/master/benchmark.js#L1545-L1546 52 | // datum.x.push(results.hz) 53 | 54 | // the time it took for a test to complete in ms 55 | datum.y.push(results.times.period * 1000) 56 | datum.rme.push(results.stats.rme) 57 | }) 58 | .on('complete', function () { 59 | fs.writeFileSync('./data/data-v3.json', JSON.stringify(data) + '\n') 60 | }) 61 | .run({ async: true }) 62 | -------------------------------------------------------------------------------- /deploy.cjs: -------------------------------------------------------------------------------- 1 | const ghpages = require('gh-pages') 2 | const { execSync } = require('node:child_process') 3 | 4 | execSync('npm run build', { stdio: 'inherit' }) 5 | execSync('cp dist/quickhull3d.js docs/', { stdio: 'inherit' }) 6 | 7 | ghpages.publish( 8 | 'docs', 9 | { 10 | nojekyll: true, 11 | add: true, 12 | async beforeAdd () {} 13 | }, 14 | function () { 15 | execSync('rm docs/quickhull3d.js', { stdio: 'inherit' }) 16 | console.log('complete!') 17 | } 18 | ) 19 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Three.js Experiments 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js' 4 | import { VertexNormalsHelper } from 'three/addons/helpers/VertexNormalsHelper.js' 5 | import { GUI } from 'three/addons/libs/lil-gui.module.min.js' 6 | 7 | import qh from 'quickhull3d' 8 | 9 | let camera, controls, scene, renderer 10 | const params = { 11 | nPoints: 100, 12 | domain: 100, 13 | originX: 0, 14 | originY: 0, 15 | originZ: 0, 16 | timeToCompute: 'please check console!' 17 | } 18 | 19 | init() 20 | 21 | function generatePointCloud() { 22 | const N_POINTS = params.nPoints 23 | const LIMIT = params.domain 24 | let i 25 | 26 | function p() { 27 | return -LIMIT + 2 * Math.random() * LIMIT 28 | } 29 | 30 | function pointGenerator() { 31 | return [params.originX + p(), params.originY + p(), params.originZ + p()] 32 | } 33 | 34 | const points = [] 35 | for (i = 0; i < N_POINTS; i += 1) { 36 | points.push(pointGenerator()) 37 | } 38 | return points 39 | } 40 | 41 | function ConvexMesh() { 42 | const points = generatePointCloud() 43 | const t0 = performance.now() 44 | const faces = qh(points) 45 | const t1 = performance.now() 46 | console.log(`nPoints=${points.length} timeToCompute = ${t1 - t0}ms`) 47 | 48 | const geometry = new THREE.BufferGeometry() 49 | const vertices = [] 50 | for (let i = 0; i < faces.length; i += 1) { 51 | const a = points[faces[i][0]] 52 | const b = points[faces[i][1]] 53 | const c = points[faces[i][2]] 54 | vertices.push(...a, ...b, ...c) 55 | } 56 | geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3)) 57 | geometry.computeVertexNormals() 58 | 59 | const polyhedra = new THREE.Mesh( 60 | geometry, 61 | new THREE.MeshNormalMaterial({ 62 | side: THREE.DoubleSide, 63 | flatShading: true 64 | }) 65 | ) 66 | return polyhedra 67 | } 68 | 69 | function rebuild(group) { 70 | group.clear() 71 | 72 | // polyhedra 73 | const polyhedra = ConvexMesh() 74 | group.add(polyhedra) 75 | 76 | // scene helpers 77 | const vertHelper = new VertexNormalsHelper(polyhedra, 0.5, 0x00ff00) 78 | group.add(vertHelper) 79 | } 80 | 81 | function init() { 82 | scene = new THREE.Scene() 83 | scene.background = new THREE.Color(0xcccccc) 84 | scene.fog = new THREE.FogExp2(0xcccccc, 0.002) 85 | 86 | renderer = new THREE.WebGLRenderer({ antialias: true }) 87 | renderer.setPixelRatio(window.devicePixelRatio) 88 | renderer.setSize(window.innerWidth, window.innerHeight) 89 | renderer.setAnimationLoop(animate) 90 | document.body.appendChild(renderer.domElement) 91 | 92 | camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000) 93 | camera.position.set(400, 200, 0) 94 | 95 | // controls 96 | 97 | controls = new OrbitControls(camera, renderer.domElement) 98 | controls.listenToKeyEvents(window) // optional 99 | 100 | // controls.addEventListener( 'change', render ); // call this only in static scenes (i.e., if there is no animation loop) 101 | 102 | controls.enableDamping = true // an animation loop is required when either damping or auto-rotation are enabled 103 | controls.dampingFactor = 0.05 104 | 105 | controls.screenSpacePanning = false 106 | 107 | controls.minDistance = 100 108 | controls.maxDistance = 500 109 | 110 | controls.maxPolarAngle = Math.PI / 2 111 | 112 | const group = new THREE.Object3D() 113 | scene.add(group) 114 | 115 | // initial build 116 | 117 | rebuild(group) 118 | 119 | // lights 120 | 121 | const dirLight1 = new THREE.DirectionalLight(0xffffff, 3) 122 | dirLight1.position.set(1, 1, 1) 123 | scene.add(dirLight1) 124 | 125 | const dirLight2 = new THREE.DirectionalLight(0x002288, 3) 126 | dirLight2.position.set(-1, -1, -1) 127 | scene.add(dirLight2) 128 | 129 | const ambientLight = new THREE.AmbientLight(0x555555) 130 | scene.add(ambientLight) 131 | 132 | // helpers 133 | const axesHelper = new THREE.AxesHelper(5) 134 | scene.add(axesHelper) 135 | 136 | const gui = new GUI() 137 | gui.add(params, 'nPoints', 10, 1000).onChange(() => rebuild(group)) 138 | gui.add(params, 'domain', 50, 150).onChange(() => rebuild(group)) 139 | gui.add(params, 'originX', -100, 100).onChange(() => rebuild(group)) 140 | gui.add(params, 'originY', -100, 100).onChange(() => rebuild(group)) 141 | gui.add(params, 'originZ', -100, 100).onChange(() => rebuild(group)) 142 | gui.add(params, 'timeToCompute') 143 | 144 | window.addEventListener('resize', onWindowResize) 145 | } 146 | 147 | function onWindowResize() { 148 | camera.aspect = window.innerWidth / window.innerHeight 149 | camera.updateProjectionMatrix() 150 | 151 | renderer.setSize(window.innerWidth, window.innerHeight) 152 | } 153 | 154 | function animate() { 155 | controls.update() // only required if controls.enableDamping = true, or if controls.autoRotate = true 156 | 157 | render() 158 | } 159 | 160 | function render() { 161 | renderer.render(scene, camera) 162 | } 163 | -------------------------------------------------------------------------------- /docs/quickhull3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauriciopoppe/quickhull3d/ce1f404866d36de74b10e82e8e5ad3ba8ef675ba/docs/quickhull3d.png -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | transform: { 3 | '^.+\\.[tj]sx?$': [ 4 | 'ts-jest', 5 | { 6 | useESM: true 7 | } 8 | ] 9 | }, 10 | resolver: 'ts-jest-resolver', 11 | testMatch: ['**/__tests__/**/*.[jt]s?(x)', '/{src,test}/**/?(*.)+(spec|test).[jt]s?(x)'], 12 | // transformIgnorePatterns: ['/node_modules/(?!gl-matrix)'], 13 | testEnvironment: 'node', 14 | extensionsToTreatAsEsm: ['.ts'] 15 | } 16 | 17 | module.exports = config 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quickhull3d", 3 | "version": "3.1.1", 4 | "description": "A quickhull implementation for 3d points", 5 | "homepage": "https://github.com/mauriciopoppe/quickhull3d", 6 | "author": { 7 | "name": "Mauricio Poppe", 8 | "url": "http://mauriciopoppe.com" 9 | }, 10 | "bugs": "https://github.com/mauriciopoppe/quickhull3d/issues", 11 | "type": "module", 12 | "main": "dist/index.js", 13 | "exports": { 14 | ".": { 15 | "import": "./dist/index.js" 16 | }, 17 | "./dist/*": "./dist/*" 18 | }, 19 | "sideEffects": false, 20 | "keywords": [ 21 | "geometry", 22 | "3d", 23 | "convex hull", 24 | "quick hull", 25 | "quickhull" 26 | ], 27 | "scripts": { 28 | "clean": "rimraf dist", 29 | "lint": "standard", 30 | "coverage": "jest --coverage", 31 | "test": "jest", 32 | "test:debug": "DEBUG=quickhull3d:* jest", 33 | "build": "npm run clean && npm run build:typescript && npm run build:webpack", 34 | "build:webpack": "NODE_ENV=production webpack", 35 | "build:typescript": "tsc", 36 | "deploy": "node deploy.cjs", 37 | "preversion": "npm run lint -s && npm run test -s && npm run build -s" 38 | }, 39 | "standard": { 40 | "ignore": [ 41 | "dist", 42 | "docs" 43 | ] 44 | }, 45 | "files": [ 46 | "dist" 47 | ], 48 | "license": "MIT", 49 | "types": "index.d.ts", 50 | "repository": { 51 | "type": "git", 52 | "url": "https://github.com/mauriciopoppe/quickhull3d" 53 | }, 54 | "dependencies": { 55 | "debug": "^4.3.4", 56 | "get-plane-normal": "^1.0.0", 57 | "gl-mat4": "^1.2.0", 58 | "gl-quat": "^1.0.0", 59 | "gl-vec4": "^1.0.1", 60 | "monotone-convex-hull-2d": "^1.0.1", 61 | "point-line-distance": "^1.0.0" 62 | }, 63 | "devDependencies": { 64 | "@babel/core": "^7.18.13", 65 | "@babel/preset-env": "^7.18.10", 66 | "@babel/preset-typescript": "^7.24.7", 67 | "@jest/globals": "^29.7.0", 68 | "@types/debug": "^4.1.12", 69 | "@types/node": "^20.14.2", 70 | "gh-pages": "^6.1.1", 71 | "jest": "^29.0.1", 72 | "rimraf": "^3.0.2", 73 | "standard": "^17.0.0", 74 | "ts-jest": "^29.1.4", 75 | "ts-jest-resolver": "^2.0.1", 76 | "ts-loader": "^9.5.1", 77 | "typescript": "^5.4.5", 78 | "webpack-cli": "^5.1.4" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Face.ts: -------------------------------------------------------------------------------- 1 | import dot from 'gl-vec3/dot' 2 | import add from 'gl-vec3/add' 3 | import subtract from 'gl-vec3/subtract' 4 | import cross from 'gl-vec3/cross' 5 | import copy from 'gl-vec3/copy' 6 | import { default as magnitude } from 'gl-vec3/length' 7 | import scale from 'gl-vec3/scale' 8 | import scaleAndAdd from 'gl-vec3/scaleAndAdd' 9 | import normalize from 'gl-vec3/normalize' 10 | import { default as $debug } from 'debug' 11 | 12 | import { Vec3Like } from './types' 13 | import { HalfEdge } from './HalfEdge' 14 | import { Vertex } from './Vertex' 15 | 16 | const debug = $debug('quickhull3d:face') 17 | 18 | export enum Mark { 19 | Visible = 0, 20 | NonConvex, 21 | Deleted 22 | } 23 | 24 | export class Face { 25 | normal: Vec3Like 26 | centroid: Vec3Like 27 | offset: number 28 | outside: Vertex 29 | mark: Mark 30 | edge: HalfEdge 31 | nVertices: number 32 | area: number 33 | 34 | constructor() { 35 | this.normal = [0, 0, 0] 36 | this.centroid = [0, 0, 0] 37 | // signed distance from face to the origin 38 | this.offset = 0 39 | // pointer to the a vertex in a double linked list this face can see 40 | this.outside = null 41 | this.mark = Mark.Visible 42 | this.edge = null 43 | this.nVertices = 0 44 | } 45 | 46 | getEdge(i: number) { 47 | let it = this.edge 48 | while (i > 0) { 49 | it = it.next 50 | i -= 1 51 | } 52 | while (i < 0) { 53 | it = it.prev 54 | i += 1 55 | } 56 | return it 57 | } 58 | 59 | computeNormal() { 60 | const e0 = this.edge 61 | const e1 = e0.next 62 | let e2 = e1.next 63 | const v2 = subtract([], e1.head().point, e0.head().point) 64 | const t = [] 65 | const v1 = [] 66 | 67 | this.nVertices = 2 68 | this.normal = [0, 0, 0] 69 | // console.log(this.normal) 70 | while (e2 !== e0) { 71 | copy(v1, v2) 72 | subtract(v2, e2.head().point, e0.head().point) 73 | add(this.normal, this.normal, cross(t, v1, v2)) 74 | e2 = e2.next 75 | this.nVertices += 1 76 | } 77 | this.area = magnitude(this.normal) 78 | // normalize the vector, since we've already calculated the area 79 | // it's cheaper to scale the vector using this quantity instead of 80 | // doing the same operation again 81 | this.normal = scale(this.normal, this.normal, 1 / this.area) 82 | } 83 | 84 | computeNormalMinArea(minArea: number) { 85 | this.computeNormal() 86 | if (this.area < minArea) { 87 | // compute the normal without the longest edge 88 | let maxEdge: HalfEdge 89 | let maxSquaredLength = 0 90 | let edge = this.edge 91 | 92 | // find the longest edge (in length) in the chain of edges 93 | do { 94 | const lengthSquared = edge.lengthSquared() 95 | if (lengthSquared > maxSquaredLength) { 96 | maxEdge = edge 97 | maxSquaredLength = lengthSquared 98 | } 99 | edge = edge.next 100 | } while (edge !== this.edge) 101 | 102 | const p1 = maxEdge.tail().point 103 | const p2 = maxEdge.head().point 104 | const maxVector = subtract([], p2, p1) 105 | const maxLength = Math.sqrt(maxSquaredLength) 106 | // maxVector is normalized after this operation 107 | scale(maxVector, maxVector, 1 / maxLength) 108 | // compute the projection of maxVector over this face normal 109 | const maxProjection = dot(this.normal, maxVector) 110 | // subtract the quantity maxEdge adds on the normal 111 | scaleAndAdd(this.normal, this.normal, maxVector, -maxProjection) 112 | // renormalize `this.normal` 113 | normalize(this.normal, this.normal) 114 | } 115 | } 116 | 117 | computeCentroid() { 118 | this.centroid = [0, 0, 0] 119 | let edge = this.edge 120 | do { 121 | add(this.centroid, this.centroid, edge.head().point) 122 | edge = edge.next 123 | } while (edge !== this.edge) 124 | scale(this.centroid, this.centroid, 1 / this.nVertices) 125 | } 126 | 127 | computeNormalAndCentroid(minArea?: number) { 128 | if (typeof minArea !== 'undefined') { 129 | this.computeNormalMinArea(minArea) 130 | } else { 131 | this.computeNormal() 132 | } 133 | this.computeCentroid() 134 | this.offset = dot(this.normal, this.centroid) 135 | } 136 | 137 | distanceToPlane(point: Vec3Like) { 138 | return dot(this.normal, point) - this.offset 139 | } 140 | 141 | /** 142 | * @private 143 | * 144 | * Connects two edges assuming that prev.head().point === next.tail().point 145 | * 146 | * @param {HalfEdge} prev 147 | * @param {HalfEdge} next 148 | */ 149 | connectHalfEdges(prev: HalfEdge, next: HalfEdge) { 150 | let discardedFace: Face 151 | if (prev.opposite.face === next.opposite.face) { 152 | // `prev` is remove a redundant edge 153 | const oppositeFace = next.opposite.face 154 | let oppositeEdge: HalfEdge 155 | if (prev === this.edge) { 156 | this.edge = next 157 | } 158 | if (oppositeFace.nVertices === 3) { 159 | // case: 160 | // remove the face on the right 161 | // 162 | // /|\ 163 | // / | \ the face on the right 164 | // / | \ --> opposite edge 165 | // / a | \ 166 | // *----*----* 167 | // / b | \ 168 | // ▾ 169 | // redundant edge 170 | // 171 | // Note: the opposite edge is actually in the face to the right 172 | // of the face to be destroyed 173 | oppositeEdge = next.opposite.prev.opposite 174 | oppositeFace.mark = Mark.Deleted 175 | discardedFace = oppositeFace 176 | } else { 177 | // case: 178 | // t 179 | // *---- 180 | // /| <- right face's redundant edge 181 | // / | opposite edge 182 | // / | ▴ / 183 | // / a | | / 184 | // *----*----* 185 | // / b | \ 186 | // ▾ 187 | // redundant edge 188 | oppositeEdge = next.opposite.next 189 | // make sure that the link `oppositeFace.edge` points correctly even 190 | // after the right face redundant edge is removed 191 | if (oppositeFace.edge === oppositeEdge.prev) { 192 | oppositeFace.edge = oppositeEdge 193 | } 194 | 195 | // /| / 196 | // / | t/opposite edge 197 | // / | / ▴ / 198 | // / a |/ | / 199 | // *----*----* 200 | // / b \ 201 | oppositeEdge.prev = oppositeEdge.prev.prev 202 | oppositeEdge.prev.next = oppositeEdge 203 | } 204 | // /| 205 | // / | 206 | // / | 207 | // / a | 208 | // *----*----* 209 | // / b ▴ \ 210 | // | 211 | // redundant edge 212 | next.prev = prev.prev 213 | next.prev.next = next 214 | 215 | // / \ \ 216 | // / \->\ 217 | // / \<-\ opposite edge 218 | // / a \ \ 219 | // *----*----* 220 | // / b ^ \ 221 | next.setOpposite(oppositeEdge) 222 | 223 | oppositeFace.computeNormalAndCentroid() 224 | } else { 225 | // trivial case 226 | // * 227 | // /|\ 228 | // / | \ 229 | // / |--> next 230 | // / a | \ 231 | // *----*----* 232 | // \ b | / 233 | // \ |--> prev 234 | // \ | / 235 | // \|/ 236 | // * 237 | prev.next = next 238 | next.prev = prev 239 | } 240 | return discardedFace 241 | } 242 | 243 | mergeAdjacentFaces(adjacentEdge: HalfEdge, discardedFaces: Array) { 244 | const oppositeEdge = adjacentEdge.opposite 245 | const oppositeFace = oppositeEdge.face 246 | 247 | discardedFaces.push(oppositeFace) 248 | oppositeFace.mark = Mark.Deleted 249 | 250 | // find the chain of edges whose opposite face is `oppositeFace` 251 | // 252 | // ===> 253 | // \ face / 254 | // * ---- * ---- * ---- * 255 | // / opposite face \ 256 | // <=== 257 | // 258 | let adjacentEdgePrev = adjacentEdge.prev 259 | let adjacentEdgeNext = adjacentEdge.next 260 | let oppositeEdgePrev = oppositeEdge.prev 261 | let oppositeEdgeNext = oppositeEdge.next 262 | 263 | // left edge 264 | while (adjacentEdgePrev.opposite.face === oppositeFace) { 265 | adjacentEdgePrev = adjacentEdgePrev.prev 266 | oppositeEdgeNext = oppositeEdgeNext.next 267 | } 268 | // right edge 269 | while (adjacentEdgeNext.opposite.face === oppositeFace) { 270 | adjacentEdgeNext = adjacentEdgeNext.next 271 | oppositeEdgePrev = oppositeEdgePrev.prev 272 | } 273 | // adjacentEdgePrev \ face / adjacentEdgeNext 274 | // * ---- * ---- * ---- * 275 | // oppositeEdgeNext / opposite face \ oppositeEdgePrev 276 | 277 | // fix the face reference of all the opposite edges that are not part of 278 | // the edges whose opposite face is not `face` i.e. all the edges that 279 | // `face` and `oppositeFace` do not have in common 280 | let edge: HalfEdge 281 | for (edge = oppositeEdgeNext; edge !== oppositeEdgePrev.next; edge = edge.next) { 282 | edge.face = this 283 | } 284 | 285 | // make sure that `face.edge` is not one of the edges to be destroyed 286 | // Note: it's important for it to be a `next` edge since `prev` edges 287 | // might be destroyed on `connectHalfEdges` 288 | this.edge = adjacentEdgeNext 289 | 290 | // connect the extremes 291 | // Note: it might be possible that after connecting the edges a triangular 292 | // face might be redundant 293 | let discardedFace 294 | discardedFace = this.connectHalfEdges(oppositeEdgePrev, adjacentEdgeNext) 295 | if (discardedFace) { 296 | discardedFaces.push(discardedFace) 297 | } 298 | discardedFace = this.connectHalfEdges(adjacentEdgePrev, oppositeEdgeNext) 299 | if (discardedFace) { 300 | discardedFaces.push(discardedFace) 301 | } 302 | 303 | this.computeNormalAndCentroid() 304 | // TODO: additional consistency checks 305 | return discardedFaces 306 | } 307 | 308 | collectIndices(): number[] { 309 | const indices = [] 310 | let edge = this.edge 311 | do { 312 | indices.push(edge.head().index) 313 | edge = edge.next 314 | } while (edge !== this.edge) 315 | return indices 316 | } 317 | 318 | static fromVertices(vertices: Vertex[], minArea = 0) { 319 | const face = new Face() 320 | const e0 = new HalfEdge(vertices[0], face) 321 | let lastE = e0 322 | for (let i = 1; i < vertices.length; i += 1) { 323 | const e = new HalfEdge(vertices[i], face) 324 | e.prev = lastE 325 | lastE.next = e 326 | lastE = e 327 | } 328 | lastE.next = e0 329 | e0.prev = lastE 330 | 331 | face.edge = e0 332 | face.computeNormalAndCentroid(minArea) 333 | if (debug.enabled) { 334 | debug('face created %j', face.collectIndices()) 335 | } 336 | return face 337 | } 338 | 339 | static createTriangle(v0: Vertex, v1: Vertex, v2: Vertex, minArea = 0) { 340 | const face = new Face() 341 | const e0 = new HalfEdge(v0, face) 342 | const e1 = new HalfEdge(v1, face) 343 | const e2 = new HalfEdge(v2, face) 344 | 345 | // join edges 346 | e0.next = e2.prev = e1 347 | e1.next = e0.prev = e2 348 | e2.next = e1.prev = e0 349 | 350 | // main half edge reference 351 | face.edge = e0 352 | face.computeNormalAndCentroid(minArea) 353 | if (debug.enabled) { 354 | debug('face created %j', face.collectIndices()) 355 | } 356 | return face 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/HalfEdge.ts: -------------------------------------------------------------------------------- 1 | import distance from 'gl-vec3/distance' 2 | import squaredDistance from 'gl-vec3/squaredDistance' 3 | import { default as $debug } from 'debug' 4 | 5 | import { Face } from './Face' 6 | import { Vertex } from './Vertex' 7 | 8 | const debug = $debug('quickhull3d:halfedge') 9 | 10 | export class HalfEdge { 11 | vertex: Vertex 12 | face: Face 13 | next: HalfEdge | null 14 | prev: HalfEdge | null 15 | opposite: HalfEdge | null 16 | 17 | constructor(vertex: Vertex, face: Face) { 18 | this.vertex = vertex 19 | this.face = face 20 | this.next = null 21 | this.prev = null 22 | this.opposite = null 23 | } 24 | 25 | head() { 26 | return this.vertex 27 | } 28 | 29 | tail() { 30 | return this.prev ? this.prev.vertex : null 31 | } 32 | 33 | length() { 34 | if (this.tail()) { 35 | return distance(this.tail().point, this.head().point) 36 | } 37 | return -1 38 | } 39 | 40 | lengthSquared() { 41 | if (this.tail()) { 42 | return squaredDistance(this.tail().point, this.head().point) 43 | } 44 | return -1 45 | } 46 | 47 | setOpposite(edge: HalfEdge) { 48 | const me = this 49 | if (debug.enabled) { 50 | debug( 51 | `opposite ${me.tail().index} <--> ${me.head().index} between ${me.face.collectIndices()}, ${edge.face.collectIndices()}` 52 | ) 53 | } 54 | this.opposite = edge 55 | edge.opposite = this 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/QuickHull.ts: -------------------------------------------------------------------------------- 1 | import pointLineDistance from 'point-line-distance' 2 | import getPlaneNormal from 'get-plane-normal' 3 | import monotoneHull from 'monotone-convex-hull-2d' 4 | import dot from 'gl-vec3/dot' 5 | import scale from 'gl-vec3/scale' 6 | import fromValues from 'gl-vec4/fromValues' 7 | import transformMat4 from 'gl-vec4/transformMat4' 8 | import fromRotationTranslation from 'gl-mat4/fromRotationTranslation' 9 | import rotationTo from 'gl-quat/rotationTo' 10 | import { default as $debug } from 'debug' 11 | 12 | import { Face as IFace } from './types' 13 | import { VertexList } from './VertexList' 14 | import { Vertex } from './Vertex' 15 | import { HalfEdge } from './HalfEdge' 16 | import { Face, Mark } from './Face' 17 | 18 | const debug = $debug('quickhull3d:quickhull') 19 | 20 | // merge types 21 | // non convex with respect to the large face 22 | enum MergeType { 23 | NonConvexWrtLargerFace = 0, 24 | NonConvex 25 | } 26 | 27 | export class QuickHullOptions { 28 | skipTriangulation?: boolean 29 | } 30 | 31 | export class QuickHull { 32 | // tolerance is the computed tolerance used for the merge. 33 | tolerance: number 34 | 35 | // faces are the faces of the hull. 36 | faces: Array 37 | 38 | // newFaces are the new faces in an iteration of the quickhull algorithm. 39 | newFaces: Array 40 | 41 | // claimed are the vertices that have been claimed. 42 | claimed: VertexList 43 | 44 | // unclaimed are the vertices that haven't been claimed. 45 | unclaimed: VertexList 46 | 47 | // vertices are the points of the hull. 48 | vertices: Array 49 | 50 | discardedFaces: Array 51 | 52 | vertexPointIndices: Array 53 | 54 | constructor(points?: Array) { 55 | if (!Array.isArray(points)) { 56 | throw TypeError('input is not a valid array') 57 | } 58 | if (points.length < 4) { 59 | throw Error('cannot build a simplex out of <4 points') 60 | } 61 | 62 | this.tolerance = -1 63 | 64 | this.faces = [] 65 | this.newFaces = [] 66 | // helpers 67 | // 68 | // let `a`, `b` be `Face` instances 69 | // let `v` be points wrapped as instance of `Vertex` 70 | // 71 | // [v, v, ..., v, v, v, ...] 72 | // ^ ^ 73 | // | | 74 | // a.outside b.outside 75 | // 76 | this.claimed = new VertexList() 77 | this.unclaimed = new VertexList() 78 | 79 | // vertices of the hull(internal representation of points) 80 | this.vertices = [] 81 | for (let i = 0; i < points.length; i += 1) { 82 | this.vertices.push(new Vertex(points[i], i)) 83 | } 84 | this.discardedFaces = [] 85 | this.vertexPointIndices = [] 86 | } 87 | 88 | addVertexToFace(vertex: Vertex, face: Face) { 89 | vertex.face = face 90 | if (!face.outside) { 91 | this.claimed.add(vertex) 92 | } else { 93 | this.claimed.insertBefore(face.outside, vertex) 94 | } 95 | face.outside = vertex 96 | } 97 | 98 | /** 99 | * Removes `vertex` for the `claimed` list of vertices, it also makes sure 100 | * that the link from `face` to the first vertex it sees in `claimed` is 101 | * linked correctly after the removal 102 | * 103 | * @param {Vertex} vertex 104 | * @param {Face} face 105 | */ 106 | removeVertexFromFace(vertex: Vertex, face: Face) { 107 | if (vertex === face.outside) { 108 | // fix face.outside link 109 | if (vertex.next && vertex.next.face === face) { 110 | // face has at least 2 outside vertices, move the `outside` reference 111 | face.outside = vertex.next 112 | } else { 113 | // vertex was the only outside vertex that face had 114 | face.outside = null 115 | } 116 | } 117 | this.claimed.remove(vertex) 118 | } 119 | 120 | /** 121 | * Removes all the visible vertices that `face` is able to see which are 122 | * stored in the `claimed` vertext list 123 | * 124 | * @param {Face} face 125 | */ 126 | removeAllVerticesFromFace(face: Face) { 127 | if (face.outside) { 128 | // pointer to the last vertex of this face 129 | // [..., outside, ..., end, outside, ...] 130 | // | | | 131 | // a a b 132 | let end = face.outside 133 | while (end.next && end.next.face === face) { 134 | end = end.next 135 | } 136 | this.claimed.removeChain(face.outside, end) 137 | // b 138 | // [ outside, ...] 139 | // | removes this link 140 | // [ outside, ..., end ] -┘ 141 | // | | 142 | // a a 143 | end.next = null 144 | return face.outside 145 | } 146 | } 147 | 148 | /** 149 | * Removes all the visible vertices that `face` is able to see, additionally 150 | * checking the following: 151 | * 152 | * If `absorbingFace` doesn't exist then all the removed vertices will be 153 | * added to the `unclaimed` vertex list 154 | * 155 | * If `absorbingFace` exists then this method will assign all the vertices of 156 | * `face` that can see `absorbingFace`, if a vertex cannot see `absorbingFace` 157 | * it's added to the `unclaimed` vertex list 158 | * 159 | * @param {Face} face 160 | * @param {Face} [absorbingFace] 161 | */ 162 | deleteFaceVertices(face: Face, absorbingFace?: Face) { 163 | const faceVertices = this.removeAllVerticesFromFace(face) 164 | if (faceVertices) { 165 | if (!absorbingFace) { 166 | // mark the vertices to be reassigned to some other face 167 | this.unclaimed.addAll(faceVertices) 168 | } else { 169 | // if there's an absorbing face try to assign as many vertices 170 | // as possible to it 171 | 172 | // the reference `vertex.next` might be destroyed on 173 | // `this.addVertexToFace` (see VertexList#add), nextVertex is a 174 | // reference to it 175 | let nextVertex: Vertex 176 | for (let vertex = faceVertices; vertex; vertex = nextVertex) { 177 | nextVertex = vertex.next 178 | const distance = absorbingFace.distanceToPlane(vertex.point) 179 | 180 | // check if `vertex` is able to see `absorbingFace` 181 | if (distance > this.tolerance) { 182 | this.addVertexToFace(vertex, absorbingFace) 183 | } else { 184 | this.unclaimed.add(vertex) 185 | } 186 | } 187 | } 188 | } 189 | } 190 | 191 | /** 192 | * Reassigns as many vertices as possible from the unclaimed list to the new 193 | * faces 194 | * 195 | * @param {Faces[]} newFaces 196 | */ 197 | resolveUnclaimedPoints(newFaces: Array) { 198 | // cache next vertex so that if `vertex.next` is destroyed it's still 199 | // recoverable 200 | let vertexNext = this.unclaimed.first() 201 | for (let vertex = vertexNext; vertex; vertex = vertexNext) { 202 | vertexNext = vertex.next 203 | let maxDistance = this.tolerance 204 | let maxFace: Face 205 | for (let i = 0; i < newFaces.length; i += 1) { 206 | const face = newFaces[i] 207 | if (face.mark === Mark.Visible) { 208 | const dist = face.distanceToPlane(vertex.point) 209 | if (dist > maxDistance) { 210 | maxDistance = dist 211 | maxFace = face 212 | } 213 | if (maxDistance > 1000 * this.tolerance) { 214 | break 215 | } 216 | } 217 | } 218 | 219 | if (maxFace) { 220 | this.addVertexToFace(vertex, maxFace) 221 | } 222 | } 223 | } 224 | 225 | /** 226 | * Checks if all the points belong to a plane (2d degenerate case) 227 | */ 228 | allPointsBelongToPlane(v0: Vertex, v1: Vertex, v2: Vertex) { 229 | const normal = getPlaneNormal([0, 0, 0], v0.point, v1.point, v2.point) 230 | const distToPlane = dot(normal, v0.point) 231 | for (const vertex of this.vertices) { 232 | const dist = dot(vertex.point, normal) 233 | if (Math.abs(dist - distToPlane) > this.tolerance) { 234 | // A vertex is not part of the plane formed by ((v0 - v1) X (v0 - v2)) 235 | return false 236 | } 237 | } 238 | return true 239 | } 240 | 241 | /** 242 | * Computes the convex hull of a plane. 243 | */ 244 | convexHull2d(v0: Vertex, v1: Vertex, v2: Vertex) { 245 | const planeNormal = getPlaneNormal([0, 0, 0], v0.point, v1.point, v2.point) 246 | 247 | // To do the rotation let's use a quaternion 248 | // first let's find a target plane to rotate to e.g. the x-z plane with a normal equal to the y-axis 249 | let basisPlaneNormal: [number, number, number] = [0, 1, 0] 250 | 251 | // Create a quaternion that represents the rotation between normal and the basisPlaneNormal. 252 | const rotation = rotationTo([], planeNormal, basisPlaneNormal) 253 | // Create a vec3 that represents a translation from the plane to the origin. 254 | const translation = scale([], planeNormal, -dot(v0.point, planeNormal)) 255 | // Create a rotation -> translation matrix from a quaternion and a vec3 256 | const matrix = fromRotationTranslation([], rotation, translation) 257 | const transformedVertices = [] 258 | for (const vertex of this.vertices) { 259 | const a = fromValues(vertex.point[0], vertex.point[1], vertex.point[2], 0) 260 | const aP = transformMat4([], a, matrix) 261 | 262 | // Make sure that the y value is close to 0 263 | if (debug.enabled) { 264 | if (aP[1] > this.tolerance) { 265 | debug(`ERROR: point ${aP} has an unexpected y value, it should be less than ${this.tolerance}`) 266 | } 267 | } 268 | transformedVertices.push([aP[0], aP[2]]) 269 | } 270 | 271 | // 2d convex hull. 272 | const hull = monotoneHull(transformedVertices) 273 | 274 | // There's a single face with the indexes of the hull. 275 | const vertices: Vertex[] = [] 276 | for (const i of hull) { 277 | vertices.push(this.vertices[i]) 278 | } 279 | 280 | const face = Face.fromVertices(vertices) 281 | this.faces = [face] 282 | } 283 | 284 | /** 285 | * Computes the extremes of a tetrahedron which will be the initial hull 286 | */ 287 | computeTetrahedronExtremes(): Vertex[] { 288 | const me = this 289 | const min = [] 290 | const max = [] 291 | 292 | // min vertex on the x,y,z directions 293 | const minVertices: Vertex[] = [] 294 | // max vertex on the x,y,z directions 295 | const maxVertices: Vertex[] = [] 296 | 297 | // initially assume that the first vertex is the min/max 298 | for (let i = 0; i < 3; i += 1) { 299 | minVertices[i] = maxVertices[i] = this.vertices[0] 300 | } 301 | // copy the coordinates of the first vertex to min/max 302 | for (let i = 0; i < 3; i += 1) { 303 | min[i] = max[i] = this.vertices[0].point[i] 304 | } 305 | 306 | // compute the min/max vertex on all 6 directions 307 | for (let i = 1; i < this.vertices.length; i += 1) { 308 | const vertex = this.vertices[i] 309 | const point = vertex.point 310 | // update the min coordinates 311 | for (let j = 0; j < 3; j += 1) { 312 | if (point[j] < min[j]) { 313 | min[j] = point[j] 314 | minVertices[j] = vertex 315 | } 316 | } 317 | // update the max coordinates 318 | for (let j = 0; j < 3; j += 1) { 319 | if (point[j] > max[j]) { 320 | max[j] = point[j] 321 | maxVertices[j] = vertex 322 | } 323 | } 324 | } 325 | 326 | // compute epsilon 327 | this.tolerance = 328 | 3 * 329 | Number.EPSILON * 330 | (Math.max(Math.abs(min[0]), Math.abs(max[0])) + 331 | Math.max(Math.abs(min[1]), Math.abs(max[1])) + 332 | Math.max(Math.abs(min[2]), Math.abs(max[2]))) 333 | if (debug.enabled) { 334 | debug('tolerance %d', me.tolerance) 335 | } 336 | 337 | // Find the two vertices with the greatest 1d separation 338 | // (max.x - min.x) 339 | // (max.y - min.y) 340 | // (max.z - min.z) 341 | let maxDistance = 0 342 | let indexMax = 0 343 | for (let i = 0; i < 3; i += 1) { 344 | const distance = maxVertices[i].point[i] - minVertices[i].point[i] 345 | if (distance > maxDistance) { 346 | maxDistance = distance 347 | indexMax = i 348 | } 349 | } 350 | const v0 = minVertices[indexMax] 351 | const v1 = maxVertices[indexMax] 352 | let v2: Vertex, v3: Vertex 353 | 354 | // the next vertex is the one farthest to the line formed by `v0` and `v1` 355 | maxDistance = 0 356 | for (let i = 0; i < this.vertices.length; i += 1) { 357 | const vertex = this.vertices[i] 358 | if (vertex !== v0 && vertex !== v1) { 359 | const distance = pointLineDistance(vertex.point, v0.point, v1.point) 360 | if (distance > maxDistance) { 361 | maxDistance = distance 362 | v2 = vertex 363 | } 364 | } 365 | } 366 | 367 | // the next vertes is the one farthest to the plane `v0`, `v1`, `v2` 368 | // normalize((v2 - v1) x (v0 - v1)) 369 | const normal = getPlaneNormal([0, 0, 0], v0.point, v1.point, v2.point) 370 | // distance from the origin to the plane 371 | const distPO = dot(v0.point, normal) 372 | maxDistance = -1 373 | for (let i = 0; i < this.vertices.length; i += 1) { 374 | const vertex = this.vertices[i] 375 | if (vertex !== v0 && vertex !== v1 && vertex !== v2) { 376 | const distance = Math.abs(dot(normal, vertex.point) - distPO) 377 | if (distance > maxDistance) { 378 | maxDistance = distance 379 | v3 = vertex 380 | } 381 | } 382 | } 383 | 384 | return [v0, v1, v2, v3] 385 | } 386 | 387 | /** 388 | * Compues the initial tetrahedron assigning to its faces all the points that 389 | * are candidates to form part of the hull 390 | */ 391 | createInitialSimplex(v0: Vertex, v1: Vertex, v2: Vertex, v3: Vertex) { 392 | const normal = getPlaneNormal([0, 0, 0], v0.point, v1.point, v2.point) 393 | const distPO = dot(v0.point, normal) 394 | 395 | // initial simplex 396 | // Taken from http://everything2.com/title/How+to+paint+a+tetrahedron 397 | // 398 | // v2 399 | // ,|, 400 | // ,7``\'VA, 401 | // ,7` |, `'VA, 402 | // ,7` `\ `'VA, 403 | // ,7` |, `'VA, 404 | // ,7` `\ `'VA, 405 | // ,7` |, `'VA, 406 | // ,7` `\ ,..ooOOTK` v3 407 | // ,7` |,.ooOOT''` AV 408 | // ,7` ,..ooOOT`\` /7 409 | // ,7` ,..ooOOT''` |, AV 410 | // ,T,..ooOOT''` `\ /7 411 | // v0 `'TTs., |, AV 412 | // `'TTs., `\ /7 413 | // `'TTs., |, AV 414 | // `'TTs., `\ /7 415 | // `'TTs., |, AV 416 | // `'TTs.,\/7 417 | // `'T` 418 | // v1 419 | // 420 | const faces = [] 421 | if (dot(v3.point, normal) - distPO < 0) { 422 | // the face is not able to see the point so `planeNormal` 423 | // is pointing outside the tetrahedron 424 | faces.push( 425 | Face.createTriangle(v0, v1, v2), 426 | Face.createTriangle(v3, v1, v0), 427 | Face.createTriangle(v3, v2, v1), 428 | Face.createTriangle(v3, v0, v2) 429 | ) 430 | 431 | // set the opposite edge 432 | for (let i = 0; i < 3; i += 1) { 433 | const j = (i + 1) % 3 434 | // join face[i] i > 0, with the first face 435 | faces[i + 1].getEdge(2).setOpposite(faces[0].getEdge(j)) 436 | // join face[i] with face[i + 1], 1 <= i <= 3 437 | faces[i + 1].getEdge(1).setOpposite(faces[j + 1].getEdge(0)) 438 | } 439 | } else { 440 | // the face is able to see the point so `planeNormal` 441 | // is pointing inside the tetrahedron 442 | faces.push( 443 | Face.createTriangle(v0, v2, v1), 444 | Face.createTriangle(v3, v0, v1), 445 | Face.createTriangle(v3, v1, v2), 446 | Face.createTriangle(v3, v2, v0) 447 | ) 448 | 449 | // set the opposite edge 450 | for (let i = 0; i < 3; i += 1) { 451 | const j = (i + 1) % 3 452 | // join face[i] i > 0, with the first face 453 | faces[i + 1].getEdge(2).setOpposite(faces[0].getEdge((3 - i) % 3)) 454 | // join face[i] with face[i + 1] 455 | faces[i + 1].getEdge(0).setOpposite(faces[j + 1].getEdge(1)) 456 | } 457 | } 458 | 459 | // the initial hull is the tetrahedron 460 | for (let i = 0; i < 4; i += 1) { 461 | this.faces.push(faces[i]) 462 | } 463 | 464 | // initial assignment of vertices to the faces of the tetrahedron 465 | const vertices = this.vertices 466 | for (let i = 0; i < vertices.length; i += 1) { 467 | const vertex = vertices[i] 468 | if (vertex !== v0 && vertex !== v1 && vertex !== v2 && vertex !== v3) { 469 | let maxDistance = this.tolerance 470 | let maxFace: Face 471 | for (let j = 0; j < 4; j += 1) { 472 | const distance = faces[j].distanceToPlane(vertex.point) 473 | if (distance > maxDistance) { 474 | maxDistance = distance 475 | maxFace = faces[j] 476 | } 477 | } 478 | 479 | if (maxFace) { 480 | this.addVertexToFace(vertex, maxFace) 481 | } 482 | } 483 | } 484 | } 485 | 486 | reindexFaceAndVertices() { 487 | // remove inactive faces 488 | const activeFaces = [] 489 | for (let i = 0; i < this.faces.length; i += 1) { 490 | const face = this.faces[i] 491 | if (face.mark === Mark.Visible) { 492 | activeFaces.push(face) 493 | } 494 | } 495 | this.faces = activeFaces 496 | } 497 | 498 | collectFaces(skipTriangulation: boolean): IFace[] { 499 | const faceIndices: IFace[] = [] 500 | for (let i = 0; i < this.faces.length; i += 1) { 501 | if (this.faces[i].mark !== Mark.Visible) { 502 | throw Error('attempt to include a destroyed face in the hull') 503 | } 504 | const indices = this.faces[i].collectIndices() 505 | if (skipTriangulation) { 506 | faceIndices.push(indices) 507 | } else { 508 | for (let j = 0; j < indices.length - 2; j += 1) { 509 | faceIndices.push([indices[0], indices[j + 1], indices[j + 2]]) 510 | } 511 | } 512 | } 513 | return faceIndices 514 | } 515 | 516 | /** 517 | * Finds the next vertex to make faces with the current hull 518 | * 519 | * - let `face` be the first face existing in the `claimed` vertex list 520 | * - if `face` doesn't exist then return since there're no vertices left 521 | * - otherwise for each `vertex` that face sees find the one furthest away 522 | * from `face` 523 | */ 524 | nextVertexToAdd() { 525 | if (!this.claimed.isEmpty()) { 526 | let eyeVertex: Vertex, vertex: Vertex 527 | let maxDistance = 0 528 | const eyeFace = this.claimed.first().face 529 | for (vertex = eyeFace.outside; vertex && vertex.face === eyeFace; vertex = vertex.next) { 530 | const distance = eyeFace.distanceToPlane(vertex.point) 531 | if (distance > maxDistance) { 532 | maxDistance = distance 533 | eyeVertex = vertex 534 | } 535 | } 536 | return eyeVertex 537 | } 538 | } 539 | 540 | /** 541 | * Computes a chain of half edges in ccw order called the `horizon`, for an 542 | * edge to be part of the horizon it must join a face that can see 543 | * `eyePoint` and a face that cannot see `eyePoint` 544 | * 545 | * @param {number[]} eyePoint - The coordinates of a point 546 | * @param {HalfEdge} crossEdge - The edge used to jump to the current `face` 547 | * @param {Face} face - The current face being tested 548 | * @param {HalfEdge[]} horizon - The edges that form part of the horizon in 549 | * ccw order 550 | */ 551 | computeHorizon(eyePoint: Vec3Like, crossEdge: HalfEdge, face: Face, horizon: HalfEdge[]) { 552 | // moves face's vertices to the `unclaimed` vertex list 553 | this.deleteFaceVertices(face) 554 | 555 | face.mark = Mark.Deleted 556 | 557 | let edge: HalfEdge 558 | if (!crossEdge) { 559 | edge = crossEdge = face.getEdge(0) 560 | } else { 561 | // start from the next edge since `crossEdge` was already analyzed 562 | // (actually `crossEdge.opposite` was the face who called this method 563 | // recursively) 564 | edge = crossEdge.next 565 | } 566 | 567 | // All the faces that are able to see `eyeVertex` are defined as follows 568 | // 569 | // v / 570 | // / <== visible face 571 | // / 572 | // | 573 | // | <== not visible face 574 | // 575 | // dot(v, visible face normal) - visible face offset > this.tolerance 576 | // 577 | do { 578 | const oppositeEdge = edge.opposite 579 | const oppositeFace = oppositeEdge.face 580 | if (oppositeFace.mark === Mark.Visible) { 581 | if (oppositeFace.distanceToPlane(eyePoint) > this.tolerance) { 582 | this.computeHorizon(eyePoint, oppositeEdge, oppositeFace, horizon) 583 | } else { 584 | horizon.push(edge) 585 | } 586 | } 587 | edge = edge.next 588 | } while (edge !== crossEdge) 589 | } 590 | 591 | /** 592 | * Creates a face with the points `eyeVertex.point`, `horizonEdge.tail` and 593 | * `horizonEdge.tail` in ccw order 594 | * 595 | * @param {Vertex} eyeVertex 596 | * @param {HalfEdge} horizonEdge 597 | * @return {HalfEdge} The half edge whose vertex is the eyeVertex 598 | */ 599 | addAdjoiningFace(eyeVertex: Vertex, horizonEdge: HalfEdge): HalfEdge { 600 | // all the half edges are created in ccw order thus the face is always 601 | // pointing outside the hull 602 | // edges: 603 | // 604 | // eyeVertex.point 605 | // / \ 606 | // / \ 607 | // 1 / \ 0 608 | // / \ 609 | // / \ 610 | // / \ 611 | // horizon.tail --- horizon.head 612 | // 2 613 | // 614 | const face = Face.createTriangle(eyeVertex, horizonEdge.tail(), horizonEdge.head()) 615 | this.faces.push(face) 616 | // join face.getEdge(-1) with the horizon's opposite edge 617 | // face.getEdge(-1) = face.getEdge(2) 618 | face.getEdge(-1).setOpposite(horizonEdge.opposite) 619 | return face.getEdge(0) 620 | } 621 | 622 | /** 623 | * Adds horizon.length faces to the hull, each face will be 'linked' with the 624 | * horizon opposite face and the face on the left/right 625 | * 626 | * @param {Vertex} eyeVertex 627 | * @param {HalfEdge[]} horizon - A chain of half edges in ccw order 628 | */ 629 | addNewFaces(eyeVertex: Vertex, horizon: HalfEdge[]) { 630 | this.newFaces = [] 631 | let firstSideEdge: HalfEdge, previousSideEdge: HalfEdge 632 | for (let i = 0; i < horizon.length; i += 1) { 633 | const horizonEdge = horizon[i] 634 | // returns the right side edge 635 | const sideEdge = this.addAdjoiningFace(eyeVertex, horizonEdge) 636 | if (!firstSideEdge) { 637 | firstSideEdge = sideEdge 638 | } else { 639 | // joins face.getEdge(1) with previousFace.getEdge(0) 640 | sideEdge.next.setOpposite(previousSideEdge) 641 | } 642 | this.newFaces.push(sideEdge.face) 643 | previousSideEdge = sideEdge 644 | } 645 | firstSideEdge.next.setOpposite(previousSideEdge) 646 | } 647 | 648 | /** 649 | * Computes the distance from `edge` opposite face's centroid to 650 | * `edge.face` 651 | * 652 | * @param {HalfEdge} edge 653 | */ 654 | oppositeFaceDistance(edge: HalfEdge) { 655 | // - A positive number when the centroid of the opposite face is above the 656 | // face i.e. when the faces are concave 657 | // - A negative number when the centroid of the opposite face is below the 658 | // face i.e. when the faces are convex 659 | return edge.face.distanceToPlane(edge.opposite.face.centroid) 660 | } 661 | 662 | /** 663 | * Merges a face with none/any/all its neighbors according to the strategy 664 | * used 665 | * 666 | * if `mergeType` is MERGE_NON_CONVEX_WRT_LARGER_FACE then the merge will be 667 | * decided based on the face with the larger area, the centroid of the face 668 | * with the smaller area will be checked against the one with the larger area 669 | * to see if it's in the merge range [tolerance, -tolerance] i.e. 670 | * 671 | * dot(centroid smaller face, larger face normal) - larger face offset > -tolerance 672 | * 673 | * Note that the first check (with +tolerance) was done on `computeHorizon` 674 | * 675 | * If the above is not true then the check is done with respect to the smaller 676 | * face i.e. 677 | * 678 | * dot(centroid larger face, smaller face normal) - smaller face offset > -tolerance 679 | * 680 | * If true then it means that two faces are non convex (concave), even if the 681 | * dot(...) - offset value is > 0 (that's the point of doing the merge in the 682 | * first place) 683 | * 684 | * If two faces are concave then the check must also be done on the other face 685 | * but this is done in another merge pass, for this to happen the face is 686 | * marked in a temporal NON_CONVEX state 687 | * 688 | * if `mergeType` is MERGE_NON_CONVEX then two faces will be merged only if 689 | * they pass the following conditions 690 | * 691 | * dot(centroid smaller face, larger face normal) - larger face offset > -tolerance 692 | * dot(centroid larger face, smaller face normal) - smaller face offset > -tolerance 693 | * 694 | * @param {Face} face 695 | * @param {MergeType} mergeType 696 | */ 697 | doAdjacentMerge(face: Face, mergeType: MergeType) { 698 | let edge = face.edge 699 | let convex = true 700 | let it = 0 701 | do { 702 | if (it >= face.nVertices) { 703 | throw Error('merge recursion limit exceeded') 704 | } 705 | const oppositeFace = edge.opposite.face 706 | let merge = false 707 | 708 | // Important notes about the algorithm to merge faces 709 | // 710 | // - Given a vertex `eyeVertex` that will be added to the hull 711 | // all the faces that cannot see `eyeVertex` are defined as follows 712 | // 713 | // dot(v, not visible face normal) - not visible offset < tolerance 714 | // 715 | // - Two faces can be merged when the centroid of one of these faces 716 | // projected to the normal of the other face minus the other face offset 717 | // is in the range [tolerance, -tolerance] 718 | // - Since `face` (given in the input for this method) has passed the 719 | // check above we only have to check the lower bound e.g. 720 | // 721 | // dot(v, not visible face normal) - not visible offset > -tolerance 722 | // 723 | if (mergeType === MergeType.NonConvex) { 724 | if ( 725 | this.oppositeFaceDistance(edge) > -this.tolerance || 726 | this.oppositeFaceDistance(edge.opposite) > -this.tolerance 727 | ) { 728 | merge = true 729 | } 730 | } else { 731 | if (face.area > oppositeFace.area) { 732 | if (this.oppositeFaceDistance(edge) > -this.tolerance) { 733 | merge = true 734 | } else if (this.oppositeFaceDistance(edge.opposite) > -this.tolerance) { 735 | convex = false 736 | } 737 | } else { 738 | if (this.oppositeFaceDistance(edge.opposite) > -this.tolerance) { 739 | merge = true 740 | } else if (this.oppositeFaceDistance(edge) > -this.tolerance) { 741 | convex = false 742 | } 743 | } 744 | } 745 | 746 | if (merge) { 747 | debug('face merge') 748 | // when two faces are merged it might be possible that redundant faces 749 | // are destroyed, in that case move all the visible vertices from the 750 | // destroyed faces to the `unclaimed` vertex list 751 | const discardedFaces = face.mergeAdjacentFaces(edge, []) 752 | for (let i = 0; i < discardedFaces.length; i += 1) { 753 | this.deleteFaceVertices(discardedFaces[i], face) 754 | } 755 | return true 756 | } 757 | 758 | edge = edge.next 759 | it += 1 760 | } while (edge !== face.edge) 761 | if (!convex) { 762 | face.mark = Mark.NonConvex 763 | } 764 | return false 765 | } 766 | 767 | /** 768 | * Adds a vertex to the hull with the following algorithm 769 | * 770 | * - Compute the `horizon` which is a chain of half edges, for an edge to 771 | * belong to this group it must be the edge connecting a face that can 772 | * see `eyeVertex` and a face which cannot see `eyeVertex` 773 | * - All the faces that can see `eyeVertex` have its visible vertices removed 774 | * from the claimed VertexList 775 | * - A new set of faces is created with each edge of the `horizon` and 776 | * `eyeVertex`, each face is connected with the opposite horizon face and 777 | * the face on the left/right 778 | * - The new faces are merged if possible with the opposite horizon face first 779 | * and then the faces on the right/left 780 | * - The vertices removed from all the visible faces are assigned to the new 781 | * faces if possible 782 | * 783 | * @param {Vertex} eyeVertex 784 | */ 785 | addVertexToHull(eyeVertex: Vertex) { 786 | const horizon: HalfEdge[] = [] 787 | 788 | this.unclaimed.clear() 789 | 790 | // remove `eyeVertex` from `eyeVertex.face` so that it can't be added to the 791 | // `unclaimed` vertex list 792 | this.removeVertexFromFace(eyeVertex, eyeVertex.face) 793 | this.computeHorizon(eyeVertex.point, null, eyeVertex.face, horizon) 794 | if (debug.enabled) { 795 | debug( 796 | 'horizon %j', 797 | horizon.map(function (edge) { 798 | return edge.head().index 799 | }) 800 | ) 801 | } 802 | this.addNewFaces(eyeVertex, horizon) 803 | 804 | debug('first merge') 805 | 806 | // first merge pass 807 | // Do the merge with respect to the larger face 808 | for (let i = 0; i < this.newFaces.length; i += 1) { 809 | const face = this.newFaces[i] 810 | if (face.mark === Mark.Visible) { 811 | // eslint-disable-next-line 812 | while (this.doAdjacentMerge(face, MergeType.NonConvexWrtLargerFace)) {} 813 | } 814 | } 815 | 816 | debug('second merge') 817 | 818 | // second merge pass 819 | // Do the merge on non convex faces (a face is marked as non convex in the 820 | // first pass) 821 | for (let i = 0; i < this.newFaces.length; i += 1) { 822 | const face = this.newFaces[i] 823 | if (face.mark === Mark.NonConvex) { 824 | face.mark = Mark.Visible 825 | // eslint-disable-next-line 826 | while (this.doAdjacentMerge(face, MergeType.NonConvexWrtLargerFace)) {} 827 | } 828 | } 829 | 830 | debug('reassigning points to newFaces') 831 | // reassign `unclaimed` vertices to the new faces 832 | this.resolveUnclaimedPoints(this.newFaces) 833 | } 834 | 835 | build(): QuickHull { 836 | let iterations = 0 837 | let eyeVertex: Vertex 838 | const [v0, v1, v2, v3] = this.computeTetrahedronExtremes() 839 | if (this.allPointsBelongToPlane(v0, v1, v2)) { 840 | this.convexHull2d(v0, v1, v2) 841 | return this 842 | } 843 | this.createInitialSimplex(v0, v1, v2, v3) 844 | while ((eyeVertex = this.nextVertexToAdd())) { 845 | iterations += 1 846 | debug(`== iteration ${iterations} ==`) 847 | debug('next vertex to add = %d %j', eyeVertex.index, eyeVertex.point) 848 | this.addVertexToHull(eyeVertex) 849 | debug(`== end iteration ${iterations}`) 850 | } 851 | this.reindexFaceAndVertices() 852 | return this 853 | } 854 | } 855 | -------------------------------------------------------------------------------- /src/Vertex.ts: -------------------------------------------------------------------------------- 1 | import { Vec3Like } from './types' 2 | import { Face } from './Face' 3 | 4 | export class Vertex { 5 | point: Vec3Like 6 | // index in the input array 7 | index: number 8 | // next is a pointer to the next Vertex 9 | next: Vertex | null 10 | // prev is a pointer to the previous Vertex 11 | prev: Vertex | null 12 | // face is the face that's able to see this point 13 | face: Face | null 14 | 15 | constructor(point: Vec3Like, index: number) { 16 | this.point = point 17 | this.index = index 18 | this.next = null 19 | this.prev = null 20 | this.face = null 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/VertexList.ts: -------------------------------------------------------------------------------- 1 | import { Vertex } from './Vertex' 2 | 3 | export class VertexList { 4 | private head: Vertex | null 5 | private tail: Vertex | null 6 | 7 | constructor() { 8 | this.head = null 9 | this.tail = null 10 | } 11 | 12 | clear() { 13 | this.head = this.tail = null 14 | } 15 | 16 | /** 17 | * Inserts a `node` before `target`, it's assumed that 18 | * `target` belongs to this doubly linked list 19 | * 20 | * @param {Vertex} target 21 | * @param {Vertex} node 22 | */ 23 | insertBefore(target: Vertex, node: Vertex) { 24 | node.prev = target.prev 25 | node.next = target 26 | if (!node.prev) { 27 | this.head = node 28 | } else { 29 | node.prev.next = node 30 | } 31 | target.prev = node 32 | } 33 | 34 | /** 35 | * Inserts a `node` after `target`, it's assumed that 36 | * `target` belongs to this doubly linked list 37 | * 38 | * @param {Vertex} target 39 | * @param {Vertex} node 40 | */ 41 | insertAfter(target: Vertex, node: Vertex) { 42 | node.prev = target 43 | node.next = target.next 44 | if (!node.next) { 45 | this.tail = node 46 | } else { 47 | node.next.prev = node 48 | } 49 | target.next = node 50 | } 51 | 52 | /** 53 | * Appends a `node` to the end of this doubly linked list 54 | * Note: `node.next` will be unlinked from `node` 55 | * Note: if `node` is part of another linked list call `addAll` instead 56 | * 57 | * @param {Vertex} node 58 | */ 59 | add(node: Vertex) { 60 | if (!this.head) { 61 | this.head = node 62 | } else { 63 | this.tail.next = node 64 | } 65 | node.prev = this.tail 66 | // since node is the new end it doesn't have a next node 67 | node.next = null 68 | this.tail = node 69 | } 70 | 71 | /** 72 | * Appends a chain of nodes where `node` is the head, 73 | * the difference with `add` is that it correctly sets the position 74 | * of the node list `tail` property 75 | * 76 | * @param {Vertex} node 77 | */ 78 | addAll(node: Vertex) { 79 | if (!this.head) { 80 | this.head = node 81 | } else { 82 | this.tail.next = node 83 | } 84 | node.prev = this.tail 85 | 86 | // find the end of the list 87 | while (node.next) { 88 | node = node.next 89 | } 90 | this.tail = node 91 | } 92 | 93 | /** 94 | * Deletes a `node` from this linked list, it's assumed that `node` is a 95 | * member of this linked list 96 | * 97 | * @param {Vertex} node 98 | */ 99 | remove(node: Vertex) { 100 | if (!node.prev) { 101 | this.head = node.next 102 | } else { 103 | node.prev.next = node.next 104 | } 105 | 106 | if (!node.next) { 107 | this.tail = node.prev 108 | } else { 109 | node.next.prev = node.prev 110 | } 111 | } 112 | 113 | /** 114 | * Removes a chain of nodes whose head is `a` and whose tail is `b`, 115 | * it's assumed that `a` and `b` belong to this list and also that `a` 116 | * comes before `b` in the linked list 117 | * 118 | * @param {Vertex} a 119 | * @param {Vertex} b 120 | */ 121 | removeChain(a: Vertex, b: Vertex) { 122 | if (!a.prev) { 123 | this.head = b.next 124 | } else { 125 | a.prev.next = b.next 126 | } 127 | 128 | if (!b.next) { 129 | this.tail = a.prev 130 | } else { 131 | b.next.prev = a.prev 132 | } 133 | } 134 | 135 | first() { 136 | return this.head 137 | } 138 | 139 | isEmpty() { 140 | return !this.head 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/debug.ts: -------------------------------------------------------------------------------- 1 | // debug replaces the module debug in prod. 2 | export default function debug() { 3 | function innerDebugger() {} 4 | innerDebugger.enabled = false 5 | return innerDebugger 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import getPlaneNormal from 'get-plane-normal' 2 | 3 | import { QuickHull, QuickHullOptions } from './QuickHull' 4 | import { Face, Vec3Like } from './types' 5 | 6 | export type Point = Vec3Like 7 | export { Vec3Like, Face, QuickHullOptions, QuickHull } 8 | 9 | export default function runner(points: Array, options: QuickHullOptions = {}): Face[] { 10 | const instance = new QuickHull(points) 11 | instance.build() 12 | return instance.collectFaces(options.skipTriangulation) 13 | } 14 | 15 | /** 16 | * Checks if a point is inside the convex hull. 17 | * 18 | * @param {Point} point - The point to check. 19 | * @param {Array} points - The points used in the space where the 20 | * convex hull is defined. 21 | * @param {Array} faces - The faces of the convex hull. 22 | */ 23 | export function isPointInsideHull(point: Vec3Like, points: Array, faces: Array) { 24 | for (let i = 0; i < faces.length; i++) { 25 | const face = faces[i] 26 | const a = points[face[0]] 27 | const b = points[face[1]] 28 | const c = points[face[2]] 29 | 30 | // Algorithm: 31 | // 1. Get the normal of the face. 32 | // 2. Get the vector from the point to the first vertex of the face. 33 | // 3. Calculate the dot product of the normal and the vector. 34 | // 4. If the dot product is positive, the point is outside the face. 35 | 36 | const planeNormal = getPlaneNormal(new Float32Array(3), a, b, c) 37 | 38 | // Get the point with respect to the first vertex of the face. 39 | const pointAbsA = [point[0] - a[0], point[1] - a[1], point[2] - a[2]] 40 | 41 | const dotProduct = planeNormal[0] * pointAbsA[0] + planeNormal[1] * pointAbsA[1] + planeNormal[2] * pointAbsA[2] 42 | 43 | if (dotProduct > 0) { 44 | return false 45 | } 46 | } 47 | return true 48 | } 49 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | type FloatArray = Float32Array | Float64Array 2 | // Vec3Like represents a 3D coordinate. 3 | export type Vec3Like = [number, number, number] | FloatArray 4 | 5 | // Face represents a 3D face, the values are indices from the input array. 6 | export type Face = number[] 7 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | // import { Vec3Like } from 'gl-matrix/vec3' 2 | 3 | declare module 'debug' { 4 | export default function debug(args: any): any 5 | } 6 | 7 | type FloatArray = Float32Array | Float64Array 8 | type Vec3Like = [number, number, number] | FloatArray 9 | 10 | declare module 'point-line-distance' { 11 | export default function pointLineDistance(point: Vec3Like, a: Vec3Like, b: Vec3Like): number 12 | } 13 | 14 | declare module 'get-plane-normal' { 15 | export default function getPlaneNormal(out: Vec3Like, a: Vec3Like, b: Vec3Like, c: Vec3Like): Vec3Like 16 | } 17 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from '@jest/globals' 2 | import assert from 'assert' 3 | import dot from 'gl-vec3/dot' 4 | import cross from 'gl-vec3/cross' 5 | import subtract from 'gl-vec3/subtract' 6 | import length from 'gl-vec3/length' 7 | import getPlaneNormal from 'get-plane-normal' 8 | 9 | import qh, { isPointInsideHull, QuickHull, Face, Vec3Like } from '../src/' 10 | 11 | const EPS = 1e-6 12 | function equalEps(a: number, b: number) { 13 | const assertion = Math.abs(a - b) < EPS 14 | expect(assertion).toBe(true) 15 | } 16 | 17 | const cube: Vec3Like[] = [ 18 | [0, 0, 0], 19 | [1, 0, 0], 20 | [0, 1, 0], 21 | [1, 1, 0], 22 | [0, 0, 1], 23 | [1, 0, 1], 24 | [0, 1, 1], 25 | [1, 1, 1] 26 | ] 27 | 28 | const tetrahedron: Vec3Like[] = [ 29 | [-2, 0, 0], 30 | [2, 0, 0], 31 | [0, 0, 1], 32 | [0, 0.5, 0] 33 | ] 34 | 35 | function isConvexHull(points: Vec3Like[], faces: Face[]) { 36 | const n = points.length 37 | let nError = 0 38 | for (let i = 0; i < faces.length; i += 1) { 39 | const normal = getPlaneNormal([0, 0, 0], points[faces[i][0]], points[faces[i][1]], points[faces[i][2]]) 40 | const offset = dot(normal, points[faces[i][0]]) 41 | for (let j = 0; j < n; j += 1) { 42 | if (faces[i].indexOf(j) === -1) { 43 | const aboveFace = dot(points[j], normal) > offset + EPS 44 | if (aboveFace) { 45 | console.log('points', points) 46 | console.log('face %j with index %d', faces[i], j) 47 | console.log('%d should be less than %d', dot(points[j], normal), offset) 48 | } 49 | nError += Number(aboveFace) 50 | } 51 | } 52 | } 53 | return nError === 0 54 | } 55 | 56 | function faceShift(f: Face) { 57 | const t = f[0] 58 | for (let i = 0; i < f.length - 1; i += 1) { 59 | f[i] = f[i + 1] 60 | } 61 | f[f.length - 1] = t 62 | } 63 | 64 | function equalShifted(f1: Face, f2: Face) { 65 | let equals = 0 66 | // the length of f1/f2 is the same, checked on equalIndexes 67 | for (let i = 0; i < f2.length; i += 1) { 68 | let singleEq = 0 69 | for (let j = 0; j < f2.length; j += 1) { 70 | singleEq += Number(f1[j] === f2[j]) 71 | } 72 | if (singleEq === f2.length) { 73 | equals += 1 74 | } 75 | faceShift(f2) 76 | } 77 | assert(equals <= 1) 78 | return !!equals 79 | } 80 | 81 | function equalIndexes(f1: Face[], f2: Face[]) { 82 | expect(f1.length).toEqual(f2.length) 83 | const f1tof2 = [] 84 | for (let i = 0; i < f1.length; i += 1) { 85 | for (let j = 0; j < f2.length; j += 1) { 86 | const eq = equalShifted(f1[i], f2[j]) 87 | if (eq) { 88 | assert(typeof f1tof2[i] === 'undefined') 89 | // @ts-ignore 90 | f1tof2[i] = j 91 | } 92 | } 93 | } 94 | for (let i = 0; i < f1.length; i += 1) { 95 | if (f1tof2[i] === undefined) { 96 | console.error(f1) 97 | console.error('face %d does not exist', i) 98 | } 99 | assert(f1tof2[i] >= 0) 100 | assert(typeof f1tof2[i] === 'number') 101 | } 102 | expect(f1tof2.length).toEqual(f2.length) 103 | } 104 | 105 | describe('QuickHull', () => { 106 | it('should have a valid constructor', function () { 107 | const instance = new QuickHull(tetrahedron) 108 | expect(instance.tolerance).toBe(-1) 109 | }) 110 | 111 | it('should throw when input is not an array', function () { 112 | expect(function () { 113 | const instance = new QuickHull() 114 | expect(instance.tolerance).toBe(-1) 115 | }).toThrow() 116 | }) 117 | 118 | it('should create an initial simplex', () => { 119 | const instance = new QuickHull(tetrahedron) 120 | const p = tetrahedron 121 | 122 | function area(p1: Vec3Like, p2: Vec3Like, p3: Vec3Like) { 123 | const c = cross( 124 | [], 125 | // @ts-ignore 126 | subtract([], p2, p1), 127 | // @ts-ignore 128 | subtract([], p3, p1) 129 | ) 130 | // @ts-ignore 131 | return length(c) 132 | } 133 | 134 | const [v0, v1, v2, v3] = instance.computeTetrahedronExtremes() 135 | instance.createInitialSimplex(v0, v1, v2, v3) 136 | expect(instance.faces.length).toBe(4) 137 | // areas (note that the area for qh is the area of the paralellogram) 138 | equalEps(instance.faces[0].area, area(p[0], p[1], p[2])) 139 | equalEps(instance.faces[0].area, 4 * 1) 140 | equalEps(instance.faces[1].area, area(p[0], p[1], p[3])) 141 | equalEps(instance.faces[1].area, 4 * 0.5) 142 | equalEps(instance.faces[2].area, area(p[1], p[2], p[3])) 143 | equalEps(instance.faces[3].area, area(p[0], p[2], p[3])) 144 | 145 | // centroids 146 | expect(instance.faces[0].centroid).toEqual([0, 0, 1 / 3]) 147 | expect(instance.faces[1].centroid).toEqual([0, 0.5 / 3, 0]) 148 | expect(instance.faces[2].centroid).toEqual([2 / 3, 0.5 / 3, 1 / 3]) 149 | expect(instance.faces[3].centroid).toEqual([-2 / 3, 0.5 / 3, 1 / 3]) 150 | }) 151 | 152 | it('should compute the next vertex to add', function () { 153 | const p: Vec3Like[] = [ 154 | [-100, 0, 0], 155 | [100, 0, 0], 156 | [0, 0, 100], 157 | [0, 50, 0], 158 | 159 | [0, -1, 0], 160 | [0, 5, 0], 161 | [0, -3, 0] 162 | ] 163 | const instance = new QuickHull(p) 164 | const [v0, v1, v2, v3] = instance.computeTetrahedronExtremes() 165 | instance.createInitialSimplex(v0, v1, v2, v3) 166 | // @ts-ignore Guaranteed to not be null because of the input. 167 | expect(instance.nextVertexToAdd().point).toEqual([0, -3, 0]) 168 | }) 169 | 170 | it('should have a method which creates the instance/builds the hull', function () { 171 | const hull = qh(tetrahedron) 172 | expect(Array.isArray(hull)).toBe(true) 173 | expect(() => { 174 | hull.forEach((face) => { 175 | face.forEach((index) => { 176 | assert(index >= 0 && index <= 3) 177 | }) 178 | }) 179 | }).not.toThrow() 180 | }) 181 | 182 | it('case: tetrahedron', function () { 183 | const points: Vec3Like[] = [ 184 | [0, 1, 0], 185 | [1, -1, 1], 186 | [-1, -1, 1], 187 | [0, -1, -1] 188 | ] 189 | equalIndexes(qh(points), [ 190 | [0, 2, 1], 191 | [0, 3, 2], 192 | [0, 1, 3], 193 | [1, 2, 3] 194 | ]) 195 | }) 196 | 197 | it('case: box (without triangulation)', function () { 198 | const points: Vec3Like[] = [ 199 | [0, 0, 0], 200 | [1, 0, 0], 201 | [0, 1, 0], 202 | [0, 0, 1], 203 | [1, 1, 0], 204 | [1, 0, 1], 205 | [0, 1, 1], 206 | [1, 1, 1] 207 | ] 208 | const faces = qh(points, { skipTriangulation: true }) 209 | expect(faces.length).toBe(6) 210 | equalIndexes(faces, [ 211 | [6, 2, 0, 3], 212 | [1, 4, 7, 5], 213 | [6, 7, 4, 2], 214 | [3, 0, 1, 5], 215 | [5, 7, 6, 3], 216 | [0, 2, 4, 1] 217 | ]) 218 | }) 219 | 220 | it('case: box (with triangulation)', function () { 221 | const points: Vec3Like[] = [ 222 | [0, 0, 0], 223 | [1, 0, 0], 224 | [0, 1, 0], 225 | [0, 0, 1], 226 | [1, 1, 0], 227 | [1, 0, 1], 228 | [0, 1, 1], 229 | [1, 1, 1] 230 | ] 231 | const faces = qh(points) 232 | expect(faces.length).toBe(12) 233 | }) 234 | 235 | it('case: box (without triangulation, additional points inside)', function () { 236 | const points: Vec3Like[] = [ 237 | [0, 0, 0], 238 | [1, 0, 0], 239 | [0, 1, 0], 240 | [0, 0, 1], 241 | [1, 1, 0], 242 | [1, 0, 1], 243 | [0, 1, 1], 244 | [1, 1, 1] 245 | ] 246 | const padding = 0.000001 247 | for (let i = 0; i < 1000; i += 1) { 248 | points.push([ 249 | padding + Math.random() * (1 - padding), 250 | padding + Math.random() * (1 - padding), 251 | padding + Math.random() * (1 - padding) 252 | ]) 253 | } 254 | const faces = qh(points, { skipTriangulation: true }) 255 | expect(faces.length).toBe(6) 256 | equalIndexes(faces, [ 257 | [6, 2, 0, 3], 258 | [1, 4, 7, 5], 259 | [6, 7, 4, 2], 260 | [3, 0, 1, 5], 261 | [5, 7, 6, 3], 262 | [0, 2, 4, 1] 263 | ]) 264 | }) 265 | 266 | it('case: octahedron', function () { 267 | const points: Vec3Like[] = [ 268 | [1, 0, 0], 269 | [0, 1, 0], 270 | [0, 0, 1], 271 | [-1, 0, 0], 272 | [0, -1, 0], 273 | [0, 0, -1] 274 | ] 275 | equalIndexes(qh(points), [ 276 | [0, 1, 2], 277 | [0, 2, 4], 278 | [0, 5, 1], 279 | [0, 4, 5], 280 | [3, 2, 1], 281 | [3, 1, 5], 282 | [3, 4, 2], 283 | [3, 5, 4] 284 | ]) 285 | }) 286 | 287 | it('predefined set of points #1', function () { 288 | const points: Vec3Like[] = [ 289 | [104, 216, 53], 290 | [104, 217, 52], 291 | [105, 216, 52], 292 | [88, 187, 43], 293 | [89, 187, 44], 294 | [89, 188, 43], 295 | [90, 187, 43] 296 | ] 297 | const faces = qh(points) 298 | expect(isConvexHull(points, faces)).toBe(true) 299 | }) 300 | 301 | it('predefined set of points #2', function () { 302 | const points: Vec3Like[] = [ 303 | [-0.8592737372964621, 83.55000647716224, 99.76234347559512], 304 | [1.525216130539775, 82.31873814947903, 27.226063096895814], 305 | [-71.64689642377198, -9.807108994573355, -20.06765645928681], 306 | [-83.98330193012953, -24.196470947936177, 45.60143379494548], 307 | [58.33653616718948, -15.815680427476764, 15.342222386971116], 308 | [-47.025314485654235, 97.0465809572488, -65.528974076733], 309 | [18.024734454229474, -43.655246682465076, -82.13481092825532], 310 | [-37.32072818093002, 1.8377598840743303, -12.133228313177824], 311 | [-92.33389408327639, 5.605767108500004, -13.743493286892772], 312 | [64.9183395318687, 52.24619274958968, -61.14645302295685] 313 | ] 314 | const faces = qh(points) 315 | expect(isConvexHull(points, faces)).toBe(true) 316 | }) 317 | 318 | it('predefined set of points #3', function () { 319 | const points = require('./issue3.json') 320 | const faces = qh(points) 321 | expect(isConvexHull(points, faces)).toBe(true) 322 | }) 323 | 324 | it('predefined set of points (dup vertices) #38', function () { 325 | let faces: Array 326 | const points = require('./issue38.json') 327 | faces = qh(points, { skipTriangulation: true }) 328 | expect(isConvexHull(points, faces)).toBe(true) 329 | expect(faces.length).toBe(6) 330 | 331 | function translate(points: Array, translation: Vec3Like): Array { 332 | return points.map((point) => [point[0] + translation[0], point[1] + translation[1], point[2] + translation[2]]) 333 | } 334 | const translatedPoints = translate(points, [0, 0, 5]) 335 | const translatedfaces = qh(translatedPoints, { skipTriangulation: true }) 336 | expect(isConvexHull(translatedPoints, faces)).toBe(true) 337 | expect(translatedfaces.length).toBe(6) 338 | }) 339 | 340 | it('point inside hull', function () { 341 | const points: Vec3Like[] = [ 342 | [0, 0, 0], 343 | [1, 0, 0], 344 | [0, 1, 0], 345 | [0, 0, 1], 346 | [1, 1, 0], 347 | [1, 0, 1], 348 | [0, 1, 1], 349 | [1, 1, 1] 350 | ] 351 | const faces = qh(points) 352 | expect(faces.length).toBe(12) 353 | // point is inside the hull 354 | expect(isPointInsideHull([0.5, 0.5, 0.5], points, faces)).toBe(true) 355 | // point is part of the hull 356 | expect(isPointInsideHull([1, 1, 1], points, faces)).toBe(true) 357 | // point is outside the hull 358 | expect(isPointInsideHull([1, 1, 1.0000001], points, faces)).toBe(false) 359 | expect(isPointInsideHull([0, 0, -0.0000001], points, faces)).toBe(false) 360 | }) 361 | 362 | describe('degenerate cases', function () { 363 | it("all points don't belong to a plane", function () { 364 | const instance = new QuickHull(cube) 365 | const [v0, v1, v2] = instance.computeTetrahedronExtremes() 366 | expect(instance.allPointsBelongToPlane(v0, v1, v2)).toBe(false) 367 | }) 368 | 369 | it('all points belong to plane (parallel to xy)', function () { 370 | const points: Vec3Like[] = [ 371 | [1, 1, 0], 372 | [2, 4, 0], 373 | [3, 5, 0], 374 | [5, 5, 0], 375 | [10, 10, 0] 376 | ] 377 | const instance = new QuickHull(points) 378 | const [v0, v1, v2] = instance.computeTetrahedronExtremes() 379 | expect(instance.allPointsBelongToPlane(v0, v1, v2)).toBe(true) 380 | }) 381 | 382 | it('all points belong to plane (skewed plane)', function () { 383 | const points: Vec3Like[] = [ 384 | [-8, 1, 0], 385 | [-7, 4, 0], 386 | [-6, 5, -2], 387 | [-7, 0, -4], 388 | [-8, 0, -1] 389 | ] 390 | const instance = new QuickHull(points) 391 | const [v0, v1, v2] = instance.computeTetrahedronExtremes() 392 | expect(instance.allPointsBelongToPlane(v0, v1, v2)).toBe(true) 393 | }) 394 | 395 | it('should compute a 2d convex hull when all points belong to plane (parallel to xy)', function () { 396 | const points: Vec3Like[] = [ 397 | [1, 1, 0], 398 | [10, 1, 0], 399 | [1, 10, 0], 400 | [2, 3, 0], 401 | [3, 4, 0], 402 | [9, 9, 0], 403 | [10, 4, 0], 404 | [4, 10, 0], 405 | [5, 8, 0], 406 | [10, 10, 0] 407 | ] 408 | const instance = new QuickHull(points).build() 409 | const faces = instance.collectFaces(true) 410 | expect(faces.length).toBe(1) 411 | expect(faces[0].length).toBe(4) 412 | for (const p of points) { 413 | expect(isPointInsideHull(p, points, faces)).toBe(true) 414 | } 415 | }) 416 | 417 | it('should compute a 2d convex hull when all points belong to plane (skewed)', function () { 418 | const points: Vec3Like[] = [ 419 | [-8, 1, 0], 420 | [-7, 4, 0], 421 | [-6, 5, -2], 422 | [-7, 0, -4], 423 | [-8, 0, -1] 424 | ] 425 | const instance = new QuickHull(points).build() 426 | const faces = instance.collectFaces(true) 427 | expect(faces.length).toBe(1) 428 | expect(faces[0].length).toBe(5) 429 | for (const p of points) { 430 | expect(isPointInsideHull(p, points, faces)).toBe(true) 431 | } 432 | }) 433 | 434 | it('predefined set of points #5 (with z=0)', function () { 435 | const points = require('./issue5.json') 436 | const faces = qh(points) 437 | expect(isConvexHull(points, faces)).toBe(true) 438 | for (const p of points) { 439 | expect(isPointInsideHull(p, points, faces)).toBe(true) 440 | } 441 | }) 442 | 443 | it('predefined set of points #5 (with z=0, no triangulation)', function () { 444 | const points = require('./issue5.json') 445 | const faces = qh(points, { skipTriangulation: true }) 446 | expect(faces.length).toBe(1) 447 | expect(isConvexHull(points, faces)).toBe(true) 448 | for (const p of points) { 449 | expect(isPointInsideHull(p, points, faces)).toBe(true) 450 | } 451 | }) 452 | }) 453 | }) 454 | -------------------------------------------------------------------------------- /test/issue3.json: -------------------------------------------------------------------------------- 1 | [ [ 106, 226, 57 ], 2 | [ 107, 224, 58 ], 3 | [ 107, 224, 56 ], 4 | [ 107, 225, 59 ], 5 | [ 107, 225, 55 ], 6 | [ 107, 226, 59 ], 7 | [ 107, 226, 55 ], 8 | [ 107, 227, 59 ], 9 | [ 107, 227, 55 ], 10 | [ 107, 228, 58 ], 11 | [ 107, 228, 56 ], 12 | [ 108, 224, 59 ], 13 | [ 108, 224, 55 ], 14 | [ 108, 225, 59 ], 15 | [ 108, 225, 55 ], 16 | [ 108, 226, 59 ], 17 | [ 108, 226, 55 ], 18 | [ 108, 227, 59 ], 19 | [ 108, 227, 55 ], 20 | [ 108, 228, 59 ], 21 | [ 108, 228, 55 ], 22 | [ 109, 223, 57 ], 23 | [ 109, 224, 59 ], 24 | [ 109, 224, 55 ], 25 | [ 109, 225, 59 ], 26 | [ 109, 225, 55 ], 27 | [ 109, 226, 60 ], 28 | [ 109, 226, 54 ], 29 | [ 109, 227, 59 ], 30 | [ 109, 227, 55 ], 31 | [ 109, 228, 59 ], 32 | [ 109, 228, 55 ], 33 | [ 109, 229, 57 ], 34 | [ 110, 224, 59 ], 35 | [ 110, 224, 55 ], 36 | [ 110, 225, 59 ], 37 | [ 110, 225, 55 ], 38 | [ 110, 226, 59 ], 39 | [ 110, 226, 55 ], 40 | [ 110, 227, 59 ], 41 | [ 110, 227, 55 ], 42 | [ 110, 228, 59 ], 43 | [ 110, 228, 55 ], 44 | [ 111, 224, 58 ], 45 | [ 111, 224, 56 ], 46 | [ 111, 225, 59 ], 47 | [ 111, 225, 55 ], 48 | [ 111, 226, 59 ], 49 | [ 111, 226, 55 ], 50 | [ 111, 227, 59 ], 51 | [ 111, 227, 55 ], 52 | [ 111, 228, 58 ], 53 | [ 111, 228, 56 ], 54 | [ 112, 226, 57 ], 55 | [ 102, 227, 52 ], 56 | [ 103, 225, 53 ], 57 | [ 103, 225, 51 ], 58 | [ 103, 226, 54 ], 59 | [ 103, 226, 50 ], 60 | [ 103, 227, 54 ], 61 | [ 103, 227, 50 ], 62 | [ 103, 228, 54 ], 63 | [ 103, 228, 50 ], 64 | [ 103, 229, 53 ], 65 | [ 103, 229, 51 ], 66 | [ 104, 225, 54 ], 67 | [ 104, 225, 50 ], 68 | [ 104, 226, 54 ], 69 | [ 104, 226, 50 ], 70 | [ 104, 227, 54 ], 71 | [ 104, 227, 50 ], 72 | [ 104, 228, 54 ], 73 | [ 104, 228, 50 ], 74 | [ 104, 229, 54 ], 75 | [ 104, 229, 50 ], 76 | [ 105, 224, 52 ], 77 | [ 105, 225, 54 ], 78 | [ 105, 225, 50 ], 79 | [ 105, 226, 54 ], 80 | [ 105, 226, 50 ], 81 | [ 105, 227, 55 ], 82 | [ 105, 227, 49 ], 83 | [ 105, 228, 54 ], 84 | [ 105, 228, 50 ], 85 | [ 105, 229, 54 ], 86 | [ 105, 229, 50 ], 87 | [ 105, 230, 52 ], 88 | [ 106, 225, 54 ], 89 | [ 106, 225, 50 ], 90 | [ 106, 226, 54 ], 91 | [ 106, 226, 50 ], 92 | [ 106, 227, 54 ], 93 | [ 106, 227, 50 ], 94 | [ 106, 228, 54 ], 95 | [ 106, 228, 50 ], 96 | [ 106, 229, 54 ], 97 | [ 106, 229, 50 ], 98 | [ 107, 225, 53 ], 99 | [ 107, 225, 51 ], 100 | [ 107, 226, 54 ], 101 | [ 107, 226, 50 ], 102 | [ 107, 227, 54 ], 103 | [ 107, 227, 50 ], 104 | [ 107, 228, 54 ], 105 | [ 107, 228, 50 ], 106 | [ 107, 229, 53 ], 107 | [ 107, 229, 51 ], 108 | [ 108, 227, 52 ], 109 | [ 103, 228, 53 ], 110 | [ 104, 226, 52 ], 111 | [ 104, 227, 55 ], 112 | [ 104, 227, 51 ], 113 | [ 104, 228, 55 ], 114 | [ 104, 228, 51 ], 115 | [ 104, 229, 55 ], 116 | [ 104, 229, 51 ], 117 | [ 104, 230, 54 ], 118 | [ 104, 230, 52 ], 119 | [ 105, 226, 55 ], 120 | [ 105, 226, 51 ], 121 | [ 105, 227, 51 ], 122 | [ 105, 228, 55 ], 123 | [ 105, 228, 51 ], 124 | [ 105, 229, 55 ], 125 | [ 105, 229, 51 ], 126 | [ 105, 230, 55 ], 127 | [ 105, 230, 51 ], 128 | [ 106, 225, 53 ], 129 | [ 106, 226, 55 ], 130 | [ 106, 226, 51 ], 131 | [ 106, 227, 55 ], 132 | [ 106, 227, 51 ], 133 | [ 106, 228, 56 ], 134 | [ 106, 229, 55 ], 135 | [ 106, 229, 51 ], 136 | [ 106, 230, 55 ], 137 | [ 106, 230, 51 ], 138 | [ 106, 231, 53 ], 139 | [ 107, 226, 51 ], 140 | [ 107, 227, 51 ], 141 | [ 107, 228, 55 ], 142 | [ 107, 228, 51 ], 143 | [ 107, 229, 55 ], 144 | [ 107, 230, 55 ], 145 | [ 107, 230, 51 ], 146 | [ 108, 226, 54 ], 147 | [ 108, 226, 52 ], 148 | [ 108, 227, 51 ], 149 | [ 108, 228, 51 ], 150 | [ 108, 229, 55 ], 151 | [ 108, 229, 51 ], 152 | [ 108, 230, 54 ], 153 | [ 108, 230, 52 ], 154 | [ 109, 228, 53 ], 155 | [ 102, 227, 49 ], 156 | [ 103, 225, 50 ], 157 | [ 103, 225, 48 ], 158 | [ 103, 226, 51 ], 159 | [ 103, 226, 47 ], 160 | [ 103, 227, 51 ], 161 | [ 103, 227, 47 ], 162 | [ 103, 228, 51 ], 163 | [ 103, 228, 47 ], 164 | [ 103, 229, 50 ], 165 | [ 103, 229, 48 ], 166 | [ 104, 225, 51 ], 167 | [ 104, 225, 47 ], 168 | [ 104, 226, 51 ], 169 | [ 104, 226, 47 ], 170 | [ 104, 227, 47 ], 171 | [ 104, 228, 47 ], 172 | [ 104, 229, 47 ], 173 | [ 105, 224, 49 ], 174 | [ 105, 225, 51 ], 175 | [ 105, 225, 47 ], 176 | [ 105, 226, 47 ], 177 | [ 105, 227, 52 ], 178 | [ 105, 227, 46 ], 179 | [ 105, 228, 47 ], 180 | [ 105, 229, 47 ], 181 | [ 105, 230, 49 ], 182 | [ 106, 225, 51 ], 183 | [ 106, 225, 47 ], 184 | [ 106, 226, 47 ], 185 | [ 106, 227, 47 ], 186 | [ 106, 228, 51 ], 187 | [ 106, 228, 47 ], 188 | [ 106, 229, 47 ], 189 | [ 107, 225, 50 ], 190 | [ 107, 225, 48 ], 191 | [ 107, 226, 47 ], 192 | [ 107, 227, 47 ], 193 | [ 107, 228, 47 ], 194 | [ 107, 229, 50 ], 195 | [ 107, 229, 48 ], 196 | [ 108, 227, 49 ], 197 | [ 106, 227, 53 ], 198 | [ 107, 225, 54 ], 199 | [ 107, 225, 52 ], 200 | [ 107, 229, 54 ], 201 | [ 107, 229, 52 ], 202 | [ 108, 225, 51 ], 203 | [ 108, 226, 51 ], 204 | [ 109, 224, 53 ], 205 | [ 109, 225, 51 ], 206 | [ 109, 226, 55 ], 207 | [ 109, 226, 51 ], 208 | [ 109, 227, 56 ], 209 | [ 109, 227, 50 ], 210 | [ 109, 228, 51 ], 211 | [ 109, 229, 55 ], 212 | [ 109, 229, 51 ], 213 | [ 109, 230, 53 ], 214 | [ 110, 225, 51 ], 215 | [ 110, 226, 51 ], 216 | [ 110, 227, 51 ], 217 | [ 110, 228, 51 ], 218 | [ 110, 229, 55 ], 219 | [ 110, 229, 51 ], 220 | [ 111, 225, 54 ], 221 | [ 111, 225, 52 ], 222 | [ 111, 226, 51 ], 223 | [ 111, 227, 51 ], 224 | [ 111, 228, 55 ], 225 | [ 111, 228, 51 ], 226 | [ 111, 229, 54 ], 227 | [ 111, 229, 52 ], 228 | [ 112, 227, 53 ], 229 | [ 108, 228, 56 ], 230 | [ 109, 226, 57 ], 231 | [ 109, 227, 58 ], 232 | [ 109, 227, 54 ], 233 | [ 109, 228, 58 ], 234 | [ 109, 228, 54 ], 235 | [ 109, 229, 58 ], 236 | [ 109, 229, 54 ], 237 | [ 109, 230, 57 ], 238 | [ 109, 230, 55 ], 239 | [ 110, 226, 58 ], 240 | [ 110, 226, 54 ], 241 | [ 110, 227, 58 ], 242 | [ 110, 227, 54 ], 243 | [ 110, 228, 58 ], 244 | [ 110, 228, 54 ], 245 | [ 110, 229, 58 ], 246 | [ 110, 229, 54 ], 247 | [ 110, 230, 58 ], 248 | [ 110, 230, 54 ], 249 | [ 111, 225, 56 ], 250 | [ 111, 226, 58 ], 251 | [ 111, 226, 54 ], 252 | [ 111, 227, 58 ], 253 | [ 111, 227, 54 ], 254 | [ 111, 228, 59 ], 255 | [ 111, 228, 53 ], 256 | [ 111, 229, 58 ], 257 | [ 111, 230, 58 ], 258 | [ 111, 230, 54 ], 259 | [ 111, 231, 56 ], 260 | [ 112, 226, 58 ], 261 | [ 112, 226, 54 ], 262 | [ 112, 227, 58 ], 263 | [ 112, 227, 54 ], 264 | [ 112, 228, 58 ], 265 | [ 112, 228, 54 ], 266 | [ 112, 229, 58 ], 267 | [ 112, 229, 54 ], 268 | [ 112, 230, 58 ], 269 | [ 112, 230, 54 ], 270 | [ 113, 226, 57 ], 271 | [ 113, 226, 55 ], 272 | [ 113, 227, 58 ], 273 | [ 113, 227, 54 ], 274 | [ 113, 228, 58 ], 275 | [ 113, 228, 54 ], 276 | [ 113, 229, 58 ], 277 | [ 113, 229, 54 ], 278 | [ 113, 230, 57 ], 279 | [ 113, 230, 55 ], 280 | [ 114, 228, 56 ], 281 | [ 107, 225, 56 ], 282 | [ 107, 226, 57 ], 283 | [ 107, 226, 53 ], 284 | [ 107, 227, 57 ], 285 | [ 107, 227, 53 ], 286 | [ 107, 228, 57 ], 287 | [ 107, 228, 53 ], 288 | [ 107, 229, 56 ], 289 | [ 108, 225, 57 ], 290 | [ 108, 225, 53 ], 291 | [ 108, 226, 57 ], 292 | [ 108, 226, 53 ], 293 | [ 108, 227, 57 ], 294 | [ 108, 227, 53 ], 295 | [ 108, 228, 57 ], 296 | [ 108, 228, 53 ], 297 | [ 108, 229, 57 ], 298 | [ 108, 229, 53 ], 299 | [ 109, 225, 57 ], 300 | [ 109, 225, 53 ], 301 | [ 109, 226, 53 ], 302 | [ 109, 227, 52 ], 303 | [ 109, 228, 57 ], 304 | [ 109, 229, 53 ], 305 | [ 110, 225, 57 ], 306 | [ 110, 225, 53 ], 307 | [ 110, 226, 57 ], 308 | [ 110, 226, 53 ], 309 | [ 110, 227, 57 ], 310 | [ 110, 227, 53 ], 311 | [ 110, 228, 57 ], 312 | [ 110, 228, 53 ], 313 | [ 110, 229, 57 ], 314 | [ 110, 229, 53 ], 315 | [ 111, 226, 57 ], 316 | [ 111, 226, 53 ], 317 | [ 111, 227, 57 ], 318 | [ 111, 227, 53 ], 319 | [ 111, 228, 57 ], 320 | [ 111, 229, 56 ], 321 | [ 112, 227, 55 ], 322 | [ 108, 228, 54 ], 323 | [ 109, 228, 56 ], 324 | [ 109, 228, 52 ], 325 | [ 109, 229, 56 ], 326 | [ 109, 229, 52 ], 327 | [ 110, 226, 56 ], 328 | [ 110, 226, 52 ], 329 | [ 110, 227, 56 ], 330 | [ 110, 227, 52 ], 331 | [ 110, 228, 56 ], 332 | [ 110, 228, 52 ], 333 | [ 110, 229, 56 ], 334 | [ 110, 229, 52 ], 335 | [ 110, 230, 56 ], 336 | [ 110, 230, 52 ], 337 | [ 111, 226, 56 ], 338 | [ 111, 226, 52 ], 339 | [ 111, 227, 56 ], 340 | [ 111, 227, 52 ], 341 | [ 111, 230, 56 ], 342 | [ 111, 230, 52 ], 343 | [ 111, 231, 54 ], 344 | [ 112, 226, 56 ], 345 | [ 112, 226, 52 ], 346 | [ 112, 227, 56 ], 347 | [ 112, 227, 52 ], 348 | [ 112, 228, 56 ], 349 | [ 112, 228, 52 ], 350 | [ 112, 229, 56 ], 351 | [ 112, 229, 52 ], 352 | [ 112, 230, 56 ], 353 | [ 112, 230, 52 ], 354 | [ 113, 226, 53 ], 355 | [ 113, 227, 56 ], 356 | [ 113, 227, 52 ], 357 | [ 113, 228, 56 ], 358 | [ 113, 228, 52 ], 359 | [ 113, 229, 56 ], 360 | [ 113, 229, 52 ], 361 | [ 113, 230, 53 ], 362 | [ 114, 228, 54 ], 363 | [ 107, 228, 52 ], 364 | [ 107, 230, 56 ], 365 | [ 107, 230, 52 ], 366 | [ 107, 231, 55 ], 367 | [ 107, 231, 53 ], 368 | [ 108, 227, 56 ], 369 | [ 108, 228, 52 ], 370 | [ 108, 229, 56 ], 371 | [ 108, 229, 52 ], 372 | [ 108, 230, 56 ], 373 | [ 108, 231, 56 ], 374 | [ 108, 231, 52 ], 375 | [ 109, 230, 56 ], 376 | [ 109, 230, 52 ], 377 | [ 109, 231, 56 ], 378 | [ 109, 231, 52 ], 379 | [ 109, 232, 54 ], 380 | [ 110, 231, 56 ], 381 | [ 110, 231, 52 ], 382 | [ 111, 228, 52 ], 383 | [ 111, 231, 55 ], 384 | [ 111, 231, 53 ], 385 | [ 105, 227, 53 ], 386 | [ 106, 225, 52 ], 387 | [ 106, 228, 55 ], 388 | [ 106, 229, 52 ], 389 | [ 108, 224, 53 ], 390 | [ 108, 227, 50 ], 391 | [ 108, 230, 53 ], 392 | [ 109, 227, 51 ], 393 | [ 110, 225, 54 ], 394 | [ 110, 225, 52 ], 395 | [ 108, 231, 54 ], 396 | [ 109, 232, 56 ], 397 | [ 109, 232, 52 ], 398 | [ 109, 233, 55 ], 399 | [ 109, 233, 53 ], 400 | [ 110, 232, 56 ], 401 | [ 110, 232, 52 ], 402 | [ 110, 233, 56 ], 403 | [ 110, 233, 52 ], 404 | [ 111, 228, 54 ], 405 | [ 111, 231, 57 ], 406 | [ 111, 231, 51 ], 407 | [ 111, 232, 56 ], 408 | [ 111, 232, 52 ], 409 | [ 111, 233, 56 ], 410 | [ 111, 233, 52 ], 411 | [ 111, 234, 54 ], 412 | [ 112, 231, 56 ], 413 | [ 112, 231, 52 ], 414 | [ 112, 232, 56 ], 415 | [ 112, 232, 52 ], 416 | [ 112, 233, 56 ], 417 | [ 112, 233, 52 ], 418 | [ 113, 229, 55 ], 419 | [ 113, 229, 53 ], 420 | [ 113, 230, 56 ], 421 | [ 113, 230, 52 ], 422 | [ 113, 231, 56 ], 423 | [ 113, 231, 52 ], 424 | [ 113, 232, 56 ], 425 | [ 113, 232, 52 ], 426 | [ 113, 233, 55 ], 427 | [ 113, 233, 53 ], 428 | [ 114, 231, 54 ], 429 | [ 105, 230, 53 ], 430 | [ 106, 228, 52 ], 431 | [ 106, 231, 55 ], 432 | [ 106, 231, 51 ], 433 | [ 106, 232, 54 ], 434 | [ 106, 232, 52 ], 435 | [ 107, 231, 51 ], 436 | [ 107, 232, 55 ], 437 | [ 107, 232, 51 ], 438 | [ 108, 230, 50 ], 439 | [ 108, 231, 55 ], 440 | [ 108, 231, 51 ], 441 | [ 108, 232, 55 ], 442 | [ 108, 232, 51 ], 443 | [ 108, 233, 53 ], 444 | [ 109, 230, 51 ], 445 | [ 109, 231, 55 ], 446 | [ 109, 231, 51 ], 447 | [ 109, 232, 55 ], 448 | [ 109, 232, 51 ], 449 | [ 110, 230, 55 ], 450 | [ 110, 230, 51 ], 451 | [ 110, 231, 55 ], 452 | [ 110, 231, 51 ], 453 | [ 110, 232, 54 ], 454 | [ 111, 230, 53 ], 455 | [ 107, 230, 53 ], 456 | [ 107, 231, 56 ], 457 | [ 107, 231, 52 ], 458 | [ 107, 232, 56 ], 459 | [ 107, 232, 52 ], 460 | [ 107, 233, 56 ], 461 | [ 107, 233, 52 ], 462 | [ 107, 234, 55 ], 463 | [ 107, 234, 53 ], 464 | [ 108, 232, 56 ], 465 | [ 108, 232, 52 ], 466 | [ 108, 233, 56 ], 467 | [ 108, 233, 52 ], 468 | [ 108, 234, 56 ], 469 | [ 108, 234, 52 ], 470 | [ 109, 232, 57 ], 471 | [ 109, 233, 56 ], 472 | [ 109, 233, 52 ], 473 | [ 109, 234, 56 ], 474 | [ 109, 234, 52 ], 475 | [ 109, 235, 54 ], 476 | [ 110, 234, 56 ], 477 | [ 110, 234, 52 ], 478 | [ 111, 230, 55 ], 479 | [ 111, 231, 52 ], 480 | [ 111, 234, 55 ], 481 | [ 111, 234, 53 ], 482 | [ 112, 232, 54 ], 483 | [ 104, 207, 60 ], 484 | [ 105, 205, 61 ], 485 | [ 105, 205, 59 ], 486 | [ 105, 206, 62 ], 487 | [ 105, 206, 58 ], 488 | [ 105, 207, 62 ], 489 | [ 105, 207, 58 ], 490 | [ 105, 208, 62 ], 491 | [ 105, 208, 58 ], 492 | [ 105, 209, 61 ], 493 | [ 105, 209, 59 ], 494 | [ 106, 205, 62 ], 495 | [ 106, 205, 58 ], 496 | [ 106, 206, 62 ], 497 | [ 106, 206, 58 ], 498 | [ 106, 207, 62 ], 499 | [ 106, 207, 58 ], 500 | [ 106, 208, 62 ], 501 | [ 106, 208, 58 ], 502 | [ 106, 209, 62 ], 503 | [ 106, 209, 58 ], 504 | [ 107, 204, 60 ], 505 | [ 107, 205, 62 ], 506 | [ 107, 205, 58 ], 507 | [ 107, 206, 62 ], 508 | [ 107, 206, 58 ], 509 | [ 107, 207, 63 ], 510 | [ 107, 207, 57 ], 511 | [ 107, 208, 62 ], 512 | [ 107, 208, 58 ], 513 | [ 107, 209, 62 ], 514 | [ 107, 209, 58 ], 515 | [ 107, 210, 60 ], 516 | [ 108, 205, 62 ], 517 | [ 108, 205, 58 ], 518 | [ 108, 206, 62 ], 519 | [ 108, 206, 58 ], 520 | [ 108, 207, 62 ], 521 | [ 108, 207, 58 ], 522 | [ 108, 208, 62 ], 523 | [ 108, 208, 58 ], 524 | [ 108, 209, 62 ], 525 | [ 108, 209, 58 ], 526 | [ 109, 205, 61 ], 527 | [ 109, 205, 59 ], 528 | [ 109, 206, 62 ], 529 | [ 109, 206, 58 ], 530 | [ 109, 207, 62 ], 531 | [ 109, 207, 58 ], 532 | [ 109, 208, 62 ], 533 | [ 109, 208, 58 ], 534 | [ 109, 209, 61 ], 535 | [ 109, 209, 59 ], 536 | [ 110, 207, 60 ], 537 | [ 98, 210, 50 ], 538 | [ 99, 208, 51 ], 539 | [ 99, 208, 49 ], 540 | [ 99, 209, 52 ], 541 | [ 99, 209, 48 ], 542 | [ 99, 210, 52 ], 543 | [ 99, 210, 48 ], 544 | [ 99, 211, 52 ], 545 | [ 99, 211, 48 ], 546 | [ 99, 212, 51 ], 547 | [ 99, 212, 49 ], 548 | [ 100, 208, 52 ], 549 | [ 100, 208, 48 ], 550 | [ 100, 209, 52 ], 551 | [ 100, 209, 48 ], 552 | [ 100, 210, 52 ], 553 | [ 100, 210, 48 ], 554 | [ 100, 211, 52 ], 555 | [ 100, 211, 48 ], 556 | [ 100, 212, 52 ], 557 | [ 100, 212, 48 ], 558 | [ 101, 207, 50 ], 559 | [ 101, 208, 52 ], 560 | [ 101, 208, 48 ], 561 | [ 101, 209, 52 ], 562 | [ 101, 209, 48 ], 563 | [ 101, 210, 53 ], 564 | [ 101, 210, 47 ], 565 | [ 101, 211, 52 ], 566 | [ 101, 211, 48 ], 567 | [ 101, 212, 52 ], 568 | [ 101, 212, 48 ], 569 | [ 101, 213, 50 ], 570 | [ 102, 208, 52 ], 571 | [ 102, 208, 48 ], 572 | [ 102, 209, 52 ], 573 | [ 102, 209, 48 ], 574 | [ 102, 210, 52 ], 575 | [ 102, 210, 48 ], 576 | [ 102, 211, 52 ], 577 | [ 102, 211, 48 ], 578 | [ 102, 212, 52 ], 579 | [ 102, 212, 48 ], 580 | [ 103, 208, 51 ], 581 | [ 103, 208, 49 ], 582 | [ 103, 209, 52 ], 583 | [ 103, 209, 48 ], 584 | [ 103, 210, 52 ], 585 | [ 103, 210, 48 ], 586 | [ 103, 211, 52 ], 587 | [ 103, 211, 48 ], 588 | [ 103, 212, 51 ], 589 | [ 103, 212, 49 ], 590 | [ 104, 210, 50 ], 591 | [ 97, 209, 48 ], 592 | [ 98, 207, 49 ], 593 | [ 98, 207, 47 ], 594 | [ 98, 208, 50 ], 595 | [ 98, 208, 46 ], 596 | [ 98, 209, 50 ], 597 | [ 98, 209, 46 ], 598 | [ 98, 210, 46 ], 599 | [ 98, 211, 49 ], 600 | [ 98, 211, 47 ], 601 | [ 99, 207, 50 ], 602 | [ 99, 207, 46 ], 603 | [ 99, 208, 50 ], 604 | [ 99, 208, 46 ], 605 | [ 99, 209, 50 ], 606 | [ 99, 209, 46 ], 607 | [ 99, 210, 50 ], 608 | [ 99, 210, 46 ], 609 | [ 99, 211, 50 ], 610 | [ 99, 211, 46 ], 611 | [ 100, 206, 48 ], 612 | [ 100, 207, 50 ], 613 | [ 100, 207, 46 ], 614 | [ 100, 208, 50 ], 615 | [ 100, 208, 46 ], 616 | [ 100, 209, 51 ], 617 | [ 100, 209, 45 ], 618 | [ 100, 210, 50 ], 619 | [ 100, 210, 46 ], 620 | [ 100, 211, 50 ], 621 | [ 100, 211, 46 ], 622 | [ 101, 207, 46 ], 623 | [ 101, 208, 50 ], 624 | [ 101, 208, 46 ], 625 | [ 101, 209, 50 ], 626 | [ 101, 209, 46 ], 627 | [ 101, 210, 50 ], 628 | [ 101, 210, 46 ], 629 | [ 101, 211, 50 ], 630 | [ 101, 211, 46 ], 631 | [ 102, 207, 49 ], 632 | [ 102, 207, 47 ], 633 | [ 102, 208, 50 ], 634 | [ 102, 208, 46 ], 635 | [ 102, 209, 50 ], 636 | [ 102, 209, 46 ], 637 | [ 102, 210, 50 ], 638 | [ 102, 210, 46 ], 639 | [ 102, 211, 49 ], 640 | [ 102, 211, 47 ], 641 | [ 96, 211, 47 ], 642 | [ 97, 209, 46 ], 643 | [ 97, 210, 49 ], 644 | [ 97, 210, 45 ], 645 | [ 97, 211, 49 ], 646 | [ 97, 211, 45 ], 647 | [ 97, 212, 49 ], 648 | [ 97, 212, 45 ], 649 | [ 97, 213, 48 ], 650 | [ 97, 213, 46 ], 651 | [ 98, 209, 49 ], 652 | [ 98, 209, 45 ], 653 | [ 98, 210, 49 ], 654 | [ 98, 210, 45 ], 655 | [ 98, 211, 45 ], 656 | [ 98, 212, 49 ], 657 | [ 98, 212, 45 ], 658 | [ 98, 213, 49 ], 659 | [ 98, 213, 45 ], 660 | [ 99, 208, 47 ], 661 | [ 99, 209, 49 ], 662 | [ 99, 209, 45 ], 663 | [ 99, 210, 49 ], 664 | [ 99, 210, 45 ], 665 | [ 99, 211, 44 ], 666 | [ 99, 212, 45 ], 667 | [ 99, 213, 49 ], 668 | [ 99, 213, 45 ], 669 | [ 99, 214, 47 ], 670 | [ 100, 209, 49 ], 671 | [ 100, 210, 49 ], 672 | [ 100, 210, 45 ], 673 | [ 100, 211, 49 ], 674 | [ 100, 211, 45 ], 675 | [ 100, 212, 49 ], 676 | [ 100, 212, 45 ], 677 | [ 100, 213, 49 ], 678 | [ 100, 213, 45 ], 679 | [ 101, 210, 49 ], 680 | [ 101, 210, 45 ], 681 | [ 101, 211, 49 ], 682 | [ 101, 211, 45 ], 683 | [ 101, 212, 49 ], 684 | [ 101, 212, 45 ], 685 | [ 101, 213, 48 ], 686 | [ 101, 213, 46 ], 687 | [ 98, 207, 45 ], 688 | [ 98, 208, 48 ], 689 | [ 98, 208, 44 ], 690 | [ 98, 209, 48 ], 691 | [ 98, 209, 44 ], 692 | [ 98, 210, 48 ], 693 | [ 98, 210, 44 ], 694 | [ 99, 207, 48 ], 695 | [ 99, 207, 44 ], 696 | [ 99, 208, 48 ], 697 | [ 99, 208, 44 ], 698 | [ 99, 209, 44 ], 699 | [ 99, 210, 44 ], 700 | [ 100, 206, 46 ], 701 | [ 100, 207, 48 ], 702 | [ 100, 207, 44 ], 703 | [ 100, 208, 44 ], 704 | [ 100, 209, 43 ], 705 | [ 100, 210, 44 ], 706 | [ 100, 211, 44 ], 707 | [ 100, 212, 46 ], 708 | [ 101, 207, 48 ], 709 | [ 101, 207, 44 ], 710 | [ 101, 208, 44 ], 711 | [ 101, 209, 44 ], 712 | [ 101, 210, 48 ], 713 | [ 101, 210, 44 ], 714 | [ 101, 211, 44 ], 715 | [ 102, 207, 45 ], 716 | [ 102, 208, 44 ], 717 | [ 102, 209, 44 ], 718 | [ 102, 210, 44 ], 719 | [ 102, 211, 45 ], 720 | [ 103, 209, 46 ], 721 | [ 98, 210, 47 ], 722 | [ 99, 211, 49 ], 723 | [ 99, 211, 45 ], 724 | [ 99, 212, 48 ], 725 | [ 99, 212, 46 ], 726 | [ 100, 208, 49 ], 727 | [ 100, 208, 45 ], 728 | [ 101, 207, 47 ], 729 | [ 101, 208, 49 ], 730 | [ 101, 208, 45 ], 731 | [ 101, 209, 49 ], 732 | [ 101, 209, 45 ], 733 | [ 101, 213, 47 ], 734 | [ 102, 208, 49 ], 735 | [ 102, 208, 45 ], 736 | [ 102, 209, 49 ], 737 | [ 102, 209, 45 ], 738 | [ 102, 210, 49 ], 739 | [ 102, 210, 45 ], 740 | [ 102, 212, 49 ], 741 | [ 102, 212, 45 ], 742 | [ 103, 208, 48 ], 743 | [ 103, 208, 46 ], 744 | [ 103, 209, 49 ], 745 | [ 103, 209, 45 ], 746 | [ 103, 210, 49 ], 747 | [ 103, 210, 45 ], 748 | [ 103, 211, 49 ], 749 | [ 103, 211, 45 ], 750 | [ 103, 212, 48 ], 751 | [ 103, 212, 46 ], 752 | [ 104, 210, 47 ], 753 | [ 95, 210, 46 ], 754 | [ 96, 208, 47 ], 755 | [ 96, 208, 45 ], 756 | [ 96, 209, 48 ], 757 | [ 96, 209, 44 ], 758 | [ 96, 210, 48 ], 759 | [ 96, 210, 44 ], 760 | [ 96, 211, 48 ], 761 | [ 96, 211, 44 ], 762 | [ 96, 212, 47 ], 763 | [ 96, 212, 45 ], 764 | [ 97, 208, 48 ], 765 | [ 97, 208, 44 ], 766 | [ 97, 209, 44 ], 767 | [ 97, 210, 48 ], 768 | [ 97, 210, 44 ], 769 | [ 97, 211, 48 ], 770 | [ 97, 211, 44 ], 771 | [ 97, 212, 48 ], 772 | [ 97, 212, 44 ], 773 | [ 98, 207, 46 ], 774 | [ 98, 210, 43 ], 775 | [ 98, 211, 48 ], 776 | [ 98, 211, 44 ], 777 | [ 98, 212, 48 ], 778 | [ 98, 212, 44 ], 779 | [ 98, 213, 46 ], 780 | [ 99, 212, 44 ], 781 | [ 100, 208, 47 ], 782 | [ 100, 209, 44 ], 783 | [ 100, 212, 47 ], 784 | [ 100, 211, 51 ], 785 | [ 100, 211, 47 ], 786 | [ 100, 212, 51 ], 787 | [ 100, 213, 51 ], 788 | [ 100, 213, 47 ], 789 | [ 100, 214, 50 ], 790 | [ 100, 214, 48 ], 791 | [ 101, 210, 51 ], 792 | [ 101, 211, 51 ], 793 | [ 101, 211, 47 ], 794 | [ 101, 212, 51 ], 795 | [ 101, 212, 47 ], 796 | [ 101, 213, 51 ], 797 | [ 101, 214, 51 ], 798 | [ 101, 214, 47 ], 799 | [ 102, 210, 51 ], 800 | [ 102, 210, 47 ], 801 | [ 102, 211, 51 ], 802 | [ 102, 212, 46 ], 803 | [ 102, 213, 51 ], 804 | [ 102, 213, 47 ], 805 | [ 102, 214, 51 ], 806 | [ 102, 214, 47 ], 807 | [ 102, 215, 49 ], 808 | [ 103, 210, 51 ], 809 | [ 103, 210, 47 ], 810 | [ 103, 211, 51 ], 811 | [ 103, 211, 47 ], 812 | [ 103, 212, 47 ], 813 | [ 103, 213, 51 ], 814 | [ 103, 213, 47 ], 815 | [ 103, 214, 51 ], 816 | [ 103, 214, 47 ], 817 | [ 104, 210, 48 ], 818 | [ 104, 211, 51 ], 819 | [ 104, 211, 47 ], 820 | [ 104, 212, 51 ], 821 | [ 104, 212, 47 ], 822 | [ 104, 213, 51 ], 823 | [ 104, 213, 47 ], 824 | [ 104, 214, 50 ], 825 | [ 104, 214, 48 ], 826 | [ 105, 212, 49 ], 827 | [ 94, 210, 46 ], 828 | [ 95, 208, 47 ], 829 | [ 95, 208, 45 ], 830 | [ 95, 209, 48 ], 831 | [ 95, 209, 44 ], 832 | [ 95, 210, 48 ], 833 | [ 95, 210, 44 ], 834 | [ 95, 211, 48 ], 835 | [ 95, 211, 44 ], 836 | [ 95, 212, 47 ], 837 | [ 95, 212, 45 ], 838 | [ 96, 208, 48 ], 839 | [ 96, 208, 44 ], 840 | [ 96, 212, 48 ], 841 | [ 96, 212, 44 ], 842 | [ 97, 207, 46 ], 843 | [ 97, 210, 43 ], 844 | [ 99, 208, 45 ], 845 | [ 99, 212, 47 ], 846 | [ 95, 211, 47 ], 847 | [ 96, 209, 46 ], 848 | [ 96, 210, 49 ], 849 | [ 96, 210, 45 ], 850 | [ 96, 211, 49 ], 851 | [ 96, 211, 45 ], 852 | [ 96, 212, 49 ], 853 | [ 96, 213, 48 ], 854 | [ 96, 213, 46 ], 855 | [ 97, 209, 49 ], 856 | [ 97, 209, 45 ], 857 | [ 97, 213, 49 ], 858 | [ 97, 213, 45 ], 859 | [ 98, 208, 47 ], 860 | [ 98, 211, 50 ], 861 | [ 98, 214, 47 ], 862 | [ 100, 209, 46 ], 863 | [ 100, 213, 48 ], 864 | [ 100, 213, 46 ], 865 | [ 94, 208, 47 ], 866 | [ 95, 206, 48 ], 867 | [ 95, 206, 46 ], 868 | [ 95, 207, 49 ], 869 | [ 95, 207, 45 ], 870 | [ 95, 208, 49 ], 871 | [ 95, 209, 49 ], 872 | [ 95, 209, 45 ], 873 | [ 96, 206, 49 ], 874 | [ 96, 206, 45 ], 875 | [ 96, 207, 49 ], 876 | [ 96, 207, 45 ], 877 | [ 96, 208, 49 ], 878 | [ 96, 209, 49 ], 879 | [ 96, 209, 45 ], 880 | [ 97, 205, 47 ], 881 | [ 97, 206, 49 ], 882 | [ 97, 206, 45 ], 883 | [ 97, 207, 49 ], 884 | [ 97, 207, 45 ], 885 | [ 97, 208, 50 ], 886 | [ 97, 211, 47 ], 887 | [ 98, 206, 49 ], 888 | [ 98, 206, 45 ], 889 | [ 98, 208, 49 ], 890 | [ 98, 208, 45 ], 891 | [ 99, 206, 48 ], 892 | [ 99, 206, 46 ], 893 | [ 99, 207, 49 ], 894 | [ 99, 207, 45 ], 895 | [ 93, 207, 46 ], 896 | [ 94, 205, 47 ], 897 | [ 94, 205, 45 ], 898 | [ 94, 206, 48 ], 899 | [ 94, 206, 44 ], 900 | [ 94, 207, 48 ], 901 | [ 94, 207, 44 ], 902 | [ 94, 208, 48 ], 903 | [ 94, 208, 44 ], 904 | [ 94, 209, 47 ], 905 | [ 94, 209, 45 ], 906 | [ 95, 205, 48 ], 907 | [ 95, 205, 44 ], 908 | [ 95, 206, 44 ], 909 | [ 95, 207, 48 ], 910 | [ 95, 207, 44 ], 911 | [ 95, 208, 48 ], 912 | [ 95, 208, 44 ], 913 | [ 96, 204, 46 ], 914 | [ 96, 205, 48 ], 915 | [ 96, 205, 44 ], 916 | [ 96, 206, 48 ], 917 | [ 96, 206, 44 ], 918 | [ 96, 207, 43 ], 919 | [ 96, 210, 46 ], 920 | [ 97, 205, 48 ], 921 | [ 97, 205, 44 ], 922 | [ 97, 206, 48 ], 923 | [ 97, 206, 44 ], 924 | [ 97, 207, 48 ], 925 | [ 97, 207, 44 ], 926 | [ 98, 205, 47 ], 927 | [ 98, 205, 45 ], 928 | [ 98, 206, 48 ], 929 | [ 98, 206, 44 ], 930 | [ 98, 207, 48 ], 931 | [ 98, 207, 44 ], 932 | [ 98, 209, 47 ], 933 | [ 95, 207, 46 ], 934 | [ 96, 205, 47 ], 935 | [ 96, 205, 45 ], 936 | [ 96, 207, 48 ], 937 | [ 96, 207, 44 ], 938 | [ 96, 209, 47 ], 939 | [ 98, 204, 46 ], 940 | [ 98, 205, 48 ], 941 | [ 98, 205, 44 ], 942 | [ 98, 207, 43 ], 943 | [ 99, 205, 48 ], 944 | [ 99, 205, 44 ], 945 | [ 99, 206, 44 ], 946 | [ 100, 205, 47 ], 947 | [ 100, 205, 45 ], 948 | [ 100, 206, 44 ], 949 | [ 100, 209, 47 ], 950 | [ 97, 206, 46 ], 951 | [ 97, 208, 49 ], 952 | [ 97, 208, 45 ], 953 | [ 97, 210, 46 ], 954 | [ 99, 205, 47 ], 955 | [ 99, 206, 49 ], 956 | [ 99, 206, 45 ], 957 | [ 99, 211, 47 ], 958 | [ 100, 206, 49 ], 959 | [ 100, 206, 45 ], 960 | [ 100, 207, 49 ], 961 | [ 100, 207, 45 ], 962 | [ 101, 206, 48 ], 963 | [ 101, 206, 46 ], 964 | [ 101, 207, 49 ], 965 | [ 101, 207, 45 ], 966 | [ 102, 208, 47 ], 967 | [ 93, 191, 52 ], 968 | [ 94, 189, 53 ], 969 | [ 94, 189, 51 ], 970 | [ 94, 190, 54 ], 971 | [ 94, 190, 50 ], 972 | [ 94, 191, 54 ], 973 | [ 94, 191, 50 ], 974 | [ 94, 192, 54 ], 975 | [ 94, 192, 50 ], 976 | [ 94, 193, 53 ], 977 | [ 94, 193, 51 ], 978 | [ 95, 189, 54 ], 979 | [ 95, 189, 50 ], 980 | [ 95, 190, 54 ], 981 | [ 95, 190, 50 ], 982 | [ 95, 191, 54 ], 983 | [ 95, 191, 50 ], 984 | [ 95, 192, 54 ], 985 | [ 95, 192, 50 ], 986 | [ 95, 193, 54 ], 987 | [ 95, 193, 50 ], 988 | [ 96, 188, 52 ], 989 | [ 96, 189, 54 ], 990 | [ 96, 189, 50 ], 991 | [ 96, 190, 54 ], 992 | [ 96, 190, 50 ], 993 | [ 96, 191, 55 ], 994 | [ 96, 191, 49 ], 995 | [ 96, 192, 54 ], 996 | [ 96, 192, 50 ], 997 | [ 96, 193, 54 ], 998 | [ 96, 193, 50 ], 999 | [ 96, 194, 52 ], 1000 | [ 97, 189, 54 ], 1001 | [ 97, 189, 50 ], 1002 | [ 97, 190, 54 ], 1003 | [ 97, 190, 50 ], 1004 | [ 97, 191, 54 ], 1005 | [ 97, 191, 50 ], 1006 | [ 97, 192, 54 ], 1007 | [ 97, 192, 50 ], 1008 | [ 97, 193, 54 ], 1009 | [ 97, 193, 50 ], 1010 | [ 98, 189, 53 ], 1011 | [ 98, 189, 51 ], 1012 | [ 98, 190, 54 ], 1013 | [ 98, 190, 50 ], 1014 | [ 98, 191, 54 ], 1015 | [ 98, 191, 50 ], 1016 | [ 98, 192, 54 ], 1017 | [ 98, 192, 50 ], 1018 | [ 98, 193, 53 ], 1019 | [ 98, 193, 51 ], 1020 | [ 99, 191, 52 ], 1021 | [ 89, 192, 45 ], 1022 | [ 90, 190, 46 ], 1023 | [ 90, 190, 44 ], 1024 | [ 90, 191, 47 ], 1025 | [ 90, 191, 43 ], 1026 | [ 90, 192, 47 ], 1027 | [ 90, 192, 43 ], 1028 | [ 90, 193, 47 ], 1029 | [ 90, 193, 43 ], 1030 | [ 90, 194, 46 ], 1031 | [ 90, 194, 44 ], 1032 | [ 91, 190, 47 ], 1033 | [ 91, 190, 43 ], 1034 | [ 91, 191, 47 ], 1035 | [ 91, 191, 43 ], 1036 | [ 91, 192, 47 ], 1037 | [ 91, 192, 43 ], 1038 | [ 91, 193, 47 ], 1039 | [ 91, 193, 43 ], 1040 | [ 91, 194, 47 ], 1041 | [ 91, 194, 43 ], 1042 | [ 92, 189, 45 ], 1043 | [ 92, 190, 47 ], 1044 | [ 92, 190, 43 ], 1045 | [ 92, 191, 47 ], 1046 | [ 92, 191, 43 ], 1047 | [ 92, 192, 48 ], 1048 | [ 92, 192, 42 ], 1049 | [ 92, 193, 47 ], 1050 | [ 92, 193, 43 ], 1051 | [ 92, 194, 47 ], 1052 | [ 92, 194, 43 ], 1053 | [ 92, 195, 45 ], 1054 | [ 93, 190, 47 ], 1055 | [ 93, 190, 43 ], 1056 | [ 93, 191, 47 ], 1057 | [ 93, 191, 43 ], 1058 | [ 93, 192, 47 ], 1059 | [ 93, 192, 43 ], 1060 | [ 93, 193, 47 ], 1061 | [ 93, 193, 43 ], 1062 | [ 93, 194, 47 ], 1063 | [ 93, 194, 43 ], 1064 | [ 94, 190, 46 ], 1065 | [ 94, 190, 44 ], 1066 | [ 94, 191, 47 ], 1067 | [ 94, 191, 43 ], 1068 | [ 94, 192, 47 ], 1069 | [ 94, 192, 43 ], 1070 | [ 94, 193, 47 ], 1071 | [ 94, 193, 43 ], 1072 | [ 94, 194, 46 ], 1073 | [ 94, 194, 44 ], 1074 | [ 95, 192, 45 ], 1075 | [ 88, 191, 44 ], 1076 | [ 89, 189, 45 ], 1077 | [ 89, 189, 43 ], 1078 | [ 89, 190, 46 ], 1079 | [ 89, 190, 42 ], 1080 | [ 89, 191, 46 ], 1081 | [ 89, 191, 42 ], 1082 | [ 89, 192, 46 ], 1083 | [ 89, 192, 42 ], 1084 | [ 89, 193, 45 ], 1085 | [ 89, 193, 43 ], 1086 | [ 90, 189, 46 ], 1087 | [ 90, 189, 42 ], 1088 | [ 90, 190, 42 ], 1089 | [ 90, 191, 46 ], 1090 | [ 90, 191, 42 ], 1091 | [ 90, 192, 46 ], 1092 | [ 90, 192, 42 ], 1093 | [ 90, 193, 46 ], 1094 | [ 90, 193, 42 ], 1095 | [ 91, 188, 44 ], 1096 | [ 91, 189, 46 ], 1097 | [ 91, 189, 42 ], 1098 | [ 91, 190, 46 ], 1099 | [ 91, 190, 42 ], 1100 | [ 91, 191, 41 ], 1101 | [ 91, 192, 46 ], 1102 | [ 91, 192, 42 ], 1103 | [ 91, 193, 46 ], 1104 | [ 91, 193, 42 ], 1105 | [ 91, 194, 44 ], 1106 | [ 92, 189, 46 ], 1107 | [ 92, 189, 42 ], 1108 | [ 92, 190, 46 ], 1109 | [ 92, 190, 42 ], 1110 | [ 92, 191, 46 ], 1111 | [ 92, 191, 42 ], 1112 | [ 92, 192, 46 ], 1113 | [ 92, 193, 46 ], 1114 | [ 92, 193, 42 ], 1115 | [ 93, 189, 45 ], 1116 | [ 93, 189, 43 ], 1117 | [ 93, 190, 46 ], 1118 | [ 93, 190, 42 ], 1119 | [ 93, 191, 46 ], 1120 | [ 93, 191, 42 ], 1121 | [ 93, 192, 46 ], 1122 | [ 93, 192, 42 ], 1123 | [ 93, 193, 45 ], 1124 | [ 94, 191, 44 ], 1125 | [ 88, 191, 42 ], 1126 | [ 89, 189, 41 ], 1127 | [ 89, 190, 44 ], 1128 | [ 89, 190, 40 ], 1129 | [ 89, 191, 44 ], 1130 | [ 89, 191, 40 ], 1131 | [ 89, 192, 44 ], 1132 | [ 89, 192, 40 ], 1133 | [ 89, 193, 41 ], 1134 | [ 90, 189, 44 ], 1135 | [ 90, 189, 40 ], 1136 | [ 90, 190, 40 ], 1137 | [ 90, 191, 44 ], 1138 | [ 90, 191, 40 ], 1139 | [ 90, 192, 44 ], 1140 | [ 90, 192, 40 ], 1141 | [ 90, 193, 44 ], 1142 | [ 90, 193, 40 ], 1143 | [ 91, 188, 42 ], 1144 | [ 91, 189, 44 ], 1145 | [ 91, 189, 40 ], 1146 | [ 91, 190, 44 ], 1147 | [ 91, 190, 40 ], 1148 | [ 91, 191, 45 ], 1149 | [ 91, 191, 39 ], 1150 | [ 91, 192, 44 ], 1151 | [ 91, 192, 40 ], 1152 | [ 91, 193, 44 ], 1153 | [ 91, 193, 40 ], 1154 | [ 91, 194, 42 ], 1155 | [ 92, 189, 44 ], 1156 | [ 92, 189, 40 ], 1157 | [ 92, 190, 44 ], 1158 | [ 92, 190, 40 ], 1159 | [ 92, 191, 44 ], 1160 | [ 92, 191, 40 ], 1161 | [ 92, 192, 44 ], 1162 | [ 92, 192, 40 ], 1163 | [ 92, 193, 44 ], 1164 | [ 92, 193, 40 ], 1165 | [ 93, 189, 41 ], 1166 | [ 93, 190, 44 ], 1167 | [ 93, 190, 40 ], 1168 | [ 93, 191, 44 ], 1169 | [ 93, 191, 40 ], 1170 | [ 93, 192, 44 ], 1171 | [ 93, 192, 40 ], 1172 | [ 93, 193, 41 ], 1173 | [ 94, 191, 42 ], 1174 | [ 86, 192, 40 ], 1175 | [ 87, 190, 41 ], 1176 | [ 87, 190, 39 ], 1177 | [ 87, 191, 42 ], 1178 | [ 87, 191, 38 ], 1179 | [ 87, 192, 42 ], 1180 | [ 87, 192, 38 ], 1181 | [ 87, 193, 42 ], 1182 | [ 87, 193, 38 ], 1183 | [ 87, 194, 41 ], 1184 | [ 87, 194, 39 ], 1185 | [ 88, 190, 42 ], 1186 | [ 88, 190, 38 ], 1187 | [ 88, 191, 38 ], 1188 | [ 88, 192, 42 ], 1189 | [ 88, 192, 38 ], 1190 | [ 88, 193, 42 ], 1191 | [ 88, 193, 38 ], 1192 | [ 88, 194, 42 ], 1193 | [ 88, 194, 38 ], 1194 | [ 89, 189, 40 ], 1195 | [ 89, 190, 38 ], 1196 | [ 89, 191, 38 ], 1197 | [ 89, 192, 43 ], 1198 | [ 89, 192, 37 ], 1199 | [ 89, 193, 42 ], 1200 | [ 89, 193, 38 ], 1201 | [ 89, 194, 42 ], 1202 | [ 89, 194, 38 ], 1203 | [ 89, 195, 40 ], 1204 | [ 90, 190, 38 ], 1205 | [ 90, 191, 38 ], 1206 | [ 90, 192, 38 ], 1207 | [ 90, 193, 38 ], 1208 | [ 90, 194, 42 ], 1209 | [ 90, 194, 38 ], 1210 | [ 91, 190, 41 ], 1211 | [ 91, 190, 39 ], 1212 | [ 91, 191, 42 ], 1213 | [ 91, 191, 38 ], 1214 | [ 91, 192, 38 ], 1215 | [ 91, 193, 38 ], 1216 | [ 91, 194, 41 ], 1217 | [ 91, 194, 39 ], 1218 | [ 86, 192, 42 ], 1219 | [ 87, 190, 43 ], 1220 | [ 87, 191, 44 ], 1221 | [ 87, 191, 40 ], 1222 | [ 87, 192, 44 ], 1223 | [ 87, 192, 40 ], 1224 | [ 87, 193, 44 ], 1225 | [ 87, 193, 40 ], 1226 | [ 87, 194, 43 ], 1227 | [ 88, 190, 44 ], 1228 | [ 88, 190, 40 ], 1229 | [ 88, 191, 40 ], 1230 | [ 88, 192, 44 ], 1231 | [ 88, 192, 40 ], 1232 | [ 88, 193, 44 ], 1233 | [ 88, 193, 40 ], 1234 | [ 88, 194, 44 ], 1235 | [ 88, 194, 40 ], 1236 | [ 89, 189, 42 ], 1237 | [ 89, 192, 39 ], 1238 | [ 89, 193, 44 ], 1239 | [ 89, 193, 40 ], 1240 | [ 89, 194, 44 ], 1241 | [ 89, 194, 40 ], 1242 | [ 89, 195, 42 ], 1243 | [ 90, 194, 40 ], 1244 | [ 91, 191, 44 ], 1245 | [ 91, 191, 40 ], 1246 | [ 87, 193, 43 ], 1247 | [ 88, 192, 45 ], 1248 | [ 88, 192, 41 ], 1249 | [ 88, 193, 45 ], 1250 | [ 88, 193, 41 ], 1251 | [ 88, 194, 45 ], 1252 | [ 88, 194, 41 ], 1253 | [ 88, 195, 44 ], 1254 | [ 88, 195, 42 ], 1255 | [ 89, 191, 45 ], 1256 | [ 89, 191, 41 ], 1257 | [ 89, 192, 41 ], 1258 | [ 89, 194, 45 ], 1259 | [ 89, 194, 41 ], 1260 | [ 89, 195, 45 ], 1261 | [ 89, 195, 41 ], 1262 | [ 90, 190, 43 ], 1263 | [ 90, 191, 45 ], 1264 | [ 90, 191, 41 ], 1265 | [ 90, 192, 45 ], 1266 | [ 90, 192, 41 ], 1267 | [ 90, 194, 45 ], 1268 | [ 90, 194, 41 ], 1269 | [ 90, 195, 45 ], 1270 | [ 90, 195, 41 ], 1271 | [ 90, 196, 43 ], 1272 | [ 91, 192, 45 ], 1273 | [ 91, 192, 41 ], 1274 | [ 91, 193, 45 ], 1275 | [ 91, 193, 41 ], 1276 | [ 91, 194, 45 ], 1277 | [ 91, 195, 45 ], 1278 | [ 91, 195, 41 ], 1279 | [ 92, 192, 45 ], 1280 | [ 92, 192, 41 ], 1281 | [ 92, 193, 45 ], 1282 | [ 92, 193, 41 ], 1283 | [ 92, 194, 45 ], 1284 | [ 92, 194, 41 ], 1285 | [ 92, 195, 44 ], 1286 | [ 92, 195, 42 ], 1287 | [ 92, 201, 46 ], 1288 | [ 93, 199, 47 ], 1289 | [ 93, 199, 45 ], 1290 | [ 93, 200, 48 ], 1291 | [ 93, 200, 44 ], 1292 | [ 93, 201, 48 ], 1293 | [ 93, 201, 44 ], 1294 | [ 93, 202, 48 ], 1295 | [ 93, 202, 44 ], 1296 | [ 93, 203, 47 ], 1297 | [ 93, 203, 45 ], 1298 | [ 94, 199, 48 ], 1299 | [ 94, 199, 44 ], 1300 | [ 94, 200, 48 ], 1301 | [ 94, 200, 44 ], 1302 | [ 94, 201, 48 ], 1303 | [ 94, 201, 44 ], 1304 | [ 94, 202, 48 ], 1305 | [ 94, 202, 44 ], 1306 | [ 94, 203, 48 ], 1307 | [ 94, 203, 44 ], 1308 | [ 95, 198, 46 ], 1309 | [ 95, 199, 48 ], 1310 | [ 95, 199, 44 ], 1311 | [ 95, 200, 48 ], 1312 | [ 95, 200, 44 ], 1313 | [ 95, 201, 49 ], 1314 | [ 95, 201, 43 ], 1315 | [ 95, 202, 48 ], 1316 | [ 95, 202, 44 ], 1317 | [ 95, 203, 48 ], 1318 | [ 95, 203, 44 ], 1319 | [ 95, 204, 46 ], 1320 | [ 96, 199, 48 ], 1321 | [ 96, 199, 44 ], 1322 | [ 96, 200, 48 ], 1323 | [ 96, 200, 44 ], 1324 | [ 96, 201, 48 ], 1325 | [ 96, 201, 44 ], 1326 | [ 96, 202, 48 ], 1327 | [ 96, 202, 44 ], 1328 | [ 96, 203, 48 ], 1329 | [ 96, 203, 44 ], 1330 | [ 97, 199, 47 ], 1331 | [ 97, 199, 45 ], 1332 | [ 97, 200, 48 ], 1333 | [ 97, 200, 44 ], 1334 | [ 97, 201, 48 ], 1335 | [ 97, 201, 44 ], 1336 | [ 97, 202, 48 ], 1337 | [ 97, 202, 44 ], 1338 | [ 97, 203, 47 ], 1339 | [ 97, 203, 45 ], 1340 | [ 98, 201, 46 ], 1341 | [ 91, 200, 45 ], 1342 | [ 92, 198, 46 ], 1343 | [ 92, 198, 44 ], 1344 | [ 92, 199, 47 ], 1345 | [ 92, 199, 43 ], 1346 | [ 92, 200, 47 ], 1347 | [ 92, 200, 43 ], 1348 | [ 92, 201, 47 ], 1349 | [ 92, 201, 43 ], 1350 | [ 92, 202, 46 ], 1351 | [ 92, 202, 44 ], 1352 | [ 93, 198, 47 ], 1353 | [ 93, 198, 43 ], 1354 | [ 93, 199, 43 ], 1355 | [ 93, 200, 47 ], 1356 | [ 93, 200, 43 ], 1357 | [ 93, 201, 47 ], 1358 | [ 93, 201, 43 ], 1359 | [ 93, 202, 47 ], 1360 | [ 93, 202, 43 ], 1361 | [ 94, 197, 45 ], 1362 | [ 94, 198, 47 ], 1363 | [ 94, 198, 43 ], 1364 | [ 94, 199, 47 ], 1365 | [ 94, 199, 43 ], 1366 | [ 94, 200, 42 ], 1367 | [ 94, 201, 47 ], 1368 | [ 94, 201, 43 ], 1369 | [ 94, 202, 47 ], 1370 | [ 94, 202, 43 ], 1371 | [ 94, 203, 45 ], 1372 | [ 95, 198, 47 ], 1373 | [ 95, 198, 43 ], 1374 | [ 95, 199, 47 ], 1375 | [ 95, 199, 43 ], 1376 | [ 95, 200, 47 ], 1377 | [ 95, 200, 43 ], 1378 | [ 95, 201, 47 ], 1379 | [ 95, 202, 47 ], 1380 | [ 95, 202, 43 ], 1381 | [ 96, 198, 46 ], 1382 | [ 96, 198, 44 ], 1383 | [ 96, 199, 47 ], 1384 | [ 96, 199, 43 ], 1385 | [ 96, 200, 47 ], 1386 | [ 96, 200, 43 ], 1387 | [ 96, 201, 47 ], 1388 | [ 96, 201, 43 ], 1389 | [ 96, 202, 46 ], 1390 | [ 97, 200, 45 ], 1391 | [ 91, 200, 42 ], 1392 | [ 92, 198, 43 ], 1393 | [ 92, 198, 41 ], 1394 | [ 92, 199, 44 ], 1395 | [ 92, 199, 40 ], 1396 | [ 92, 200, 44 ], 1397 | [ 92, 200, 40 ], 1398 | [ 92, 201, 44 ], 1399 | [ 92, 201, 40 ], 1400 | [ 92, 202, 43 ], 1401 | [ 92, 202, 41 ], 1402 | [ 93, 198, 44 ], 1403 | [ 93, 198, 40 ], 1404 | [ 93, 199, 44 ], 1405 | [ 93, 199, 40 ], 1406 | [ 93, 200, 40 ], 1407 | [ 93, 201, 40 ], 1408 | [ 93, 202, 40 ], 1409 | [ 94, 197, 42 ], 1410 | [ 94, 198, 44 ], 1411 | [ 94, 198, 40 ], 1412 | [ 94, 199, 40 ], 1413 | [ 94, 200, 45 ], 1414 | [ 94, 200, 39 ], 1415 | [ 94, 201, 40 ], 1416 | [ 94, 202, 40 ], 1417 | [ 94, 203, 42 ], 1418 | [ 95, 198, 44 ], 1419 | [ 95, 198, 40 ], 1420 | [ 95, 199, 40 ], 1421 | [ 95, 200, 40 ], 1422 | [ 95, 201, 44 ], 1423 | [ 95, 201, 40 ], 1424 | [ 95, 202, 40 ], 1425 | [ 96, 198, 43 ], 1426 | [ 96, 198, 41 ], 1427 | [ 96, 199, 40 ], 1428 | [ 96, 200, 40 ], 1429 | [ 96, 201, 40 ], 1430 | [ 96, 202, 43 ], 1431 | [ 96, 202, 41 ], 1432 | [ 97, 200, 42 ], 1433 | [ 94, 201, 46 ], 1434 | [ 95, 199, 45 ], 1435 | [ 95, 201, 48 ], 1436 | [ 95, 203, 47 ], 1437 | [ 95, 203, 45 ], 1438 | [ 97, 198, 46 ], 1439 | [ 97, 199, 48 ], 1440 | [ 97, 199, 44 ], 1441 | [ 97, 201, 49 ], 1442 | [ 97, 201, 43 ], 1443 | [ 97, 203, 48 ], 1444 | [ 97, 203, 44 ], 1445 | [ 97, 204, 46 ], 1446 | [ 98, 199, 48 ], 1447 | [ 98, 199, 44 ], 1448 | [ 98, 200, 48 ], 1449 | [ 98, 200, 44 ], 1450 | [ 98, 201, 48 ], 1451 | [ 98, 201, 44 ], 1452 | [ 98, 202, 48 ], 1453 | [ 98, 202, 44 ], 1454 | [ 98, 203, 48 ], 1455 | [ 98, 203, 44 ], 1456 | [ 99, 199, 47 ], 1457 | [ 99, 199, 45 ], 1458 | [ 99, 200, 48 ], 1459 | [ 99, 200, 44 ], 1460 | [ 99, 201, 48 ], 1461 | [ 99, 201, 44 ], 1462 | [ 99, 202, 48 ], 1463 | [ 99, 202, 44 ], 1464 | [ 99, 203, 47 ], 1465 | [ 99, 203, 45 ], 1466 | [ 100, 201, 46 ], 1467 | [ 89, 200, 45 ], 1468 | [ 90, 198, 46 ], 1469 | [ 90, 198, 44 ], 1470 | [ 90, 199, 47 ], 1471 | [ 90, 199, 43 ], 1472 | [ 90, 200, 47 ], 1473 | [ 90, 200, 43 ], 1474 | [ 90, 201, 47 ], 1475 | [ 90, 201, 43 ], 1476 | [ 90, 202, 46 ], 1477 | [ 90, 202, 44 ], 1478 | [ 91, 198, 47 ], 1479 | [ 91, 198, 43 ], 1480 | [ 91, 199, 47 ], 1481 | [ 91, 199, 43 ], 1482 | [ 91, 200, 47 ], 1483 | [ 91, 200, 43 ], 1484 | [ 91, 201, 47 ], 1485 | [ 91, 201, 43 ], 1486 | [ 91, 202, 47 ], 1487 | [ 91, 202, 43 ], 1488 | [ 92, 197, 45 ], 1489 | [ 92, 198, 47 ], 1490 | [ 92, 200, 48 ], 1491 | [ 92, 200, 42 ], 1492 | [ 92, 202, 47 ], 1493 | [ 92, 203, 45 ], 1494 | [ 94, 198, 46 ], 1495 | [ 94, 200, 47 ], 1496 | [ 94, 200, 43 ], 1497 | [ 94, 202, 46 ], 1498 | [ 95, 200, 45 ], 1499 | [ 91, 197, 44 ], 1500 | [ 91, 197, 42 ], 1501 | [ 91, 198, 45 ], 1502 | [ 91, 198, 41 ], 1503 | [ 91, 199, 45 ], 1504 | [ 91, 199, 41 ], 1505 | [ 91, 200, 41 ], 1506 | [ 91, 201, 44 ], 1507 | [ 91, 201, 42 ], 1508 | [ 92, 197, 41 ], 1509 | [ 92, 198, 45 ], 1510 | [ 92, 199, 45 ], 1511 | [ 92, 199, 41 ], 1512 | [ 92, 200, 45 ], 1513 | [ 92, 200, 41 ], 1514 | [ 92, 201, 45 ], 1515 | [ 92, 201, 41 ], 1516 | [ 93, 196, 43 ], 1517 | [ 93, 197, 45 ], 1518 | [ 93, 197, 41 ], 1519 | [ 93, 198, 45 ], 1520 | [ 93, 198, 41 ], 1521 | [ 93, 199, 46 ], 1522 | [ 93, 200, 45 ], 1523 | [ 93, 200, 41 ], 1524 | [ 93, 201, 45 ], 1525 | [ 93, 201, 41 ], 1526 | [ 94, 197, 41 ], 1527 | [ 94, 198, 45 ], 1528 | [ 94, 198, 41 ], 1529 | [ 94, 199, 45 ], 1530 | [ 94, 199, 41 ], 1531 | [ 94, 200, 41 ], 1532 | [ 94, 201, 45 ], 1533 | [ 94, 201, 41 ], 1534 | [ 95, 197, 44 ], 1535 | [ 95, 197, 42 ], 1536 | [ 95, 198, 45 ], 1537 | [ 95, 198, 41 ], 1538 | [ 95, 199, 41 ], 1539 | [ 95, 200, 41 ], 1540 | [ 95, 201, 42 ], 1541 | [ 92, 198, 48 ], 1542 | [ 92, 199, 49 ], 1543 | [ 92, 200, 49 ], 1544 | [ 92, 201, 49 ], 1545 | [ 92, 202, 48 ], 1546 | [ 93, 198, 49 ], 1547 | [ 93, 199, 49 ], 1548 | [ 93, 200, 49 ], 1549 | [ 93, 201, 49 ], 1550 | [ 93, 202, 49 ], 1551 | [ 93, 202, 45 ], 1552 | [ 94, 197, 47 ], 1553 | [ 94, 198, 49 ], 1554 | [ 94, 199, 49 ], 1555 | [ 94, 200, 50 ], 1556 | [ 94, 201, 49 ], 1557 | [ 94, 202, 49 ], 1558 | [ 94, 202, 45 ], 1559 | [ 94, 203, 47 ], 1560 | [ 95, 198, 49 ], 1561 | [ 95, 199, 49 ], 1562 | [ 95, 200, 49 ], 1563 | [ 95, 201, 45 ], 1564 | [ 95, 202, 49 ], 1565 | [ 95, 202, 45 ], 1566 | [ 96, 198, 48 ], 1567 | [ 96, 199, 49 ], 1568 | [ 96, 199, 45 ], 1569 | [ 96, 200, 49 ], 1570 | [ 96, 200, 45 ], 1571 | [ 96, 201, 49 ], 1572 | [ 96, 201, 45 ], 1573 | [ 97, 200, 47 ], 1574 | [ 89, 184, 46 ], 1575 | [ 90, 182, 47 ], 1576 | [ 90, 182, 45 ], 1577 | [ 90, 183, 48 ], 1578 | [ 90, 183, 44 ], 1579 | [ 90, 184, 48 ], 1580 | [ 90, 184, 44 ], 1581 | [ 90, 185, 48 ], 1582 | [ 90, 185, 44 ], 1583 | [ 90, 186, 47 ], 1584 | [ 90, 186, 45 ], 1585 | [ 91, 182, 48 ], 1586 | [ 91, 182, 44 ], 1587 | [ 91, 183, 48 ], 1588 | [ 91, 183, 44 ], 1589 | [ 91, 184, 48 ], 1590 | [ 91, 184, 44 ], 1591 | [ 91, 185, 48 ], 1592 | [ 91, 185, 44 ], 1593 | [ 91, 186, 48 ], 1594 | [ 91, 186, 44 ], 1595 | [ 92, 181, 46 ], 1596 | [ 92, 182, 48 ], 1597 | [ 92, 182, 44 ], 1598 | [ 92, 183, 48 ], 1599 | [ 92, 183, 44 ], 1600 | [ 92, 184, 49 ], 1601 | [ 92, 184, 43 ], 1602 | [ 92, 185, 48 ], 1603 | [ 92, 185, 44 ], 1604 | [ 92, 186, 48 ], 1605 | [ 92, 186, 44 ], 1606 | [ 92, 187, 46 ], 1607 | [ 93, 182, 48 ], 1608 | [ 93, 182, 44 ], 1609 | [ 93, 183, 48 ], 1610 | [ 93, 183, 44 ], 1611 | [ 93, 184, 48 ], 1612 | [ 93, 184, 44 ], 1613 | [ 93, 185, 48 ], 1614 | [ 93, 185, 44 ], 1615 | [ 93, 186, 48 ], 1616 | [ 93, 186, 44 ], 1617 | [ 94, 182, 47 ], 1618 | [ 94, 182, 45 ], 1619 | [ 94, 183, 48 ], 1620 | [ 94, 183, 44 ], 1621 | [ 94, 184, 48 ], 1622 | [ 94, 184, 44 ], 1623 | [ 94, 185, 48 ], 1624 | [ 94, 185, 44 ], 1625 | [ 94, 186, 47 ], 1626 | [ 94, 186, 45 ], 1627 | [ 95, 184, 46 ], 1628 | [ 85, 186, 41 ], 1629 | [ 86, 184, 42 ], 1630 | [ 86, 184, 40 ], 1631 | [ 86, 185, 43 ], 1632 | [ 86, 185, 39 ], 1633 | [ 86, 186, 43 ], 1634 | [ 86, 186, 39 ], 1635 | [ 86, 187, 43 ], 1636 | [ 86, 187, 39 ], 1637 | [ 86, 188, 42 ], 1638 | [ 86, 188, 40 ], 1639 | [ 87, 184, 43 ], 1640 | [ 87, 184, 39 ], 1641 | [ 87, 185, 43 ], 1642 | [ 87, 185, 39 ], 1643 | [ 87, 186, 43 ], 1644 | [ 87, 186, 39 ], 1645 | [ 87, 187, 43 ], 1646 | [ 87, 187, 39 ], 1647 | [ 87, 188, 43 ], 1648 | [ 87, 188, 39 ], 1649 | [ 88, 183, 41 ], 1650 | [ 88, 184, 43 ], 1651 | [ 88, 184, 39 ], 1652 | [ 88, 185, 43 ], 1653 | [ 88, 185, 39 ], 1654 | [ 88, 186, 44 ], 1655 | [ 88, 186, 38 ], 1656 | [ 88, 187, 43 ], 1657 | [ 88, 187, 39 ], 1658 | [ 88, 188, 43 ], 1659 | [ 88, 188, 39 ], 1660 | [ 88, 189, 41 ], 1661 | [ 89, 184, 43 ], 1662 | [ 89, 184, 39 ], 1663 | [ 89, 185, 43 ], 1664 | [ 89, 185, 39 ], 1665 | [ 89, 186, 43 ], 1666 | [ 89, 186, 39 ], 1667 | [ 89, 187, 43 ], 1668 | [ 89, 187, 39 ], 1669 | [ 89, 188, 43 ], 1670 | [ 89, 188, 39 ], 1671 | [ 90, 184, 42 ], 1672 | [ 90, 184, 40 ], 1673 | [ 90, 185, 43 ], 1674 | [ 90, 185, 39 ], 1675 | [ 90, 186, 43 ], 1676 | [ 90, 186, 39 ], 1677 | [ 90, 187, 43 ], 1678 | [ 90, 187, 39 ], 1679 | [ 90, 188, 42 ], 1680 | [ 90, 188, 40 ], 1681 | [ 91, 186, 41 ], 1682 | [ 87, 185, 44 ], 1683 | [ 87, 185, 42 ], 1684 | [ 87, 186, 45 ], 1685 | [ 87, 186, 41 ], 1686 | [ 87, 187, 45 ], 1687 | [ 87, 187, 41 ], 1688 | [ 87, 188, 45 ], 1689 | [ 87, 188, 41 ], 1690 | [ 87, 189, 44 ], 1691 | [ 87, 189, 42 ], 1692 | [ 88, 185, 45 ], 1693 | [ 88, 185, 41 ], 1694 | [ 88, 186, 45 ], 1695 | [ 88, 186, 41 ], 1696 | [ 88, 187, 45 ], 1697 | [ 88, 187, 41 ], 1698 | [ 88, 188, 45 ], 1699 | [ 88, 188, 41 ], 1700 | [ 88, 189, 45 ], 1701 | [ 89, 185, 45 ], 1702 | [ 89, 185, 41 ], 1703 | [ 89, 186, 45 ], 1704 | [ 89, 186, 41 ], 1705 | [ 89, 187, 46 ], 1706 | [ 89, 187, 40 ], 1707 | [ 89, 188, 45 ], 1708 | [ 89, 188, 41 ], 1709 | [ 89, 190, 43 ], 1710 | [ 90, 185, 45 ], 1711 | [ 90, 185, 41 ], 1712 | [ 90, 186, 41 ], 1713 | [ 90, 187, 45 ], 1714 | [ 90, 187, 41 ], 1715 | [ 90, 188, 45 ], 1716 | [ 90, 188, 41 ], 1717 | [ 90, 189, 45 ], 1718 | [ 90, 189, 41 ], 1719 | [ 91, 185, 42 ], 1720 | [ 91, 186, 45 ], 1721 | [ 91, 187, 45 ], 1722 | [ 91, 187, 41 ], 1723 | [ 91, 188, 45 ], 1724 | [ 91, 188, 41 ], 1725 | [ 92, 187, 43 ], 1726 | [ 82, 187, 41 ], 1727 | [ 83, 185, 42 ], 1728 | [ 83, 185, 40 ], 1729 | [ 83, 186, 43 ], 1730 | [ 83, 186, 39 ], 1731 | [ 83, 187, 43 ], 1732 | [ 83, 187, 39 ], 1733 | [ 83, 188, 43 ], 1734 | [ 83, 188, 39 ], 1735 | [ 83, 189, 42 ], 1736 | [ 83, 189, 40 ], 1737 | [ 84, 185, 43 ], 1738 | [ 84, 185, 39 ], 1739 | [ 84, 186, 43 ], 1740 | [ 84, 186, 39 ], 1741 | [ 84, 187, 43 ], 1742 | [ 84, 187, 39 ], 1743 | [ 84, 188, 43 ], 1744 | [ 84, 188, 39 ], 1745 | [ 84, 189, 43 ], 1746 | [ 84, 189, 39 ], 1747 | [ 85, 184, 41 ], 1748 | [ 85, 185, 43 ], 1749 | [ 85, 185, 39 ], 1750 | [ 85, 186, 43 ], 1751 | [ 85, 186, 39 ], 1752 | [ 85, 187, 44 ], 1753 | [ 85, 187, 38 ], 1754 | [ 85, 188, 43 ], 1755 | [ 85, 188, 39 ], 1756 | [ 85, 189, 43 ], 1757 | [ 85, 189, 39 ], 1758 | [ 85, 190, 41 ], 1759 | [ 86, 188, 43 ], 1760 | [ 86, 188, 39 ], 1761 | [ 86, 189, 43 ], 1762 | [ 86, 189, 39 ], 1763 | [ 87, 185, 40 ], 1764 | [ 87, 189, 40 ], 1765 | [ 81, 186, 40 ], 1766 | [ 82, 184, 41 ], 1767 | [ 82, 184, 39 ], 1768 | [ 82, 185, 42 ], 1769 | [ 82, 185, 38 ], 1770 | [ 82, 186, 42 ], 1771 | [ 82, 186, 38 ], 1772 | [ 82, 187, 42 ], 1773 | [ 82, 187, 38 ], 1774 | [ 82, 188, 41 ], 1775 | [ 82, 188, 39 ], 1776 | [ 83, 184, 42 ], 1777 | [ 83, 184, 38 ], 1778 | [ 83, 185, 38 ], 1779 | [ 83, 186, 42 ], 1780 | [ 83, 186, 38 ], 1781 | [ 83, 187, 42 ], 1782 | [ 83, 187, 38 ], 1783 | [ 83, 188, 42 ], 1784 | [ 83, 188, 38 ], 1785 | [ 84, 183, 40 ], 1786 | [ 84, 184, 42 ], 1787 | [ 84, 184, 38 ], 1788 | [ 84, 185, 42 ], 1789 | [ 84, 185, 38 ], 1790 | [ 84, 186, 37 ], 1791 | [ 84, 187, 42 ], 1792 | [ 84, 187, 38 ], 1793 | [ 84, 188, 42 ], 1794 | [ 84, 188, 38 ], 1795 | [ 84, 189, 40 ], 1796 | [ 85, 184, 42 ], 1797 | [ 85, 184, 38 ], 1798 | [ 85, 185, 42 ], 1799 | [ 85, 185, 38 ], 1800 | [ 85, 186, 42 ], 1801 | [ 85, 186, 38 ], 1802 | [ 85, 187, 42 ], 1803 | [ 85, 188, 42 ], 1804 | [ 85, 188, 38 ], 1805 | [ 86, 184, 41 ], 1806 | [ 86, 184, 39 ], 1807 | [ 86, 185, 42 ], 1808 | [ 86, 185, 38 ], 1809 | [ 86, 186, 42 ], 1810 | [ 86, 186, 38 ], 1811 | [ 86, 187, 42 ], 1812 | [ 86, 187, 38 ], 1813 | [ 86, 188, 41 ], 1814 | [ 87, 186, 40 ], 1815 | [ 83, 186, 41 ], 1816 | [ 84, 184, 40 ], 1817 | [ 84, 188, 40 ], 1818 | [ 85, 184, 43 ], 1819 | [ 85, 184, 39 ], 1820 | [ 85, 187, 43 ], 1821 | [ 85, 187, 39 ], 1822 | [ 86, 183, 41 ], 1823 | [ 86, 184, 43 ], 1824 | [ 86, 186, 44 ], 1825 | [ 86, 189, 41 ], 1826 | [ 88, 184, 42 ], 1827 | [ 88, 184, 40 ], 1828 | [ 88, 186, 43 ], 1829 | [ 88, 186, 39 ], 1830 | [ 88, 188, 42 ], 1831 | [ 88, 188, 40 ], 1832 | [ 81, 186, 38 ], 1833 | [ 82, 184, 37 ], 1834 | [ 82, 185, 40 ], 1835 | [ 82, 185, 36 ], 1836 | [ 82, 186, 40 ], 1837 | [ 82, 186, 36 ], 1838 | [ 82, 187, 40 ], 1839 | [ 82, 187, 36 ], 1840 | [ 82, 188, 37 ], 1841 | [ 83, 184, 40 ], 1842 | [ 83, 184, 36 ], 1843 | [ 83, 185, 36 ], 1844 | [ 83, 186, 40 ], 1845 | [ 83, 186, 36 ], 1846 | [ 83, 187, 40 ], 1847 | [ 83, 187, 36 ], 1848 | [ 83, 188, 40 ], 1849 | [ 83, 188, 36 ], 1850 | [ 84, 183, 38 ], 1851 | [ 84, 184, 36 ], 1852 | [ 84, 185, 40 ], 1853 | [ 84, 185, 36 ], 1854 | [ 84, 186, 41 ], 1855 | [ 84, 186, 35 ], 1856 | [ 84, 187, 40 ], 1857 | [ 84, 187, 36 ], 1858 | [ 84, 188, 36 ], 1859 | [ 84, 189, 38 ], 1860 | [ 85, 184, 40 ], 1861 | [ 85, 184, 36 ], 1862 | [ 85, 185, 40 ], 1863 | [ 85, 185, 36 ], 1864 | [ 85, 186, 40 ], 1865 | [ 85, 186, 36 ], 1866 | [ 85, 187, 40 ], 1867 | [ 85, 187, 36 ], 1868 | [ 85, 188, 40 ], 1869 | [ 85, 188, 36 ], 1870 | [ 86, 184, 37 ], 1871 | [ 86, 185, 40 ], 1872 | [ 86, 185, 36 ], 1873 | [ 86, 186, 40 ], 1874 | [ 86, 186, 36 ], 1875 | [ 86, 187, 40 ], 1876 | [ 86, 187, 36 ], 1877 | [ 86, 188, 37 ], 1878 | [ 87, 186, 38 ], 1879 | [ 82, 187, 39 ], 1880 | [ 83, 186, 37 ], 1881 | [ 83, 187, 41 ], 1882 | [ 83, 187, 37 ], 1883 | [ 83, 188, 41 ], 1884 | [ 83, 188, 37 ], 1885 | [ 83, 189, 38 ], 1886 | [ 84, 185, 41 ], 1887 | [ 84, 185, 37 ], 1888 | [ 84, 187, 41 ], 1889 | [ 84, 187, 37 ], 1890 | [ 84, 188, 41 ], 1891 | [ 84, 188, 37 ], 1892 | [ 84, 189, 41 ], 1893 | [ 84, 189, 37 ], 1894 | [ 85, 185, 41 ], 1895 | [ 85, 185, 37 ], 1896 | [ 85, 186, 37 ], 1897 | [ 85, 188, 41 ], 1898 | [ 85, 188, 37 ], 1899 | [ 85, 189, 41 ], 1900 | [ 85, 189, 37 ], 1901 | [ 85, 190, 39 ], 1902 | [ 86, 185, 41 ], 1903 | [ 86, 185, 37 ], 1904 | [ 86, 186, 41 ], 1905 | [ 86, 186, 37 ], 1906 | [ 86, 187, 41 ], 1907 | [ 86, 187, 37 ], 1908 | [ 86, 189, 37 ], 1909 | [ 87, 185, 38 ], 1910 | [ 87, 186, 37 ], 1911 | [ 87, 187, 37 ], 1912 | [ 87, 188, 37 ], 1913 | [ 87, 189, 38 ], 1914 | [ 88, 188, 44 ], 1915 | [ 88, 190, 45 ], 1916 | [ 88, 190, 41 ], 1917 | [ 88, 191, 45 ], 1918 | [ 88, 191, 41 ], 1919 | [ 89, 190, 45 ], 1920 | [ 89, 190, 41 ], 1921 | [ 91, 189, 45 ], 1922 | [ 91, 189, 41 ], 1923 | [ 91, 190, 45 ], 1924 | [ 92, 188, 44 ], 1925 | [ 92, 188, 42 ], 1926 | [ 92, 189, 41 ], 1927 | [ 92, 190, 45 ], 1928 | [ 92, 190, 41 ], 1929 | [ 92, 191, 45 ], 1930 | [ 92, 191, 41 ], 1931 | [ 86, 189, 42 ], 1932 | [ 87, 188, 44 ], 1933 | [ 87, 188, 40 ], 1934 | [ 87, 190, 44 ], 1935 | [ 87, 190, 40 ], 1936 | [ 87, 191, 43 ], 1937 | [ 87, 191, 41 ], 1938 | [ 88, 187, 44 ], 1939 | [ 88, 187, 40 ], 1940 | [ 88, 189, 44 ], 1941 | [ 88, 189, 40 ], 1942 | [ 89, 186, 42 ], 1943 | [ 89, 187, 44 ], 1944 | [ 89, 188, 44 ], 1945 | [ 89, 188, 40 ], 1946 | [ 89, 189, 39 ], 1947 | [ 90, 187, 44 ], 1948 | [ 90, 187, 40 ], 1949 | [ 90, 188, 44 ], 1950 | [ 91, 187, 43 ], 1951 | [ 91, 188, 40 ], 1952 | [ 89, 187, 42 ], 1953 | [ 89, 188, 46 ], 1954 | [ 89, 188, 42 ], 1955 | [ 89, 189, 46 ], 1956 | [ 90, 186, 46 ], 1957 | [ 90, 186, 42 ], 1958 | [ 90, 187, 46 ], 1959 | [ 90, 187, 42 ], 1960 | [ 90, 188, 46 ], 1961 | [ 91, 186, 46 ], 1962 | [ 91, 186, 42 ], 1963 | [ 91, 187, 46 ], 1964 | [ 91, 187, 42 ], 1965 | [ 91, 188, 47 ], 1966 | [ 92, 186, 46 ], 1967 | [ 92, 186, 42 ], 1968 | [ 92, 187, 42 ], 1969 | [ 92, 188, 46 ], 1970 | [ 93, 186, 45 ], 1971 | [ 93, 186, 43 ], 1972 | [ 93, 187, 46 ], 1973 | [ 93, 187, 42 ], 1974 | [ 93, 188, 46 ], 1975 | [ 93, 188, 42 ], 1976 | [ 93, 189, 46 ], 1977 | [ 93, 189, 42 ], 1978 | [ 93, 190, 45 ], 1979 | [ 94, 188, 44 ], 1980 | [ 91, 193, 48 ], 1981 | [ 91, 194, 48 ], 1982 | [ 91, 195, 48 ], 1983 | [ 91, 195, 44 ], 1984 | [ 91, 196, 47 ], 1985 | [ 91, 196, 45 ], 1986 | [ 92, 193, 48 ], 1987 | [ 92, 194, 48 ], 1988 | [ 92, 194, 44 ], 1989 | [ 92, 195, 48 ], 1990 | [ 92, 196, 48 ], 1991 | [ 92, 196, 44 ], 1992 | [ 93, 192, 48 ], 1993 | [ 93, 193, 48 ], 1994 | [ 93, 193, 44 ], 1995 | [ 93, 194, 49 ], 1996 | [ 93, 195, 48 ], 1997 | [ 93, 195, 44 ], 1998 | [ 93, 196, 48 ], 1999 | [ 93, 196, 44 ], 2000 | [ 93, 197, 46 ], 2001 | [ 94, 192, 48 ], 2002 | [ 94, 192, 44 ], 2003 | [ 94, 193, 48 ], 2004 | [ 94, 193, 44 ], 2005 | [ 94, 194, 48 ], 2006 | [ 94, 195, 48 ], 2007 | [ 94, 195, 44 ], 2008 | [ 94, 196, 48 ], 2009 | [ 94, 196, 44 ], 2010 | [ 95, 192, 47 ], 2011 | [ 95, 193, 48 ], 2012 | [ 95, 193, 44 ], 2013 | [ 95, 194, 48 ], 2014 | [ 95, 194, 44 ], 2015 | [ 95, 195, 48 ], 2016 | [ 95, 195, 44 ], 2017 | [ 95, 196, 47 ], 2018 | [ 95, 196, 45 ], 2019 | [ 96, 194, 46 ], 2020 | [ 91, 189, 43 ], 2021 | [ 91, 191, 46 ], 2022 | [ 93, 188, 44 ], 2023 | [ 93, 191, 41 ], 2024 | [ 93, 193, 46 ], 2025 | [ 93, 193, 42 ], 2026 | [ 93, 194, 44 ], 2027 | [ 94, 189, 46 ], 2028 | [ 94, 189, 42 ], 2029 | [ 94, 190, 42 ], 2030 | [ 94, 191, 46 ], 2031 | [ 94, 192, 46 ], 2032 | [ 94, 192, 42 ], 2033 | [ 94, 193, 46 ], 2034 | [ 94, 193, 42 ], 2035 | [ 95, 189, 45 ], 2036 | [ 95, 189, 43 ], 2037 | [ 95, 190, 46 ], 2038 | [ 95, 190, 42 ], 2039 | [ 95, 191, 46 ], 2040 | [ 95, 191, 42 ], 2041 | [ 95, 192, 46 ], 2042 | [ 95, 192, 42 ], 2043 | [ 95, 193, 45 ], 2044 | [ 95, 193, 43 ], 2045 | [ 96, 191, 44 ], 2046 | [ 89, 184, 48 ], 2047 | [ 90, 182, 49 ], 2048 | [ 90, 183, 50 ], 2049 | [ 90, 183, 46 ], 2050 | [ 90, 184, 50 ], 2051 | [ 90, 184, 46 ], 2052 | [ 90, 185, 50 ], 2053 | [ 90, 185, 46 ], 2054 | [ 90, 186, 49 ], 2055 | [ 91, 182, 50 ], 2056 | [ 91, 182, 46 ], 2057 | [ 91, 183, 50 ], 2058 | [ 91, 183, 46 ], 2059 | [ 91, 184, 50 ], 2060 | [ 91, 184, 46 ], 2061 | [ 91, 185, 50 ], 2062 | [ 91, 185, 46 ], 2063 | [ 91, 186, 50 ], 2064 | [ 92, 181, 48 ], 2065 | [ 92, 182, 50 ], 2066 | [ 92, 182, 46 ], 2067 | [ 92, 183, 50 ], 2068 | [ 92, 183, 46 ], 2069 | [ 92, 184, 51 ], 2070 | [ 92, 184, 45 ], 2071 | [ 92, 185, 50 ], 2072 | [ 92, 185, 46 ], 2073 | [ 92, 186, 50 ], 2074 | [ 92, 187, 48 ], 2075 | [ 93, 182, 50 ], 2076 | [ 93, 182, 46 ], 2077 | [ 93, 183, 50 ], 2078 | [ 93, 183, 46 ], 2079 | [ 93, 184, 50 ], 2080 | [ 93, 184, 46 ], 2081 | [ 93, 185, 50 ], 2082 | [ 93, 185, 46 ], 2083 | [ 93, 186, 50 ], 2084 | [ 93, 186, 46 ], 2085 | [ 94, 182, 49 ], 2086 | [ 94, 183, 50 ], 2087 | [ 94, 183, 46 ], 2088 | [ 94, 184, 50 ], 2089 | [ 94, 184, 46 ], 2090 | [ 94, 185, 50 ], 2091 | [ 94, 185, 46 ], 2092 | [ 94, 186, 49 ], 2093 | [ 95, 184, 48 ], 2094 | [ 85, 187, 41 ], 2095 | [ 85, 187, 37 ], 2096 | [ 86, 190, 39 ], 2097 | [ 87, 185, 41 ], 2098 | [ 87, 185, 37 ], 2099 | [ 87, 189, 41 ], 2100 | [ 87, 189, 37 ], 2101 | [ 88, 185, 40 ], 2102 | [ 88, 185, 38 ], 2103 | [ 88, 186, 37 ], 2104 | [ 88, 187, 37 ], 2105 | [ 88, 188, 37 ], 2106 | [ 88, 189, 38 ], 2107 | [ 83, 183, 39 ], 2108 | [ 83, 183, 37 ], 2109 | [ 84, 183, 36 ], 2110 | [ 84, 186, 40 ], 2111 | [ 84, 186, 36 ], 2112 | [ 85, 182, 38 ], 2113 | [ 85, 183, 40 ], 2114 | [ 85, 183, 36 ], 2115 | [ 85, 185, 35 ], 2116 | [ 86, 183, 40 ], 2117 | [ 86, 183, 36 ], 2118 | [ 86, 184, 36 ], 2119 | [ 87, 183, 39 ], 2120 | [ 87, 183, 37 ], 2121 | [ 87, 184, 40 ], 2122 | [ 87, 184, 36 ], 2123 | [ 87, 185, 36 ], 2124 | [ 87, 186, 36 ], 2125 | [ 83, 183, 41 ], 2126 | [ 84, 183, 42 ], 2127 | [ 84, 186, 42 ], 2128 | [ 84, 186, 38 ], 2129 | [ 85, 182, 40 ], 2130 | [ 85, 183, 42 ], 2131 | [ 85, 183, 38 ], 2132 | [ 86, 183, 42 ], 2133 | [ 86, 183, 38 ], 2134 | [ 86, 184, 38 ], 2135 | [ 87, 183, 41 ], 2136 | [ 87, 184, 42 ], 2137 | [ 87, 184, 38 ], 2138 | [ 87, 186, 42 ], 2139 | [ 84, 181, 42 ], 2140 | [ 84, 181, 40 ], 2141 | [ 84, 182, 43 ], 2142 | [ 84, 182, 39 ], 2143 | [ 84, 183, 43 ], 2144 | [ 84, 183, 39 ], 2145 | [ 84, 184, 43 ], 2146 | [ 84, 184, 39 ], 2147 | [ 85, 181, 43 ], 2148 | [ 85, 181, 39 ], 2149 | [ 85, 182, 43 ], 2150 | [ 85, 182, 39 ], 2151 | [ 85, 183, 43 ], 2152 | [ 85, 183, 39 ], 2153 | [ 86, 180, 41 ], 2154 | [ 86, 181, 43 ], 2155 | [ 86, 181, 39 ], 2156 | [ 86, 182, 43 ], 2157 | [ 86, 182, 39 ], 2158 | [ 86, 183, 44 ], 2159 | [ 87, 181, 43 ], 2160 | [ 87, 181, 39 ], 2161 | [ 87, 182, 43 ], 2162 | [ 87, 182, 39 ], 2163 | [ 87, 183, 43 ], 2164 | [ 88, 181, 42 ], 2165 | [ 88, 181, 40 ], 2166 | [ 88, 182, 43 ], 2167 | [ 88, 182, 39 ], 2168 | [ 88, 183, 43 ], 2169 | [ 88, 183, 39 ], 2170 | [ 88, 185, 42 ], 2171 | [ 89, 183, 41 ], 2172 | [ 82, 182, 40 ], 2173 | [ 83, 180, 41 ], 2174 | [ 83, 180, 39 ], 2175 | [ 83, 181, 42 ], 2176 | [ 83, 181, 38 ], 2177 | [ 83, 182, 42 ], 2178 | [ 83, 182, 38 ], 2179 | [ 83, 183, 42 ], 2180 | [ 83, 183, 38 ], 2181 | [ 83, 184, 41 ], 2182 | [ 83, 184, 39 ], 2183 | [ 84, 180, 42 ], 2184 | [ 84, 180, 38 ], 2185 | [ 84, 181, 38 ], 2186 | [ 84, 182, 42 ], 2187 | [ 84, 182, 38 ], 2188 | [ 85, 179, 40 ], 2189 | [ 85, 180, 42 ], 2190 | [ 85, 180, 38 ], 2191 | [ 85, 181, 42 ], 2192 | [ 85, 181, 38 ], 2193 | [ 85, 182, 37 ], 2194 | [ 86, 180, 42 ], 2195 | [ 86, 180, 38 ], 2196 | [ 86, 181, 42 ], 2197 | [ 86, 181, 38 ], 2198 | [ 86, 182, 42 ], 2199 | [ 86, 182, 38 ], 2200 | [ 87, 180, 41 ], 2201 | [ 87, 180, 39 ], 2202 | [ 87, 181, 42 ], 2203 | [ 87, 181, 38 ], 2204 | [ 87, 182, 42 ], 2205 | [ 87, 182, 38 ], 2206 | [ 87, 183, 42 ], 2207 | [ 87, 183, 38 ], 2208 | [ 87, 184, 41 ], 2209 | [ 88, 182, 40 ], 2210 | [ 85, 182, 41 ], 2211 | [ 85, 183, 44 ], 2212 | [ 85, 184, 44 ], 2213 | [ 85, 185, 44 ], 2214 | [ 86, 182, 44 ], 2215 | [ 86, 182, 40 ], 2216 | [ 86, 184, 44 ], 2217 | [ 86, 185, 44 ], 2218 | [ 87, 182, 44 ], 2219 | [ 87, 182, 40 ], 2220 | [ 87, 183, 44 ], 2221 | [ 87, 183, 40 ], 2222 | [ 87, 184, 45 ], 2223 | [ 87, 186, 44 ], 2224 | [ 87, 187, 42 ], 2225 | [ 88, 182, 44 ], 2226 | [ 88, 183, 44 ], 2227 | [ 88, 183, 40 ], 2228 | [ 88, 184, 44 ], 2229 | [ 88, 185, 44 ], 2230 | [ 88, 186, 40 ], 2231 | [ 89, 182, 43 ], 2232 | [ 89, 182, 41 ], 2233 | [ 89, 183, 44 ], 2234 | [ 89, 183, 40 ], 2235 | [ 89, 184, 44 ], 2236 | [ 89, 184, 40 ], 2237 | [ 89, 185, 44 ], 2238 | [ 89, 185, 40 ], 2239 | [ 80, 183, 38 ], 2240 | [ 81, 181, 39 ], 2241 | [ 81, 181, 37 ], 2242 | [ 81, 182, 40 ], 2243 | [ 81, 182, 36 ], 2244 | [ 81, 183, 40 ], 2245 | [ 81, 183, 36 ], 2246 | [ 81, 184, 40 ], 2247 | [ 81, 184, 36 ], 2248 | [ 81, 185, 39 ], 2249 | [ 81, 185, 37 ], 2250 | [ 82, 181, 40 ], 2251 | [ 82, 181, 36 ], 2252 | [ 82, 182, 36 ], 2253 | [ 82, 183, 40 ], 2254 | [ 82, 183, 36 ], 2255 | [ 82, 184, 40 ], 2256 | [ 82, 184, 36 ], 2257 | [ 83, 180, 38 ], 2258 | [ 83, 181, 40 ], 2259 | [ 83, 181, 36 ], 2260 | [ 83, 182, 40 ], 2261 | [ 83, 182, 36 ], 2262 | [ 83, 183, 35 ], 2263 | [ 84, 181, 36 ], 2264 | [ 84, 182, 40 ], 2265 | [ 84, 182, 36 ], 2266 | [ 85, 181, 37 ], 2267 | [ 85, 182, 36 ], 2268 | [ 81, 184, 39 ], 2269 | [ 82, 182, 38 ], 2270 | [ 82, 183, 41 ], 2271 | [ 82, 183, 37 ], 2272 | [ 82, 185, 41 ], 2273 | [ 82, 185, 37 ], 2274 | [ 83, 182, 41 ], 2275 | [ 83, 182, 37 ], 2276 | [ 83, 184, 37 ], 2277 | [ 83, 185, 41 ], 2278 | [ 83, 185, 37 ], 2279 | [ 84, 181, 39 ], 2280 | [ 84, 182, 41 ], 2281 | [ 84, 182, 37 ], 2282 | [ 84, 183, 41 ], 2283 | [ 84, 183, 37 ], 2284 | [ 85, 183, 41 ], 2285 | [ 85, 183, 37 ], 2286 | [ 85, 184, 37 ], 2287 | [ 86, 183, 37 ], 2288 | [ 81, 184, 41 ], 2289 | [ 82, 182, 42 ], 2290 | [ 82, 183, 43 ], 2291 | [ 82, 183, 39 ], 2292 | [ 82, 184, 43 ], 2293 | [ 82, 185, 43 ], 2294 | [ 82, 185, 39 ], 2295 | [ 83, 182, 43 ], 2296 | [ 83, 182, 39 ], 2297 | [ 83, 183, 43 ], 2298 | [ 83, 184, 43 ], 2299 | [ 83, 185, 43 ], 2300 | [ 83, 185, 39 ], 2301 | [ 84, 181, 41 ], 2302 | [ 84, 184, 44 ], 2303 | [ 86, 183, 43 ], 2304 | [ 86, 183, 39 ], 2305 | [ 87, 187, 38 ], 2306 | [ 88, 183, 42 ], 2307 | [ 88, 183, 38 ], 2308 | [ 88, 184, 38 ], 2309 | [ 88, 186, 42 ], 2310 | [ 88, 187, 42 ], 2311 | [ 88, 187, 38 ], 2312 | [ 89, 183, 39 ], 2313 | [ 89, 184, 42 ], 2314 | [ 89, 184, 38 ], 2315 | [ 89, 185, 42 ], 2316 | [ 89, 185, 38 ], 2317 | [ 89, 186, 38 ], 2318 | [ 89, 187, 41 ], 2319 | [ 90, 185, 40 ], 2320 | [ 89, 193, 46 ], 2321 | [ 89, 194, 46 ], 2322 | [ 89, 195, 46 ], 2323 | [ 89, 196, 45 ], 2324 | [ 89, 196, 43 ], 2325 | [ 90, 195, 46 ], 2326 | [ 90, 195, 42 ], 2327 | [ 90, 196, 46 ], 2328 | [ 90, 196, 42 ], 2329 | [ 91, 195, 46 ], 2330 | [ 91, 195, 42 ], 2331 | [ 91, 196, 46 ], 2332 | [ 91, 196, 42 ], 2333 | [ 92, 194, 46 ], 2334 | [ 92, 194, 42 ], 2335 | [ 92, 195, 46 ], 2336 | [ 92, 196, 46 ], 2337 | [ 92, 196, 42 ], 2338 | [ 93, 192, 45 ], 2339 | [ 93, 194, 46 ], 2340 | [ 93, 194, 42 ], 2341 | [ 93, 195, 46 ], 2342 | [ 93, 195, 42 ], 2343 | [ 93, 196, 45 ], 2344 | [ 84, 184, 41 ], 2345 | [ 84, 184, 37 ], 2346 | [ 85, 181, 41 ], 2347 | [ 86, 180, 39 ], 2348 | [ 86, 181, 41 ], 2349 | [ 86, 181, 37 ], 2350 | [ 86, 182, 41 ], 2351 | [ 86, 182, 37 ], 2352 | [ 87, 181, 41 ], 2353 | [ 87, 181, 37 ], 2354 | [ 87, 182, 41 ], 2355 | [ 87, 182, 37 ], 2356 | [ 87, 184, 37 ], 2357 | [ 88, 181, 38 ], 2358 | [ 88, 182, 41 ], 2359 | [ 88, 182, 37 ], 2360 | [ 88, 183, 37 ], 2361 | [ 88, 184, 41 ], 2362 | [ 88, 184, 37 ], 2363 | [ 83, 178, 46 ], 2364 | [ 84, 176, 47 ], 2365 | [ 84, 176, 45 ], 2366 | [ 84, 177, 48 ], 2367 | [ 84, 177, 44 ], 2368 | [ 84, 178, 48 ], 2369 | [ 84, 178, 44 ], 2370 | [ 84, 179, 48 ], 2371 | [ 84, 179, 44 ], 2372 | [ 84, 180, 47 ], 2373 | [ 84, 180, 45 ], 2374 | [ 85, 176, 48 ], 2375 | [ 85, 176, 44 ], 2376 | [ 85, 177, 48 ], 2377 | [ 85, 177, 44 ], 2378 | [ 85, 178, 48 ], 2379 | [ 85, 178, 44 ], 2380 | [ 85, 179, 48 ], 2381 | [ 85, 179, 44 ], 2382 | [ 85, 180, 48 ], 2383 | [ 85, 180, 44 ], 2384 | [ 86, 175, 46 ], 2385 | [ 86, 176, 48 ], 2386 | [ 86, 176, 44 ], 2387 | [ 86, 177, 48 ], 2388 | [ 86, 177, 44 ], 2389 | [ 86, 178, 49 ], 2390 | [ 86, 178, 43 ], 2391 | [ 86, 179, 48 ], 2392 | [ 86, 179, 44 ], 2393 | [ 86, 180, 48 ], 2394 | [ 86, 180, 44 ], 2395 | [ 86, 181, 46 ], 2396 | [ 87, 176, 48 ], 2397 | [ 87, 176, 44 ], 2398 | [ 87, 177, 48 ], 2399 | [ 87, 177, 44 ], 2400 | [ 87, 178, 48 ], 2401 | [ 87, 178, 44 ], 2402 | [ 87, 179, 48 ], 2403 | [ 87, 179, 44 ], 2404 | [ 87, 180, 48 ], 2405 | [ 87, 180, 44 ], 2406 | [ 88, 176, 47 ], 2407 | [ 88, 176, 45 ], 2408 | [ 88, 177, 48 ], 2409 | [ 88, 177, 44 ], 2410 | [ 88, 178, 48 ], 2411 | [ 88, 178, 44 ], 2412 | [ 88, 179, 48 ], 2413 | [ 88, 179, 44 ], 2414 | [ 88, 180, 47 ], 2415 | [ 88, 180, 45 ], 2416 | [ 89, 178, 46 ], 2417 | [ 78, 181, 36 ], 2418 | [ 79, 179, 37 ], 2419 | [ 79, 179, 35 ], 2420 | [ 79, 180, 38 ], 2421 | [ 79, 180, 34 ], 2422 | [ 79, 181, 38 ], 2423 | [ 79, 181, 34 ], 2424 | [ 79, 182, 38 ], 2425 | [ 79, 182, 34 ], 2426 | [ 79, 183, 37 ], 2427 | [ 79, 183, 35 ], 2428 | [ 80, 179, 38 ], 2429 | [ 80, 179, 34 ], 2430 | [ 80, 180, 38 ], 2431 | [ 80, 180, 34 ], 2432 | [ 80, 181, 38 ], 2433 | [ 80, 181, 34 ], 2434 | [ 80, 182, 38 ], 2435 | [ 80, 182, 34 ], 2436 | [ 80, 183, 34 ], 2437 | [ 81, 178, 36 ], 2438 | [ 81, 179, 38 ], 2439 | [ 81, 179, 34 ], 2440 | [ 81, 180, 38 ], 2441 | [ 81, 180, 34 ], 2442 | [ 81, 181, 33 ], 2443 | [ 81, 182, 38 ], 2444 | [ 81, 182, 34 ], 2445 | [ 81, 183, 38 ], 2446 | [ 81, 183, 34 ], 2447 | [ 82, 179, 38 ], 2448 | [ 82, 179, 34 ], 2449 | [ 82, 180, 38 ], 2450 | [ 82, 180, 34 ], 2451 | [ 82, 181, 38 ], 2452 | [ 82, 181, 34 ], 2453 | [ 82, 182, 34 ], 2454 | [ 82, 183, 38 ], 2455 | [ 82, 183, 34 ], 2456 | [ 83, 179, 37 ], 2457 | [ 83, 179, 35 ], 2458 | [ 83, 180, 34 ], 2459 | [ 83, 181, 34 ], 2460 | [ 83, 182, 34 ], 2461 | [ 81, 178, 39 ], 2462 | [ 81, 178, 37 ], 2463 | [ 81, 179, 40 ], 2464 | [ 81, 179, 36 ], 2465 | [ 81, 180, 40 ], 2466 | [ 81, 180, 36 ], 2467 | [ 81, 181, 40 ], 2468 | [ 81, 181, 36 ], 2469 | [ 81, 182, 39 ], 2470 | [ 81, 182, 37 ], 2471 | [ 82, 178, 40 ], 2472 | [ 82, 178, 36 ], 2473 | [ 82, 179, 40 ], 2474 | [ 82, 179, 36 ], 2475 | [ 82, 180, 40 ], 2476 | [ 82, 180, 36 ], 2477 | [ 83, 177, 38 ], 2478 | [ 83, 178, 40 ], 2479 | [ 83, 178, 36 ], 2480 | [ 83, 179, 40 ], 2481 | [ 83, 179, 36 ], 2482 | [ 83, 180, 35 ], 2483 | [ 84, 178, 40 ], 2484 | [ 84, 178, 36 ], 2485 | [ 84, 179, 40 ], 2486 | [ 84, 179, 36 ], 2487 | [ 84, 180, 40 ], 2488 | [ 84, 180, 36 ], 2489 | [ 85, 178, 39 ], 2490 | [ 85, 178, 37 ], 2491 | [ 85, 179, 36 ], 2492 | [ 85, 180, 40 ], 2493 | [ 85, 180, 36 ], 2494 | [ 85, 181, 40 ], 2495 | [ 85, 181, 36 ], 2496 | [ 82, 180, 41 ], 2497 | [ 82, 180, 37 ], 2498 | [ 82, 181, 41 ], 2499 | [ 82, 181, 37 ], 2500 | [ 82, 182, 41 ], 2501 | [ 82, 182, 37 ], 2502 | [ 83, 179, 41 ], 2503 | [ 83, 180, 37 ], 2504 | [ 83, 181, 41 ], 2505 | [ 83, 181, 37 ], 2506 | [ 84, 178, 39 ], 2507 | [ 84, 179, 41 ], 2508 | [ 84, 179, 37 ], 2509 | [ 84, 180, 41 ], 2510 | [ 84, 180, 37 ], 2511 | [ 85, 179, 41 ], 2512 | [ 85, 179, 37 ], 2513 | [ 85, 180, 41 ], 2514 | [ 85, 180, 37 ], 2515 | [ 86, 179, 40 ], 2516 | [ 86, 179, 38 ], 2517 | [ 86, 180, 37 ], 2518 | [ 82, 177, 39 ], 2519 | [ 83, 175, 40 ], 2520 | [ 83, 175, 38 ], 2521 | [ 83, 176, 41 ], 2522 | [ 83, 176, 37 ], 2523 | [ 83, 177, 41 ], 2524 | [ 83, 177, 37 ], 2525 | [ 83, 178, 41 ], 2526 | [ 83, 178, 37 ], 2527 | [ 83, 179, 38 ], 2528 | [ 84, 175, 41 ], 2529 | [ 84, 175, 37 ], 2530 | [ 84, 176, 41 ], 2531 | [ 84, 176, 37 ], 2532 | [ 84, 177, 41 ], 2533 | [ 84, 177, 37 ], 2534 | [ 84, 178, 41 ], 2535 | [ 84, 178, 37 ], 2536 | [ 85, 174, 39 ], 2537 | [ 85, 175, 41 ], 2538 | [ 85, 175, 37 ], 2539 | [ 85, 176, 41 ], 2540 | [ 85, 176, 37 ], 2541 | [ 85, 177, 42 ], 2542 | [ 85, 177, 36 ], 2543 | [ 85, 178, 41 ], 2544 | [ 85, 180, 39 ], 2545 | [ 86, 175, 41 ], 2546 | [ 86, 175, 37 ], 2547 | [ 86, 176, 41 ], 2548 | [ 86, 176, 37 ], 2549 | [ 86, 177, 41 ], 2550 | [ 86, 177, 37 ], 2551 | [ 86, 178, 41 ], 2552 | [ 86, 178, 37 ], 2553 | [ 86, 179, 41 ], 2554 | [ 86, 179, 37 ], 2555 | [ 87, 175, 40 ], 2556 | [ 87, 175, 38 ], 2557 | [ 87, 176, 41 ], 2558 | [ 87, 176, 37 ], 2559 | [ 87, 177, 41 ], 2560 | [ 87, 177, 37 ], 2561 | [ 87, 178, 41 ], 2562 | [ 87, 178, 37 ], 2563 | [ 87, 179, 40 ], 2564 | [ 87, 179, 38 ], 2565 | [ 88, 177, 39 ], 2566 | [ 84, 176, 39 ], 2567 | [ 84, 177, 42 ], 2568 | [ 84, 177, 38 ], 2569 | [ 84, 178, 42 ], 2570 | [ 84, 178, 38 ], 2571 | [ 84, 179, 42 ], 2572 | [ 84, 179, 38 ], 2573 | [ 84, 180, 39 ], 2574 | [ 85, 176, 42 ], 2575 | [ 85, 176, 38 ], 2576 | [ 85, 177, 38 ], 2577 | [ 85, 178, 42 ], 2578 | [ 85, 178, 38 ], 2579 | [ 85, 179, 42 ], 2580 | [ 85, 179, 38 ], 2581 | [ 86, 175, 40 ], 2582 | [ 86, 176, 42 ], 2583 | [ 86, 176, 38 ], 2584 | [ 86, 177, 42 ], 2585 | [ 86, 177, 38 ], 2586 | [ 86, 179, 42 ], 2587 | [ 86, 181, 40 ], 2588 | [ 87, 176, 42 ], 2589 | [ 87, 176, 38 ], 2590 | [ 87, 177, 42 ], 2591 | [ 87, 177, 38 ], 2592 | [ 87, 178, 42 ], 2593 | [ 87, 178, 38 ], 2594 | [ 87, 179, 42 ], 2595 | [ 87, 180, 42 ], 2596 | [ 87, 180, 38 ], 2597 | [ 88, 176, 41 ], 2598 | [ 88, 176, 39 ], 2599 | [ 88, 177, 42 ], 2600 | [ 88, 177, 38 ], 2601 | [ 88, 178, 42 ], 2602 | [ 88, 178, 38 ], 2603 | [ 88, 179, 42 ], 2604 | [ 88, 179, 38 ], 2605 | [ 88, 180, 41 ], 2606 | [ 88, 180, 39 ], 2607 | [ 89, 178, 40 ], 2608 | [ 80, 178, 39 ], 2609 | [ 81, 176, 40 ], 2610 | [ 81, 176, 38 ], 2611 | [ 81, 177, 41 ], 2612 | [ 81, 177, 37 ], 2613 | [ 81, 178, 41 ], 2614 | [ 81, 179, 41 ], 2615 | [ 81, 179, 37 ], 2616 | [ 82, 176, 41 ], 2617 | [ 82, 176, 37 ], 2618 | [ 82, 177, 41 ], 2619 | [ 82, 177, 37 ], 2620 | [ 82, 178, 41 ], 2621 | [ 82, 178, 37 ], 2622 | [ 82, 179, 41 ], 2623 | [ 82, 179, 37 ], 2624 | [ 83, 175, 39 ], 2625 | [ 83, 178, 42 ], 2626 | [ 83, 181, 39 ], 2627 | [ 85, 176, 40 ], 2628 | [ 85, 177, 41 ], 2629 | [ 85, 177, 37 ], 2630 | [ 86, 178, 39 ], 2631 | [ 84, 179, 43 ], 2632 | [ 84, 179, 39 ], 2633 | [ 84, 180, 43 ], 2634 | [ 84, 181, 43 ], 2635 | [ 85, 178, 43 ], 2636 | [ 85, 179, 43 ], 2637 | [ 85, 179, 39 ], 2638 | [ 85, 180, 43 ], 2639 | [ 86, 179, 43 ], 2640 | [ 86, 179, 39 ], 2641 | [ 87, 178, 43 ], 2642 | [ 87, 178, 39 ], 2643 | [ 87, 179, 43 ], 2644 | [ 87, 179, 39 ], 2645 | [ 87, 180, 43 ], 2646 | [ 88, 178, 40 ], 2647 | [ 88, 179, 43 ], 2648 | [ 88, 179, 39 ], 2649 | [ 88, 180, 43 ], 2650 | [ 88, 181, 43 ], 2651 | [ 88, 181, 39 ], 2652 | [ 88, 182, 42 ], 2653 | [ 89, 180, 41 ], 2654 | [ 82, 178, 42 ], 2655 | [ 82, 178, 38 ], 2656 | [ 82, 179, 42 ], 2657 | [ 82, 180, 42 ], 2658 | [ 82, 181, 39 ], 2659 | [ 83, 177, 42 ], 2660 | [ 83, 178, 38 ], 2661 | [ 83, 179, 42 ], 2662 | [ 83, 180, 42 ], 2663 | [ 84, 176, 40 ], 2664 | [ 86, 177, 39 ], 2665 | [ 86, 178, 42 ], 2666 | [ 86, 178, 38 ] ] 2667 | -------------------------------------------------------------------------------- /test/issue38.json: -------------------------------------------------------------------------------- 1 | [ 2 | [1, 1, 1], 3 | [1, 1, -1], 4 | [1, -1, 1], 5 | [1, -1, -1], 6 | [-1, 1, -1], 7 | [-1, 1, 1], 8 | [-1, -1, -1], 9 | [-1, -1, 1], 10 | [-1, 1, -1], 11 | [1, 1, -1], 12 | [-1, 1, 1], 13 | [1, 1, 1], 14 | [-1, -1, 1], 15 | [1, -1, 1], 16 | [-1, -1, -1], 17 | [1, -1, -1], 18 | [-1, 1, 1], 19 | [1, 1, 1], 20 | [-1, -1, 1], 21 | [1, -1, 1], 22 | [1, 1, -1], 23 | [-1, 1, -1], 24 | [1, -1, -1], 25 | [-1, -1, -1] 26 | ] 27 | -------------------------------------------------------------------------------- /test/issue5.json: -------------------------------------------------------------------------------- 1 | [ [ 38, 89, 0 ], 2 | [ 85, 91, 0 ], 3 | [ 94, 89, 0 ], 4 | [ 70, 40, 0 ], 5 | [ 63, 90, 0 ], 6 | [ 60, 52, 0 ], 7 | [ 20, 16, 0 ], 8 | [ 38, 13, 0 ], 9 | [ 25, 82, 0 ], 10 | [ 7, 80, 0 ], 11 | [ 28, 80, 0 ], 12 | [ 43, 64, 0 ], 13 | [ 48, 68, 0 ], 14 | [ 86, 64, 0 ], 15 | [ 52, 60, 0 ], 16 | [ 56, 75, 0 ], 17 | [ 47, 3, 0 ], 18 | [ 68, 13, 0 ], 19 | [ 50, 44, 0 ], 20 | [ 82, 54, 0 ], 21 | [ 96, 45, 0 ], 22 | [ 85, 1, 0 ], 23 | [ 30, 24, 0 ], 24 | [ 54, 3, 0 ], 25 | [ 65, 95, 0 ], 26 | [ 95, 7, 0 ], 27 | [ 97, 42, 0 ], 28 | [ 44, 73, 0 ], 29 | [ 79, 55, 0 ], 30 | [ 72, 34, 0 ], 31 | [ 8, 18, 0 ], 32 | [ 79, 46, 0 ], 33 | [ 92, 32, 0 ], 34 | [ 61, 29, 0 ], 35 | [ 21, 7, 0 ], 36 | [ 24, 25, 0 ], 37 | [ 41, 32, 0 ], 38 | [ 72, 91, 0 ], 39 | [ 1, 63, 0 ], 40 | [ 18, 8, 0 ], 41 | [ 44, 99, 0 ], 42 | [ 59, 90, 0 ], 43 | [ 12, 15, 0 ], 44 | [ 81, 60, 0 ], 45 | [ 21, 10, 0 ], 46 | [ 9, 71, 0 ], 47 | [ 12, 86, 0 ], 48 | [ 99, 41, 0 ], 49 | [ 32, 48, 0 ], 50 | [ 40, 20, 0 ], 51 | [ 47, 89, 0 ], 52 | [ 41, 9, 0 ], 53 | [ 29, 80, 0 ], 54 | [ 56, 63, 0 ], 55 | [ 36, 11, 0 ], 56 | [ 25, 14, 0 ], 57 | [ 44, 40, 0 ], 58 | [ 60, 41, 0 ], 59 | [ 32, 81, 0 ], 60 | [ 64, 56, 0 ], 61 | [ 41, 90, 0 ], 62 | [ 29, 73, 0 ], 63 | [ 68, 97, 0 ], 64 | [ 91, 90, 0 ], 65 | [ 57, 0, 0 ], 66 | [ 67, 34, 0 ], 67 | [ 64, 8, 0 ], 68 | [ 66, 65, 0 ], 69 | [ 39, 90, 0 ], 70 | [ 72, 62, 0 ], 71 | [ 63, 13, 0 ], 72 | [ 45, 94, 0 ], 73 | [ 2, 34, 0 ] ] 74 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "preserve", 5 | "moduleResolution": "bundler", 6 | "esModuleInterop": true, 7 | "allowJs": true, 8 | "allowSyntheticDefaultImports": true, 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | "declaration": true, 13 | "paths": { 14 | "*": ["node_modules/*"] 15 | }, 16 | "typeRoots": [ 17 | "src/types", 18 | "node_modules/@types" 19 | ] 20 | }, 21 | "include": ["./src/**/*.ts", "./src/**/*.js", "./src/**/*.d.ts"], 22 | "exclude": ["src/stories/**/*"] 23 | } 24 | -------------------------------------------------------------------------------- /webpack.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | 6 | const plugins = [] 7 | if (isProduction) { 8 | plugins.push(new webpack.NormalModuleReplacementPlugin(/debug/, './debug.ts')) 9 | } 10 | 11 | module.exports = { 12 | entry: './src/index.ts', 13 | mode: isProduction ? 'production' : 'development', 14 | devtool: isProduction ? 'nosources-source-map' : 'inline-source-map', 15 | experiments: { 16 | outputModule: true 17 | }, 18 | output: { 19 | path: path.resolve(__dirname, 'dist'), 20 | filename: 'quickhull3d.js', 21 | library: { 22 | type: 'module' 23 | } 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.[jt]sx?$/, 29 | loader: 'ts-loader', 30 | exclude: /node_modules/ 31 | } 32 | ] 33 | }, 34 | resolve: { 35 | extensions: ['.tsx', '.ts', '.js'] 36 | }, 37 | plugins 38 | } 39 | --------------------------------------------------------------------------------