├── .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 | [](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 | [](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 | [](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 | [](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 |
--------------------------------------------------------------------------------