├── .github └── FUNDING.yml ├── .gitignore ├── CHANGELOG.md ├── CREDITS.md ├── LICENSE.md ├── README.md ├── Test.hxproj ├── assets ├── 9x9_holed_square.png ├── big.png ├── bord.png ├── complex.png ├── horz_line.png ├── issue11.png ├── issue12.png ├── line.png ├── nazca_monkey.png ├── opaque_black.png ├── opaque_white.png ├── openfl.svg ├── pirate_small.png ├── py_figure.png ├── small_rect.png ├── star.png ├── super_mario.png ├── text.png └── transparent.png ├── bin └── html5 │ └── bin │ ├── assets │ ├── 9x9_holed_square.png │ ├── big.png │ ├── bord.png │ ├── complex.png │ ├── horz_line.png │ ├── issue11.png │ ├── issue12.png │ ├── line.png │ ├── nazca_monkey.png │ ├── opaque_black.png │ ├── opaque_white.png │ ├── pirate_small.png │ ├── py_figure.png │ ├── small_rect.png │ ├── star.png │ ├── super_mario.png │ ├── text.png │ └── transparent.png │ ├── favicon.png │ ├── index.html │ └── manifest │ └── default.json ├── docs ├── OpenflDemo.js ├── OpenflDemo.js.map ├── OpenflDemo.swf ├── assets │ ├── 9x9_holed_square.png │ ├── big.png │ ├── bord.png │ ├── complex.png │ ├── horz_line.png │ ├── issue11.png │ ├── issue12.png │ ├── line.png │ ├── nazca_monkey.png │ ├── opaque_black.png │ ├── opaque_white.png │ ├── pirate_small.png │ ├── py_figure.png │ ├── small_rect.png │ ├── star.png │ ├── super_mario.png │ ├── text.png │ └── transparent.png ├── favicon.png ├── index.html └── manifest │ └── default.json ├── openflDemo.hxproj ├── project.xml ├── screenshot.png ├── src ├── DrawUtils.hx ├── GeomAlgoTest.hx ├── OpenflDemo.hx ├── Test.hx └── hxGeomAlgo │ ├── Bayazit.hx │ ├── CCLabeler.hx │ ├── Chaikin.hx │ ├── Debug.hx │ ├── EarCut.hx │ ├── Heap.hx │ ├── HertelMehlhorn.hx │ ├── HomogCoord.hx │ ├── HxPoint.hx │ ├── IsoContours.hx │ ├── MarchingSquares.hx │ ├── PairDeque.hx │ ├── PoleOfInaccessibility.hx │ ├── PolyTools.hx │ ├── RamerDouglasPeucker.hx │ ├── SnoeyinkKeil.hx │ ├── Tess2.hx │ ├── Version.hx │ ├── Visibility.hx │ ├── VisvalingamWhyatt.hx │ └── WuYongZhang.hx └── test.xml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [azrafe7] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build and Release Folders 2 | bin/ 3 | bin-debug/ 4 | bin-release/ 5 | 6 | # Other files and folders 7 | .settings/ 8 | 9 | # Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties` 10 | # should NOT be excluded as they contain compiler settings and other important 11 | # information for Eclipse / Flash Builder. 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.0.x (from 16 feb 2014) 2 | - Added first impl. of MarchingSquares and RamerDouglasPeucker 3 | - MarchingSquares: Fixed bug where it wouldn't work on fully opaque bitmapdata 4 | - MarchingSquares: Added alpha threshold parameter 5 | - MarchingSquares: Recognize illegal states 6 | - MarchingSquares: Added clipRect support 7 | - RDP: Fixed bug where it wouldn't work on circle-like polylines 8 | - RDP: Ported Grumdrig's distanceToSegment to use in place of Karthaus' perpendicularDistance 9 | - Added OpenFL demo 10 | - Added EarClipper 11 | - EarClipper: Support array of points instead of flat arrays 12 | - EarClipper: Refactored to single class 13 | - Added Bayazit poly decomp 14 | - Added PolyTools class and refactored common code 15 | - Added initial code for Keil decomposition 16 | - Added first impl. of Visibility polygon 17 | - Visibility: added HomogCoord class 18 | - Visibility: Fixes for points discovery on polygon edges 19 | - Visibility: Refactored into static class 20 | - SnoeyinkKeil: first impl. 21 | - SnoeyinkKeil: finished coding missing parts/fixed bugs 22 | - Added abstract HxPoint 23 | - Added initial impl. of Connected Components Labeling 24 | - CCLabeler: refactored and fixed various bugs 25 | - CCLabeler: added 4-connectivity option 26 | - MarchingSquares: fixed bug causing duplicate points 27 | - Added first impl. of Visvalingam-Whyatt simplification (+ MinHeap) 28 | - Visv-Whyatt: completed and improved 29 | - Tess2: ported Mikko Mononen's tess2.js 30 | - PolyTools adds conversion to/from fl(o)at array and switches to distanceSquared in segmentIntersect 31 | - CCLabeler: improved performance swapping ByteArray with Vector 32 | - CCLabeler: added areaMap to store components' area while labeling 33 | - MarchingSquares: switching to Vector (vs ByteArray) 34 | - Introducing simple Debug.assert() in the codebase and compiler defines (GEOM\_CHECKS/NO\_GEOM\_CHECKS) 35 | - Bug fixes for some degenerate cases (poly.length < 3) 36 | 37 | ### 0.1.0 (from 29 sep 2014) 38 | - Version class added (starting at 0.1.0) 39 | - MarchingSquares: fixing fully opaque bitmapdata again 40 | - Using PosInfos in Debug.assert() 41 | - RDP: setting epsilon below 1 now exits without doing any simplification 42 | 43 | ### 0.2.0 (from 21 may 2015) w/ breaking changes 44 | - Compatible with new haxe 3.2.0 release onward (!) 45 | - MarchingSquares & CCLabeler: switched to hxPixels in place of BitmapData (removing openfl direct dependency) 46 | - MarchingSquares & CCLabeler: dropped support for clipRect 47 | - CCLabeler: minor improvements 48 | - Swapped UInt to Int (again ;) 49 | - Updated demo and added timings for the various algorithms 50 | - PairDeque: fix? edge case 51 | - Visv-Whyatt: changes/fixes to MinHeap, also improving performance 52 | - Refactored Heap into its own class 53 | - PolyTools: fixed and improved findDuplicatePoints(), added flatten() 54 | - Tess2: shortcut methods for boolean ops on polygons 55 | - MarchingSquares: force output inside source boundaries, and fix findStartPoint() 56 | - IsoContours: initial implementation 57 | - Add hxPixels dependency 58 | - Add some interactivity to openfl demo (and more test images) 59 | - IsoContours: improve performance (esp. AdjacencyMap) 60 | - Tess2: experimental Delaunay refinement 61 | - Snoeyink-Keil: fix issue #11 (related to polygon reconstruction by diagonals) 62 | - PolyTools: some additions and small changes 63 | - EarCut: ported from mapbox/earcut 64 | - EarClipper: ditched in favor of EarCut 65 | - Bayazit: fixed bug with some polys (by looking at how dyn4j solved it) 66 | - -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | **hxGeomAlgo** is based on: 2 | 3 | - HaxeFoundation (http://haxe-foundation.org/) 4 | - OpenFL (http://www.openfl.org/) 5 | - Marching Squares (Contour Tracing) 6 | - https://web.archive.org/web/20180316055432/http://devblog.phillipspiess.com/better%20know%20an%20algorithm/2010/02/23/better-know-marching-squares.html (Phil Spiess) 7 | - http://www.tomgibara.com/computer-vision/marching-squares (Tom Gibara) 8 | - Ramer-Douglas-Peucker (Polyline Simplification) 9 | - http://karthaus.nl/rdp/ (Marius Karthaus) 10 | - http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment (Grumdrig) 11 | - Ear Clipping (Triangulation and Poly Decomposition) 12 | - https://github.com/mapbox/earcut (Vladimir Agafonkin) 13 | - http://www.ewjordan.com/earClip/ (Java - by Eric Jordan) 14 | - Bayazit (Poly Decomposition) 15 | - https://web.archive.org/web/20140701210122/http://mnbayazit.com/406/bayazit (Mark Bayazit) 16 | - http://mnbayazit.com/406/credit 17 | - http://www.dyn4j.org/ (Java - by William Bittle) 18 | - Visibilty Polygon and Homogeneous Coords (2D) 19 | - http://www.cs.ubc.ca/~snoeyink/demos/convdecomp/VPDemo.html (Jack Snoeyink) 20 | - Snoeyink-Keil (Minimum Convex Decomposition) 21 | - http://www.cs.ubc.ca/~snoeyink/demos/convdecomp/MCDDemo.html (Jack Snoeyink & Mark Keil) 22 | - [J. Mark Keil](http://www.informatik.uni-trier.de/~ley/pers/hd/k/Keil:J=_Mark), [Jack Snoeyink](http://www.informatik.uni-trier.de/~ley/pers/hd/s/Snoeyink:Jack.html): On the Time Bound for Convex Decomposition of Simple Polygons. [Int. J. Comput. Geometry Appl. 12](http://www.informatik.uni-trier.de/~ley/db/journals/ijcga/ijcga12.html#KeilS02)(3): 181-192 (2002) 23 | - Connected Components Labeling and Contour Tracing (w/ Holes) 24 | - Fu Chang, Chun-jen Chen, Chi-jen Lu: [A linear-time component-labeling algorithm using contour tracing technique](http://www.iis.sinica.edu.tw/papers/fchang/1362-F.pdf) (2004) 25 | - Visvalingam-Whyatt (Polyline Simplification) 26 | - Visvalingam M., Whyatt J. D.: [Line generalisation by repeated elimination of the smallest area](https://hydra.hull.ac.uk/resources/hull:8338) (1992) 27 | - http://bost.ocks.org/mike/simplify/ (Mike Bostock) 28 | - https://github.com/jonasmalacofilho/dheap (Jonas Malaco Filho) 29 | - http://en.wikipedia.org/wiki/Binary_heap (Binary (Min)Heap) 30 | - Tess2 (Tesselation to Triangles and Convex Polygons, Poly Boolean Ops) 31 | - GLU Libtess (by Eric Veach, July 1994) 32 | - https://github.com/memononen/tess2.js (Mikko Mononen, Aug 2013) 33 | - IsoContours (Contour Tracing) 34 | - http://en.wikipedia.org/wiki/Marching_squares 35 | - https://github.com/deltaluca/nape (Luca Deltodesco) 36 | - https://github.com/scikit-image/scikit-image (scikit-image team) 37 | - Hertel-Mehlhorn (Convex Polygons from Arbitrary Triangulation) 38 | - https://github.com/ivanfratric/polypartition (Ivan Fratric) 39 | - https://web.archive.org/web/20140102033642/http://www.philvaz.com/compgeom/ (Phil Porvaznik) 40 | - Chaikin (Recursive Curve Smoothing) 41 | - George Merrill Chaikin: [An algorithm for high-speed curve generation](https://sci-hub.st/10.1016/0146-664X(74)90028-8) (1974) 42 | - https://sighack.com/post/chaikin-curves (Manohar Vanga) 43 | - Wu-Yong-Zhang-Zhang (Multi-step Chaikin Curve Smoothing) 44 | - Ling Wu, Jun-Hai Yong, You-Wei Zhang, and Li Zhang: [Multi-step Subdivision Algorithm for Chaikin Curves](https://sci-hub.st/10.1007/978-3-540-30497-5_188) (2004) 45 | - http://win.doomitalia.it/varie/chaikin.pdf (Fabio Roman, 2009) 46 | 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## Credits 2 | 3 | **hxGeomAlgo** is based on the work of many developers and it wouldn't exist if it weren't for them. See the [CREDITS](CREDITS.md) file for a full list. 4 | 5 | ## License 6 | 7 | **hxGeomAlgo** is developed by Giuseppe Di Mauro (azrafe7) and all code is released under the MIT license unless specified otherwise. 8 | 9 | > The MIT License (MIT) 10 | > 11 | > Copyright (c) 2013-2016 Giuseppe Di Mauro (azrafe7) 12 | > 13 | > Permission is hereby granted, free of charge, to any person obtaining a copy 14 | > of this software and associated documentation files (the "Software"), to deal 15 | > in the Software without restriction, including without limitation the rights 16 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | > copies of the Software, and to permit persons to whom the Software is 18 | > furnished to do so, subject to the following conditions: 19 | > 20 | > The above copyright notice and this permission notice shall be included in all 21 | > copies or substantial portions of the Software. 22 | > 23 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | > SOFTWARE. 30 | 31 | The `Tess2` part of the library is released under the SGI Free Sofware License B v2.0: 32 | 33 | > SGI FREE SOFTWARE LICENSE B (Version 2.0, Sept. 18, 2008) 34 | > 35 | > Copyright (C) [dates of first publication] Silicon Graphics, Inc. 36 | > All Rights Reserved. 37 | > 38 | > Permission is hereby granted, free of charge, to any person obtaining a copy 39 | > of this software and associated documentation files (the "Software"), to deal 40 | > in the Software without restriction, including without limitation the rights 41 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 42 | > of the Software, and to permit persons to whom the Software is furnished to do so, 43 | > subject to the following conditions: 44 | > 45 | > The above copyright notice including the dates of first publication and either this 46 | > permission notice or a reference to http://oss.sgi.com/projects/FreeB/ shall be 47 | > included in all copies or substantial portions of the Software. 48 | > 49 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 50 | > INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 51 | > PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL SILICON GRAPHICS, INC. 52 | > BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 53 | > TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 54 | > OR OTHER DEALINGS IN THE SOFTWARE. 55 | > 56 | > Except as contained in this notice, the name of Silicon Graphics, Inc. shall not 57 | > be used in advertising or otherwise to promote the sale, use or other dealings in 58 | > this Software without prior written authorization from Silicon Graphics, Inc. 59 | > 60 | > Copyright (c) 1994 Eric Veach 61 | > 62 | > Copyright (c) 2013 Mikko Mononen 63 | > 64 | > Copyright (c) 2014 Giuseppe Di Mauro (azrafe7) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hxGeomAlgo 2 | ========== 3 | 4 | Small collection of computational geometry algorithms in Haxe. 5 | 6 | ![](screenshot.png) 7 | 8 | [swf demo](https://azrafe7.github.io/hxGeomAlgo/OpenflDemo.swf) | [js demo](https://azrafe7.github.io/hxGeomAlgo/index.html) 9 | 10 | #### [Marching Squares (Contour Tracing)](https://en.wikipedia.org/w/index.php?title=Marching_squares&oldid=342542650) 11 | 12 | Based on: 13 | 14 | - https://web.archive.org/web/20180316055432/http://devblog.phillipspiess.com/better%20know%20an%20algorithm/2010/02/23/better-know-marching-squares.html (C# - by Phil Spiess) 15 | - http://www.tomgibara.com/computer-vision/marching-squares (Java - by Tom Gibara) 16 | 17 | #### [Ramer-Douglas-Peucker (Polyline Simplification)](http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm) 18 | 19 | Based on: 20 | 21 | - http://karthaus.nl/rdp/ (JS - by Marius Karthaus) 22 | - http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment (JS - by Grumdrig) 23 | 24 | #### [Ear Clipping (Triangulation and Poly Decomposition w/ Hole Support)](http://en.wikipedia.org/wiki/Ear_clipping#Ear_clipping_method) 25 | 26 | Based on: 27 | 28 | - https://github.com/mapbox/earcut (JS - by Vladimir Agafonkin) 29 | - http://www.ewjordan.com/earClip/ (Java - by Eric Jordan) 30 | 31 | #### [Bayazit (Poly Decomposition)](https://web.archive.org/web/20140810120355/http://mnbayazit.com/406/overview) 32 | 33 | Based on: 34 | 35 | - https://web.archive.org/web/20140701210122/http://mnbayazit.com/406/bayazit (C - by Mark Bayazit) 36 | - http://www.dyn4j.org/ (Java - by William Bittle) 37 | 38 | #### [Visibility Polygon](http://en.wikipedia.org/wiki/Visibility_polygon) and [Homogeneous Coords (2D)](http://en.wikipedia.org/wiki/Homogeneous_coordinates) 39 | 40 | Based on: 41 | 42 | - http://www.cs.ubc.ca/~snoeyink/demos/convdecomp/VPDemo.html (Java - by Jack Snoeyink) 43 | 44 | #### [Snoeyink-Keil (Minimum Convex Decomposition)](http://www.cs.ubc.ca/~snoeyink/demos/convdecomp/MCDDemo.html) 45 | 46 | Based on: 47 | 48 | - http://www.cs.ubc.ca/~snoeyink/demos/convdecomp/MCDDemo.html (Java - by Jack Snoeyink & Mark Keil) 49 | - [J. Mark Keil](http://www.informatik.uni-trier.de/~ley/pers/hd/k/Keil:J=_Mark), [Jack Snoeyink](http://www.informatik.uni-trier.de/~ley/pers/hd/s/Snoeyink:Jack.html): On the Time Bound for Convex Decomposition of Simple Polygons. [Int. J. Comput. Geometry Appl. 12](http://www.informatik.uni-trier.de/~ley/db/journals/ijcga/ijcga12.html#KeilS02)(3): 181-192 (2002) 50 | 51 | #### [Connected Components Labeling and Contour Tracing (w/ Hole Support)](http://en.wikipedia.org/wiki/Connected-component_labeling) 52 | 53 | Based on: 54 | 55 | - Fu Chang, Chun-jen Chen, Chi-jen Lu: [A linear-time component-labeling algorithm using contour tracing technique](http://www.iis.sinica.edu.tw/papers/fchang/1362-F.pdf) (2004) 56 | 57 | #### [Visvalingam-Whyatt (Polyline Simplification)](http://bost.ocks.org/mike/simplify/) 58 | 59 | Based on: 60 | 61 | - Visvalingam M., Whyatt J. D.: [Line generalisation by repeated elimination of the smallest area](https://hydra.hull.ac.uk/resources/hull:8338) (1992) 62 | - http://bost.ocks.org/mike/simplify/ (JS - by Mike Bostock) 63 | - https://github.com/jonasmalacofilho/dheap (Haxe - by Jonas Malaco Filho) 64 | - http://en.wikipedia.org/wiki/Binary_heap (Binary (Min)Heap) 65 | 66 | #### [Tess2 (Tesselation to Triangles and Convex Polygons, Poly Boolean Ops)](https://rawgit.com/azrafe7/tess2.js/master/test/index.html) 67 | 68 | Based on: 69 | 70 | - GLU Libtess (by Eric Veach, July 1994) 71 | - [tess2.js](https://github.com/memononen/tess2.js) (JS - by Mikko Mononen, Aug 2013) 72 | 73 | #### [IsoContours (Contour Tracing)](https://en.wikipedia.org/wiki/Contour_line) 74 | 75 | Based on: 76 | 77 | - http://en.wikipedia.org/wiki/Marching_squares 78 | - https://github.com/deltaluca/nape (Haxe - by Luca Deltodesco) 79 | - https://github.com/scikit-image/scikit-image (Python - by scikit-image team) 80 | 81 | #### [Hertel-Mehlhorn (Convex Polygons from Arbitrary Triangulation)](https://www8.cs.umu.se/kurser/TDBA77/VT06/algorithms/BOOK/BOOK5/NODE194.HTM) 82 | 83 | Based on: 84 | 85 | - https://github.com/ivanfratric/polypartition (CPP - by Ivan Fratric) 86 | - https://web.archive.org/web/20140102033642/http://www.philvaz.com/compgeom/ (by Phil Porvaznik) 87 | 88 | #### [Chaikin (Recursive Curve Smoothing)](https://observablehq.com/@pamacha/chaikins-algorithm) 89 | 90 | Based on: 91 | 92 | - George Merrill Chaikin: [An algorithm for high-speed curve generation](https://sci-hub.st/10.1016/0146-664X(74)90028-8) (1974) 93 | - https://sighack.com/post/chaikin-curves (Java - by Manohar Vanga) 94 | 95 | #### [Wu-Yong-Zhang-Zhang (Multi-step Chaikin Curve Smoothing)](https://observablehq.com/@pamacha/chaikins-algorithm) 96 | 97 | Based on: 98 | 99 | - Ling Wu, Jun-Hai Yong, You-Wei Zhang, and Li Zhang: [Multi-step Subdivision Algorithm for Chaikin Curves](https://sci-hub.st/10.1007/978-3-540-30497-5_188) (2004) 100 | - http://win.doomitalia.it/varie/chaikin.pdf (Matlab - by Fabio Roman, 2009) 101 | 102 | ## Dependencies 103 | The only dependency is [hxPixels](https://github.com/azrafe7/hxPixels), and only for the algorithms needing access to pixels. 104 | 105 | ## Usage 106 | Code is extensively doc-commented, and I tried my best to make it easy/intuitive to use. 107 | You can also take a look at the demo for a quick overview on how to use the different classes. 108 | 109 | ## Credits 110 | 111 | **hxGeomAlgo** is based on the work of many developers and it wouldn't exist if it weren't for them. See the [CREDITS](CREDITS.md) file for details. 112 | 113 | ## License 114 | 115 | **hxGeomAlgo** is developed by Giuseppe Di Mauro (azrafe7) and released under the MIT license (except for `Tess2.hx`, which is licensed under SGI B 2.0). See the [LICENSE](LICENSE.md) file for details. 116 | -------------------------------------------------------------------------------- /Test.hxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | "$(CompilerPath)/haxelib" run lime build "$(OutputFile)" $(TargetBuild) -$(BuildConfig) -Dfdb 51 | 52 | 53 | 54 | 55 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /assets/9x9_holed_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/9x9_holed_square.png -------------------------------------------------------------------------------- /assets/big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/big.png -------------------------------------------------------------------------------- /assets/bord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/bord.png -------------------------------------------------------------------------------- /assets/complex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/complex.png -------------------------------------------------------------------------------- /assets/horz_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/horz_line.png -------------------------------------------------------------------------------- /assets/issue11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/issue11.png -------------------------------------------------------------------------------- /assets/issue12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/issue12.png -------------------------------------------------------------------------------- /assets/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/line.png -------------------------------------------------------------------------------- /assets/nazca_monkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/nazca_monkey.png -------------------------------------------------------------------------------- /assets/opaque_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/opaque_black.png -------------------------------------------------------------------------------- /assets/opaque_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/opaque_white.png -------------------------------------------------------------------------------- /assets/pirate_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/pirate_small.png -------------------------------------------------------------------------------- /assets/py_figure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/py_figure.png -------------------------------------------------------------------------------- /assets/small_rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/small_rect.png -------------------------------------------------------------------------------- /assets/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/star.png -------------------------------------------------------------------------------- /assets/super_mario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/super_mario.png -------------------------------------------------------------------------------- /assets/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/text.png -------------------------------------------------------------------------------- /assets/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/assets/transparent.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/9x9_holed_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/9x9_holed_square.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/big.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/bord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/bord.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/complex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/complex.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/horz_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/horz_line.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/issue11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/issue11.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/issue12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/issue12.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/line.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/nazca_monkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/nazca_monkey.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/opaque_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/opaque_black.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/opaque_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/opaque_white.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/pirate_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/pirate_small.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/py_figure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/py_figure.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/small_rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/small_rect.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/star.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/super_mario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/super_mario.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/text.png -------------------------------------------------------------------------------- /bin/html5/bin/assets/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/assets/transparent.png -------------------------------------------------------------------------------- /bin/html5/bin/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/bin/html5/bin/favicon.png -------------------------------------------------------------------------------- /bin/html5/bin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | demo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | 39 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /bin/html5/bin/manifest/default.json: -------------------------------------------------------------------------------- 1 | {"name":null,"assets":"aoy4:pathy29:assets%2F9x9_holed_square.pngy4:sizei155y4:typey5:IMAGEy2:idR1y7:preloadtgoR0y16:assets%2Fbig.pngR2i45602R3R4R5R7R6tgoR0y17:assets%2Fbord.pngR2i1742R3R4R5R8R6tgoR0y20:assets%2Fcomplex.pngR2i2731R3R4R5R9R6tgoR0y22:assets%2Fhorz_line.pngR2i140R3R4R5R10R6tgoR0y20:assets%2Fissue11.pngR2i8571R3R4R5R11R6tgoR0y20:assets%2Fissue12.pngR2i289R3R4R5R12R6tgoR0y17:assets%2Fline.pngR2i499R3R4R5R13R6tgoR0y25:assets%2Fnazca_monkey.pngR2i1965R3R4R5R14R6tgoR0y25:assets%2Fopaque_black.pngR2i232R3R4R5R15R6tgoR0y25:assets%2Fopaque_white.pngR2i230R3R4R5R16R6tgoR0y25:assets%2Fpirate_small.pngR2i28757R3R4R5R17R6tgoR0y22:assets%2Fpy_figure.pngR2i5350R3R4R5R18R6tgoR0y23:assets%2Fsmall_rect.pngR2i144R3R4R5R19R6tgoR0y17:assets%2Fstar.pngR2i1415R3R4R5R20R6tgoR0y24:assets%2Fsuper_mario.pngR2i1325R3R4R5R21R6tgoR0y17:assets%2Ftext.pngR2i3866R3R4R5R22R6tgoR0y24:assets%2Ftransparent.pngR2i139R3R4R5R23R6tgh","rootPath":null,"version":2,"libraryArgs":[],"libraryType":null} -------------------------------------------------------------------------------- /docs/OpenflDemo.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/OpenflDemo.swf -------------------------------------------------------------------------------- /docs/assets/9x9_holed_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/9x9_holed_square.png -------------------------------------------------------------------------------- /docs/assets/big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/big.png -------------------------------------------------------------------------------- /docs/assets/bord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/bord.png -------------------------------------------------------------------------------- /docs/assets/complex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/complex.png -------------------------------------------------------------------------------- /docs/assets/horz_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/horz_line.png -------------------------------------------------------------------------------- /docs/assets/issue11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/issue11.png -------------------------------------------------------------------------------- /docs/assets/issue12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/issue12.png -------------------------------------------------------------------------------- /docs/assets/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/line.png -------------------------------------------------------------------------------- /docs/assets/nazca_monkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/nazca_monkey.png -------------------------------------------------------------------------------- /docs/assets/opaque_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/opaque_black.png -------------------------------------------------------------------------------- /docs/assets/opaque_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/opaque_white.png -------------------------------------------------------------------------------- /docs/assets/pirate_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/pirate_small.png -------------------------------------------------------------------------------- /docs/assets/py_figure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/py_figure.png -------------------------------------------------------------------------------- /docs/assets/small_rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/small_rect.png -------------------------------------------------------------------------------- /docs/assets/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/star.png -------------------------------------------------------------------------------- /docs/assets/super_mario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/super_mario.png -------------------------------------------------------------------------------- /docs/assets/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/text.png -------------------------------------------------------------------------------- /docs/assets/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/assets/transparent.png -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/docs/favicon.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | demo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | 39 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/manifest/default.json: -------------------------------------------------------------------------------- 1 | {"name":null,"assets":"aoy4:pathy29:assets%2F9x9_holed_square.pngy4:sizei155y4:typey5:IMAGEy2:idR1y7:preloadtgoR0y16:assets%2Fbig.pngR2i45602R3R4R5R7R6tgoR0y17:assets%2Fbord.pngR2i1742R3R4R5R8R6tgoR0y20:assets%2Fcomplex.pngR2i2731R3R4R5R9R6tgoR0y22:assets%2Fhorz_line.pngR2i140R3R4R5R10R6tgoR0y20:assets%2Fissue11.pngR2i8571R3R4R5R11R6tgoR0y20:assets%2Fissue12.pngR2i289R3R4R5R12R6tgoR0y17:assets%2Fline.pngR2i499R3R4R5R13R6tgoR0y25:assets%2Fnazca_monkey.pngR2i1965R3R4R5R14R6tgoR0y25:assets%2Fopaque_black.pngR2i232R3R4R5R15R6tgoR0y25:assets%2Fopaque_white.pngR2i230R3R4R5R16R6tgoR0y25:assets%2Fpirate_small.pngR2i28757R3R4R5R17R6tgoR0y22:assets%2Fpy_figure.pngR2i5350R3R4R5R18R6tgoR0y23:assets%2Fsmall_rect.pngR2i144R3R4R5R19R6tgoR0y17:assets%2Fstar.pngR2i1415R3R4R5R20R6tgoR0y24:assets%2Fsuper_mario.pngR2i1325R3R4R5R21R6tgoR0y17:assets%2Ftext.pngR2i3866R3R4R5R22R6tgoR0y24:assets%2Ftransparent.pngR2i139R3R4R5R23R6tgh","rootPath":null,"version":2,"libraryArgs":[],"libraryType":null} -------------------------------------------------------------------------------- /openflDemo.hxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | "$(CompilerPath)/haxelib" run lime build "$(OutputFile)" $(TargetBuild) -$(BuildConfig) -Dfdb 48 | 49 | 50 | 51 | 52 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azrafe7/hxGeomAlgo/8c3f9af36ecac6796354435ab4a003ad95618399/screenshot.png -------------------------------------------------------------------------------- /src/DrawUtils.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import flash.display.BitmapData; 4 | import flash.display.Graphics; 5 | import flash.display.PNGEncoderOptions; 6 | import flash.display.Sprite; 7 | import flash.filters.GlowFilter; 8 | import flash.text.TextField; 9 | import flash.text.TextFieldAutoSize; 10 | import flash.text.TextFormat; 11 | import flash.text.TextFormatAlign; 12 | import flash.utils.ByteArray; 13 | 14 | import hxGeomAlgo.Bayazit; 15 | import hxGeomAlgo.HxPoint; 16 | import hxGeomAlgo.PoleOfInaccessibility; 17 | import hxGeomAlgo.PolyTools; 18 | import hxGeomAlgo.PolyTools.Poly; 19 | 20 | #if (sys) 21 | import sys.io.File; 22 | import sys.io.FileOutput; 23 | #end 24 | 25 | 26 | typedef DrawSettings = { 27 | @:optional var showPoints:Bool; 28 | @:optional var showLabels:Bool; 29 | @:optional var showCentroids:Bool; 30 | @:optional var showSteinerPoints:Bool; 31 | @:optional var showReflexPoints:Bool; 32 | @:optional var showArrows:Bool; 33 | @:optional var showPIA:Bool; 34 | @:optional var fill:Bool; 35 | @:optional var close:Bool; 36 | @:optional var color:Int; 37 | } 38 | 39 | class DrawUtils { 40 | 41 | static var sprite:Sprite = null; 42 | static var g:Graphics = null; 43 | 44 | static public var THICKNESS:Float = .5; 45 | static public var COLOR:Int = 0xFF0000; 46 | static var CENTROID_COLOR:Int = 0x00FF00; 47 | static public var ALPHA:Float = 1.; 48 | 49 | static var TEXT_COLOR:Int = 0xFFFFFF; 50 | static var TEXT_FONT:String = "_typewriter"; 51 | static var TEXT_SIZE:Int = 12; 52 | static var TEXT_OFFSET:Float = 0; 53 | static var TEXT_OUTLINE:GlowFilter = new GlowFilter(0xFF000000, 1, 2, 2, 6); 54 | 55 | static public var DEFAULT_DRAW_SETTINGS:DrawSettings = { 56 | showPoints: false, 57 | showLabels: false, 58 | showCentroids: false, 59 | showSteinerPoints: true, 60 | showReflexPoints: false, 61 | showArrows: false, 62 | showPIA: false, 63 | fill: true, 64 | close: true, 65 | }; 66 | 67 | 68 | static public function init(sprite:Sprite):Void { 69 | DrawUtils.sprite = sprite; 70 | DrawUtils.g = sprite.graphics; 71 | DrawUtils.g.lineStyle(THICKNESS, COLOR, ALPHA); 72 | } 73 | 74 | static public function config(extra:DrawSettings):DrawSettings { 75 | var newSettings:DrawSettings = {}; 76 | for (f in Reflect.fields(DEFAULT_DRAW_SETTINGS)) { 77 | Reflect.setField(newSettings, f, Reflect.field(DEFAULT_DRAW_SETTINGS, f)); 78 | } 79 | for (f in Reflect.fields(extra)) { 80 | Reflect.setField(newSettings, f, Reflect.field(extra, f)); 81 | } 82 | return newSettings; 83 | } 84 | 85 | static public function testOrientation(polys:Array):String { 86 | var res = "none"; 87 | 88 | for (i in 0...polys.length) { 89 | var poly = polys[i]; 90 | var orientation = PolyTools.isCCW(poly) ? "CCW" : " CW"; 91 | 92 | if (i == 0) { 93 | res = orientation; 94 | } else if (res != orientation) { 95 | res = "mixed"; 96 | break; 97 | } 98 | } 99 | 100 | return res; 101 | } 102 | 103 | static public function testConvex(polys:Array):String { 104 | var res = "none"; 105 | 106 | for (i in 0...polys.length) { 107 | var poly = polys[i]; 108 | var convex = PolyTools.isConvex(poly) ? "convex" : "not convex"; 109 | 110 | if (i == 0) { 111 | res = convex; 112 | } else if (res != convex) { 113 | res = "mixed"; 114 | break; 115 | } 116 | } 117 | 118 | return res; 119 | } 120 | 121 | static public function testSimple(polys:Array):String { 122 | var res = "none"; 123 | 124 | for (i in 0...polys.length) { 125 | var poly = polys[i]; 126 | var convex = PolyTools.isSimple(poly) ? "simple" : "not simple"; 127 | 128 | if (i == 0) { 129 | res = convex; 130 | } else if (res != convex) { 131 | res = "mixed"; 132 | break; 133 | } 134 | } 135 | 136 | return res; 137 | } 138 | 139 | static public function savePNG(bmd:BitmapData, fileName:String) { 140 | #if (sys) 141 | var ba:ByteArray = bmd.encode(bmd.rect, new PNGEncoderOptions()); 142 | var file:FileOutput = sys.io.File.write(fileName, true); 143 | file.writeString(ba.toString()); 144 | file.close(); 145 | trace('BitmapData saved as "${fileName}".'); 146 | #end 147 | } 148 | 149 | static public function dumpPoly(poly:Array, reverse:Bool = false):Void { 150 | var len = poly.length; 151 | var str = "poly dump: "; 152 | for (i in 0...len) { 153 | var p = poly[reverse ? len - i - 1 : i]; 154 | str += p.x + "," + p.y + ","; 155 | } 156 | trace(str); 157 | } 158 | 159 | static public function drawPoints(points:Array, x:Float, y:Float, radius:Float = 2):Void 160 | { 161 | for (i in 0...points.length) { 162 | var p = points[i]; 163 | g.drawCircle(x + p.x, y + p.y, radius); 164 | } 165 | } 166 | 167 | static public function drawCircle(center:HxPoint, x:Float, y:Float, radius:Float):Void { 168 | if (Math.isFinite(radius)) { 169 | g.lineStyle(THICKNESS, CENTROID_COLOR); 170 | g.drawCircle(x + center.x, y + center.y, 1); 171 | g.drawCircle(x + center.x, y + center.y, radius); 172 | //g.lineStyle(THICKNESS, COLOR); 173 | } 174 | } 175 | 176 | // Draw arrow head at `q` 177 | static public function drawArrowHead(p:HxPoint, q:HxPoint, x:Float, y:Float, length:Float = 7, angleDeg:Float = 15):Void 178 | { 179 | var dx = p.x - q.x; 180 | var dy = p.y - q.y; 181 | var l = Math.sqrt(dx * dx + dy * dy); 182 | var cos = Math.cos(Math.PI * angleDeg / 180); 183 | var sin = Math.sin(Math.PI * angleDeg / 180); 184 | dx = (dx / l) * length; 185 | dy = (dy / l) * length; 186 | var end1 = new HxPoint(q.x + (dx * cos + dy * -sin), q.y + (dx * sin + dy * cos)); 187 | var end2 = new HxPoint(q.x + (dx * cos + dy * sin), q.y + (dx * -sin + dy * cos)); 188 | g.moveTo(end1.x + x, end1.y + y); 189 | g.lineTo((x + q.x), (y + q.y)); 190 | g.lineTo(end2.x + x, end2.y + y); 191 | //g.lineTo(end1.x + x, end1.y + y); // close head 192 | //g.moveTo((X + q.x), (Y + q.y)); 193 | } 194 | 195 | static public function drawPointsLabels(points:Array, x:Float, y:Float):Void 196 | { 197 | var len = points.length; 198 | var i = len - 1; 199 | while (i >= 0) { 200 | var p = points[i]; 201 | var label = getTextField("" + i, 0, 0, Std.int(TEXT_SIZE * .75)); 202 | var fmt = label.getTextFormat(); 203 | fmt.align = TextFormatAlign.LEFT; 204 | label.setTextFormat(fmt); 205 | label.x = x + p.x; 206 | label.y = y + p.y - TEXT_SIZE; 207 | sprite.addChild(label); 208 | i--; 209 | } 210 | } 211 | 212 | static public function drawPoly(points:Array, x:Float, y:Float, settings:DrawSettings):Void 213 | { 214 | if (points.length <= 0) return; 215 | 216 | var color = settings.color != null ? settings.color : COLOR; 217 | g.lineStyle(THICKNESS, color, ALPHA); 218 | 219 | // points 220 | if (settings.showPoints) drawPoints(points, x, y); 221 | 222 | // lines 223 | if (settings.fill) g.beginFill(color, .5); 224 | g.moveTo(x + points[0].x, y + points[0].y); 225 | for (i in 1...points.length) { 226 | var p = points[i]; 227 | g.lineTo(x + p.x, y + p.y); 228 | } 229 | if (settings.close) g.lineTo(x + points[0].x, y + points[0].y); 230 | if (settings.fill) g.endFill(); 231 | 232 | if (settings.showArrows) { 233 | g.lineStyle(THICKNESS, color, ALPHA); 234 | if (settings.fill) g.beginFill(color, .3); 235 | for (i in 1...points.length) { 236 | var p = points[i - 1]; 237 | var q = points[i]; 238 | drawArrowHead(p, q, x, y); 239 | } 240 | if (settings.fill) g.endFill(); 241 | g.lineStyle(THICKNESS, color, ALPHA); 242 | } 243 | 244 | // labels 245 | if (settings.showLabels) drawPointsLabels(points, x, y); 246 | 247 | // centroids 248 | if (settings.showCentroids) { 249 | var c = PolyTools.getCentroid(points); 250 | g.lineStyle(THICKNESS, CENTROID_COLOR); 251 | g.drawCircle(x + c.x, y + c.y, 2); 252 | //g.lineStyle(THICKNESS, color); 253 | } 254 | } 255 | 256 | static public function drawPaths(paths:Array>, x:Float, y:Float, settings:DrawSettings):Void 257 | { 258 | if (paths.length <= 0) return; 259 | 260 | var color = settings.color != null ? settings.color : COLOR; 261 | 262 | var data = new flash.Vector(); 263 | var commands = new flash.Vector(); 264 | 265 | for (path in paths) { 266 | var len = path.length; 267 | 268 | for (i in 0...len) { 269 | if (i == 0) commands.push(1); // moveTo 270 | else commands.push(2); // lineTo 271 | 272 | data.push(x + path[i].x); 273 | data.push(y + path[i].y); 274 | } 275 | // close 276 | if (settings.fill) { 277 | commands.push(2); 278 | data.push(x + path[0].x); 279 | data.push(y + path[0].y); 280 | } 281 | } 282 | 283 | if (settings.fill) g.beginFill(color, .5); 284 | g.drawPath(commands, data, flash.display.GraphicsPathWinding.EVEN_ODD); 285 | if (settings.fill) g.endFill(); 286 | 287 | // PoleOfInaccessibility 288 | if (settings.showPIA) { 289 | var p = PoleOfInaccessibility.calculate(paths); 290 | var r = PoleOfInaccessibility.pointToPolygonDist(p.x, p.y, paths); 291 | drawCircle(p, x, y, r); 292 | } 293 | } 294 | 295 | static public function drawPolys(polys:Array, x:Float, y:Float, settings:DrawSettings):Void 296 | { 297 | for (poly in polys) { 298 | drawPoly(poly, x, y, settings); 299 | } 300 | 301 | // PoleOfInaccessibility 302 | if (settings.showPIA) { 303 | var p = PoleOfInaccessibility.calculate(polys); 304 | var r = PoleOfInaccessibility.pointToPolygonDist(p.x, p.y, polys); 305 | drawCircle(p, x, y, r); 306 | } 307 | } 308 | 309 | static public function drawDecompositionBayazit(polys:Array, x:Float, y:Float, settings:DrawSettings):Void 310 | { 311 | drawPolys(polys, x, y, settings); 312 | 313 | var color = settings.color != null ? settings.color : COLOR; 314 | 315 | // draw Reflex and Steiner points 316 | if (settings.showReflexPoints) { 317 | g.lineStyle(THICKNESS, (color >> 1) | color, ALPHA); 318 | for (p in Bayazit.reflexVertices) g.drawCircle(x + p.x, y + p.y, 2); 319 | } 320 | 321 | if (settings.showSteinerPoints) { 322 | g.lineStyle(THICKNESS, (color >> 2) | color, ALPHA); 323 | for (p in Bayazit.steinerPoints) g.drawCircle(x + p.x, y + p.y, 2); 324 | } 325 | //g.lineStyle(THICKNESS, COLOR, ALPHA); 326 | } 327 | 328 | static public function getTextField(text:String = "", x:Float, y:Float, ?size:Int):TextField 329 | { 330 | var tf:TextField = new TextField(); 331 | var fmt:TextFormat = new TextFormat(TEXT_FONT, null, TEXT_COLOR); 332 | //fmt.align = TextFormatAlign.CENTER; 333 | fmt.size = size == null ? TEXT_SIZE : size; 334 | tf.autoSize = TextFieldAutoSize.LEFT; 335 | tf.defaultTextFormat = fmt; 336 | tf.selectable = false; 337 | tf.x = x; 338 | tf.y = y + TEXT_OFFSET; 339 | tf.filters = [TEXT_OUTLINE]; 340 | tf.text = text; 341 | return tf; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/OpenflDemo.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import flash.display.Sprite; 4 | import flash.events.KeyboardEvent; 5 | import flash.events.MouseEvent; 6 | import flash.events.Event; 7 | import flash.system.System; 8 | import flash.display.BitmapData; 9 | 10 | 11 | class OpenflDemo extends Sprite { 12 | 13 | static var assets:Array = [ 14 | "assets/pirate_small.png", // 0 15 | "assets/super_mario.png", // 1 - from http://www.newgrounds.com/art/view/petelavadigger/super-mario-pixel 16 | "assets/nazca_monkey.png", // 2 17 | "assets/small_rect.png", // 3 18 | "assets/star.png", // 4 19 | "assets/text.png", // 5 20 | "assets/transparent.png", // 6 21 | "assets/opaque_black.png", // 7 22 | "assets/complex.png", // 8 23 | "assets/big.png", // 9 24 | "assets/bord.png", // 10 25 | "assets/line.png", // 11 26 | "assets/opaque_white.png", // 12 27 | "assets/py_figure.png", // 13 28 | "assets/issue11.png", // 14 29 | "assets/9x9_holed_square.png", // 15 30 | ]; 31 | 32 | static var currAssetIdx:Int = 0; 33 | static var asset:String; 34 | static var geomAlgoTest:GeomAlgoTest; 35 | 36 | var lastMouseX:Float = 0.0; 37 | var lastMouseY:Float = 0.0; 38 | var isMouseDown:Bool = false; 39 | 40 | static public function main():Void { 41 | new OpenflDemo(); 42 | } 43 | 44 | public function new() { 45 | super(); 46 | 47 | flash.Lib.current.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); 48 | flash.Lib.current.stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); 49 | flash.Lib.current.stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp); 50 | flash.Lib.current.stage.addEventListener(MouseEvent.MOUSE_WHEEL, onMouseWheel); 51 | flash.Lib.current.stage.addEventListener(Event.ENTER_FRAME, onEnterFrame); 52 | 53 | asset = assets[currAssetIdx]; 54 | geomAlgoTest = new GeomAlgoTest('$asset', '$currAssetIdx/${assets.length - 1}:'); 55 | 56 | flash.Lib.current.addChild(geomAlgoTest); 57 | } 58 | 59 | inline function zoom(factor:Float) { 60 | flash.Lib.current.scaleX *= factor; 61 | flash.Lib.current.scaleY *= factor; 62 | } 63 | 64 | public function onKeyDown(e:KeyboardEvent):Void 65 | { 66 | if (e.keyCode == 27) { 67 | quit(); 68 | } 69 | 70 | // zoom 71 | if (e.charCode == "+".code) { 72 | zoom(1.25); 73 | } else if (e.charCode == "-".code) { 74 | zoom(.8); 75 | } 76 | 77 | // screenshot 78 | if (e.charCode == "s".code) { 79 | var bounds = geomAlgoTest.getBounds(geomAlgoTest); 80 | var bmd = new BitmapData(Math.ceil(bounds.right), Math.ceil(bounds.bottom), true, 0); 81 | bmd.draw(geomAlgoTest); 82 | GeomAlgoTest.savePNG(bmd, "capture.png"); 83 | } 84 | 85 | var deltaIdx = 0; 86 | var moveDelta = 12; 87 | var moveMult = e.shiftKey ? 4 : 1; 88 | 89 | moveDelta *= moveMult; 90 | 91 | // keys to move camera around and cycle through assets 92 | if (e.charCode == "j".code || e.charCode == "J".code || e.keyCode == 39) { // right 93 | geomAlgoTest.x -= moveDelta; 94 | if (e.ctrlKey) deltaIdx = 1; 95 | } 96 | if (e.charCode == "g".code || e.charCode == "G".code || e.keyCode == 37) { // left 97 | geomAlgoTest.x += moveDelta; 98 | if (e.ctrlKey) deltaIdx = -1; 99 | } 100 | if (e.charCode == "h".code || e.charCode == "H".code || e.keyCode == 40) geomAlgoTest.y -= moveDelta; // down 101 | if (e.charCode == "y".code || e.charCode == "Y".code || e.keyCode == 38) geomAlgoTest.y += moveDelta; // up 102 | 103 | // reset pos & zoom 104 | if (e.charCode == "r".code || e.charCode == "R".code) { 105 | geomAlgoTest.x = 0; 106 | geomAlgoTest.y = 0; 107 | flash.Lib.current.scaleX = 1; 108 | flash.Lib.current.scaleY = 1; 109 | } 110 | 111 | if (e.charCode >= "0".code && e.charCode <= "9".code) { 112 | currAssetIdx = -1; 113 | deltaIdx = e.charCode - "0".code + 1; 114 | } 115 | 116 | if (deltaIdx != 0) { 117 | currAssetIdx = (currAssetIdx + deltaIdx + assets.length) % assets.length; 118 | 119 | asset = assets[currAssetIdx]; 120 | flash.Lib.current.removeChild(geomAlgoTest); 121 | geomAlgoTest = new GeomAlgoTest('$asset', '$currAssetIdx/${assets.length - 1}:'); 122 | flash.Lib.current.addChild(geomAlgoTest); 123 | } 124 | } 125 | 126 | public function onMouseDown(e:MouseEvent):Void 127 | { 128 | lastMouseX = flash.Lib.current.stage.mouseX; 129 | lastMouseY = flash.Lib.current.stage.mouseY; 130 | isMouseDown = true; 131 | } 132 | 133 | public function onMouseUp(e:MouseEvent):Void 134 | { 135 | isMouseDown = false; 136 | } 137 | 138 | public function onMouseWheel(e:MouseEvent):Void 139 | { 140 | // zoom 141 | if (e.delta > 0) { 142 | zoom(1.25); 143 | } else if (e.delta < 0) { 144 | zoom(.8); 145 | } 146 | } 147 | 148 | public function onEnterFrame(e:Event):Void 149 | { 150 | if (isMouseDown) { 151 | geomAlgoTest.x += flash.Lib.current.stage.mouseX - lastMouseX; 152 | geomAlgoTest.y += flash.Lib.current.stage.mouseY - lastMouseY; 153 | lastMouseX = flash.Lib.current.stage.mouseX; 154 | lastMouseY = flash.Lib.current.stage.mouseY; 155 | } 156 | } 157 | 158 | static public function quit():Void 159 | { 160 | #if (flash || html5) 161 | System.exit(1); 162 | #else 163 | Sys.exit(1); 164 | #end 165 | } 166 | } -------------------------------------------------------------------------------- /src/Test.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import flash.display.Sprite; 4 | import flash.events.Event; 5 | import flash.events.KeyboardEvent; 6 | import flash.events.MouseEvent; 7 | import flash.system.System; 8 | import flash.display.BitmapData; 9 | import flash.text.TextField; 10 | import hxGeomAlgo.HxPoint; 11 | import hxGeomAlgo.PoleOfInaccessibility; 12 | 13 | import hxGeomAlgo.PolyTools.Poly; 14 | 15 | import DrawUtils.*; 16 | 17 | using hxGeomAlgo.PolyTools; 18 | 19 | 20 | typedef PolyState = { 21 | var poly:Poly; 22 | @:optional var color:Int; 23 | } 24 | 25 | 26 | class Test extends Sprite { 27 | 28 | var color:Int; 29 | var poly:Poly = []; 30 | 31 | var polys:Array = []; 32 | var resPolys:Array = []; 33 | 34 | var closed:Bool = false; 35 | var justClosed:Bool = false; 36 | 37 | var readyToCalc:Bool = false; 38 | var processed:Bool = false; 39 | 40 | var mouse:HxPoint = new HxPoint(); 41 | 42 | var info:TextField; 43 | var infoText:String; 44 | 45 | var overlaySprite:Sprite; 46 | 47 | static public function main():Void { 48 | flash.Lib.current.addChild(new Test()); 49 | } 50 | 51 | public function new() { 52 | super(); 53 | 54 | flash.Lib.current.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); 55 | flash.Lib.current.stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove); 56 | flash.Lib.current.stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); 57 | flash.Lib.current.stage.addEventListener(Event.ENTER_FRAME, onEnterFrame); 58 | 59 | DrawUtils.init(this); 60 | 61 | addChild(overlaySprite = new Sprite()); 62 | addChild(info = getTextField("xxxx", 20, 20)); 63 | //drawCircle(new HxPoint(20, 20), 0, 0, 30); 64 | } 65 | 66 | public function reset():Void { 67 | poly = []; 68 | polys = []; 69 | resPolys = []; 70 | closed = justClosed = false; 71 | readyToCalc = false; 72 | processed = false; 73 | } 74 | 75 | 76 | public function onEnterFrame(e):Void { 77 | graphics.clear(); 78 | 79 | // set color for curr poly 80 | color = COLOR >> (8 * polys.length); 81 | graphics.lineStyle(THICKNESS, color, ALPHA); 82 | 83 | info.text = 'Mouse: ${mouse.x}, ${mouse.y} (resPolys: ${resPolys.length})'; 84 | 85 | // curr poly 86 | drawPoly(poly, 0, 0, config( { showPoints:true, fill:justClosed, close:closed, color:color } )); 87 | 88 | // mouse segment 89 | if (!closed && poly.length > 0) { 90 | drawPoly([poly[poly.length - 1], mouse], 0, 0, config( { showPoints:true, fill:false, close:closed, color:color } )); 91 | } 92 | 93 | // push closed poly to polys 94 | if (justClosed) { 95 | polys.push( { poly:poly.concat([]), color:color } ); 96 | poly = []; 97 | closed = justClosed = false; 98 | } 99 | 100 | // draw completed polys 101 | for (p in polys) { 102 | drawPoly(p.poly, 0, 0, config( { showPoints:true, fill:true, close:true, color:p.color } )); 103 | } 104 | 105 | readyToCalc = polys.length == 2; 106 | 107 | // process input 108 | if (readyToCalc && !processed) { 109 | resPolys = []; 110 | overlaySprite.removeChildren(); 111 | 112 | var res = polys[0].poly.clip(polys[1].poly); 113 | 114 | for (i in 0...res.length) { 115 | resPolys.push( { poly:res[i], color:COLOR >> (8 * i) } ); 116 | 117 | var pia = PoleOfInaccessibility.calculate([res[i]]); 118 | //overlaySprite.addChild(getTextField("" + i, pia.x, pia.y)); 119 | overlaySprite.graphics.lineStyle(THICKNESS, COLOR >> (8 * i), 1); 120 | overlaySprite.graphics.drawCircle(pia.x, pia.y, 4); 121 | } 122 | 123 | trace(res.length); 124 | 125 | /* 126 | polys[1].poly.reverse(); 127 | res = polys[0].poly.clip(polys[1].poly); 128 | 129 | resPolys.push({poly:res}); 130 | */ 131 | 132 | processed = true; 133 | } 134 | 135 | // draw processed polys 136 | for (i in 0...resPolys.length) { 137 | var p = resPolys[i]; 138 | 139 | var labels = false; 140 | //if (i == resPolys.length - 1) labels = true; 141 | 142 | drawPoly(p.poly, 300, 0, config( { showPoints:true, fill:true, close:true, color:p.color, showLabels:labels } )); 143 | overlaySprite.x = 300; 144 | } 145 | 146 | } 147 | 148 | public function onMouseMove(e:MouseEvent):Void { 149 | mouse.x = e.stageX; 150 | mouse.y = e.stageY; 151 | } 152 | 153 | public function onMouseDown(e:MouseEvent):Void { 154 | poly.push(mouse.clone()); 155 | } 156 | 157 | public function onKeyDown(e:KeyboardEvent):Void 158 | { 159 | if (e.keyCode == 27) { 160 | quit(); 161 | } 162 | 163 | // remove last poly 164 | if (e.charCode == "1".code) { 165 | polys.pop(); 166 | processed = false; 167 | } 168 | 169 | // close 170 | if (e.charCode == " ".code) { 171 | if (poly.length > 1) { 172 | closed = justClosed = true; 173 | } 174 | } 175 | 176 | // reset 177 | if (e.charCode == "r".code) { 178 | reset(); 179 | } 180 | 181 | // zoom 182 | if (e.charCode == "+".code) { 183 | flash.Lib.current.scaleX *= 1.25; 184 | flash.Lib.current.scaleY *= 1.25; 185 | } else if (e.charCode == "-".code) { 186 | flash.Lib.current.scaleX *= .8; 187 | flash.Lib.current.scaleY *= .8; 188 | } 189 | 190 | // screenshot 191 | #if sys 192 | if (e.charCode == "s".code) { 193 | var bounds = getBounds(geomAlgoTest); 194 | var bmd = new BitmapData(Math.ceil(bounds.right), Math.ceil(bounds.bottom), true, 0); 195 | bmd.draw(this); 196 | DrawUtils.savePNG(bmd, "capture.png"); 197 | } 198 | #end 199 | 200 | var deltaIdx = 0; 201 | var moveDelta = 12; 202 | var moveMult = e.shiftKey ? 4 : 1; 203 | 204 | moveDelta *= moveMult; 205 | 206 | // keys to move camera around and cycle through assets 207 | if (e.charCode == "j".code || e.charCode == "J".code || e.keyCode == 39) { // right 208 | this.x -= moveDelta; 209 | if (e.ctrlKey) deltaIdx = 1; 210 | } 211 | if (e.charCode == "g".code || e.charCode == "G".code || e.keyCode == 37) { // left 212 | this.x += moveDelta; 213 | if (e.ctrlKey) deltaIdx = -1; 214 | } 215 | if (e.charCode == "h".code || e.charCode == "H".code || e.keyCode == 40) this.y -= moveDelta; // down 216 | if (e.charCode == "y".code || e.charCode == "Y".code || e.keyCode == 38) this.y += moveDelta; // up 217 | } 218 | 219 | static public function quit():Void 220 | { 221 | #if (flash || html5) 222 | System.exit(1); 223 | #else 224 | Sys.exit(1); 225 | #end 226 | } 227 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/Bayazit.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Bayazit polygon decomposition implementation. 3 | * NOTE: Should work only for SIMPLE polygons (not self-intersecting, without holes). 4 | * 5 | * Based on: 6 | * 7 | * @see https://web.archive.org/web/20140701210122/http://mnbayazit.com/406/bayazit (C - by Mark Bayazit) 8 | * @see https://web.archive.org/web/20140701210122/http://mnbayazit.com/406/credit 9 | * @see http://www.dyn4j.org/ (Java - by William Bittle) 10 | * 11 | * Other credits should go to papers/work of: 12 | * 13 | * @see http://mnbayazit.com/406/files/PolygonDecomp-Keil.pdf (Keil) 14 | * @see http://mnbayazit.com/406/files/OnTheTimeBound-Snoeyink.pdf (Snoeyink & Keil) 15 | * @see http://www.cs.sfu.ca/~binay/ (Dr. Bhattacharya) 16 | * 17 | * @author azrafe7 18 | */ 19 | 20 | package hxGeomAlgo; 21 | 22 | using hxGeomAlgo.PolyTools; 23 | 24 | 25 | @:expose 26 | class Bayazit 27 | { 28 | 29 | static private var poly:Poly; // cw version of simplePoly - used internally 30 | static private var visibility:Map>; // maps vertices' indices to visible ones - used internally 31 | 32 | static public var reflexVertices:Array; 33 | static public var steinerPoints:Array; 34 | 35 | static public var reversed:Bool; // true if the _internal_ indices have been reversed 36 | 37 | /** 38 | * Decomposes `simplePoly` into a near-minimum number of convex polygons. 39 | */ 40 | static public function decomposePoly(simplePoly:Poly):Array { 41 | var res = new Array(); 42 | 43 | reflexVertices = []; 44 | steinerPoints = []; 45 | visibility = new Map(); 46 | 47 | if (simplePoly.length < 3) return res; 48 | 49 | poly = new Poly(); 50 | for (p in simplePoly) poly.push(new HxPoint(p.x, p.y)); 51 | reversed = poly.makeCW(); // make poly cw (in place) 52 | 53 | _decomposePoly(poly, res); 54 | 55 | return res; 56 | } 57 | 58 | /** Used internally by decomposePoly(). */ 59 | static private function _decomposePoly(poly:Poly, polys:Array) { 60 | var upperInt:HxPoint = new HxPoint(), lowerInt:HxPoint = new HxPoint(), 61 | p:HxPoint = new HxPoint(), closestVert:HxPoint = new HxPoint(); 62 | var upperDist:Float = 0, lowerDist:Float = 0, d:Float = 0, closestDist:Float = 0; 63 | var upperIdx:Int = 0, lowerIdx:Int = 0, closestIdx:Int = 0; 64 | var upperPoly:Poly = new Poly(), lowerPoly:Poly = new Poly(); 65 | 66 | for (i in 0...poly.length) { 67 | if (poly.isReflex(i)) { 68 | visibility[i] = Visibility.getVisibleIndicesFrom(poly, i); 69 | reflexVertices.push(poly[i]); 70 | upperDist = lowerDist = Math.POSITIVE_INFINITY; 71 | for (j in 0...poly.length) { 72 | if (poly.at(i - 1).isLeft(poly.at(i), poly.at(j)) && 73 | poly.at(i - 1).isRightOrOn(poly.at(i), poly.at(j - 1))) // if line intersects with an edge 74 | { 75 | p = PolyTools.intersection(poly.at(i - 1), poly.at(i), poly.at(j), poly.at(j - 1)); // find the point of intersection 76 | if (poly.at(i + 1).isRight(poly.at(i), p)) { // make sure it's inside the poly 77 | d = poly[i].distanceSquared(p); 78 | if (d < lowerDist) { // keep only the closest intersection 79 | lowerDist = d; 80 | lowerInt = p; 81 | lowerIdx = j; 82 | } 83 | } 84 | } 85 | 86 | if (poly.at(i + 1).isLeft(poly.at(i), poly.at(j + 1)) && 87 | poly.at(i + 1).isRightOrOn(poly.at(i), poly.at(j))) 88 | { 89 | p = PolyTools.intersection(poly.at(i + 1), poly.at(i), poly.at(j), poly.at(j + 1)); 90 | if (poly.at(i - 1).isLeft(poly.at(i), p)) { 91 | d = poly[i].distanceSquared(p); 92 | if (d < upperDist) { 93 | upperDist = d; 94 | upperInt = p; 95 | upperIdx = j; 96 | } 97 | } 98 | } 99 | } 100 | 101 | // if there are no vertices to connect to, choose a point in the middle 102 | if (lowerIdx == (upperIdx + 1) % poly.length) { 103 | 104 | //trace('Case 1: Vertex($i), lowerIdx($lowerIdx), upperIdx($upperIdx), poly.length(${poly.length})'); 105 | 106 | p.x = (lowerInt.x + upperInt.x) / 2; 107 | p.y = (lowerInt.y + upperInt.y) / 2; 108 | steinerPoints.push(p); 109 | 110 | if (i < upperIdx) { 111 | for (k in i...upperIdx + 1) lowerPoly.push(poly[k]); 112 | lowerPoly.push(p); 113 | upperPoly.push(p); 114 | if (lowerIdx != 0) for (k in lowerIdx...poly.length) upperPoly.push(poly[k]); 115 | for (k in 0...i + 1) upperPoly.push(poly[k]); 116 | } else { 117 | if (i != 0) for (k in i...poly.length) lowerPoly.push(poly[k]); 118 | for (k in 0...upperIdx + 1) lowerPoly.push(poly[k]); 119 | lowerPoly.push(p); 120 | upperPoly.push(p); 121 | for (k in lowerIdx...i + 1) upperPoly.push(poly[k]); 122 | } 123 | 124 | } else { 125 | 126 | // connect to the closest point within the triangle 127 | //trace('Case 2: Vertex($i), closestIdx($closestIdx), poly.length(${poly.length}), $lowerIdx, $upperIdx'); 128 | 129 | if (lowerIdx > upperIdx) { 130 | upperIdx += poly.length; 131 | } 132 | closestDist = Math.POSITIVE_INFINITY; 133 | for (j in lowerIdx...upperIdx + 1) { 134 | if (poly.at(i - 1).isLeftOrOn(poly.at(i), poly.at(j)) 135 | && poly.at(i + 1).isRightOrOn(poly.at(i), poly.at(j))) 136 | { 137 | d = poly.at(i).distanceSquared(poly.at(j)); 138 | if (d < closestDist) { 139 | var ijVisible = visibility[i].indexOf(j % poly.length) >= 0; 140 | if (ijVisible) { 141 | closestDist = d; 142 | closestVert = poly.at(j); 143 | closestIdx = j % poly.length; 144 | } 145 | } 146 | } 147 | } 148 | 149 | if (i < closestIdx) { 150 | for (k in i...closestIdx + 1) lowerPoly.push(poly[k]); 151 | if (closestIdx != 0) for (k in closestIdx...poly.length) upperPoly.push(poly[k]); 152 | for (k in 0...i + 1) upperPoly.push(poly[k]); 153 | } else { 154 | if (i != 0) for (k in i...poly.length) lowerPoly.push(poly[k]); 155 | for (k in 0...closestIdx + 1) lowerPoly.push(poly[k]); 156 | for (k in closestIdx...i + 1) upperPoly.push(poly[k]); 157 | } 158 | } 159 | 160 | // solve smallest poly first 161 | if (lowerPoly.length < upperPoly.length) { 162 | _decomposePoly(lowerPoly, polys); 163 | _decomposePoly(upperPoly, polys); 164 | } else { 165 | _decomposePoly(upperPoly, polys); 166 | _decomposePoly(lowerPoly, polys); 167 | } 168 | return; 169 | } 170 | } 171 | polys.push(poly); 172 | } 173 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/CCLabeler.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Connected components labeling (8 and 4-connectivity) implementation. 3 | * 4 | * Based on the paper of: 5 | * 6 | * Fu Chang, Chun-jen Chen, Chi-jen Lu: A linear-time component-labeling algorithm using contour tracing technique (2004) 7 | * 8 | * @see http://www.iis.sinica.edu.tw/papers/fchang/1362-F.pdf 9 | * 10 | * @author azrafe7 11 | */ 12 | 13 | package hxGeomAlgo; 14 | 15 | import hxGeomAlgo.HxPoint; 16 | import hxGeomAlgo.PolyTools.Poly; 17 | import hxPixels.Pixels; 18 | 19 | 20 | @:expose 21 | enum Connectivity { 22 | FOUR_CONNECTED; 23 | EIGHT_CONNECTED; 24 | } 25 | 26 | 27 | @:expose 28 | class CCLabeler 29 | { 30 | #if js 31 | static function __init__() { 32 | PolyTools.exposeEnum(Connectivity); 33 | } 34 | #end 35 | 36 | /** Minimum alpha value to consider a pixel opaque (in the range 1-255). */ 37 | public var alphaThreshold:Int; 38 | 39 | /** Pixels containing the labeling info. */ 40 | public var labelMap:Pixels; 41 | 42 | /** Whether to store contours' points while labeling. */ 43 | public var traceContours:Bool; 44 | 45 | /** Whether to compute and store components' area (in areaMap) while labeling. */ 46 | public var calcArea:Bool; 47 | 48 | /** Type of connectivity to search for. */ 49 | public var connectivity:Connectivity; 50 | 51 | /** 52 | * Contours' points found while labeling (external contours 53 | * are in clockwise order, while internal ones are in ccw order). 54 | */ 55 | public var contours(default, null):Array; 56 | 57 | /** Count of pixels belonging to the same connected component, indexed by label color (as returned by labelToColor()). */ 58 | public var areaMap(default, null):Map; 59 | 60 | /** Number of connected components found. */ 61 | public var numComponents(default, null):Int; 62 | 63 | 64 | private var MARKED:Int = 0xFFFFFFFF; 65 | private var UNLABELED:Int = 0x00000000; 66 | 67 | private var searchDir:Array<{dx:Int, dy:Int}> = [ 68 | {dx: 1, dy: 0}, // 0 69 | {dx: 1, dy: 1}, // 1 +-------x 70 | {dx: 0, dy: 1}, // 2 | 5 6 7 71 | {dx: -1, dy: 1}, // 3 | 4 P 0 72 | {dx: -1, dy: 0}, // 4 | 3 2 1 73 | {dx: -1, dy: -1}, // 5 y 74 | {dx: 0, dy: -1}, // 6 75 | {dx: 1, dy: -1} // 7 76 | ]; 77 | 78 | private var tracingDir:Int = 0; 79 | private var labelIndex:Int = 0; 80 | private var contourPoint:HxPoint = new HxPoint(); 81 | private var secondContourPoint:HxPoint = new HxPoint(); 82 | 83 | private var sourcePixels:Pixels; 84 | private var markedPixels:Pixels; 85 | 86 | private var width:Int; 87 | private var height:Int; 88 | 89 | private var colors:Array = []; 90 | private var hue:Float = .60; 91 | 92 | /** 93 | * Constructor. 94 | * 95 | * @param pixels Pixels to use as source for labeling. 96 | * @param alphaThreshold Minimum alpha value to consider a pixel opaque (in the range 1-255). 97 | * @param traceContours Whether to store contours' points while labeling. 98 | * @param connectivity Type of connectivity to search for (defaults to EIGHT_CONNECTED). 99 | * @param calcArea Whether to compute and store components' area (in areaMap) while labeling. 100 | */ 101 | public function new(pixels:Pixels, alphaThreshold:Int = 1, traceContours:Bool = true, ?connectivity:Connectivity, calcArea:Bool = false) 102 | { 103 | setSource(pixels); 104 | 105 | this.alphaThreshold = alphaThreshold; 106 | this.connectivity = connectivity != null ? connectivity : EIGHT_CONNECTED; 107 | this.traceContours = traceContours; 108 | this.calcArea = calcArea; 109 | numComponents = 0; 110 | } 111 | 112 | /** 113 | * Updates the Pixels to use as source. 114 | * 115 | * NOTE: If you modifiy your source between calls to run() you may 116 | * also want to re-set the source so that the internal representation gets updated too. 117 | */ 118 | public function setSource(pixels:Pixels) 119 | { 120 | this.sourcePixels = pixels; 121 | 122 | width = this.sourcePixels.width; 123 | height = this.sourcePixels.height; 124 | labelMap = new Pixels(width, height); 125 | labelMap.format = pixels.format; 126 | labelMap.fillRect(0, 0, width, height, UNLABELED); 127 | markedPixels = new Pixels(width, height); 128 | markedPixels.format = pixels.format; 129 | markedPixels.fillRect(0, 0, width, height, 0); 130 | } 131 | 132 | /** 133 | * Labels connected components and writes them in the returned Pixels (also stored in `labelMap`). 134 | * If `traceContours` has been set, it also saves contours' points in the `contours` variable. 135 | */ 136 | public function run():Pixels 137 | { 138 | contours = new Array(); 139 | areaMap = new Map(); 140 | numComponents = 0; 141 | labelIndex = 0; 142 | 143 | var isLabeled:Bool; 144 | var leftLabeledPixel:Int; 145 | 146 | var x, y = 0; 147 | while (y < height) { 148 | x = 0; 149 | while (x < width) { 150 | isLabeled = getPixel32(labelMap, x, y, UNLABELED) != UNLABELED; 151 | if (isPixelSolid(x, y)) 152 | { 153 | if (!isLabeled && !isPixelSolid(x, y - 1)) { // external contour 154 | //trace("external contour @ " + x + "," + y); 155 | setLabel(x, y, labelToColor(labelIndex)); 156 | isLabeled = true; 157 | contourTrace(x, y, labelToColor(labelIndex), 7); 158 | labelIndex++; 159 | } 160 | if (!isPixelSolid(x, y + 1) && getPixel32(markedPixels, x, y + 1, MARKED) != MARKED) { // internal contour 161 | //trace("internal contour @ " + x + "," + y); 162 | if (!isLabeled) { 163 | leftLabeledPixel = getPixel32(labelMap, x - 1, y); 164 | setLabel(x, y, leftLabeledPixel); 165 | isLabeled = true; 166 | } 167 | contourTrace(x, y, getPixel32(labelMap, x, y), 3); 168 | } 169 | if (!isLabeled) // internal point not belonging to any contour 170 | { 171 | //trace("internal point @ " + x + "," + y); 172 | leftLabeledPixel = getPixel32(labelMap, x - 1, y); 173 | setLabel(x, y, leftLabeledPixel); 174 | } 175 | } 176 | x++; 177 | } 178 | y++; 179 | } 180 | 181 | numComponents = labelIndex; 182 | return labelMap; 183 | } 184 | 185 | /** 186 | * Traces the contour starting at `x`, `y`. 187 | * 188 | * @param x Starting x of the contour 189 | * @param y Starting y of the contour 190 | * @param labelColor Color to use 191 | * @param dir Initial tracing direction 192 | */ 193 | private function contourTrace(x:Int, y:Int, labelColor:Int, dir:Int) 194 | { 195 | var startX:Int = x, 196 | startY:Int = y, 197 | poly:Poly = null, 198 | nextPointExists; 199 | 200 | if (traceContours) { 201 | poly = new Poly(); 202 | poly.push(new HxPoint(x, y)); 203 | contours.push(poly); 204 | } 205 | 206 | contourPoint.setTo(x, y); 207 | tracingDir = dir; 208 | //trace(x, y, StringTools.hex(getPixel32(sourcePixels(x, y)), "dir: " + tracingDir); 209 | var firstPoint = true; 210 | while (true) { 211 | nextPointExists = nextOnContour(x, y, contourPoint); 212 | if (firstPoint) { 213 | secondContourPoint.setTo(contourPoint.x, contourPoint.y); 214 | firstPoint = false; 215 | } 216 | if (nextPointExists) { 217 | tracingDir = (tracingDir + 6) % 8; // update direction 218 | x = Std.int(contourPoint.x); 219 | y = Std.int(contourPoint.y); 220 | //trace(x, y, StringTools.hex(getPixel32(sourcePixels, x, y)), "dir: " + tracingDir); 221 | if (x == startX && y == startY) { // we're back to starting point 222 | nextOnContour(x, y, contourPoint); 223 | 224 | // break if next point is the same we found with the first call to nextOnContour 225 | // (which can actually be different based on tracing direction - f.e. in an x-shaped pattern) 226 | if (contourPoint.x == secondContourPoint.x && contourPoint.y == secondContourPoint.y) { 227 | break; 228 | } 229 | } else { // found next point on contour 230 | if (traceContours) { 231 | poly.push(contourPoint.clone()); 232 | } 233 | setLabel(x, y, labelColor); 234 | } 235 | } else { // isolated pixel 236 | break; 237 | } 238 | } 239 | } 240 | 241 | /** Finds the next point on contour and stores it into `nextPoint` (returns false if no next point exists). */ 242 | private function nextOnContour(x:Int, y:Int, nextPoint:HxPoint):Bool 243 | { 244 | var isolatedPixel = true, 245 | cx, cy, 246 | numSteps = 8, 247 | step = 1; 248 | 249 | // if we're in FOUR_CONNECTED mode then only even values of `tracingDir` are possible 250 | // (i.e. no diagonals and we advance by two) 251 | if (connectivity == Connectivity.FOUR_CONNECTED) { 252 | if (tracingDir & 1 == 1) tracingDir = (tracingDir + 1) % 8; 253 | numSteps = 4; 254 | step = 2; 255 | } 256 | 257 | var dir = tracingDir; 258 | 259 | for (i in 0...numSteps) { 260 | cx = x + searchDir[tracingDir].dx; 261 | cy = y + searchDir[tracingDir].dy; 262 | nextPoint.setTo(cx, cy); 263 | if (isPixelSolid(cx, cy)) { 264 | isolatedPixel = false; 265 | break; 266 | } else { // set non-solid pixel as marked 267 | //trace("- " + cx + "," + cy); 268 | setPixel32(markedPixels, cx, cy, MARKED); 269 | } 270 | tracingDir = (tracingDir + step) % 8; 271 | } 272 | 273 | return !isolatedPixel; 274 | } 275 | 276 | /** 277 | * Maps `label` to a color. 278 | * Override this to use your own label-to-color mapping. 279 | * 280 | * NOTE: Avoid using 0x00000000 as a returned value, as it's used 281 | * interally to identify unlabeled pixels. 282 | */ 283 | public function labelToColor(label:Int):Int 284 | { 285 | if (label >= colors.length) { 286 | colors[label] = 0xFF000000 | getColorFromHSV(hue, .9, 1); 287 | hue = (hue + .12) % 1.0; 288 | } 289 | return colors[label]; 290 | } 291 | 292 | /** 293 | * Override this to have a way to add logic everytime a pixel is labeled. 294 | */ 295 | private function setLabel(x:Int, y:Int, labelColor:Int):Void 296 | { 297 | setPixel32(labelMap, x, y, labelColor); 298 | } 299 | 300 | /** 301 | * Returns the 32-bit pixel color at position (`x`, `y`) from `pixels`. 302 | * If the specified position is out of bounds, `outerValue` is returned. 303 | */ 304 | private function getPixel32(pixels:Pixels, x:Int, y:Int, outerValue:Int = 0):Int 305 | { 306 | var res = outerValue; 307 | if (!isOutOfBounds(x, y)) { 308 | res = pixels.getPixel32(x, y); 309 | } 310 | return res; 311 | } 312 | 313 | /** 314 | * Writes a 32-bit pixel `color` at position (`x`, `y`) in `pixels`. 315 | * If the specified position is out of bounds nothing is written. 316 | */ 317 | private function setPixel32(pixels:Pixels, x:Int, y:Int, color:Int):Void 318 | { 319 | if (!isOutOfBounds(x, y)) { 320 | pixels.setPixel32(x, y, color); 321 | if (calcArea && pixels == labelMap) { 322 | var a:Null = areaMap.exists(color) ? areaMap.get(color) : 0; 323 | areaMap.set(color, a + 1); 324 | } 325 | } 326 | } 327 | 328 | /** 329 | * Returns true if the pixel at `x`, `y` is opaque (according to `alphaThreshold`). 330 | * Override this to use your own criteria to identify solid pixels. 331 | */ 332 | private function isPixelSolid(x:Int, y:Int):Bool { 333 | return (!isOutOfBounds(x, y) && sourcePixels.getByte((y * width + x) << 2) >= alphaThreshold); 334 | } 335 | 336 | inline private function isOutOfBounds(x:Int, y:Int):Bool { 337 | return (x < 0 || y < 0 || x >= width || y >= height); 338 | } 339 | 340 | private function getColorFromHSV(h:Float, s:Float, v:Float):Int 341 | { 342 | h = Std.int(h * 360); 343 | var hi:Int = Math.floor(h / 60) % 6, 344 | f:Float = h / 60 - Math.floor(h / 60), 345 | p:Float = (v * (1 - s)), 346 | q:Float = (v * (1 - f * s)), 347 | t:Float = (v * (1 - (1 - f) * s)); 348 | switch (hi) 349 | { 350 | case 0: return Std.int(v * 255) << 16 | Std.int(t * 255) << 8 | Std.int(p * 255); 351 | case 1: return Std.int(q * 255) << 16 | Std.int(v * 255) << 8 | Std.int(p * 255); 352 | case 2: return Std.int(p * 255) << 16 | Std.int(v * 255) << 8 | Std.int(t * 255); 353 | case 3: return Std.int(p * 255) << 16 | Std.int(q * 255) << 8 | Std.int(v * 255); 354 | case 4: return Std.int(t * 255) << 16 | Std.int(p * 255) << 8 | Std.int(v * 255); 355 | case 5: return Std.int(v * 255) << 16 | Std.int(p * 255) << 8 | Std.int(q * 255); 356 | default: return 0; 357 | } 358 | return 0; 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/hxGeomAlgo/Chaikin.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Chaikin curve smoothing, recursive implementation. 3 | * 4 | * Based on: 5 | * 6 | * @see https://sighack.com/post/chaikin-curves (Java - Manohar Vanga) 7 | * 8 | * Other credits should go to papers/work of: 9 | * 10 | * Chaikin, G. M. (1974). An algorithm for high-speed curve generation. Computer Graphics and Image Processing, 3(4), 346–349 11 | * @see https://sci-hub.st/10.1016/0146-664X(74)90028-8 (George Merrill Chaikin) 12 | * 13 | * @author azrafe7 14 | */ 15 | 16 | package hxGeomAlgo; 17 | 18 | using hxGeomAlgo.PolyTools; 19 | 20 | 21 | @:expose 22 | class Chaikin 23 | { 24 | 25 | static public function smooth(poly:Poly, iterations:Int = 3, close:Bool = false, ratio:Float = .25):Poly 26 | { 27 | var smoothedPoints = _smooth(poly, iterations, close, ratio); 28 | 29 | /* 30 | * Now we have to deal with one corner case. In the case 31 | * of open shapes, the first and last endpoints shouldn't 32 | * be moved. 33 | */ 34 | if (!close && smoothedPoints.length > 2) { 35 | smoothedPoints[0] = poly[0]; 36 | smoothedPoints[smoothedPoints.length - 1] = poly[poly.length - 1]; 37 | } 38 | 39 | return smoothedPoints; 40 | } 41 | 42 | static inline function cut(a:HxPoint, b:HxPoint, ratio:Float, newAB:Poly):Void 43 | { 44 | newAB[0] = PolyTools.lerpPoints(a, b, ratio); 45 | newAB[1] = PolyTools.lerpPoints(b, a, ratio); 46 | } 47 | 48 | static function _smooth(poly:Poly, iterations:Int, close:Bool, ratio:Float):Poly 49 | { 50 | if (iterations <= 0 || poly.length <= 2) { 51 | return poly.copy(); 52 | } 53 | 54 | var smoothedPoints = new Poly(); 55 | var newAB = new Poly(); 56 | newAB.resize(2); 57 | 58 | if (ratio > 0.5) ratio = 1.0 - ratio; 59 | 60 | /* 61 | * Step 1: Figure out how many corners the shape has 62 | * depending on whether it's open or closed. 63 | */ 64 | var numCorners = poly.length; 65 | if (!close) 66 | numCorners -= 1; 67 | 68 | /* 69 | * Step 2: Since we don't have access to edges directly 70 | * do a pairwise iteration over vertices instead. 71 | * Same thing. 72 | */ 73 | for (i in 0...numCorners) { 74 | 75 | // Get the i'th and (i+1)'th vertex to work on that edge. 76 | var a = poly.at(i); 77 | var b = poly.at(i + 1); 78 | 79 | // Step 3: Break it using our cut() function 80 | cut(a, b, ratio, newAB); 81 | 82 | // For all edges add both vertices 83 | // returned by our cut() method 84 | smoothedPoints.push(newAB[0]); 85 | smoothedPoints.push(newAB[1]); 86 | } 87 | 88 | return _smooth(smoothedPoints, iterations - 1, close, ratio); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/hxGeomAlgo/Debug.hx: -------------------------------------------------------------------------------- 1 | package hxGeomAlgo; 2 | 3 | import haxe.PosInfos; 4 | 5 | 6 | /** 7 | * @author azrafe7 8 | */ 9 | class Debug 10 | { 11 | /** 12 | * Used for sanity-checks throughout the code when in debug mode (or if -D GEOM_CHECKS is passed to the compiler). 13 | * Should be automatically stripped out by the compiler in release mode (or if -D NO_GEOM_CHECKS is passed to the compiler). 14 | */ 15 | #if ((debug && !NO_GEOM_CHECKS) || GEOM_CHECKS) 16 | static public function assert(cond:Bool, ?message:String, ?pos:PosInfos) { 17 | if (!cond) { 18 | throw pos.fileName + ":" + pos.lineNumber + ": ASSERT FAILED! " + (message != null ? message : ""); 19 | } 20 | } 21 | #elseif (!debug || NO_GEOM_CHECKS) 22 | inline static public function assert(cond:Bool, ?message:String, ?pos:PosInfos) { 23 | return; 24 | } 25 | #end 26 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/EarCut.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Ear clipping implementation - polygon triangulation and triangles polygonization. 3 | * NOTE: Should work only for non self-intersecting polygons (but holes are supported). 4 | * 5 | * Based on: 6 | * 7 | * @see https://github.com/mapbox/earcut (JS - by Vladimir Agafonkin) 8 | * @see http://www.ewjordan.com/earClip/ (Java - by Eric Jordan) 9 | * 10 | * @author azrafe7 11 | */ 12 | 13 | package hxGeomAlgo; 14 | 15 | import haxe.ds.ArraySort; 16 | import hxGeomAlgo.PolyTools; 17 | 18 | 19 | @:expose 20 | class EarCut 21 | { 22 | /** 23 | * Triangulates a polygon. 24 | * 25 | * @param poly Array of points defining the polygon. 26 | * @param holes Optional array of holes in the poly. 27 | * @return An array of Triangle resulting from the triangulation. 28 | */ 29 | static public function triangulate(poly:Poly, ?holes:Array = null):Array { 30 | var data = PolyTools.toFloatArray(poly); 31 | 32 | var holeIndices = null; 33 | var allVertices = poly; 34 | 35 | // concat poly and holes' vertices 36 | if (holes != null && holes.length > 0) { 37 | 38 | allVertices = poly.concat([]); 39 | PolyTools.flatten(holes, allVertices); 40 | 41 | holeIndices = []; 42 | var holeIdx = poly.length; 43 | for (hole in holes) { 44 | for (f in PolyTools.toFloatArray(hole)) data.push(f); 45 | holeIndices.push(holeIdx); 46 | holeIdx += hole.length; 47 | } 48 | } 49 | 50 | var triIndices:Array = earcut(data, holeIndices); 51 | 52 | var res:Array = []; 53 | var i = 0; 54 | while (i < triIndices.length) { 55 | var tri:Tri = [ 56 | allVertices[triIndices[i]], 57 | allVertices[triIndices[i + 1]], 58 | allVertices[triIndices[i + 2]] 59 | ]; 60 | res.push(tri); 61 | i += 3; 62 | } 63 | 64 | return res; 65 | } 66 | 67 | 68 | /** Original API from mapbox (see triangulate() for a wrapper around this) */ 69 | static public function earcut(data:Array, ?holeIndices:Array, dim:Int = 2):Array { 70 | 71 | var hasHoles:Bool = holeIndices != null && holeIndices.length > 0; 72 | var outerLen:Int = hasHoles ? holeIndices[0] * dim : data.length; 73 | var outerNode:EarNode = linkedList(data, 0, outerLen, dim, true); 74 | var triangles = []; 75 | 76 | if (outerNode == null) return triangles; 77 | 78 | outerNode = filterPoints(outerNode); 79 | 80 | var minX, minY, maxX, maxY, x, y, size; 81 | minX = minY = maxX = maxY = x = y = size = Math.NaN; 82 | 83 | if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim); 84 | 85 | // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox 86 | if (data.length > 80 * dim) { 87 | minX = maxX = data[0]; 88 | minY = maxY = data[1]; 89 | 90 | var i = dim; 91 | while (i < outerLen) { 92 | x = data[i]; 93 | y = data[i + 1]; 94 | if (x < minX) minX = x; 95 | if (y < minY) minY = y; 96 | if (x > maxX) maxX = x; 97 | if (y > maxY) maxY = y; 98 | i += dim; 99 | } 100 | 101 | // minX, minY and size are later used to transform coords into integers for z-order calculation 102 | size = Math.max(maxX - minX, maxY - minY); 103 | } 104 | 105 | earcutLinked(outerNode, triangles, dim, minX, minY, size); 106 | 107 | return triangles; 108 | } 109 | 110 | // create a circular doubly linked list from polygon points in the specified winding order 111 | static function linkedList(data:Array, start:Int, end:Int, dim:Int, clockwise:Bool):EarNode { 112 | var i, last:EarNode = null; 113 | 114 | if (clockwise == (signedArea(data, start, end, dim) > 0)) { 115 | i = start; 116 | while (i < end) { 117 | last = insertNode(i, data[i], data[i + 1], last); 118 | i += dim; 119 | } 120 | } else { 121 | i = end - dim; 122 | while (i >= start) { 123 | last = insertNode(i, data[i], data[i + 1], last); 124 | i -= dim; 125 | } 126 | } 127 | 128 | if (last != null && equals(last, last.next)) { 129 | removeNode(last); 130 | last = last.next; 131 | } 132 | 133 | return last; 134 | } 135 | 136 | // eliminate colinear or duplicate points 137 | static function filterPoints(start:EarNode, ?end:EarNode = null):EarNode { 138 | if (start == null) return start; 139 | if (end == null) end = start; 140 | 141 | var p = start, 142 | again; 143 | 144 | do { 145 | again = false; 146 | 147 | if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) == 0)) { 148 | removeNode(p); 149 | p = end = p.prev; 150 | if (p == p.next) return null; 151 | again = true; 152 | 153 | } else { 154 | p = p.next; 155 | } 156 | } while (again || p != end); 157 | 158 | return end; 159 | } 160 | 161 | // main ear slicing loop which triangulates a polygon (given as a linked list) 162 | static function earcutLinked(ear:EarNode, triangles:Array, dim:Int, minX:Float, minY:Float, size:Float, pass = 0):Void { 163 | if (ear == null) return; 164 | 165 | // interlink polygon nodes in z-order 166 | if (pass == 0 && !Math.isNaN(size)) indexCurve(ear, minX, minY, size); 167 | 168 | var stop = ear, 169 | prev, next; 170 | 171 | // iterate through ears, slicing them one by one 172 | while (ear.prev != ear.next) { 173 | prev = ear.prev; 174 | next = ear.next; 175 | 176 | if ((!Math.isNaN(size) ? isEarHashed(ear, minX, minY, size) : isEar(ear))) { 177 | // cut off the triangle 178 | triangles.push(Std.int(prev.i / dim)); 179 | triangles.push(Std.int(ear.i / dim)); 180 | triangles.push(Std.int(next.i / dim)); 181 | 182 | removeNode(ear); 183 | 184 | // skipping the next vertice leads to less sliver triangles 185 | ear = next.next; 186 | stop = next.next; 187 | 188 | continue; 189 | } 190 | 191 | ear = next; 192 | 193 | // if we looped through the whole remaining polygon and can't find any more ears 194 | if (ear == stop) { 195 | // try filtering points and slicing again 196 | if (pass == 0) { 197 | earcutLinked(filterPoints(ear), triangles, dim, minX, minY, size, 1); 198 | 199 | // if this didn't work, try curing all small self-intersections locally 200 | } else if (pass == 1) { 201 | ear = cureLocalIntersections(ear, triangles, dim); 202 | earcutLinked(ear, triangles, dim, minX, minY, size, 2); 203 | 204 | // as a last resort, try splitting the remaining polygon into two 205 | } else if (pass == 2) { 206 | splitEarcut(ear, triangles, dim, minX, minY, size); 207 | } 208 | 209 | break; 210 | } 211 | } 212 | } 213 | 214 | // check whether a polygon node forms a valid ear with adjacent nodes 215 | static function isEar(ear:EarNode):Bool { 216 | var a = ear.prev, 217 | b = ear, 218 | c = ear.next; 219 | 220 | if (area(a, b, c) >= 0) return false; // reflex, can't be an ear 221 | 222 | // now make sure we don't have other points inside the potential ear 223 | var p = ear.next.next; 224 | 225 | while (p != ear.prev) { 226 | if (pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && 227 | area(p.prev, p, p.next) >= 0) return false; 228 | p = p.next; 229 | } 230 | 231 | return true; 232 | } 233 | 234 | static function isEarHashed(ear:EarNode, minX:Float, minY:Float, size:Float):Bool { 235 | var a = ear.prev, 236 | b = ear, 237 | c = ear.next; 238 | 239 | if (area(a, b, c) >= 0) return false; // reflex, can't be an ear 240 | 241 | // triangle bbox; min & max are calculated like this for speed 242 | var minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x), 243 | minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y), 244 | maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x), 245 | maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y); 246 | 247 | // z-order range for the current triangle bbox; 248 | var minZ = zOrder(minTX, minTY, minX, minY, size), 249 | maxZ = zOrder(maxTX, maxTY, minX, minY, size); 250 | 251 | // first look for points inside the triangle in increasing z-order 252 | var p = ear.nextZ; 253 | 254 | while (p != null && p.z <= maxZ) { 255 | if (p != ear.prev && p != ear.next && 256 | pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && 257 | area(p.prev, p, p.next) >= 0) return false; 258 | p = p.nextZ; 259 | } 260 | 261 | // then look for points in decreasing z-order 262 | p = ear.prevZ; 263 | 264 | while (p != null && p.z >= minZ) { 265 | if (p != ear.prev && p != ear.next && 266 | pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && 267 | area(p.prev, p, p.next) >= 0) return false; 268 | p = p.prevZ; 269 | } 270 | 271 | return true; 272 | } 273 | 274 | // go through all polygon nodes and cure small local self-intersections 275 | static function cureLocalIntersections(start:EarNode, triangles:Array, dim:Int):EarNode { 276 | var p = start; 277 | do { 278 | var a:EarNode = p.prev, 279 | b:EarNode = p.next.next; 280 | 281 | if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) { 282 | 283 | triangles.push(Std.int(a.i / dim)); 284 | triangles.push(Std.int(p.i / dim)); 285 | triangles.push(Std.int(b.i / dim)); 286 | 287 | // remove two nodes involved 288 | removeNode(p); 289 | removeNode(p.next); 290 | 291 | p = start = b; 292 | } 293 | p = p.next; 294 | } while (p != start); 295 | 296 | return p; 297 | } 298 | 299 | // try splitting polygon into two and triangulate them independently 300 | static function splitEarcut(start:EarNode, triangles:Array, dim:Int, minX:Float, minY:Float, size:Float):Void { 301 | // look for a valid diagonal that divides the polygon into two 302 | var a = start; 303 | do { 304 | var b = a.next.next; 305 | while (b != a.prev) { 306 | if (a.i != b.i && isValidDiagonal(a, b)) { 307 | // split the polygon in two by the diagonal 308 | var c = splitPolygon(a, b); 309 | 310 | // filter colinear points around the cuts 311 | a = filterPoints(a, a.next); 312 | c = filterPoints(c, c.next); 313 | 314 | // run earcut on each half 315 | earcutLinked(a, triangles, dim, minX, minY, size); 316 | earcutLinked(c, triangles, dim, minX, minY, size); 317 | return; 318 | } 319 | b = b.next; 320 | } 321 | a = a.next; 322 | } while (a != start); 323 | } 324 | 325 | // link every hole into the outer loop, producing a single-ring polygon without holes 326 | static function eliminateHoles(data:Array, holeIndices:Array, outerNode:EarNode, dim:Int):EarNode { 327 | var queue:Array = [], 328 | i, len, start, end, list; 329 | 330 | i = 0; 331 | len = holeIndices.length; 332 | while (i < len) { 333 | start = holeIndices[i] * dim; 334 | end = i < len - 1 ? holeIndices[i + 1] * dim : data.length; 335 | list = linkedList(data, start, end, dim, false); 336 | if (list == list.next) list.steiner = true; 337 | queue.push(getLeftmost(list)); 338 | i++; 339 | } 340 | 341 | ArraySort.sort(queue, compareX); 342 | 343 | // process holes from left to right 344 | for (i in 0...queue.length) { 345 | eliminateHole(queue[i], outerNode); 346 | outerNode = filterPoints(outerNode, outerNode.next); 347 | } 348 | 349 | return outerNode; 350 | } 351 | 352 | static inline function compareX(a:EarNode, b:EarNode):Int { 353 | return Std.int(a.x - b.x); 354 | } 355 | 356 | // find a bridge between vertices that connects hole with an outer ring and and link it 357 | static function eliminateHole(hole:EarNode, outerNode:EarNode):Void { 358 | outerNode = findHoleBridge(hole, outerNode); 359 | if (outerNode != null) { 360 | var b = splitPolygon(outerNode, hole); 361 | filterPoints(b, b.next); 362 | } 363 | } 364 | 365 | // David Eberly's algorithm for finding a bridge between hole and outer polygon 366 | static function findHoleBridge(hole:EarNode, outerNode:EarNode):EarNode { 367 | var p = outerNode, 368 | hx = hole.x, 369 | hy = hole.y, 370 | qx = Math.NEGATIVE_INFINITY, 371 | m = null; 372 | 373 | // find a segment intersected by a ray from the hole's leftmost point to the left; 374 | // segment's endpoint with lesser x will be potential connection point 375 | do { 376 | if (hy <= p.y && hy >= p.next.y) { 377 | var x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y); 378 | if (x <= hx && x > qx) { 379 | qx = x; 380 | if (x == hx) { 381 | if (hy == p.y) return p; 382 | if (hy == p.next.y) return p.next; 383 | } 384 | m = p.x < p.next.x ? p : p.next; 385 | } 386 | } 387 | p = p.next; 388 | } while (p != outerNode); 389 | 390 | if (m == null) return null; 391 | 392 | if (hx == qx) return m.prev; // hole touches outer segment; pick lower endpoint 393 | 394 | // look for points inside the triangle of hole point, segment intersection and endpoint; 395 | // if there are no points found, we have a valid connection; 396 | // otherwise choose the point of the minimum angle with the ray as connection point 397 | 398 | var stop = m, 399 | mx = m.x, 400 | my = m.y, 401 | tanMin = Math.POSITIVE_INFINITY, 402 | tan; 403 | 404 | p = m.next; 405 | 406 | while (p != stop) { 407 | if (hx >= p.x && p.x >= mx && 408 | pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) { 409 | 410 | tan = Math.abs(hy - p.y) / (hx - p.x); // tangential 411 | 412 | if ((tan < tanMin || (tan == tanMin && p.x > m.x)) && locallyInside(p, hole)) { 413 | m = p; 414 | tanMin = tan; 415 | } 416 | } 417 | 418 | p = p.next; 419 | } 420 | 421 | return m; 422 | } 423 | 424 | // interlink polygon nodes in z-order 425 | static function indexCurve(start:EarNode, minX:Float, minY:Float, size:Float):Void { 426 | var p:EarNode = start; 427 | do { 428 | if (p.z == null) p.z = zOrder(p.x, p.y, minX, minY, size); 429 | p.prevZ = p.prev; 430 | p.nextZ = p.next; 431 | p = p.next; 432 | } while (p != start); 433 | 434 | p.prevZ.nextZ = null; 435 | p.prevZ = null; 436 | 437 | sortLinked(p); 438 | } 439 | 440 | // Simon Tatham's linked list merge sort algorithm 441 | // http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html 442 | static function sortLinked(list:EarNode):EarNode { 443 | var i, p:EarNode = null, q:EarNode = null, e:EarNode = null, tail:EarNode = null, numMerges, pSize, qSize, 444 | inSize = 1; 445 | 446 | do { 447 | p = list; 448 | list = null; 449 | tail = null; 450 | numMerges = 0; 451 | 452 | while (p != null) { 453 | numMerges++; 454 | q = p; 455 | pSize = 0; 456 | for (i in 0...inSize) { 457 | pSize++; 458 | q = q.nextZ; 459 | if (q == null) break; 460 | } 461 | 462 | qSize = inSize; 463 | 464 | while (pSize > 0 || (qSize > 0 && q != null)) { 465 | 466 | if (pSize == 0) { 467 | e = q; 468 | q = q.nextZ; 469 | qSize--; 470 | } else if (qSize == 0 || q == null) { 471 | e = p; 472 | p = p.nextZ; 473 | pSize--; 474 | } else if (p.z <= q.z) { 475 | e = p; 476 | p = p.nextZ; 477 | pSize--; 478 | } else { 479 | e = q; 480 | q = q.nextZ; 481 | qSize--; 482 | } 483 | 484 | if (tail != null) tail.nextZ = e; 485 | else list = e; 486 | 487 | e.prevZ = tail; 488 | tail = e; 489 | } 490 | 491 | p = q; 492 | } 493 | 494 | tail.nextZ = null; 495 | inSize *= 2; 496 | 497 | } while (numMerges > 1); 498 | 499 | return list; 500 | } 501 | 502 | // z-order of a point given coords and size of the data bounding box 503 | static function zOrder(x:Float, y:Float, minX:Float, minY:Float, size:Float):Int { 504 | // coords are transformed into non-negative 15-bit integer range 505 | var _x = Std.int(32767 * (x - minX) / size); 506 | var _y = Std.int(32767 * (y - minY) / size); 507 | 508 | _x = (_x | (_x << 8)) & 0x00FF00FF; 509 | _x = (_x | (_x << 4)) & 0x0F0F0F0F; 510 | _x = (_x | (_x << 2)) & 0x33333333; 511 | _x = (_x | (_x << 1)) & 0x55555555; 512 | 513 | _y = (_y | (_y << 8)) & 0x00FF00FF; 514 | _y = (_y | (_y << 4)) & 0x0F0F0F0F; 515 | _y = (_y | (_y << 2)) & 0x33333333; 516 | _y = (_y | (_y << 1)) & 0x55555555; 517 | 518 | return _x | (_y << 1); 519 | } 520 | 521 | // find the leftmost node of a polygon ring 522 | static function getLeftmost(start:EarNode):EarNode { 523 | var p = start, 524 | leftmost = start; 525 | do { 526 | if (p.x < leftmost.x) leftmost = p; 527 | p = p.next; 528 | } while (p != start); 529 | 530 | return leftmost; 531 | } 532 | 533 | // check if a point lies within a convex triangle 534 | static function pointInTriangle(ax:Float, ay:Float, bx:Float, by:Float, cx:Float, cy:Float, px:Float, py:Float):Bool { 535 | return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 && 536 | (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 && 537 | (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0; 538 | } 539 | 540 | // check if a diagonal between two polygon nodes is valid (lies in polygon interior) 541 | static function isValidDiagonal(a:EarNode, b:EarNode):Bool { 542 | return a.next.i != b.i && a.prev.i != b.i && !intersectsPolygon(a, b) && 543 | locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b); 544 | } 545 | 546 | // signed area of a triangle 547 | static inline function area(p:EarNode, q:EarNode, r:EarNode):Float { 548 | return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y); 549 | } 550 | 551 | // check if two points are equal 552 | static inline function equals(p1:EarNode, p2:EarNode):Bool { 553 | return p1.x == p2.x && p1.y == p2.y; 554 | } 555 | 556 | // check if two segments intersect 557 | static function intersects(p1:EarNode, q1:EarNode, p2:EarNode, q2:EarNode):Bool { 558 | if ((equals(p1, q1) && equals(p2, q2)) || 559 | (equals(p1, q2) && equals(p2, q1))) return true; 560 | return (area(p1, q1, p2) > 0) != (area(p1, q1, q2) > 0) && 561 | (area(p2, q2, p1) > 0) != (area(p2, q2, q1) > 0); 562 | } 563 | 564 | // check if a polygon diagonal intersects any polygon segments 565 | static function intersectsPolygon(a:EarNode, b:EarNode):Bool { 566 | var p = a; 567 | do { 568 | if (p.i != a.i && p.next.i != a.i && p.i != b.i && p.next.i != b.i && 569 | intersects(p, p.next, a, b)) return true; 570 | p = p.next; 571 | } while (p != a); 572 | 573 | return false; 574 | } 575 | 576 | // check if a polygon diagonal is locally inside the polygon 577 | static function locallyInside(a:EarNode, b:EarNode):Bool { 578 | return area(a.prev, a, a.next) < 0 ? 579 | area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 : 580 | area(a, b, a.prev) < 0 || area(a, a.next, b) < 0; 581 | } 582 | 583 | // check if the middle point of a polygon diagonal is inside the polygon 584 | static function middleInside(a:EarNode, b:EarNode):Bool { 585 | var p = a, 586 | inside = false, 587 | px = (a.x + b.x) / 2, 588 | py = (a.y + b.y) / 2; 589 | do { 590 | if (((p.y > py) != (p.next.y > py)) && (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x)) 591 | inside = !inside; 592 | p = p.next; 593 | } while (p != a); 594 | 595 | return inside; 596 | } 597 | 598 | // link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two; 599 | // if one belongs to the outer ring and another to a hole, it merges it into a single ring 600 | static function splitPolygon(a:EarNode, b:EarNode):EarNode { 601 | var a2 = new EarNode(a.i, a.x, a.y), 602 | b2 = new EarNode(b.i, b.x, b.y), 603 | an = a.next, 604 | bp = b.prev; 605 | 606 | a.next = b; 607 | b.prev = a; 608 | 609 | a2.next = an; 610 | an.prev = a2; 611 | 612 | b2.next = a2; 613 | a2.prev = b2; 614 | 615 | bp.next = b2; 616 | b2.prev = bp; 617 | 618 | return b2; 619 | } 620 | 621 | // create a node and optionally link it with previous one (in a circular doubly linked list) 622 | static function insertNode(i:Int, x:Float, y:Float, last:EarNode = null):EarNode { 623 | var p = new EarNode(i, x, y); 624 | 625 | if (last == null) { 626 | p.prev = p; 627 | p.next = p; 628 | 629 | } else { 630 | p.next = last.next; 631 | p.prev = last; 632 | last.next.prev = p; 633 | last.next = p; 634 | } 635 | return p; 636 | } 637 | 638 | static function removeNode(p:EarNode):Void { 639 | p.next.prev = p.prev; 640 | p.prev.next = p.next; 641 | 642 | if (p.prevZ != null) p.prevZ.nextZ = p.nextZ; 643 | if (p.nextZ != null) p.nextZ.prevZ = p.prevZ; 644 | } 645 | 646 | 647 | /** 648 | * Returns a percentage difference between the polygon area and its triangulation area; 649 | * used to verify correctness of triangulation 650 | */ 651 | static public function deviation(data:Array, holeIndices:Array, dim:Int, triangles:Array):Float { 652 | var hasHoles = (holeIndices != null) && holeIndices.length > 0; 653 | var outerLen = hasHoles ? holeIndices[0] * dim : data.length; 654 | 655 | var polygonArea = Math.abs(signedArea(data, 0, outerLen, dim)); 656 | if (hasHoles) { 657 | var i = 0; 658 | var len = holeIndices.length; 659 | while (i < len) { 660 | var start = holeIndices[i] * dim; 661 | var end = i < len - 1 ? holeIndices[i + 1] * dim : data.length; 662 | polygonArea -= Math.abs(signedArea(data, start, end, dim)); 663 | i++; 664 | } 665 | } 666 | 667 | var trianglesArea = 0.; 668 | var i = 0; 669 | while (i < triangles.length) { 670 | var a = triangles[i] * dim; 671 | var b = triangles[i + 1] * dim; 672 | var c = triangles[i + 2] * dim; 673 | trianglesArea += Math.abs( 674 | (data[a] - data[c]) * (data[b + 1] - data[a + 1]) - 675 | (data[a] - data[b]) * (data[c + 1] - data[a + 1])); 676 | i += 3; 677 | } 678 | 679 | return polygonArea == 0 && trianglesArea == 0 ? 0 : 680 | Math.abs((trianglesArea - polygonArea) / polygonArea); 681 | } 682 | 683 | static function signedArea(data:Array, start:Int, end:Int, dim:Int):Float { 684 | var sum = 0.; 685 | var i = start; 686 | var j = end - dim; 687 | 688 | while (i < end) { 689 | sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]); 690 | j = i; 691 | i += dim; 692 | } 693 | return sum; 694 | } 695 | 696 | /** 697 | * Turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts 698 | * @return An object of type {vertices:Array, holes:Array, dimensions:Int} you can pass to earcut() 699 | */ 700 | static public function flatten(data:Array>>) { 701 | var dim = data[0][0].length, 702 | result = {vertices: [], holes: [], dimensions: dim}, 703 | holeIndex = 0; 704 | 705 | for (i in 0...data.length) { 706 | for (j in 0...data[i].length) { 707 | for (d in 0...dim) result.vertices.push(data[i][j][d]); 708 | } 709 | if (i > 0) { 710 | holeIndex += data[i - 1].length; 711 | result.holes.push(holeIndex); 712 | } 713 | } 714 | return result; 715 | } 716 | 717 | 718 | /** 719 | * Merges triangles (defining a triangulated concave polygon) into a set of convex polygons. 720 | * 721 | * @param triangulation An array of triangles defining the concave polygon. 722 | * @return An array of convex polygons being a decomposition of the original concave polygon. 723 | */ 724 | public static function polygonize(triangulation:Array):Array 725 | { 726 | var polys = new Array(); 727 | 728 | if (triangulation.length == 0) 729 | { 730 | return []; 731 | } 732 | else 733 | { 734 | var covered = new Array(); 735 | for (i in 0...triangulation.length) covered[i] = false; 736 | 737 | var notDone:Bool = true; 738 | while (notDone) 739 | { 740 | var poly:Poly = null; 741 | 742 | var currTri:Int = -1; 743 | for (i in 0...triangulation.length) 744 | { 745 | if (covered[i]) continue; 746 | currTri = i; 747 | break; 748 | } 749 | if (currTri == -1) 750 | { 751 | notDone = false; 752 | } 753 | else 754 | { 755 | poly = triangulation[currTri]; 756 | covered[currTri] = true; 757 | for (i in 0...triangulation.length) 758 | { 759 | if (covered[i]) continue; 760 | var newPoly:Poly = addTriangle(poly, triangulation[i]); 761 | if (newPoly == null) continue; 762 | if (PolyTools.isConvex(newPoly)) 763 | { 764 | poly = newPoly; 765 | covered[i] = true; 766 | } 767 | } 768 | 769 | polys.push(poly); 770 | } 771 | } 772 | } 773 | 774 | return polys; 775 | } 776 | 777 | /** 778 | * Tries to add a triangle to the polygon. 779 | * Assumes bitwise equality of join vertices. 780 | * 781 | * @return null if it can't connect properly. 782 | */ 783 | static public function addTriangle(poly:Poly, t:Tri):Poly 784 | { 785 | // first, find vertices that connect 786 | var firstP:Int = -1; 787 | var firstT:Int = -1; 788 | var secondP:Int = -1; 789 | var secondT:Int = -1; 790 | 791 | for (i in 0...poly.length) 792 | { 793 | if (t[0].x == poly[i].x && t[0].y == poly[i].y) 794 | { 795 | if (firstP == -1) 796 | { 797 | firstP = i; firstT = 0; 798 | } 799 | else 800 | { 801 | secondP = i; secondT = 0; 802 | } 803 | } 804 | else if (t[1].x == poly[i].x && t[1].y == poly[i].y) 805 | { 806 | if (firstP == -1) 807 | { 808 | firstP = i; firstT = 1; 809 | } 810 | else 811 | { 812 | secondP = i; secondT = 1; 813 | } 814 | } 815 | else if (t[2].x == poly[i].x && t[2].y == poly[i].y) 816 | { 817 | if (firstP == -1) 818 | { 819 | firstP = i; firstT = 2; 820 | } 821 | else 822 | { 823 | secondP = i; secondT = 2; 824 | } 825 | } 826 | else 827 | { 828 | //trace(t); 829 | //trace(firstP, firstT, secondP, secondT); 830 | } 831 | } 832 | 833 | // fix ordering if first should be last vertex of poly 834 | if (firstP == 0 && secondP == poly.length - 1) 835 | { 836 | firstP = poly.length - 1; 837 | secondP = 0; 838 | } 839 | 840 | // didn't find it 841 | if (secondP == -1) 842 | return null; 843 | 844 | // find tip index on triangle 845 | var tipT:Int = 0; 846 | if (tipT == firstT || tipT == secondT) tipT = 1; 847 | if (tipT == firstT || tipT == secondT) tipT = 2; 848 | 849 | var newPoints:Array = new Array(); 850 | 851 | for (i in 0...poly.length) 852 | { 853 | newPoints.push(poly[i]); 854 | 855 | if (i == firstP) 856 | newPoints.push(t[tipT]); 857 | } 858 | 859 | return newPoints; 860 | } 861 | } 862 | 863 | 864 | @:allow(hxGeomAlgo.EarCut) 865 | class EarNode { 866 | 867 | var i:Int; 868 | 869 | var x:Float; 870 | var y:Float; 871 | 872 | var prev:EarNode; 873 | var next:EarNode; 874 | 875 | var z:Null; 876 | 877 | var prevZ:EarNode; 878 | var nextZ:EarNode; 879 | 880 | var steiner:Bool; 881 | 882 | 883 | public function new(i:Int, x:Float, y:Float) { 884 | // vertice index in coordinates array 885 | this.i = i; 886 | 887 | // vertex coordinates 888 | this.x = x; 889 | this.y = y; 890 | 891 | // previous and next vertice nodes in a polygon ring 892 | this.prev = null; 893 | this.next = null; 894 | 895 | // z-order curve value 896 | this.z = null; 897 | 898 | // previous and next nodes in z-order 899 | this.prevZ = null; 900 | this.nextZ = null; 901 | 902 | // indicates whether this is a steiner point 903 | this.steiner = false; 904 | } 905 | 906 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/Heap.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Heap implementation. 3 | * 4 | * Based on: 5 | * 6 | * @see http://en.wikipedia.org/wiki/Binary_heap (Binary Heap) 7 | * @see https://github.com/jonasmalacofilho/dheap (Haxe - Jonas Malaco Filho) 8 | * 9 | * @author azrafe7 10 | */ 11 | 12 | package hxGeomAlgo; 13 | 14 | import hxGeomAlgo.Debug; 15 | 16 | 17 | /** Heap elements must implement this interface. */ 18 | interface Heapable { 19 | 20 | /** Used internally by Heap. Do not modify. */ 21 | var position:Int; 22 | 23 | /** 24 | * Returns the result of comparing `this` to `other`, where the result is expected to be: 25 | * less than zero if `this` < `other` 26 | * equal to zero if `this` == `other` 27 | * greater than zero if `this` > `other` 28 | */ 29 | function compare(other:T):Int; 30 | } 31 | 32 | 33 | /** 34 | * Heap implementation (over Array). 35 | * 36 | * Note: depending on the compare function (i.e. if it's ascending or descending), 37 | * it will act as a MinHeap or MaxHeap (meaning that `pop()` will return the smallest 38 | * or the largest element respectively). 39 | * 40 | * @author azrafe7 41 | */ 42 | class Heap> 43 | { 44 | private var data:Array; 45 | 46 | public function new():Void 47 | { 48 | data = new Array(); 49 | } 50 | 51 | /** Number of elements in the Heap. */ 52 | public var length(default, null):Int = 0; 53 | 54 | /** Inserts `obj` into the Heap. */ 55 | public function push(obj:T):Void 56 | { 57 | var i = length; 58 | set(obj, i); 59 | length++; 60 | if (length > 1) bubbleUp(i); 61 | } 62 | 63 | /** Returns the root element (i.e. the smallest or largest, depending on compare()) and removes it from the Heap. Or null if the Heap is empty. */ 64 | public function pop():T 65 | { 66 | if (length == 0) return null; 67 | 68 | var res = data[0]; 69 | var len = length; 70 | var lastObj = data[len - 1]; 71 | data[len - 1] = null; 72 | length--; 73 | if (len > 1) { 74 | set(lastObj, 0); 75 | bubbleDown(0); 76 | } 77 | 78 | return res; 79 | } 80 | 81 | /** Returns the root element (i.e. the smallest or largest, depending on compare()) without removing it from the Heap. Or null if the Heap is empty. */ 82 | public function top():T 83 | { 84 | return length > 0 ? data[0] : null; 85 | } 86 | 87 | /** Removes `obj` from the Heap. Checks for correctness are only done in debug. */ 88 | public function remove(obj:T):Int 89 | { 90 | var pos = obj.position; 91 | Debug.assert((pos >= 0 && pos < length), "Object not found."); 92 | Debug.assert(data[pos] == obj, '`obj` and retrieved object at $pos don\'t match.'); 93 | 94 | var len = length; 95 | var lastObj = data[len - 1]; 96 | data[len - 1] = null; 97 | length--; 98 | if (pos != len - 1) { 99 | set(lastObj, pos); 100 | lastObj.compare(obj) < 0 ? bubbleUp(pos) : bubbleDown(pos); 101 | } 102 | return pos; 103 | } 104 | 105 | inline public function clear():Void 106 | { 107 | #if (flash || js) 108 | untyped data.length = 0; 109 | #else 110 | data.splice(0, length); 111 | #end 112 | length = 0; 113 | } 114 | 115 | private function bubbleDown(i:Int):Void 116 | { 117 | var left = leftOf(i); 118 | var right = rightOf(i); 119 | var curr = i; 120 | if (left < length && data[left].compare(data[curr]) < 0) curr = left; 121 | if (right < length && data[right].compare(data[curr]) < 0) curr = right; 122 | if (curr != i) { 123 | swap(curr, i); 124 | bubbleDown(curr); 125 | } 126 | } 127 | 128 | private function bubbleUp(i:Int):Void 129 | { 130 | while (i > 0 && !(data[i].compare(data[parentOf(i)]) > 0)) { 131 | var parent = parentOf(i); 132 | swap(parent, i); 133 | i = parent; 134 | } 135 | } 136 | 137 | public function validate():Void 138 | { 139 | if (length > 0) _validate(0); 140 | } 141 | 142 | public function toArray():Array 143 | { 144 | return [].concat(data); 145 | } 146 | 147 | private function _validate(i:Int):Void 148 | { 149 | var len = length; 150 | var left = leftOf(i); 151 | var right = rightOf(i); 152 | 153 | if (left < len) { 154 | Debug.assert(data[i].compare(data[left]) <= 0, 'Broken heap invariant (parent@$i > leftChild@$left).'); 155 | _validate(leftOf(i)); 156 | } 157 | if (right < len) { 158 | Debug.assert(data[i].compare(data[right]) <= 0, 'Broken heap invariant (parent@$i > rightChild@$right).'); 159 | _validate(rightOf(i)); 160 | } 161 | } 162 | 163 | private function set(obj:T, index:Int):Void 164 | { 165 | data[index] = obj; 166 | obj.position = index; 167 | } 168 | 169 | inline private function leftOf(i:Int):Int 170 | { 171 | return 2 * i + 1; 172 | } 173 | 174 | inline private function rightOf(i:Int):Int 175 | { 176 | return 2 * i + 2; 177 | } 178 | 179 | inline private function parentOf(i:Int):Int 180 | { 181 | return (i - 1) >> 1; 182 | } 183 | 184 | inline private function swap(i:Int, j:Int):Void 185 | { 186 | var temp = data[i]; 187 | set(data[j], i); 188 | set(temp, j); 189 | } 190 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/HertelMehlhorn.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Hertel-Mehlhorn convex polygonization from arbitrary triangulation. 3 | * 4 | * Based on: 5 | * 6 | * @see https://github.com/ivanfratric/polypartition (C - by Ivan Fratric) 7 | * @see https://web.archive.org/web/20140102033642/http://www.philvaz.com/compgeom/ (by Phil Porvaznik) 8 | * 9 | * @author azrafe7 10 | */ 11 | 12 | package hxGeomAlgo; 13 | 14 | using hxGeomAlgo.PolyTools; 15 | 16 | 17 | @:expose 18 | class HertelMehlhorn 19 | { 20 | 21 | static public var diagonals:Array<{from:HxPoint, to:HxPoint}>; 22 | 23 | /** 24 | * Merges triangles (defining a triangulated polygon) into a set of convex polygons. 25 | * 26 | * @param triangulation An array of triangles defining the polygon. 27 | * @return An array of convex polygons being a decomposition of the original polygon. 28 | */ 29 | static public function polygonize(triangulation:Array):Array { 30 | var res:Array = triangulation.concat([]); 31 | diagonals = []; 32 | 33 | var poly:Poly, qoly:Poly, newPoly:Poly; 34 | var d1:HxPoint, d2:HxPoint; 35 | var p:HxPoint, q:HxPoint; 36 | var isDiagonal:Bool; 37 | 38 | var polyIt, qolyIt, j; 39 | var outerIt = 0; 40 | var innerIt = 0; 41 | while (outerIt < res.length) { 42 | 43 | poly = res[outerIt]; 44 | qoly = res[0]; 45 | 46 | polyIt = 0; 47 | qolyIt = 0; 48 | while (polyIt < poly.length) { 49 | d1 = poly[polyIt]; 50 | d2 = poly.at(polyIt + 1); 51 | 52 | isDiagonal = false; 53 | 54 | innerIt = outerIt + 1; 55 | while (innerIt < res.length) { 56 | qoly = res[innerIt]; 57 | 58 | qolyIt = 0; 59 | while (qolyIt < qoly.length) { 60 | q = qoly[qolyIt]; 61 | if (d2.x != q.x || d2.y != q.y) { 62 | qolyIt++; 63 | continue; 64 | } 65 | q = qoly.at(qolyIt + 1); 66 | if (d1.x != q.x || d1.y != q.y) { 67 | qolyIt++; 68 | continue; 69 | } 70 | isDiagonal = true; 71 | break; 72 | } 73 | if (isDiagonal) break; 74 | innerIt++; 75 | } 76 | 77 | if (!isDiagonal) { 78 | polyIt++; 79 | continue; 80 | } 81 | 82 | var d = ( { from:d1, to:d2 } ); // d1 == poly[polyIt] and d2 == qoly[qolyIt] 83 | diagonals.push(d); 84 | 85 | p = poly.at(polyIt + 2); 86 | q = qoly.at(qolyIt - 1); 87 | 88 | // check if reflex 89 | if (PolyTools.isRight(d1, q, p)) { 90 | polyIt++; 91 | continue; 92 | } 93 | if (PolyTools.isRight(d2, p, q)) { 94 | polyIt++; 95 | continue; 96 | } 97 | 98 | // merge into newPoly 99 | newPoly = []; 100 | j = polyIt + 1; 101 | while (poly.wrappedIdx(j) != polyIt) { 102 | newPoly.push(poly.at(j)); 103 | j++; 104 | } 105 | j = qolyIt + 1; 106 | while (qoly.wrappedIdx(j) != qolyIt) { 107 | newPoly.push(qoly.at(j)); 108 | j++; 109 | } 110 | 111 | // add only if merged poly is convex (otherwise move to next triangle) 112 | if (PolyTools.isConvex(newPoly)) { 113 | res.splice(innerIt, 1); 114 | res[outerIt] = newPoly; 115 | poly = newPoly; 116 | 117 | polyIt = -1; // restart with newPoly 118 | } 119 | 120 | polyIt++; 121 | } 122 | 123 | outerIt++; 124 | } 125 | 126 | return res; 127 | } 128 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/HomogCoord.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Homogeneous Coordinates class. 3 | * 4 | * Based on: 5 | * 6 | * @see http://www.cs.ubc.ca/~snoeyink/demos/convdecomp/VPDemo.html (Java - by Jack Snoeyink) 7 | * 8 | * @author Jack Snoeyink 9 | * @author azrafe7 10 | */ 11 | 12 | package hxGeomAlgo; 13 | 14 | 15 | 16 | class HomogCoord 17 | { 18 | public static var INFINITY:HomogCoord = new HomogCoord(); 19 | 20 | public var x:Float; 21 | public var y:Float; 22 | public var w:Float; 23 | 24 | public function new(x:Float = 0, y:Float = 0, w:Float = 1) { 25 | this.x = x; 26 | this.y = y; 27 | this.w = w; 28 | } 29 | 30 | public function add(p:HomogCoord):HomogCoord { x += p.x; y += p.y; return this; } 31 | 32 | public function sub(p:HomogCoord):HomogCoord { x -= p.x; y -= p.y; return this; } 33 | 34 | public function neg():HomogCoord { w = -w; x = -x; y = -y; return this; } 35 | 36 | public function mul(m:Float):HomogCoord { w *= m; x *= m; y *= m; return this; } 37 | 38 | public function div(m:Float):HomogCoord { w /= m; x /= m; y /= m; return this; } 39 | 40 | public function normalize() { return div(length()); } 41 | 42 | public function lengthSquared() { return x * x + y * y; } 43 | 44 | public function length() { return Math.sqrt(this.lengthSquared()); } 45 | 46 | public function perp():HomogCoord { var tmp:Float = -y; y = x; x = tmp; return this; } 47 | 48 | public function dotPoint(p:HxPoint) { return w + x * p.x + y * p.y;} 49 | 50 | public function dot(p:HomogCoord) { return w * p.w + x * p.x + y * p.y; } 51 | 52 | public function perpdot(p:HomogCoord) { return x * p.y - y * p.x; } 53 | 54 | public function dotperp(p:HomogCoord) { return - x * p.y + y * p.x; } 55 | 56 | public function equals(p:HomogCoord) { return (p.w * x == w * p.x) && (p.w * y == w * p.y); } 57 | 58 | public function left(p:HxPoint) { return dotPoint(p) > 0; } 59 | 60 | public function right(p:HxPoint) { return dotPoint(p) < 0; } 61 | 62 | static public function det(p:HomogCoord, q:HomogCoord, r:HomogCoord) { 63 | return p.w * q.perpdot(r) - q.w * p.perpdot(r) + r.w * p.perpdot(q); 64 | } 65 | 66 | static public function ccw(p:HomogCoord, q:HomogCoord, r:HomogCoord) { 67 | return det(p, q, r) > 0; 68 | } 69 | 70 | static public function cw(p:HomogCoord, q:HomogCoord, r:HomogCoord) { 71 | return det(p, q, r) < 0; 72 | } 73 | 74 | public function toScreen():HxPoint { 75 | return new HxPoint(x/w, -y/w); 76 | } 77 | 78 | public function toPoint():HxPoint { return new HxPoint(x/w, y/w); } 79 | 80 | public function meet(p:HomogCoord):HomogCoord { 81 | return new HomogCoord(p.w * y - w * p.y, w * p.x - p.w * x, x * p.y - y * p.x); 82 | } 83 | 84 | public function meetPoint(p:HxPoint):HomogCoord { 85 | return new HomogCoord(y - w * p.y, w * p.x - x, x * p.y - y * p.x); 86 | } 87 | 88 | public function toString() { 89 | return " (w:" + w + "; x:" + x + ", y:" + y + ") "; 90 | } 91 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/HxPoint.hx: -------------------------------------------------------------------------------- 1 | package hxGeomAlgo; 2 | 3 | /** 4 | * Minimal Point class (auto-converting to/from flash.geom.Point and {x:Float, y:Float}). 5 | * 6 | * @author azrafe7 7 | */ 8 | @:expose 9 | abstract HxPoint(HxPointData) from HxPointData to HxPointData 10 | { 11 | static public var EMPTY(default, never) = new HxPoint(Math.NaN, Math.NaN); 12 | 13 | public var x(get, set):Float; 14 | inline private function get_x():Float { return this.x; } 15 | inline private function set_x(value:Float):Float { return this.x = value; } 16 | 17 | public var y(get, set):Float; 18 | inline private function get_y():Float { return this.y; } 19 | inline private function set_y(value:Float):Float { return this.y = value; } 20 | 21 | public function new(x:Float=0, y:Float=0) 22 | { 23 | this = new HxPointData(x, y); 24 | } 25 | 26 | inline public function setTo(newX:Float, newY:Float):Void 27 | { 28 | x = newX; 29 | y = newY; 30 | } 31 | 32 | inline public function equals(p:HxPoint):Bool 33 | { 34 | return (p != null && this.x == p.x && this.y == p.y); 35 | } 36 | 37 | inline public function clone():HxPoint 38 | { 39 | return new HxPoint(x, y); 40 | } 41 | 42 | inline public function toString() 43 | { 44 | return '(${x}, ${y})'; 45 | } 46 | 47 | #if (flash || openfl) 48 | @:from inline static function fromFlashPoint(p:flash.geom.Point) 49 | { 50 | return new HxPoint(p.x, p.y); 51 | } 52 | 53 | @:to inline function toFlashPoint() 54 | { 55 | return new flash.geom.Point(x, y); 56 | } 57 | #end 58 | 59 | @:from inline static function fromPointStruct(p:{x:Float, y:Float}) 60 | { 61 | return new HxPoint(p.x, p.y); 62 | } 63 | 64 | @:to inline function toPointStruct() 65 | { 66 | return { x:x, y:y }; 67 | } 68 | } 69 | 70 | 71 | class HxPointData 72 | { 73 | public var x:Float; 74 | public var y:Float; 75 | 76 | inline public function new(x:Float=0, y:Float=0) 77 | { 78 | this.x = x; 79 | this.y = y; 80 | } 81 | 82 | inline public function toString() 83 | { 84 | return '(${x}, ${y})'; 85 | } 86 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/IsoContours.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * IsoContours implementation (Clockwise). 3 | * 4 | * Based on: 5 | * 6 | * @see http://en.wikipedia.org/wiki/Marching_squares 7 | * @see https://github.com/deltaluca/nape (by Luca Deltodesco) 8 | * @see https://github.com/scikit-image/scikit-image (by scikit-image team) 9 | * 10 | * @author azrafe7 11 | */ 12 | 13 | package hxGeomAlgo; 14 | 15 | import haxe.ds.Vector; 16 | import haxe.ds.ObjectMap; 17 | import hxPixels.Pixels; 18 | 19 | 20 | /** function(pixels:Pixels, x:Int, y:Int):Float */ 21 | typedef IsoFunction = Pixels->Int->Int->Float; 22 | 23 | 24 | @:expose 25 | class IsoContours 26 | { 27 | 28 | public var isoFunction:IsoFunction; 29 | 30 | var pixels:Pixels; 31 | var width:Int; 32 | var height:Int; 33 | 34 | var values:Vector; 35 | 36 | var adjacencyMap:AdjacencyMap; 37 | 38 | /** 39 | * Constructor. 40 | * 41 | * @param pixels Pixels to use as source. 42 | * @param isoFunction Function mapping individual pixels to a Float value. Defaults to `isoAlpha()`. 43 | */ 44 | public function new(pixels:Pixels, isoFunction:IsoFunction = null) 45 | { 46 | this.pixels = pixels; 47 | this.width = pixels.width; 48 | this.height = pixels.height; 49 | this.values = null; 50 | 51 | this.isoFunction = (isoFunction != null) ? isoFunction : isoAlpha; 52 | } 53 | 54 | /** 55 | * Returns an Array of contours (each contour being an Array of HxPoint) based on where the data crosses the `isoValue`. 56 | * 57 | * A contour in the output will be in CCW winding order if it's a hole, and in CW order otherwise. 58 | * 59 | * @param isoValue Data values crossing this value will get the corresponding pixel pos inserted in a contour. 60 | * @param addBorders If true this will work as if the source had a 1px wide border around it (handled in isoFunction()). 61 | * @param recalcValues Whether isoFunction() needs to be re-run through all pixels. 62 | */ 63 | public function find(isoValue:Float = 0, addBorders:Bool = true, recalcValues:Bool = true):Array> { 64 | 65 | march(isoValue, addBorders, recalcValues); 66 | 67 | var contours = merge(); 68 | 69 | return contours; 70 | } 71 | 72 | function merge() { 73 | 74 | var isoLines = []; 75 | 76 | var segment = null; 77 | while ((segment = adjacencyMap.getFirstSegment()) != null) { 78 | 79 | var start = segment.from; 80 | var end = segment.to; 81 | 82 | var reversedIsoLine = [start]; 83 | var isoLine = [end]; 84 | 85 | while (true) { 86 | end = adjacencyMap.getEndingPointOf(end); 87 | start = adjacencyMap.getStartingPointOf(start); 88 | 89 | if (end != null) { 90 | isoLine.push(end); 91 | } 92 | if (start != null) { 93 | reversedIsoLine.push(start); 94 | } 95 | 96 | if (start == null && end == null) break; 97 | } 98 | 99 | reversedIsoLine.reverse(); 100 | isoLines.push(reversedIsoLine.concat(isoLine)); 101 | } 102 | 103 | return isoLines; 104 | } 105 | 106 | function march(isoValue:Float = 0, addBorders:Bool = true, recalcValues:Bool = true) { 107 | 108 | adjacencyMap = new AdjacencyMap(); 109 | var paddedWidth = width + 2; 110 | var paddedHeight = height + 2; 111 | 112 | // run isoFunction through all pixels 113 | if (recalcValues || values == null) { 114 | values = new Vector(paddedWidth * paddedHeight); 115 | 116 | for (y in 0...paddedHeight) { 117 | 118 | for (x in 0...paddedWidth) { 119 | 120 | var pos = y * paddedWidth + x; 121 | var value = isoFunction(pixels, x - 1, y - 1); 122 | values[pos] = value; 123 | } 124 | } 125 | } 126 | 127 | // adjust loop variables 128 | var offset = -.5; 129 | var startX = 0; 130 | var startY = 0; 131 | var endX = width + 1; 132 | var endY = height + 1; 133 | if (!addBorders) { 134 | startX = 1; 135 | startY = 1; 136 | endX = width; 137 | endY = height; 138 | } 139 | 140 | // march 141 | for (y in startY...endY) { 142 | 143 | for (x in startX...endX) { 144 | 145 | // calc binaryIdx (CW from msb) 146 | var pos = y * paddedWidth + x; 147 | var topLeft = values[pos]; 148 | var topRight = values[pos + 1]; 149 | var bottomRight = values[pos + 1 + paddedWidth]; 150 | var bottomLeft = values[pos + paddedWidth]; 151 | 152 | var binaryIdx = 0; 153 | if (topLeft > isoValue) binaryIdx += 8; 154 | if (topRight > isoValue) binaryIdx += 4; 155 | if (bottomRight > isoValue) binaryIdx += 2; 156 | if (bottomLeft > isoValue) binaryIdx += 1; 157 | 158 | if (binaryIdx != 0 && binaryIdx != 15) { 159 | 160 | var topPoint = new HxPoint(offset + x + interp(isoValue, topLeft, topRight), offset + y); 161 | var leftPoint = new HxPoint(offset + x, offset + y + interp(isoValue, topLeft, bottomLeft)); 162 | var rightPoint = new HxPoint(offset + x + 1, offset + y + interp(isoValue, topRight, bottomRight)); 163 | var bottomPoint = new HxPoint(offset + x + interp(isoValue, bottomLeft, bottomRight), offset + y + 1); 164 | 165 | // resolve saddle ambiguities by using central (/average) value 166 | if (binaryIdx == 5 || binaryIdx == 10) { 167 | var avgValue = (topLeft + topRight + bottomRight + bottomLeft) / 4; 168 | if (avgValue <= 0) binaryIdx = ~binaryIdx & 15; // flip binaryIdx 169 | } 170 | 171 | // add segments (pairs of points) based on binaryIdx. 172 | // consistent order is enforced, meaning that the first point of a 173 | // segment is guaranteed to be the second point of another segment 174 | // (except for head and tail segments of open isolines of course) 175 | switch (binaryIdx) { 176 | case 1: 177 | adjacencyMap.addSegment(leftPoint, bottomPoint); 178 | case 2: 179 | adjacencyMap.addSegment(bottomPoint, rightPoint); 180 | case 3: 181 | adjacencyMap.addSegment(leftPoint, rightPoint); 182 | case 4: 183 | adjacencyMap.addSegment(rightPoint, topPoint); 184 | case 5: // saddle 185 | adjacencyMap.addSegment(leftPoint, topPoint); 186 | adjacencyMap.addSegment(rightPoint, bottomPoint); 187 | case 6: 188 | adjacencyMap.addSegment(bottomPoint, topPoint); 189 | case 7: 190 | adjacencyMap.addSegment(leftPoint, topPoint); 191 | case 8: 192 | adjacencyMap.addSegment(topPoint, leftPoint); 193 | case 9: 194 | adjacencyMap.addSegment(topPoint, bottomPoint); 195 | case 10: // saddle 196 | adjacencyMap.addSegment(bottomPoint, leftPoint); 197 | adjacencyMap.addSegment(topPoint, rightPoint); 198 | case 11: 199 | adjacencyMap.addSegment(topPoint, rightPoint); 200 | case 12: 201 | adjacencyMap.addSegment(rightPoint, leftPoint); 202 | case 13: 203 | adjacencyMap.addSegment(rightPoint, bottomPoint); 204 | case 14: 205 | adjacencyMap.addSegment(bottomPoint, leftPoint); 206 | default: 207 | } 208 | } 209 | } 210 | } 211 | } 212 | 213 | public function interp(isoValue:Float, fromValue:Float, toValue:Float):Float { 214 | if (fromValue == toValue) return 0; 215 | return (isoValue - fromValue) / (toValue - fromValue); 216 | } 217 | 218 | static public function isoAlpha(pixels:Pixels, x:Int, y:Int):Float { 219 | if (isOutOfBounds(pixels, x, y)) return 0; 220 | else return pixels.getPixel32(x, y).A; 221 | } 222 | 223 | inline static public function isOutOfBounds(pixels:Pixels, x:Int, y:Int):Bool { 224 | return (x < 0 || y < 0 || x >= pixels.width || y >= pixels.height); 225 | } 226 | } 227 | 228 | 229 | /** 230 | * Stores segments and provides a quick way of finding adjacent/consecutive points. 231 | * 232 | * You can query it to fetch: 233 | * - the starting/ending point of a segment (given the other end) 234 | * - the first available segment (in insertion order) 235 | * 236 | * Both types of queries will automatically remove the related segment (if any) from the structure. 237 | */ 238 | class AdjacencyMap { 239 | 240 | var pointSet:Map; 241 | 242 | var firstIdx:Int = 0; 243 | var segments:Array; 244 | 245 | var mapStartToEnd:ObjectMap>; 246 | var mapEndToStart:ObjectMap>; 247 | 248 | public function new():Void { 249 | pointSet = new Map(); 250 | segments = []; 251 | mapStartToEnd = new ObjectMap(); 252 | mapEndToStart = new ObjectMap(); 253 | } 254 | 255 | public function addSegment(from:HxPoint, to:HxPoint):Void { 256 | if (from.equals(to)) return; 257 | 258 | var fromKey = from.toString(); 259 | var toKey = to.toString(); 260 | 261 | if (!pointSet.exists(fromKey)) pointSet[fromKey] = from; 262 | else from = pointSet[fromKey]; 263 | 264 | if (!pointSet.exists(toKey)) pointSet[toKey] = to; 265 | else to = pointSet[toKey]; 266 | 267 | var idx = segments.length; 268 | segments.push(new Segment(from, to)); 269 | 270 | if (mapStartToEnd.exists(from)) mapStartToEnd.get(from).push(idx); 271 | else mapStartToEnd.set(from, [idx]); 272 | 273 | if (mapEndToStart.exists(to)) mapEndToStart.get(to).push(idx); 274 | else mapEndToStart.set(to, [idx]); 275 | } 276 | 277 | public function getStartingPointOf(end:HxPoint):HxPoint { 278 | if (end == null) return null; 279 | 280 | var start = null; 281 | 282 | if (mapEndToStart.exists(end)) { 283 | var entry = mapEndToStart.get(end); 284 | var idx = entry[0]; 285 | start = segments[idx].from; 286 | removeSegmentAt(idx); 287 | } 288 | 289 | return start; 290 | } 291 | 292 | public function getEndingPointOf(start:HxPoint):HxPoint { 293 | if (start == null) return null; 294 | 295 | var end = null; 296 | 297 | if (mapStartToEnd.exists(start)) { 298 | var entry = mapStartToEnd.get(start); 299 | var idx = entry[0]; 300 | end = segments[idx].to; 301 | removeSegmentAt(idx); 302 | } 303 | 304 | return end; 305 | } 306 | 307 | public function getFirstSegment():Segment { 308 | var segment = null; 309 | 310 | for (i in firstIdx...segments.length) { 311 | segment = segments[i]; 312 | if (segment != null) { 313 | removeSegmentAt(i); 314 | firstIdx = i; 315 | break; 316 | } 317 | } 318 | 319 | return segment; 320 | } 321 | 322 | function removeSegmentAt(i:Int) { 323 | var segment = segments[i]; 324 | 325 | var start = segment.from; 326 | var end = segment.to; 327 | 328 | var entry = mapStartToEnd.get(start); 329 | entry.remove(i); 330 | if (entry.length == 0) mapStartToEnd.remove(start); 331 | 332 | entry = mapEndToStart.get(end); 333 | entry.remove(i); 334 | if (entry.length == 0) mapEndToStart.remove(end); 335 | 336 | segments[i] = null; 337 | } 338 | } 339 | 340 | private class Segment { 341 | public var from:HxPoint; 342 | public var to:HxPoint; 343 | 344 | public function new(from:HxPoint, to:HxPoint):Void { 345 | this.from = from; 346 | this.to = to; 347 | } 348 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/MarchingSquares.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Marching Squares implementation (Counterclockwise). 3 | * 4 | * Based on: 5 | * 6 | * @see https://web.archive.org/web/20180316055432/http://devblog.phillipspiess.com/better%20know%20an%20algorithm/2010/02/23/better-know-marching-squares.html (C# - by Phil Spiess) 7 | * @see http://www.tomgibara.com/computer-vision/marching-squares (Java - by Tom Gibara) 8 | * 9 | * @author azrafe7 10 | */ 11 | 12 | package hxGeomAlgo; 13 | 14 | import hxPixels.Pixels; 15 | 16 | 17 | enum StepDirection { 18 | NONE; 19 | UP; 20 | LEFT; 21 | DOWN; 22 | RIGHT; 23 | } 24 | 25 | 26 | @:expose 27 | class MarchingSquares 28 | { 29 | /** Minimum alpha value to consider a pixel opaque (in the range 0-255). */ 30 | public var alphaThreshold:Int; 31 | 32 | private var prevStep:StepDirection = StepDirection.NONE; 33 | private var nextStep:StepDirection = StepDirection.NONE; 34 | 35 | private var pixels:Pixels; 36 | private var width:Int; 37 | private var height:Int; 38 | 39 | 40 | /** 41 | * Constructor. 42 | * 43 | * @param pixels Pixels to use as source. 44 | * @param alphaThreshold Minimum alpha value to consider a pixel opaque (in the range 0-255). 45 | */ 46 | public function new(pixels:Pixels, alphaThreshold:Int = 1) 47 | { 48 | setSource(pixels); 49 | 50 | this.alphaThreshold = alphaThreshold; 51 | } 52 | 53 | /** 54 | * Updates the Pixels to use as source. 55 | * 56 | * NOTE: If you modifiy your source between calls to march()/walkPerimeter you may 57 | * also want to re-set the source so that the internal representation gets updated too. 58 | */ 59 | public function setSource(pixels:Pixels) 60 | { 61 | this.pixels = pixels; 62 | width = this.pixels.width; 63 | height = this.pixels.height; 64 | } 65 | 66 | /** 67 | * Finds the perimeter. 68 | * 69 | * @param startPoint Start from this point (if null it will be calculated automatically). 70 | * @return An array containing the points on the perimeter, or an empty array if no perimeter is found. 71 | */ 72 | public function march(startPoint:HxPoint = null):Array 73 | { 74 | if (startPoint == null) startPoint = findStartPoint(); 75 | if (startPoint == null) return []; 76 | 77 | var perimeter = walkPerimeter(Std.int(startPoint.x), Std.int(startPoint.y)); 78 | 79 | // remove end point if start == end 80 | if (perimeter.length > 1 && perimeter[0].equals(perimeter[perimeter.length - 1])) perimeter.pop(); 81 | 82 | return perimeter; 83 | } 84 | 85 | /** 86 | * Finds the first opaque pixel location (starting from top-left corner, or from the specified line). 87 | * 88 | * @return The first opaque pixel location, or null if not found. 89 | */ 90 | public function findStartPoint(line:Int = 0):HxPoint { 91 | var found = false; 92 | 93 | for (y in line...height) { 94 | for (x in 0...width) { 95 | if (isPixelSolid(x, y)) return new HxPoint(x, y); 96 | } 97 | } 98 | 99 | return null; 100 | } 101 | 102 | /** 103 | * Finds points belonging to the perimeter starting from `startX`, `startY`. 104 | * 105 | * NOTE: The perimeter (if exists) is guaranteed to be fully contained in the souce boundaries 106 | * and will start on a solid pixel. The points found when going up or right might be 1px away 107 | * from the solid pixels though (see https://github.com/azrafe7/as3GeomAlgo/issues/1#issuecomment-108634264). 108 | */ 109 | private function walkPerimeter(startX:Int, startY:Int):Array 110 | { 111 | // clamp to source boundaries 112 | if (startX < 0) startX = 0; 113 | if (startX > width) startX = width; 114 | if (startY < 0) startY = 0; 115 | if (startY > height) startY = height; 116 | 117 | var lastAddedPoint = new HxPoint(-1, -1); 118 | var pointList = new Array(); 119 | 120 | var x:Int = startX; 121 | var y:Int = startY; 122 | 123 | // loop until we return to the starting point 124 | var done = false; 125 | while (!done) { 126 | step(x, y); 127 | 128 | // add perimeter point to return list, 129 | // but adjusting for out of bounds cases and 130 | // skipping duplicate points 131 | var newPoint = new HxPoint(x >= width ? x - 1 : x, y >= height ? y - 1 : y); 132 | if (!lastAddedPoint.equals(newPoint)) { 133 | pointList.push(newPoint); 134 | lastAddedPoint = newPoint; 135 | } 136 | 137 | switch (nextStep) 138 | { 139 | case StepDirection.UP: y--; 140 | case StepDirection.LEFT: x--; 141 | case StepDirection.DOWN: y++; 142 | case StepDirection.RIGHT: x++; 143 | default: Debug.assert(false, "Illegal state at point (x: " + x + ", y: " + y + ")."); 144 | } 145 | 146 | done = (x == startX && y == startY); 147 | } 148 | 149 | return pointList; 150 | } 151 | 152 | /** Calculates the next state for pixel at `x`, `y`. */ 153 | private function step(x:Int, y:Int):Void 154 | { 155 | var upLeft = isPixelSolid(x - 1, y - 1); 156 | var upRight = isPixelSolid(x, y - 1); 157 | var downLeft = isPixelSolid(x - 1, y); 158 | var downRight = isPixelSolid(x, y); 159 | 160 | // save previous step 161 | prevStep = nextStep; 162 | 163 | // calc current state 164 | var state:Int = 0; 165 | 166 | if (upLeft) state |= 1; 167 | if (upRight) state |= 2; 168 | if (downLeft) state |= 4; 169 | if (downRight) state |= 8; 170 | 171 | Debug.assert(state != 0 && state != 15, "Error: point (x: " + x + ", y: " + y + ") doesn't lie on perimeter."); 172 | 173 | switch (state) 174 | { 175 | case 1, 5, 13: 176 | nextStep = StepDirection.UP; 177 | case 2, 3, 7: 178 | nextStep = StepDirection.RIGHT; 179 | case 4, 12, 14: 180 | nextStep = StepDirection.LEFT; 181 | case 6: 182 | nextStep = (prevStep == StepDirection.UP ? StepDirection.LEFT : StepDirection.RIGHT); 183 | case 8, 10, 11: 184 | nextStep = StepDirection.DOWN; 185 | case 9: 186 | nextStep = (prevStep == StepDirection.RIGHT ? StepDirection.UP : StepDirection.DOWN); 187 | default: 188 | Debug.assert(false, "Illegal state at point (x: " + x + ", y: " + y + ")."); 189 | } 190 | } 191 | 192 | /** 193 | * Returns true if the pixel at `x`, `y` is opaque (according to `alphaThreshold`). 194 | * Override this to use your own logic to identify solid pixels. 195 | */ 196 | private function isPixelSolid(x:Int, y:Int):Bool { 197 | return (x >= 0 && y >= 0 && x < width && y < height && (pixels.getByte((y * width + x) << 2) >= alphaThreshold)); 198 | } 199 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/PairDeque.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * PairDeque data structure. 3 | * 4 | * Based on: 5 | * 6 | * @see http://www.cs.ubc.ca/~snoeyink/demos/convdecomp/MCDDemo.html (Java - by Jack Snoeyink) 7 | * 8 | * This is a hybrid between a stack and a deque, for storing pairs of 9 | * integers that form non-nested intervals in increasing order, back to 10 | * front. It is used in minimum convex decomposition. 11 | * 12 | * It is assumed that pushes come first (perhaps with some pops from 13 | * pushNarrow) and happen at the front of the deque. Then two independent 14 | * front and back pointers for popping. Unlike a standard deque, front 15 | * and back don't interfere: if you pop off one side, the element is 16 | * still available for the other side. Thus, this is more like a pair of 17 | * stacks that have the same elements in reverse order. 18 | * 19 | * @author Jack Snoeyink 20 | * @author azrafe7 21 | */ 22 | 23 | package hxGeomAlgo; 24 | 25 | 26 | class PairDeque { 27 | private var front:Array; // first elements of pair 28 | private var back:Array; // second elements of pair 29 | 30 | // pointers point at the valid element. I.e., if there are none, they 31 | // can point off the end of the list. 32 | public var frontTopIdx(default, null):Int; // front stack pointer 33 | public var backTopIdx(default, null):Int; // back stack pointer 34 | public var lastIdx(default, null):Int; // the "high-water mark" for restores 35 | 36 | public function new() { 37 | lastIdx = frontTopIdx = -1; 38 | backTopIdx = 0; 39 | front = new Array(); 40 | back = new Array(); 41 | } 42 | 43 | public function push(i:Int, j:Int) { // we push only onto the front 44 | if (front.length <= ++frontTopIdx) { // make room if necessary 45 | front.push(-1); 46 | back.push(-1); 47 | } 48 | front[frontTopIdx] = i; 49 | back[frontTopIdx] = j; 50 | lastIdx = frontTopIdx; 51 | } 52 | 53 | public function pushNarrow(i:Int, j:Int) { // no nesting--> frontTopIdx < i && backTopIdx < j 54 | if ((!isFrontEmpty()) && (i <= frontTop())) return; // don't push wider 55 | while ((!isFrontEmpty()) && (backBottom() >= j)) popFront(); // pop until narrower: backTopIdx < j 56 | push(i, j); 57 | } 58 | 59 | public function isFrontEmpty():Bool { return frontTopIdx < 0; } 60 | public function frontHasNext():Bool { return frontTopIdx > 0; } 61 | public function flush() { lastIdx = frontTopIdx = -1; } 62 | public function frontTop():Int { 63 | if (frontTopIdx < 0) return 0; // NOTE: investigate edge cases where this happens (it shouldn't!) 64 | return front[frontTopIdx]; } 65 | public function frontPeekNext():Int { return front[frontTopIdx - 1]; } 66 | public function backBottom():Int { return back[frontTopIdx]; } 67 | public function popFront():Int { return front[frontTopIdx--]; } 68 | public function restore() { backTopIdx = 0; frontTopIdx = lastIdx; } // return to high-water mark 69 | 70 | public function isBackEmpty():Bool { return backTopIdx > lastIdx ; } 71 | public function backHasNext():Bool { return backTopIdx < lastIdx; } 72 | public function frontBottom():Int { return front[backTopIdx]; } 73 | public function backPeekNext():Int { return back[backTopIdx + 1]; } 74 | public function backTop():Int { return back[backTopIdx]; } 75 | public function popBack():Int { return back[backTopIdx++]; } 76 | 77 | public function toString():String { 78 | var stringBuffer:StringBuf = new StringBuf(); 79 | stringBuffer.add("fp:" + frontTopIdx + ", bp:" + backTopIdx + ", last:" + lastIdx + ": "); 80 | for (i in 0...lastIdx + 1) stringBuffer.add(front[i] + "," + back[i] + " "); 81 | return stringBuffer.toString(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/hxGeomAlgo/PoleOfInaccessibility.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * An algorithm for finding polygon pole of inaccessibility, the most distant internal point from 3 | * the polygon outline (not to be confused with centroid). 4 | * 5 | * Based on: 6 | * 7 | * @see https://github.com/mapbox/polylabel/commit/64fe157 (JS - by Vladimir Agafonkin) 8 | * 9 | * @author azrafe7 10 | */ 11 | 12 | package hxGeomAlgo; 13 | 14 | 15 | import hxGeomAlgo.Heap.Heapable; 16 | import hxGeomAlgo.PolyTools; 17 | 18 | 19 | @:expose 20 | class PoleOfInaccessibility 21 | { 22 | static var SQRT2:Float; 23 | static function __init__():Void { 24 | SQRT2 = Math.sqrt(2.0); 25 | } 26 | 27 | static public function calculate(poly:Array, precision:Float = 1.0, debug:Bool = false):HxPoint { 28 | if (poly == null || poly.length <= 0) return HxPoint.EMPTY; 29 | 30 | var minX = Math.POSITIVE_INFINITY; 31 | var minY = Math.POSITIVE_INFINITY; 32 | var maxX = Math.NEGATIVE_INFINITY; 33 | var maxY = Math.NEGATIVE_INFINITY; 34 | 35 | // find the bounding box 36 | for (ring in poly) { 37 | for (p in ring) { 38 | if (p.x < minX) minX = p.x; 39 | if (p.y < minY) minY = p.y; 40 | if (p.x > maxX) maxX = p.x; 41 | if (p.y > maxY) maxY = p.y; 42 | } 43 | } 44 | 45 | var width = maxX - minX; 46 | var height = maxY - minY; 47 | var cellSize = Math.min(width, height); 48 | var h = cellSize / 2; 49 | 50 | 51 | // a priority queue of cells in order of their "potential" (max distance to polygon) 52 | var cellQueue = new Heap(); 53 | 54 | if (cellSize == 0.0) return new HxPoint(minX, minY); 55 | 56 | // cover polygon with initial cells 57 | var x = minX; 58 | var y = minY; 59 | while (x < maxX) { 60 | y = minY; 61 | while (y < maxY) { 62 | cellQueue.push(new Cell(x + h, y + h, h, poly)); 63 | y += cellSize; 64 | } 65 | x += cellSize; 66 | } 67 | 68 | // take centroid as the first best guess 69 | var bestCell = getCentroidCell(poly); 70 | 71 | // special case for rectangular polygons 72 | var bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, poly); 73 | if (bboxCell.d > bestCell.d) bestCell = bboxCell; 74 | 75 | var numProbes = cellQueue.length; 76 | 77 | while (cellQueue.length > 0) { 78 | // pick the most promising cell from the queue 79 | var cell = cellQueue.pop(); 80 | 81 | // update the best cell if we found a better one 82 | if (cell.d > bestCell.d) { 83 | bestCell = cell; 84 | if (debug) trace('found best ${Math.round(1e4 * cell.d) / 1e4} after ${numProbes} probes'); 85 | } 86 | 87 | // do not drill down further if there's no chance of a better solution 88 | if (cell.max - bestCell.d <= precision) continue; 89 | 90 | // split the cell into four cells 91 | h = cell.h / 2; 92 | cellQueue.push(new Cell(cell.x - h, cell.y - h, h, poly)); 93 | cellQueue.push(new Cell(cell.x + h, cell.y - h, h, poly)); 94 | cellQueue.push(new Cell(cell.x - h, cell.y + h, h, poly)); 95 | cellQueue.push(new Cell(cell.x + h, cell.y + h, h, poly)); 96 | numProbes += 4; 97 | } 98 | 99 | if (debug) { 100 | trace('num probes: ' + numProbes); 101 | trace('best distance: ' + bestCell.d); 102 | } 103 | 104 | return new HxPoint(bestCell.x, bestCell.y); 105 | } 106 | 107 | /** Signed distance from point to polygon outline (negative if point is outside) */ 108 | static public function pointToPolygonDist(x:Float, y:Float, poly:Array):Float { 109 | var inside = false; 110 | var minDistSq = Math.POSITIVE_INFINITY; 111 | 112 | for (k in 0...poly.length) { 113 | var ring = poly[k]; 114 | 115 | var i = 0; 116 | var len = ring.length; 117 | var j = len - 1; 118 | //for (var i = 0, len = ring.length, j = len - 1; i < len; j = i++) { 119 | while (i < len) { 120 | var a = ring[i]; 121 | var b = ring[j]; 122 | 123 | if (((a.y > y) != (b.y > y)) && 124 | (x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x)) inside = !inside; 125 | 126 | minDistSq = Math.min(minDistSq, getSegDistSq(x, y, a, b)); 127 | j = i++; 128 | } 129 | } 130 | 131 | return (inside ? 1 : -1) * Math.sqrt(minDistSq); 132 | } 133 | 134 | /** Get polygon centroid */ 135 | static public function getCentroidCell(poly:Array):Cell { 136 | var area = 0.0; 137 | var x = 0.0; 138 | var y = 0.0; 139 | var points = poly[0]; 140 | 141 | var i = 0; 142 | var len = points.length; 143 | var j = len - 1; 144 | //for (var i = 0, len = points.length, j = len - 1; i < len; j = i++) { 145 | while (i < len) { 146 | var a = points[i]; 147 | var b = points[j]; 148 | var f = a.x * b.y - b.x * a.y; 149 | x += (a.x + b.x) * f; 150 | y += (a.y + b.y) * f; 151 | area += f * 3; 152 | i++; 153 | } 154 | 155 | if (area == 0.0) return new Cell(points[0].x, points[0].y, 0, poly); 156 | return new Cell(x / area, y / area, 0, poly); 157 | } 158 | 159 | /** Get squared distance from a point to a segment */ 160 | static public function getSegDistSq(px:Float, py:Float, a:HxPoint, b:HxPoint):Float { 161 | 162 | var x = a.x; 163 | var y = a.y; 164 | var dx = b.x - x; 165 | var dy = b.y - y; 166 | 167 | if (dx != 0 || dy != 0) { 168 | 169 | var t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy); 170 | 171 | if (t > 1) { 172 | x = b.x; 173 | y = b.y; 174 | 175 | } else if (t > 0) { 176 | x += dx * t; 177 | y += dy * t; 178 | } 179 | } 180 | 181 | dx = px - x; 182 | dy = py - y; 183 | 184 | return dx * dx + dy * dy; 185 | } 186 | } 187 | 188 | 189 | @:access(hxGeomAlgo.PoleOfInaccessibility) 190 | private class Cell implements Heapable 191 | { 192 | public var position:Int; 193 | 194 | public var x:Float; // cell center x 195 | public var y:Float; // cell center y 196 | public var h:Float; // half cell size 197 | public var d:Null; // distance from cell center to polygon 198 | public var max:Float; // max distance to polygon within a cell 199 | 200 | 201 | public function new(x:Float, y:Float, h:Float, polygon:Array) { 202 | this.x = x; 203 | this.y = y; 204 | this.h = h; 205 | this.d = PoleOfInaccessibility.pointToPolygonDist(x, y, polygon); 206 | this.max = this.d + this.h * PoleOfInaccessibility.SQRT2; 207 | } 208 | 209 | // compare max 210 | public function compare(other:Cell):Int { 211 | var diff = other.max - max; 212 | return diff < 0 ? -1 : diff > 0 ? 1 : 0; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/hxGeomAlgo/PolyTools.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Collection of functions to make working with HxPoint and Poly easier. 3 | * 4 | * Some of these have been based on: 5 | * 6 | * @see http://mnbayazit.com/406/bayazit (C - by Mark Bayazit) 7 | * @see http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment (JS - Grumdrig) 8 | * 9 | * @author azrafe7 10 | */ 11 | 12 | package hxGeomAlgo; 13 | 14 | import hxGeomAlgo.HxPoint; 15 | 16 | 17 | typedef Poly = Array; 18 | 19 | typedef Tri = Poly; // assumes Array of length 3 20 | 21 | @:expose 22 | typedef Diagonal = { 23 | var from:Int; 24 | var to:Int; 25 | } 26 | 27 | 28 | @:expose 29 | class PolyTools 30 | { 31 | static private var point:HxPoint = new HxPoint(); // used internally 32 | 33 | static public var zero:HxPoint = new HxPoint(0, 0); 34 | 35 | static public var EPSILON:Float = .00000001; 36 | 37 | 38 | /** Returns true if `poly` is counterclockwise (assumes y axis pointing down). */ 39 | static public function isCCW(poly:Poly):Bool { 40 | if (poly.length <= 2) return true; 41 | 42 | var signedArea = 0.; 43 | for (i in 0...poly.length) { 44 | signedArea += at(poly, i - 1).x * poly[i].y - poly[i].x * at(poly, i - 1).y; 45 | } 46 | 47 | return signedArea < 0; 48 | } 49 | 50 | /** Returns true if `poly` is clockwise (assumes y axis pointing down). */ 51 | static public function isCW(poly:Poly):Bool { 52 | if (poly.length <= 2) return true; 53 | 54 | var signedArea = 0.; 55 | for (i in 0...poly.length) { 56 | signedArea += at(poly, i - 1).x * poly[i].y - poly[i].x * at(poly, i - 1).y; 57 | } 58 | 59 | return signedArea > 0; 60 | } 61 | 62 | /** Makes `poly` counterclockwise (in place). Returns true if reversed. */ 63 | static public function makeCCW(poly:Poly):Bool { 64 | var reversed = false; 65 | 66 | // reverse poly if not counterlockwise 67 | if (!isCCW(poly)) { 68 | poly.reverse(); 69 | reversed = true; 70 | } 71 | 72 | return reversed; 73 | } 74 | 75 | /** Makes `poly` clockwise (in place). Returns true if reversed. */ 76 | static public function makeCW(poly:Poly):Bool { 77 | var reversed = false; 78 | 79 | // reverse poly if counterlockwise 80 | if (isCCW(poly)) { 81 | poly.reverse(); 82 | reversed = true; 83 | } 84 | 85 | return reversed; 86 | } 87 | 88 | /** 89 | * Assuming the polygon is simple (not self-intersecting), checks if it is convex. 90 | **/ 91 | static public function isConvex(poly:Poly):Bool 92 | { 93 | var isPositive:Null = null; 94 | 95 | for (i in 0...poly.length) { 96 | var lower:Int = (i == 0 ? poly.length - 1 : i - 1); 97 | var middle:Int = i; 98 | var upper:Int = (i == poly.length - 1 ? 0 : i + 1); 99 | var dx0:Float = poly[middle].x - poly[lower].x; 100 | var dy0:Float = poly[middle].y - poly[lower].y; 101 | var dx1:Float = poly[upper].x - poly[middle].x; 102 | var dy1:Float = poly[upper].y - poly[middle].y; 103 | var cross:Float = dx0 * dy1 - dx1 * dy0; 104 | 105 | // cross product should have same sign 106 | // for each vertex if poly is convex. 107 | var newIsPositive:Bool = (cross > 0 ? true : false); 108 | 109 | if (cross == 0) continue; // handle collinear case 110 | 111 | if (isPositive == null) 112 | isPositive = newIsPositive; 113 | else if (isPositive != newIsPositive) { 114 | return false; 115 | } 116 | } 117 | 118 | return true; 119 | } 120 | 121 | /** 122 | * Checks if the polygon is simple (not self-intersecting). 123 | **/ 124 | static public function isSimple(poly:Poly):Bool 125 | { 126 | var len:Int = poly.length; 127 | 128 | if (len <= 3) return true; 129 | 130 | for (i in 0...len) { 131 | // first segment 132 | var p0:Int = i; 133 | var p1:Int = i == len - 1 ? 0 : i + 1; 134 | 135 | for (j in i + 1...len) { 136 | // second segment 137 | var q0:Int = j; 138 | var q1:Int = j == len - 1 ? 0 : j + 1; 139 | 140 | // check for intersection between segment p and segment q. 141 | // if the intersection point exists and is different from the endpoints, 142 | // then the poly is not simple 143 | var intersection:HxPoint = segmentIntersect(poly[p0], poly[p1], poly[q0], poly[q1]); 144 | if (intersection != null 145 | && !(distance(intersection, poly[p0]) < EPSILON || distance(intersection, poly[p1]) < EPSILON) 146 | && !(distance(intersection, poly[q0]) < EPSILON || distance(intersection, poly[q1]) < EPSILON)) 147 | { 148 | return false; 149 | } 150 | } 151 | } 152 | 153 | return true; 154 | } 155 | 156 | /** 157 | * Returns the intersection point between segments p0-p1 and q0-q1. Null if no intersection is found. 158 | */ 159 | static public function segmentIntersect(p0:HxPoint, p1:HxPoint, q0:HxPoint, q1:HxPoint):HxPoint 160 | { 161 | var intersectionPoint:HxPoint; 162 | var a1:Float, a2:Float; 163 | var b1:Float, b2:Float; 164 | var c1:Float, c2:Float; 165 | 166 | a1 = p1.y - p0.y; 167 | b1 = p0.x - p1.x; 168 | c1 = p1.x * p0.y - p0.x * p1.y; 169 | a2 = q1.y - q0.y; 170 | b2 = q0.x - q1.x; 171 | c2 = q1.x * q0.y - q0.x * q1.y; 172 | 173 | var denom:Float = a1 * b2 - a2 * b1; 174 | if (denom == 0){ 175 | return null; 176 | } 177 | 178 | intersectionPoint = new HxPoint(); 179 | intersectionPoint.x = (b1 * c2 - b2 * c1) / denom; 180 | intersectionPoint.y = (a2 * c1 - a1 * c2) / denom; 181 | 182 | // check to see if distance between intersection and endpoints 183 | // is longer than actual segments. 184 | // return null otherwise. 185 | var p0p1 = distanceSquared(p0, p1); 186 | var q0q1 = distanceSquared(q0, q1); 187 | 188 | if (distanceSquared(intersectionPoint, p1) > p0p1) return null; 189 | if (distanceSquared(intersectionPoint, p0) > p0p1) return null; 190 | if (distanceSquared(intersectionPoint, q1) > q0q1) return null; 191 | if (distanceSquared(intersectionPoint, q0) > q0q1) return null; 192 | 193 | return intersectionPoint; 194 | } 195 | 196 | /** 197 | * Returns indices of duplicate points in `poly` (or an empty array if none are found). 198 | * NOTE: indices in the result are guaranteed to be in ascending order. 199 | * 200 | * @param consecutiveOnly if true only equal adjacent points are reported 201 | * @param wrapAround if true also first vs last point will be checked 202 | */ 203 | static public function findDuplicatePoints(poly:Poly, consecutiveOnly:Bool = true, wrapAround:Bool = true):Array 204 | { 205 | var len = poly.length; 206 | if (len <= 1) return []; 207 | var dupIndices = []; 208 | 209 | for (i in 0...len - 1) { 210 | var j = i + 1; 211 | while (j < len) { 212 | var foundDup = poly[i].equals(poly[j]); 213 | if (foundDup) dupIndices.push(i); 214 | if (consecutiveOnly || (foundDup && !consecutiveOnly)) break; 215 | j++; 216 | } 217 | } 218 | if (wrapAround && consecutiveOnly && poly[0].equals(poly[len - 1])) dupIndices.push(len - 1); 219 | 220 | return dupIndices; 221 | } 222 | 223 | /** Finds the intersection point between lines extending the segments `p1`-`p2` and `q1`-`q2`. Returns null if they're parallel. */ 224 | @:noUsing static public function intersection(p1:HxPoint, p2:HxPoint, q1:HxPoint, q2:HxPoint):HxPoint 225 | { 226 | var res:HxPoint = null; 227 | var a1 = p2.y - p1.y; 228 | var b1 = p1.x - p2.x; 229 | var c1 = a1 * p1.x + b1 * p1.y; 230 | var a2 = q2.y - q1.y; 231 | var b2 = q1.x - q2.x; 232 | var c2 = a2 * q1.x + b2 * q1.y; 233 | var det = a1 * b2 - a2 * b1; 234 | if (!eq(det, 0)) { // lines are not parallel 235 | res = new HxPoint(); 236 | res.x = (b2 * c1 - b1 * c2) / det; 237 | res.y = (a1 * c2 - a2 * c1) / det; 238 | } /*else { 239 | trace("parallel"); 240 | }*/ 241 | return res; 242 | } 243 | 244 | /** Returns true if `poly` vertex at idx is a reflex vertex. */ 245 | static public function isReflex(poly:Poly, idx:Int):Bool 246 | { 247 | return isRight(at(poly, idx - 1), at(poly, idx), at(poly, idx + 1)); 248 | } 249 | 250 | /** Gets `poly` vertex at `idx` (wrapping around if needed). */ 251 | static inline public function at(poly:Poly, idx:Int):HxPoint 252 | { 253 | idx = wrappedIdx(poly, idx); 254 | return poly[idx]; 255 | } 256 | 257 | /** Gets usable `idx` from `poly` (wrapping around if needed). */ 258 | static inline public function wrappedIdx(poly:Poly, idx:Int):Int 259 | { 260 | var len:Int = poly.length; 261 | if (idx < 0) idx += len; 262 | return idx % len; 263 | } 264 | 265 | /** Gets the side (signed area) of `p` relative to the line extending `b`-`a` (> 0 -> left, < 0 -> right, == 0 -> collinear). */ 266 | static inline public function side(p:HxPoint, a:HxPoint, b:HxPoint):Float 267 | { 268 | return (((a.x - p.x) * (b.y - p.y)) - ((b.x - p.x) * (a.y - p.y))); 269 | } 270 | 271 | /** Returns true if `p` is on the left of the line extending `b`-`a`. */ 272 | static inline public function isLeft(p:HxPoint, a:HxPoint, b:HxPoint):Bool 273 | { 274 | return side(p, a, b) > 0; 275 | } 276 | 277 | /** Returns true if `p` is on the left or collinear to the line extending `b`-`a`. */ 278 | static inline public function isLeftOrOn(p:HxPoint, a:HxPoint, b:HxPoint):Bool 279 | { 280 | return side(p, a, b) >= 0; 281 | } 282 | 283 | /** Returns true if `p` is on the right of the line extending `b`-`a`. */ 284 | static inline public function isRight(p:HxPoint, a:HxPoint, b:HxPoint):Bool 285 | { 286 | return side(p, a, b) < 0; 287 | } 288 | 289 | /** Returns true if `p` is on the right or collinear to the line extending `b`-`a`. */ 290 | static inline public function isRightOrOn(p:HxPoint, a:HxPoint, b:HxPoint):Bool 291 | { 292 | return side(p, a, b) <= 0; 293 | } 294 | 295 | /** Returns true if the specified triangle is degenerate (collinear points). */ 296 | static inline public function isCollinear(p:HxPoint, a:HxPoint, b:HxPoint):Bool 297 | { 298 | return side(p, a, b) == 0; 299 | } 300 | 301 | /** Distance from `v` to `w`. */ 302 | inline static public function distance(v:HxPoint, w:HxPoint) { return Math.sqrt(distanceSquared(v, w)); } 303 | 304 | /** Perpendicular distance from `p` to line segment `v`-`w`. */ 305 | inline static public function distanceToSegment(p:HxPoint, v:HxPoint, w:HxPoint) { return Math.sqrt(distanceToSegmentSquared(p, v, w)); } 306 | 307 | /** Squared distance from `v` to `w`. */ 308 | inline static public function distanceSquared(v:HxPoint, w:HxPoint):Float { return sqr(v.x - w.x) + sqr(v.y - w.y); } 309 | 310 | /** Squared perpendicular distance from `p` to line segment `v`-`w`. */ 311 | static public function distanceToSegmentSquared(p:HxPoint, v:HxPoint, w:HxPoint):Float { 312 | var l2:Float = distanceSquared(v, w); 313 | if (l2 == 0) return distanceSquared(p, v); 314 | var t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2; 315 | if (t < 0) return distanceSquared(p, v); 316 | if (t > 1) return distanceSquared(p, w); 317 | point.setTo(v.x + t * (w.x - v.x), v.y + t * (w.y - v.y)); 318 | return distanceSquared(p, point); 319 | } 320 | 321 | static public function getCentroid(poly:Poly):HxPoint { 322 | var c = new HxPoint(); 323 | var area = getArea(poly); 324 | if (area != 0) { 325 | var len = poly.length; 326 | for (i in 0...len) { 327 | var p0 = poly[i]; 328 | var p1 = poly[(i + 1) % len]; 329 | var m = p0.x * p1.y - p1.x * p0.y; 330 | c.x += (p0.x + p1.x) * m; 331 | c.y += (p0.y + p1.y) * m; 332 | } 333 | 334 | c.x /= 6 * area; 335 | c.y /= 6 * area; 336 | } else { // avoid division by zero and return an empty Point 337 | c = HxPoint.EMPTY; 338 | } 339 | return c; 340 | } 341 | 342 | static public function getArea(poly:Poly):Float { 343 | var area = 0.0; 344 | var len = poly.length; 345 | for (i in 0...len) { 346 | var p0 = poly[i]; 347 | var p1 = poly[(i + 1) % len]; 348 | area += p0.x * p1.y - p1.x * p0.y; 349 | } 350 | return area = .5 * area; 351 | } 352 | 353 | static public function meet(p:HxPoint, q:HxPoint):HomogCoord 354 | { 355 | return new HomogCoord(p.y - q.y, q.x - p.x, p.x * q.y - p.y * q.x); 356 | } 357 | 358 | /** Dot product. */ 359 | static public function dot(p:HxPoint, q:HxPoint):Float 360 | { 361 | return p.x * q.x + p.y * q.y; 362 | } 363 | 364 | /** Returns `x` squared. */ 365 | @:noUsing inline static public function sqr(x:Float):Float { return x * x; } 366 | 367 | /** Returns true if `a` is _acceptably_ equal to `b` (i.e. `a` is within EPSILON distance from `b`). */ 368 | @:noUsing static inline public function eq(a:Float, b:Float):Bool 369 | { 370 | return Math.abs(a - b) <= EPSILON; 371 | } 372 | 373 | /** Empties an array of its contents. */ 374 | static inline public function clear(array:Array) 375 | { 376 | #if (cpp || php || hl || neko || eval) 377 | array.splice(0, array.length); 378 | #else 379 | untyped array.length = 0; 380 | #end 381 | } 382 | 383 | /** Converts a poly defined by an Array to an Array (appending values to `out` if specified). */ 384 | static public function toFloatArray(poly:Poly, ?out:Array):Array 385 | { 386 | out = (out != null) ? out : new Array(); 387 | 388 | for (p in poly) { 389 | out.push(p.x); 390 | out.push(p.y); 391 | } 392 | 393 | return out; 394 | } 395 | 396 | /** Reverses the coords of the 2D float array `poly` (doing it `inPlace` if specified). */ 397 | static public function reverseFloatArray(poly:Array, inPlace:Bool = false):Array 398 | { 399 | var res = inPlace ? poly : new Array(); 400 | 401 | var nPoints = poly.length >> 1; 402 | for (i in 0...nPoints) { 403 | var xPos = (nPoints - i - 1) * 2; 404 | res[i * 2] = (poly[xPos]); 405 | res[i * 2 + 1] = (poly[xPos + 1]); 406 | } 407 | 408 | return res; 409 | } 410 | 411 | /** Converts an Array of Arrays into a 'flattened' Array (appending values to `out` if specified). */ 412 | static public function flatten(array:Array>, ?out:Array):Array 413 | { 414 | var res = (out != null) ? out : []; 415 | 416 | for (arr in array) { 417 | for (item in arr) res.push(item); 418 | } 419 | 420 | return res; 421 | } 422 | 423 | /** Converts a poly defined by an Array to an Array (appending values to `out` if specified). */ 424 | static public function toPointArray(poly:Array, ?out:Poly):Poly 425 | { 426 | out = (out != null) ? out : new Poly(); 427 | 428 | var size = poly.length; 429 | if (poly.length % 2 == 1) size--; 430 | 431 | for (i in 0...size >> 1) { 432 | out.push(new HxPoint(poly[i * 2], poly[i * 2 + 1])); 433 | } 434 | 435 | return out; 436 | } 437 | 438 | /** Converts a string into an array of HxPoints. */ 439 | @:noUsing static public function parsePoints(str:String):Poly { 440 | var floats = ~/[^-eE\.\d]+/g.split(str).filter(function(val) { 441 | return val != null && val != ""; 442 | }).map(Std.parseFloat); 443 | 444 | var pts = new Poly(); 445 | 446 | var n = floats.length; 447 | Debug.assert(n % 2 == 0, 'Parsed string must contain an even number of parseable floats (${n} found).'); 448 | 449 | for (i in 0...Std.int(n / 2)) { 450 | pts.push(new HxPoint(floats[i * 2], floats[i * 2 + 1])); 451 | } 452 | 453 | return pts; 454 | } 455 | 456 | /** Expands a line into a rectangular poly, offsetting it by half-`thickness` along its normals. */ 457 | @:noUsing static public function inflateLine(start:HxPoint, end:HxPoint, thickness:Float):Poly { 458 | var halfWidth = thickness / 2; 459 | var dx = end.x - start.x; 460 | var dy = end.y - start.y; 461 | var len = Math.sqrt(sqr(dx) + sqr(dy)); 462 | var nx = (dx / len) * halfWidth; 463 | var ny = (dy / len) * halfWidth; 464 | return [new HxPoint(start.x - ny, start.y + nx), new HxPoint(end.x - ny, end.y + nx), 465 | new HxPoint(end.x + ny, end.y - nx), new HxPoint(start.x + ny, start.y - nx)]; 466 | } 467 | 468 | /** 469 | * Clips `subjPoly` with `clipPoly` (using the Sutherland-Hodgman algorithm). 470 | * 471 | * NOTE: expects simple polygons, and `clipPoly` MUST be convex. 472 | * 473 | * @see https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm 474 | */ 475 | static public function clip(subjPoly:Poly, clipPoly:Poly):Array { 476 | Debug.assert(clipPoly.length >= 3 && isConvex(clipPoly), "`clipPoly` must be a valid convex poly"); 477 | 478 | var res = []; 479 | var output = subjPoly; 480 | 481 | var isInside = isCCW(clipPoly) ? isRight : isLeft; 482 | 483 | var clipEdgeStart:HxPoint; 484 | var clipEdgeEnd:HxPoint; 485 | var inputEdgeStart:HxPoint; 486 | var inputEdgeEnd:HxPoint; 487 | 488 | var clipLen = clipPoly.length; 489 | 490 | for (i in 0...clipLen) { 491 | clipEdgeStart = clipPoly[i]; 492 | clipEdgeEnd = clipPoly[wrappedIdx(clipPoly, i + 1)]; 493 | 494 | var input = output; 495 | output = []; 496 | inputEdgeStart = input[input.length - 1]; 497 | for (j in 0...input.length) { 498 | inputEdgeEnd = input[j]; 499 | 500 | if (isInside(inputEdgeEnd, clipEdgeStart, clipEdgeEnd)) { 501 | if (!isInside(inputEdgeStart, clipEdgeStart, clipEdgeEnd)) { 502 | var intersectionPoint = intersection(inputEdgeStart, inputEdgeEnd, clipEdgeStart, clipEdgeEnd); 503 | if (intersectionPoint != null) output.push(intersectionPoint); 504 | } 505 | output.push(inputEdgeEnd); 506 | } else if (isInside(inputEdgeStart, clipEdgeStart, clipEdgeEnd)) { 507 | var intersectionPoint = intersection(inputEdgeStart, inputEdgeEnd, clipEdgeStart, clipEdgeEnd); 508 | if (intersectionPoint != null) output.push(intersectionPoint); 509 | } 510 | inputEdgeStart = inputEdgeEnd; 511 | } 512 | res.push(output); 513 | } 514 | 515 | return res; 516 | } 517 | 518 | /** Linear interpolation. */ 519 | @:noUsing 520 | static inline public function lerp(a:Float, b:Float, t:Float):Float { 521 | return (1.0 - t) * a + t * b; 522 | } 523 | 524 | /** Linear interpolation between points. */ 525 | @:noUsing 526 | static inline public function lerpPoints(a:HxPoint, b:HxPoint, t:Float):HxPoint { 527 | return new HxPoint(lerp(a.x, b.x, t), lerp(a.y, b.y, t)); 528 | } 529 | 530 | /** Used internally to expose enums in js. */ 531 | @:noUsing @:noCompletion static public function exposeEnum(enumClass:Enum, ?as:String) { 532 | #if (js && !jsprime) 533 | var dotPath = (as != null ? as : enumClass.getName()).split("."); 534 | untyped { 535 | var exports = $hx_exports; 536 | var i = 0; 537 | while (i < dotPath.length - 1) { 538 | var currPath = dotPath[i]; 539 | exports[currPath] = exports[currPath] || { }; 540 | exports = exports[currPath]; 541 | i++; 542 | } 543 | exports[dotPath[i]] = enumClass; 544 | } 545 | #end 546 | } 547 | } 548 | -------------------------------------------------------------------------------- /src/hxGeomAlgo/RamerDouglasPeucker.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Ramer-Douglas-Peucker implementation. 3 | * 4 | * Based on: 5 | * 6 | * @see http://karthaus.nl/rdp/ (JS - by Marius Karthaus) 7 | * @see http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment (JS - Grumdrig) 8 | * 9 | * @author azrafe7 10 | */ 11 | 12 | package hxGeomAlgo; 13 | 14 | 15 | import hxGeomAlgo.PolyTools; 16 | 17 | @:expose 18 | class RamerDouglasPeucker 19 | { 20 | /** 21 | * Simplify polyline. 22 | * 23 | * @param points Array of points defining the polyline. 24 | * @param epsilon Perpendicular distance threshold (typically in the range (0..2]). 25 | * @return An array of points defining the simplified polyline. 26 | */ 27 | static public function simplify(points:Array, epsilon:Float = 1):Array 28 | { 29 | var firstPoint = points[0]; 30 | var lastPoint = points[points.length - 1]; 31 | 32 | if (points.length < 2) { 33 | return [].concat(points); 34 | } 35 | 36 | var index = -1; 37 | var dist = 0.; 38 | for (i in 1...points.length - 1) { 39 | var currDist = PolyTools.distanceToSegment(points[i], firstPoint, lastPoint); 40 | if (currDist > dist) { 41 | dist = currDist; 42 | index = i; 43 | } 44 | } 45 | 46 | if (dist > epsilon){ 47 | // recurse 48 | var l1 = points.slice(0, index + 1); 49 | var l2 = points.slice(index); 50 | var r1 = simplify(l1, epsilon); 51 | var r2 = simplify(l2, epsilon); 52 | // concat r2 to r1 minus the end/startpoint that will be the same 53 | var rs = r1.slice(0, r1.length - 1).concat(r2); 54 | return rs; 55 | } else { 56 | return [firstPoint, lastPoint]; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/SnoeyinkKeil.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Snoeyink-Keil minimum convex polygon decomposition implementation. 3 | * NOTE: Should work only for SIMPLE polygons (not self-intersecting, without holes). 4 | * 5 | * Based on: 6 | * 7 | * @see http://www.cs.ubc.ca/~snoeyink/demos/convdecomp/MCDDemo.html (Java - Jack Snoeyink) 8 | * 9 | * Other credits should go to papers/work of: 10 | * 11 | * J. Mark Keil, Jack Snoeyink: On the Time Bound for Convex Decomposition of Simple Polygons. Int. J. Comput. Geometry Appl. 12(3): 181-192 (2002) 12 | * @see http://www.cs.ubc.ca/spider/snoeyink/papers/convdecomp.ps.gz (Snoeyink & Keil) 13 | * @see http://mnbayazit.com/406/files/OnTheTimeBound-Snoeyink.pdf (Snoeyink & Keil) 14 | * 15 | * @author azrafe7 16 | */ 17 | 18 | package hxGeomAlgo; 19 | 20 | 21 | import haxe.ds.ArraySort; 22 | import haxe.ds.IntMap.IntMap; 23 | import hxGeomAlgo.SnoeyinkKeil.DecompPoly; 24 | 25 | 26 | using hxGeomAlgo.PolyTools; 27 | 28 | 29 | @:expose 30 | class SnoeyinkKeil 31 | { 32 | 33 | static private var poly:Poly; // cw version of simplePoly - used internally 34 | 35 | static public var reversed:Bool; // true if the _internal_ indices have been reversed 36 | 37 | static public var diagonals:Array; // stores diagonals' indices (as found by _decompByDiags()) 38 | 39 | /** Decomposes `simplePoly` into a minimum number of convex polygons. */ 40 | static public function decomposePoly(simplePoly:Poly):Array { 41 | var res = new Array(); 42 | 43 | var indices = decomposePolyIndices(simplePoly); 44 | 45 | for (polyIndices in indices) { 46 | var currPoly = new Poly(); 47 | res.push(currPoly); 48 | for (idx in polyIndices) { 49 | currPoly.push(simplePoly[idx]); 50 | } 51 | } 52 | 53 | return res; 54 | } 55 | 56 | /** Decomposes `simplePoly` into a minimum number of convex polygons and returns their vertices' indices. */ 57 | static public function decomposePolyIndices(simplePoly:Poly):Array> { 58 | var res = new Array>(); 59 | diagonals = []; 60 | if (simplePoly.length < 3) return res; 61 | 62 | poly = new Poly(); 63 | for (p in simplePoly) poly.push(new HxPoint(p.x, p.y)); 64 | reversed = poly.makeCW(); // make poly cw (in place) 65 | 66 | var i, j, k; 67 | var n = poly.length; 68 | var decomp = new DecompPoly(poly); 69 | decomp.init(); 70 | 71 | for (l in 3...n) { 72 | i = decomp.reflexIter(); 73 | 74 | while (i + l < n) { 75 | //trace("reflex: " + i + " vis:" + decomp.visible(i, i + l) + " " + poly.at(i)); 76 | if (decomp.visible(i, k = i + l)) { 77 | decomp.initPairs(i, k); 78 | if (decomp.isReflex(k)) { 79 | for (j in i + 1...k) decomp.typeA(i, j, k); 80 | } else { 81 | j = decomp.reflexIter(i + 1); 82 | while (j < k - 1) { 83 | decomp.typeA(i, j, k); 84 | j = decomp.reflexNext(j); 85 | } 86 | 87 | decomp.typeA(i, k - 1, k); // do this, reflex or not. 88 | } 89 | } 90 | 91 | i = decomp.reflexNext(i); 92 | } 93 | 94 | k = decomp.reflexIter(l); 95 | while (k < n) { 96 | 97 | if (!decomp.isReflex(i = k - l) && decomp.visible(i, k)) { 98 | decomp.initPairs(i, k); 99 | decomp.typeB(i, i + 1, k); // do this, reflex or not. 100 | 101 | j = decomp.reflexIter(i + 2); 102 | while (j < k) { 103 | decomp.typeB(i, j, k); 104 | j = decomp.reflexNext(j); 105 | } 106 | } 107 | 108 | k = decomp.reflexNext(k); 109 | } 110 | } 111 | decomp.guard = 3 * n; 112 | decomp.recoverSolution(0, n - 1); 113 | 114 | res = decomp.decompIndices(); 115 | 116 | if (reversed) { 117 | for (poly in res) { 118 | for (i in 0...poly.length) poly[i] = n - poly[i] - 1; 119 | } 120 | for (d in diagonals) { 121 | var tmp = d.from; 122 | d.from = n - d.to - 1; 123 | d.to = n - tmp - 1; 124 | } 125 | } 126 | 127 | return res; 128 | } 129 | } 130 | 131 | 132 | class DecompPoly { 133 | public static var INFINITY:Int = 100000; 134 | public static var BAD:Int = 999990; 135 | public static var NONE:Int = 0; 136 | 137 | public var guard:Int; 138 | 139 | public var poly:Poly; // the polygon 140 | private var n:Int; // number of vertices 141 | private var subDecomp:SubDecomp; // the subproblems in n x r space 142 | 143 | // for reflexIter 144 | private var _reflexFirst:Int; 145 | private var _reflexNext:Array; 146 | private var _reflexFlag:Array; 147 | 148 | // intermediate and final result 149 | private var _indicesSet:IntMap = new IntMap(); 150 | private var _polys:Array = new Array(); 151 | private var _diags:Array = []; 152 | 153 | 154 | public function new(poly:Poly) { 155 | this.poly = poly; 156 | n = poly.length; 157 | } 158 | 159 | public function init() { 160 | initReflex(); 161 | subDecomp = new SubDecomp(_reflexFlag); 162 | initVisibility(); 163 | initSubProblems(); 164 | } 165 | 166 | private function initReflex() { 167 | _reflexFlag = new Array(); 168 | _reflexNext = new Array(); 169 | 170 | // init arrays 171 | for (i in 0...n) { 172 | _reflexFlag[i] = false; 173 | _reflexNext[i] = -1; 174 | } 175 | 176 | // find reflex vertices 177 | var wrap:Int = 0; 178 | _reflexFlag[wrap] = true; // by convention 179 | var i = n - 1; 180 | while (i > 0) { 181 | _reflexFlag[i] = poly.at(i - 1).isRight(poly.at(i), poly.at(wrap)); 182 | wrap = i; 183 | i--; 184 | } 185 | 186 | _reflexFirst = n; // for reflexIter 187 | i = n - 1; 188 | while (i >= 0) { 189 | _reflexNext[i] = _reflexFirst; 190 | if (isReflex(i)) _reflexFirst = i; 191 | i--; 192 | } 193 | //trace(_reflexNext); 194 | } 195 | 196 | public function isReflex(i:Int) { return _reflexFlag[i]; } 197 | 198 | public function reflexNext(i:Int):Int { return _reflexNext[i]; } 199 | 200 | /* a cheap iterator through reflex vertices; each vertex knows the 201 | index of the next reflex vertex. */ 202 | public function reflexIter(?n:Int):Int { // start w/ n or 1st reflex after... 203 | if (n == null || n <= 0) return _reflexFirst; 204 | if (n > _reflexNext.length) return _reflexNext.length; 205 | return _reflexNext[n - 1]; 206 | } 207 | 208 | public function visible(i:Int, j:Int):Bool { return subDecomp.weight(i, j) < BAD; } 209 | 210 | public function initVisibility() { // initReflex() first 211 | var visIndices:Array; 212 | var i:Int = reflexIter(); 213 | while (i < n) { 214 | visIndices = Visibility.getVisibleIndicesFrom(poly, i); 215 | 216 | while (visIndices.length > 0) { 217 | var j:Int = visIndices.pop(); 218 | if (j < i) subDecomp.setWeight(j, i, INFINITY); 219 | else subDecomp.setWeight(i, j, INFINITY); 220 | } 221 | 222 | i = _reflexNext[i]; 223 | } 224 | } 225 | 226 | private function setAfter(i:Int) { // i reflex 227 | Debug.assert(isReflex(i), "Non reflex i in setAfter(" + i + ")"); 228 | subDecomp.setWeight(i, i + 1, 0); 229 | if (visible(i, i + 2)) subDecomp.initWithWeight(i, i + 2, 0, i + 1, i + 1); 230 | } 231 | 232 | private function setBefore(i:Int) { // i reflex 233 | Debug.assert(isReflex(i), "Non reflex i in setBefore(" + i + ")"); 234 | subDecomp.setWeight(i - 1, i, 0); 235 | if (visible(i - 2, i)) subDecomp.initWithWeight(i - 2, i, 0, i - 1, i - 1); 236 | } 237 | 238 | public function initSubProblems() { // initVisibility first 239 | var i:Int; 240 | 241 | i = reflexIter(); 242 | if (i == 0) { setAfter(i); i = _reflexNext[i]; } 243 | if (i == 1) { subDecomp.setWeight(0, 1, 0); setAfter(i); i = _reflexNext[i]; } 244 | while (i < n - 2) { setBefore(i); setAfter(i); i = _reflexNext[i]; } 245 | if (i == n - 2) { setBefore(i); subDecomp.setWeight(i, i + 1, 0); i = _reflexNext[i];} 246 | if (i == n - 1) { setBefore(i); } 247 | } 248 | 249 | public function initPairs(i:Int, k:Int) { 250 | subDecomp.init(i, k); 251 | } 252 | 253 | public function recoverSolution(i:Int, k:Int) { 254 | var j:Int; 255 | guard--; 256 | Debug.assert(guard >= 0, "Can't recover " + i + "," + k); 257 | if (k - i <= 1) return; 258 | var pair:PairDeque = subDecomp.pairs(i, k); 259 | //trace(i, k, pair); 260 | if (isReflex(i)) { 261 | j = pair.backTop(); 262 | recoverSolution(j, k); 263 | if (j - i > 1) { 264 | if (pair.frontBottom() != pair.backTop()) { 265 | var pd:PairDeque = subDecomp.pairs(i, j); 266 | pd.restore(); 267 | while ((!pd.isBackEmpty()) && pair.frontBottom() != pd.frontBottom()) pd.popBack(); 268 | //if (!pd.isBackEmpty()) throw "Emptied pd " + i + "," + j + "," + k + " " + pair.toString(); 269 | } 270 | recoverSolution(i, j); 271 | } 272 | } 273 | else { 274 | j = pair.frontTop(); 275 | recoverSolution(i, j); 276 | if (k - j > 1) { 277 | if (pair.frontTop() != pair.backBottom()) { 278 | var pd:PairDeque = subDecomp.pairs(j, k); 279 | pd.restore(); 280 | while (!pd.isFrontEmpty() && pair.backBottom() != pd.backBottom()) pd.popFront(); 281 | //if (!pd.isFrontEmpty()) throw "Emptied pd " + i + "," + j + "," + k + " " + pair.toString(); 282 | } 283 | recoverSolution(j, k); 284 | } 285 | } 286 | } 287 | 288 | public function typeA(i:Int, j:Int, k:Int) { /* i reflex; use jk */ 289 | // System.out.print("\nA "+i+","+j+","+k+":"); 290 | // assert(reflex(i), "non reflex i in typeA("+i+","+j+","+k+")"); 291 | // assert(k-i > 1, "too small in typeA("+i+","+j+","+k+")"); 292 | if (!visible(i,j)) return; 293 | var top:Int = j; 294 | var w:Int = subDecomp.weight(i, j); 295 | 296 | if (k - j > 1) { 297 | if (!visible(j, k)) return; 298 | w += subDecomp.weight(j, k) + 1; 299 | } 300 | if (j - i > 1) { // check if must use ij, too. 301 | var pair:PairDeque = subDecomp.pairs(i, j); 302 | if (!poly.at(k).isLeft(poly.at(j), poly.at(pair.backTop()))) { 303 | 304 | while (pair.backHasNext() && !poly.at(k).isLeft(poly.at(j), poly.at(pair.backPeekNext()))) pair.popBack(); 305 | 306 | if (!pair.isBackEmpty() && !poly.at(k).isRight(poly.at(i), poly.at(pair.frontBottom()))) top = pair.frontBottom(); 307 | else w++; // yes, need ij. top = j already 308 | 309 | } else w++; // yes, need ij. top = j already 310 | } 311 | update(i, k, w, top, j); 312 | } 313 | 314 | public function typeB(i:Int, j:Int, k:Int) { /* k reflex, i not. */ 315 | // System.out.print("\nB "+i+","+j+","+k+":"); 316 | if (!visible(j, k)) return; 317 | var top:Int = j; 318 | var w:Int = subDecomp.weight(j, k); 319 | 320 | if (j - i > 1) { 321 | if (!visible(i, j)) return; 322 | w += subDecomp.weight(i, j) + 1; 323 | } 324 | if (k - j > 1) { // check if must use jk, too. 325 | var pair:PairDeque = subDecomp.pairs(j, k); 326 | if (!poly.at(i).isRight(poly.at(j), poly.at(pair.frontTop()))) { 327 | 328 | while (pair.frontHasNext() && !poly.at(i).isRight(poly.at(j), poly.at(pair.frontPeekNext()))) pair.popFront(); 329 | 330 | if (!pair.isFrontEmpty() && !poly.at(i).isLeft(poly.at(k), poly.at(pair.backBottom()))) top = pair.backBottom(); 331 | else w++; // yes, use jk. top=j already 332 | 333 | } else w++; // yes, use jk. top=j already 334 | } 335 | update(i, k, w, j, top); 336 | } 337 | 338 | 339 | /** 340 | * We have a new solution for subprob a,b with weight w, using 341 | * i,j. If it is better than previous solutions, we update. 342 | * We assume that a < b and i < j. 343 | */ 344 | public function update(a:Int, b:Int, w:Int, i:Int, j:Int) { 345 | //trace("update(" + a + "," + b + " w:" + w + " " + i + "," + j + ")"); 346 | var ow:Int = subDecomp.weight(a, b); 347 | if (w <= ow) { 348 | var pair:PairDeque = subDecomp.pairs(a, b); 349 | if (w < ow) { 350 | pair.flush(); 351 | subDecomp.setWeight(a, b, w); 352 | } 353 | pair.pushNarrow(i, j); 354 | } 355 | } 356 | 357 | // TODO: see how to improve this 358 | private function _decompByDiags(i:Int, k:Int, outIndices:Array>, level:Int = 0) { 359 | //trace('level -> $level'); 360 | 361 | if (level == 0) { 362 | _indicesSet.set(0, true); 363 | _indicesSet.set(poly.length - 1, true); 364 | //trace("diag " + i + "-" + k + " " + poly.at(i) + " " + poly.at(k)); 365 | } 366 | 367 | var j:Int; 368 | var ijReal = true, jkReal = true; 369 | var nDiags:Int = 0; 370 | 371 | if (k - i <= 1) return; 372 | 373 | var pair:PairDeque = subDecomp.pairs(i, k); 374 | if (isReflex(i)) { 375 | j = pair.backTop(); 376 | ijReal = (pair.frontBottom() == pair.backTop()); 377 | } else { 378 | j = pair.frontTop(); 379 | jkReal = (pair.backBottom() == pair.frontTop()); } 380 | 381 | if (ijReal) { 382 | _indicesSet.set(i, true); 383 | _indicesSet.set(j, true); 384 | _diags.push( { from:i, to:j } ); 385 | //trace("diag " + i + "-" + j + " " + poly.at(i) + " " + poly.at(j)); 386 | nDiags++; 387 | } 388 | 389 | if (jkReal) { 390 | _indicesSet.set(j, true); 391 | _indicesSet.set(k, true); 392 | _diags.push( { from:j, to:k } ); 393 | //trace("diag " + j + "-" + k + " " + poly.at(j) + " " + poly.at(k)); 394 | nDiags++; 395 | } 396 | 397 | guard--; 398 | Debug.assert(guard >= 0, "Infinite loop diag " + i + "," + k); 399 | 400 | if (nDiags > 1) { // add new decomposing poly 401 | var indices:Array = [for (k in _indicesSet.keys()) k]; 402 | indices.sort(intCmp); 403 | 404 | if (indices.length > 0) { 405 | outIndices.push(indices); 406 | _indicesSet = new IntMap(); 407 | } 408 | } 409 | 410 | // if we just found a real i-j diag, follow it first (should fix issue #11) 411 | if (ijReal && j - i > 1) { 412 | _decompByDiags(j, k, outIndices, level + 1); 413 | _decompByDiags(i, j, outIndices, level + 1); 414 | } else { 415 | _decompByDiags(i, j, outIndices, level + 1); 416 | _decompByDiags(j, k, outIndices, level + 1); 417 | } 418 | } 419 | 420 | private inline function intCmp(a:Int, b:Int):Int { 421 | if (a == b) return 0; 422 | else if (b < a) return 1; 423 | else return -1; 424 | } 425 | 426 | /** Returns the vertices' indices of each decomposing poly. */ 427 | public function decompIndices():Array> 428 | { 429 | var res = new Array>(); 430 | guard = 3 * n; 431 | _decompByDiags(0, poly.length - 1, res); 432 | SnoeyinkKeil.diagonals = _diags; 433 | 434 | return res; 435 | } 436 | 437 | public function toString():String { 438 | return poly.length + ": " + poly.toString(); 439 | } 440 | } 441 | 442 | 443 | /** 444 | * This class stores all subproblems for a decomposition by dynamic 445 | * programming. 446 | * It uses an indirect addressing into arrays that have all the 447 | * reflex vertices first, so that I can allocate only O(nr) space. 448 | */ 449 | class SubDecomp { 450 | private var wt:Array>; 451 | private var pd:Array>; 452 | private var rx:Array; // indirect index so reflex come first 453 | 454 | public function new(reflex:Array) { 455 | var n = reflex.length, r = 0, j; 456 | 457 | rx = new Array(); 458 | 459 | for (i in 0...n) rx[i] = reflex[i] ? r++ : 0; 460 | 461 | j = r; 462 | wt = new Array>(); 463 | pd = new Array>(); 464 | for (i in 0...n) { 465 | if (!reflex[i]) rx[i] = j++; 466 | wt[i] = new Array(); 467 | pd[i] = new Array(); 468 | for (k in 0...n) { 469 | if (i < r || k < r) { 470 | wt[i][k] = DecompPoly.BAD; 471 | pd[i][k] = null; 472 | } else { 473 | break; 474 | } 475 | } 476 | } 477 | 478 | //trace(rx); 479 | } 480 | 481 | public function setWeight(i:Int, j:Int, w:Int) { 482 | wt[rx[i]][rx[j]] = w; 483 | } 484 | 485 | public function weight(i:Int, j:Int):Int { 486 | return wt[rx[i]][rx[j]]; 487 | } 488 | 489 | public function pairs(i:Int, j:Int):PairDeque { 490 | return pd[rx[i]][rx[j]]; 491 | } 492 | 493 | public function init(i:Int, j:Int):PairDeque { 494 | return pd[rx[i]][rx[j]] = new PairDeque(); 495 | } 496 | 497 | public function initWithWeight(i:Int, j:Int, w:Int, a:Int, b:Int) { 498 | setWeight(i, j, w); 499 | init(i, j).push(a,b); 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /src/hxGeomAlgo/Version.hx: -------------------------------------------------------------------------------- 1 | package hxGeomAlgo; 2 | 3 | /** 4 | * @author azrafe7 5 | */ 6 | @:expose 7 | class Version 8 | { 9 | inline public static var major:Int = 0; 10 | inline public static var minor:Int = 5; 11 | inline public static var patch:Int = 0; 12 | 13 | static public function toString():String 14 | { 15 | return '$major.$minor.$patch'; 16 | } 17 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/Visibility.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Visibility polygon implementation. 3 | * NOTE: Should work only for SIMPLE polygons (not self-intersecting, without holes). 4 | * 5 | * Based on: 6 | * 7 | * @see http://www.cs.ubc.ca/~snoeyink/demos/convdecomp/VPDemo.html (Java - by Jack Snoeyink) 8 | * 9 | * Other credits should go to papers/work of: 10 | * 11 | * @see http://www.stanford.edu/~tunococ/papers/avis81optimal.pdf (Avis & Toussaint) 12 | * 13 | * @author azrafe7 14 | */ 15 | 16 | package hxGeomAlgo; 17 | 18 | 19 | import hxGeomAlgo.Debug; 20 | import hxGeomAlgo.HomogCoord; 21 | 22 | 23 | using hxGeomAlgo.PolyTools; 24 | 25 | 26 | /** polygon vertex types: 27 | 28 | LLID-------------------RLID 29 | | | 30 | | | 31 | --------LWALL RWALL--------- 32 | */ 33 | enum VertexType { 34 | UNKNOWN; 35 | RIGHT_LID; 36 | LEFT_LID; 37 | RIGHT_WALL; 38 | LEFT_WALL; 39 | } 40 | 41 | 42 | @:expose 43 | class Visibility 44 | { 45 | inline static private var NOT_SAVED:Int = -1; 46 | 47 | static private var origPoint:HxPoint; // origin of visibility polygon 48 | static private var stack:Array = new Array(); // stack holds indices of visibility polygon 49 | static private var vertexType:Array = new Array(); // types of vertices 50 | static private var stackTop:Int; // stack pointer to top element 51 | static private var poly:Poly; // cw version of simplePoly - used internally 52 | 53 | static private var leftLidIdx:Int; 54 | static private var rightLidIdx:Int; 55 | 56 | static public var reversed:Bool; // true if the _internal_ indices have been reversed 57 | 58 | /** Returns an array of indices representing the vertices of `simplePoly` visible from `origIdx`. */ 59 | static public function getVisibleIndicesFrom(simplePoly:Poly, origIdx:Int = 0):Array { 60 | var res = new Array(); 61 | 62 | poly = new Poly(); 63 | stack.clear(); 64 | vertexType.clear(); 65 | 66 | if (simplePoly.length <= 0) return res; 67 | 68 | // init 69 | stackTop = -1; 70 | for (i in 0...simplePoly.length) { 71 | poly.push(new HxPoint(simplePoly[i].x, simplePoly[i].y)); 72 | stack.push(-1); 73 | vertexType.push(VertexType.UNKNOWN); 74 | } 75 | reversed = poly.makeCW(); // make poly cw (in place) 76 | if (reversed) { 77 | origIdx = poly.length - origIdx - 1; 78 | } 79 | 80 | // build 81 | var edgeJ:HomogCoord; // during the loops, this is the line p[j-1]->p[j] 82 | origPoint = poly[origIdx]; 83 | var j:Int = origIdx; 84 | push(j++, VertexType.RIGHT_WALL); // origPoint & p[1] on Visible Poly 85 | do { // loop always pushes p[j] and increments j. 86 | push(j++, VertexType.RIGHT_WALL); 87 | if (j >= poly.length + origIdx) break; // we are done. 88 | edgeJ = poly.at(j - 1).meet(poly.at(j)); 89 | if (edgeJ.left(origPoint)) { 90 | continue; // easiest case: add edge to Visible Poly. 91 | } 92 | // else p[j] backtracks, we must determine where 93 | if (!(edgeJ.left(poly.at(j - 2)))) { // p[j] is above last Visible Poly edge 94 | j = exitRightBay(poly, j, poly.at(stack[stackTop]), HomogCoord.INFINITY); 95 | push(j++, VertexType.RIGHT_LID); 96 | continue; // exits bay; push next two 97 | } 98 | 99 | saveLid(); // else p[j] below top edge; becomes lid or pops 100 | do { // p[j] hides some of Visible Poly; break loop when can push p[j]. 101 | //trace("do j: " + j + " lid: " + leftLidIdx + " " + rightLidIdx); 102 | if (origPoint.isLeft(poly.at(stack[stackTop]), poly.at(j))) { // saved lid ok so far... 103 | if (poly.at(j).isRight(poly.at(j + 1), origPoint)) j++; // continue to hide 104 | else if (edgeJ.left(poly.at(j + 1))) { // or turns up into bay 105 | j = exitLeftBay(poly, j, poly.at(j), poly.at(leftLidIdx).meet(poly.at(leftLidIdx - 1))) + 1; 106 | } else { // or turns down; put saved lid back & add new Visible Poly edge 107 | restoreLid(); 108 | push(j++, VertexType.LEFT_WALL); 109 | break; 110 | } 111 | edgeJ = poly.at(j - 1).meet(poly.at(j)); // loop continues with new j; update edgeJ 112 | } else { // lid is no longer visible 113 | if (!(edgeJ.left(poly.at(stack[stackTop])))) { // entered RBay, must get out 114 | //if (rightLidIdx != NOT_SAVED) throw "no RLid saved " + leftLidIdx + " " + rightLidIdx; 115 | j = exitRightBay(poly, j, poly.at(stack[stackTop]), edgeJ.neg()); // exits bay; 116 | push(j++, VertexType.RIGHT_LID); 117 | break; // found new visible lid to add to Visible Poly. 118 | } 119 | else saveLid(); // save new lid from Visible Poly; continue to hide Visible Poly. 120 | } 121 | } while (true); 122 | //trace("exit j: " + j + " lid: " + leftLidIdx + " " + rightLidIdx); 123 | } while (j < poly.length + origIdx); // don't push origin again. 124 | 125 | //var s:String = ""; 126 | for (i in 0...stackTop + 1) { 127 | //s += (stack[i] % poly.length) + Std.string(vertexType[i]) + " "; 128 | if (vertexType[i] == VertexType.LEFT_WALL || vertexType[i] == VertexType.RIGHT_WALL) { 129 | var idx = stack[i] % poly.length; 130 | if (reversed) idx = poly.length - idx - 1; // reverse indices 131 | res.push(idx); 132 | } 133 | } 134 | //trace(s); 135 | 136 | return res; 137 | } 138 | 139 | /** Returns an array of all the points of `simplePoly` visible from `origIdx` (may include points not in `simplePoly`). */ 140 | static public function getVisiblePolyFrom(simplePoly:Poly, origIdx:Int = 0):Poly { 141 | var indices = getVisibleIndicesFrom(simplePoly, origIdx); 142 | var res = new Poly(); 143 | 144 | if (indices.length <= 0) return res; 145 | 146 | var q:HomogCoord; 147 | var last:HxPoint = poly.at(stack[stackTop]); 148 | var lastPushed:HxPoint = null; 149 | var lastType:VertexType = VertexType.UNKNOWN; 150 | var vType:VertexType = UNKNOWN; 151 | for (i in 0...stackTop + 1) { 152 | vType = vertexType[i]; 153 | 154 | if (vType == VertexType.RIGHT_LID) { 155 | q = origPoint.meet(last).meet(poly.at(stack[i]).meet(poly.at(stack[i + 1]))); 156 | if (lastPushed != null && !lastPushed.equals(last)) { 157 | res.push(last.clone()); 158 | } 159 | res.push(q.toPoint()); 160 | } else if (vType == VertexType.LEFT_WALL) { 161 | q = origPoint.meet(poly.at(stack[i])).meet(poly.at(stack[i - 2]).meet(poly.at(stack[i - 1]))); 162 | res.push(q.toPoint()); 163 | } else { 164 | if ((vType == VertexType.RIGHT_WALL && lastType == VertexType.RIGHT_LID) || 165 | (vType == VertexType.LEFT_LID && lastType == VertexType.RIGHT_LID)) { 166 | // skip this one 167 | } else { 168 | res.push(last.clone()); 169 | } 170 | } 171 | lastPushed = res[res.length - 1]; 172 | last = poly.at(stack[i]); 173 | lastType = vType; 174 | 175 | //if (lastPushed.equals(last)) trace("duplicate " + last); 176 | } 177 | 178 | return res; 179 | } 180 | 181 | /** 182 | * Exits from a right bay: proceeds from j, j++, ... until exiting 183 | * the bay defined to the right of the line from origPoint through 184 | * point bot to line lid. Returns j such that (j, j+1) forms a new lid 185 | * of this bay. Assumes that poly.at(j) is not left of the line 186 | * origPoint->bot. 187 | */ 188 | static private function exitRightBay(poly:Poly, j:Int, bot:HxPoint, lid:HomogCoord):Int { 189 | var windingNum:Int = 0; // winding number 190 | var mouth:HomogCoord = origPoint.meet(bot); 191 | var lastLeft:Bool, currLeft:Bool = false; 192 | 193 | while (++j < 3 * poly.length) { 194 | lastLeft = currLeft; 195 | currLeft = mouth.left(poly.at(j)); 196 | 197 | // if cross ray origPoint->bot, update winding num 198 | if ((currLeft != lastLeft) && (poly.at(j - 1).isLeft(poly.at(j), origPoint) == currLeft)) { 199 | if (!currLeft) windingNum--; 200 | else if (windingNum++ == 0) { // on 0->1 transitions, check window 201 | var edge:HomogCoord = poly.at(j - 1).meet(poly.at(j)); 202 | if (edge.left(bot) && (!HomogCoord.cw(mouth, edge, lid))) 203 | return j - 1; // j exits window! 204 | } 205 | } 206 | } 207 | 208 | Debug.assert(false, "ERROR: We never exited RBay " + bot + " " + lid + " " + windingNum); 209 | return j; 210 | } 211 | 212 | /** 213 | * Exits from a left bay: proceeds from j, j++, ... until exiting 214 | * the bay defined to the right of the line from origPoint through 215 | * point bot to line lid. Returns j such that (j, j+1) forms a new lid 216 | * of this bay. Assumes that poly.at(j) is not right of the line 217 | * origPoint->bot. 218 | */ 219 | static private function exitLeftBay(poly:Poly, j:Int, bot:HxPoint, lid:HomogCoord):Int { 220 | var windingNum:Int = 0; // winding number 221 | var mouth:HomogCoord = origPoint.meet(bot); 222 | var lastRight:Bool, currRight:Bool = false; // called with !right(org,bot,pj) 223 | 224 | while (++j < 3 * poly.length) { 225 | lastRight = currRight; 226 | currRight = mouth.right(poly.at(j)); 227 | 228 | // if cross ray origPoint->bot, update winding num 229 | if ((currRight != lastRight) && (poly.at(j - 1).isRight(poly.at(j), origPoint) == currRight)) { 230 | if (!currRight) windingNum++; 231 | else if (windingNum-- == 0) { // on 0->-1 transitions, check window 232 | var edge:HomogCoord = poly.at(j - 1).meet(poly.at(j)); 233 | if (edge.right(bot) && (!HomogCoord.cw(mouth, edge, lid))) 234 | return j - 1; // j exits window! 235 | } 236 | } 237 | } 238 | 239 | Debug.assert(false, "ERROR: We never exited LBay " + bot + " " + lid + " " + windingNum); 240 | return j; 241 | } 242 | 243 | static private function push(idx:Int, vType:VertexType) { 244 | stackTop++; 245 | stack[stackTop] = idx; 246 | vertexType[stackTop] = vType; 247 | } 248 | 249 | static private function saveLid():Void 250 | { 251 | //trace("saveLid " + stackTop); 252 | if (vertexType[stackTop] == VertexType.LEFT_WALL) stackTop--; // for LEFT_WALL, lid is previous two 253 | leftLidIdx = stack[stackTop--]; 254 | if (vertexType[stackTop] == VertexType.RIGHT_LID) rightLidIdx = stack[stackTop--]; // if not RIGHT_LID, just leave on top(). 255 | else rightLidIdx = NOT_SAVED; 256 | } 257 | 258 | static private function restoreLid():Void 259 | { 260 | //trace("restoreLid " + leflLidIdx + " " + rightLidIdx); 261 | if (rightLidIdx != NOT_SAVED) push(rightLidIdx, VertexType.RIGHT_LID); 262 | push(leftLidIdx, VertexType.LEFT_LID); 263 | } 264 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/VisvalingamWhyatt.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Visvalingam-Whyatt implementation. 3 | * 4 | * Based on: 5 | * 6 | * Visvalingam M., Whyatt J. D.: Line generalisation by repeated elimination of the smallest area (1992) 7 | * @see https://hydra.hull.ac.uk/resources/hull:8338 (Visvalingam, Whyatt) 8 | * @see http://bost.ocks.org/mike/simplify/ (JS - by Mike Bostock) 9 | * 10 | * @author azrafe7 11 | */ 12 | 13 | package hxGeomAlgo; 14 | 15 | import hxGeomAlgo.Heap; 16 | import hxGeomAlgo.Debug; 17 | 18 | 19 | /** Specifies the method to use in the simplification process. */ 20 | @:expose 21 | enum SimplificationMethod { 22 | MaxPoints(n:Int); // Allow max n points (n > 2). 23 | ThresholdArea(a:Float); // Filter out all triangles with area <= a. 24 | Ratio(r:Float); // Retain r ratio of all points (0 < r <= 1). 25 | } 26 | 27 | 28 | @:expose 29 | class VisvalingamWhyatt 30 | { 31 | #if js 32 | static function __init__() { 33 | PolyTools.exposeEnum(SimplificationMethod); 34 | } 35 | #end 36 | 37 | static private var method:SimplificationMethod; 38 | static private var minHeap:Heap; 39 | 40 | /** 41 | * Simplify polyline. 42 | * 43 | * @param points Array of points defining the polyline. 44 | * @param method The method to use in the simplification process. 45 | * @return An array of points defining the simplified polyline. 46 | */ 47 | static public function simplify(points:Array, method:SimplificationMethod = null):Array 48 | { 49 | var numPoints = points.length; 50 | if (numPoints < 3) return [].concat(points); 51 | VisvalingamWhyatt.method = method == null ? SimplificationMethod.ThresholdArea(0) : method; 52 | 53 | var thresholdArea:Float = 0.0; 54 | var maxPoints = numPoints; 55 | switch (VisvalingamWhyatt.method) 56 | { 57 | case MaxPoints(n) : maxPoints = n; 58 | case ThresholdArea(a): thresholdArea = a; 59 | case Ratio(r) : maxPoints = Std.int(points.length * r); 60 | default: 61 | } 62 | if (maxPoints < 2) maxPoints = 2; 63 | if (thresholdArea < 0) thresholdArea = 0; 64 | 65 | minHeap = new Heap(); 66 | var triangles = []; 67 | var triangle:Triangle; 68 | 69 | // add triangles to array, and filter by threshold area 70 | for (i in 1...numPoints - 1) { 71 | triangle = new Triangle(points[i - 1], points[i], points[i + 1]); 72 | triangle.calcArea(); 73 | if (triangle.area > thresholdArea) { 74 | triangles.push(triangle); 75 | } 76 | } 77 | 78 | // assign prev, next to triangles, adjust vertices, 79 | // recalc area and add them to minHeap 80 | var numTriangles = triangles.length; 81 | for (i in 0...numTriangles) { 82 | triangle = triangles[i]; 83 | if (i > 0) { 84 | triangle.prev = triangles[i - 1]; 85 | triangle.points[0] = triangle.prev.points[1]; 86 | } else { 87 | triangle.points[0] = points[0]; 88 | } 89 | if (i < numTriangles - 1) { 90 | triangle.next = triangles[i + 1]; 91 | triangle.points[2] = triangle.next.points[1]; 92 | } else { 93 | triangle.points[2] = points[numPoints - 1]; 94 | } 95 | triangle.calcArea(); 96 | minHeap.push(triangle); 97 | } 98 | 99 | // filter triangles 100 | var firstTriangle = triangles[0]; 101 | while (minHeap.length > maxPoints - 2) { 102 | triangle = minHeap.pop(); 103 | 104 | if (triangle.prev != null) { 105 | triangle.prev.next = triangle.next; 106 | triangle.prev.points[2] = triangle.points[2]; 107 | updateTriangle(triangle.prev); 108 | } else if (triangle.next != null) { 109 | firstTriangle = triangle.next; 110 | } 111 | 112 | if (triangle.next != null) { 113 | triangle.next.prev = triangle.prev; 114 | triangle.next.points[0] = triangle.points[0]; 115 | updateTriangle(triangle.next); 116 | } 117 | #if (GEOM_CHECKS) 118 | minHeap.validate(); 119 | #end 120 | } 121 | 122 | var res = [points[0]]; 123 | triangle = maxPoints > 2 ? firstTriangle : null; 124 | while (triangle != null) { 125 | res.push(triangle.points[1]); 126 | triangle = triangle.next; 127 | } 128 | res.push(points[numPoints - 1]); 129 | 130 | return res; 131 | } 132 | 133 | static private function updateTriangle(triangle:Triangle) 134 | { 135 | minHeap.remove(triangle); 136 | triangle.calcArea(); 137 | minHeap.push(triangle); 138 | } 139 | } 140 | 141 | 142 | private class Triangle implements Heapable 143 | { 144 | public var position:Int; 145 | 146 | public var points:Array; 147 | public var prev:Triangle = null; 148 | public var next:Triangle = null; 149 | public var area:Float = 0; 150 | 151 | public function new(a:HxPoint, b:HxPoint, c:HxPoint):Void 152 | { 153 | points = [a, b, c]; 154 | } 155 | 156 | /** @see http://web.archive.org/web/20120305071015/http://www.btinternet.com/~se16/hgb/triangle.htm */ 157 | public function calcArea():Float 158 | { 159 | //trace('(${points[0].x} - ${points[2].x}) * (${points[1].y} - ${points[0].y}) - (${points[0].x} - ${points[1].x}) * (${points[2].y} - ${points[0].y})'); 160 | area = ((points[0].x * points[2].y - points[2].x * points[0].y) + 161 | (points[1].x * points[0].y - points[0].x * points[1].y) + 162 | (points[2].x * points[1].y - points[1].x * points[2].y)) * .5; 163 | if (area < 0) area = -area; 164 | return area; 165 | } 166 | 167 | public function compare(other:Triangle):Int { 168 | var diff = area - other.area; 169 | return diff < 0 ? -1 : diff > 0 ? 1 : 0; 170 | } 171 | } -------------------------------------------------------------------------------- /src/hxGeomAlgo/WuYongZhang.hx: -------------------------------------------------------------------------------- 1 | /** 2 | * Chaikin curve smoothing, multi-step implementation. 3 | * 4 | * Based on: 5 | * 6 | * Wu, L., Yong, J.-H., Zhang, Y.-W., & Zhang, L. (2004). Multi-step Subdivision Algorithm for Chaikin Curves. Lecture Notes in Computer Science, 1232–1238. doi:10.1007/978-3-540-30497-5_188 7 | * @see https://sci-hub.st/10.1007/978-3-540-30497-5_188 (Ling Wu, Jun-Hai Yong, You-Wei Zhang, and Li Zhang) 8 | * 9 | * Fabio Roman (2009). Algoritmo di Chaikin e sue evoluzioni. 10 | * @see http://win.doomitalia.it/varie/chaikin.pdf (Matlab - Fabio Roman) 11 | * 12 | * @author azrafe7 13 | */ 14 | 15 | package hxGeomAlgo; 16 | 17 | using hxGeomAlgo.PolyTools; 18 | using hxGeomAlgo.WuYongZhang; 19 | 20 | 21 | @:expose 22 | class WuYongZhang 23 | { 24 | 25 | // cached values 26 | static var F:Map> = new Map(); // F(j,k) 27 | static var G:Map> = new Map(); // G(j,k) 28 | static var H:Map> = new Map(); // H(j,k) 29 | 30 | static public function buildCache(k:Int) 31 | { 32 | var pow2_minus1 = Math.pow(2, -1); 33 | var pow2_minusK_minus1 = Math.pow(2, -k - 1); 34 | var pow2_minusK = Math.pow(2, -k); 35 | var pow2_K = Std.int(Math.pow(2, k)); 36 | 37 | // exit early if cache is already built for k 38 | if (F.exists(pow2_K) && F[pow2_K].exists(k)) return; 39 | 40 | for (j in 1...pow2_K + 1) { 41 | if (!F.exists(j)) { 42 | F[j] = new Map(); 43 | G[j] = new Map(); 44 | H[j] = new Map(); 45 | } 46 | if (!F[j].exists(k)) { 47 | F[j][k] = pow2_minus1 - pow2_minusK_minus1 - (j - 1) * (pow2_minusK - j * Math.pow(2, -2 * k - 1)); 48 | G[j][k] = pow2_minus1 + pow2_minusK_minus1 + (j - 1) * (pow2_minusK - j * Math.pow(2, -2 * k)); 49 | H[j][k] = (j - 1) * j * Math.pow(2, -2 * k - 1); 50 | } 51 | } 52 | } 53 | 54 | /** In-place point scaling. */ 55 | static inline function scale(a:HxPoint, s:Float):HxPoint { 56 | a.setTo(s * a.x, s * a.y); 57 | return a; 58 | } 59 | 60 | /** In-place point sum (`a` will be modified and returned). */ 61 | static inline function add(a:HxPoint, b:HxPoint):HxPoint { 62 | a.setTo(a.x + b.x, a.y + b.y); 63 | return a; 64 | } 65 | 66 | static public function smooth(poly:Poly, iterations:Int = 3, close:Bool = false):Poly 67 | { 68 | if (iterations <= 0 || poly.length <= 2) { 69 | return poly.copy(); 70 | } 71 | 72 | var P0 = poly.copy(); 73 | var k = iterations; 74 | 75 | if (close) { 76 | P0.push(P0[0]); 77 | P0.push(P0[1]); 78 | } 79 | 80 | buildCache(k); 81 | 82 | var smoothedPoints = new Poly(); 83 | var n0 = P0.length - 1; 84 | var newLength = Std.int(Math.pow(2, k) * n0 - Math.pow(2, k) + 2); 85 | smoothedPoints.resize(newLength); 86 | 87 | var factor1 = (Math.pow(2, -1) + Math.pow(2, -k - 1)); 88 | var factor2 = (Math.pow(2, -1) - Math.pow(2, -k - 1)); 89 | 90 | if (!close) { 91 | smoothedPoints[0] = poly[0].clone(); 92 | smoothedPoints[newLength - 1] = poly[poly.length - 1].clone(); 93 | } else { 94 | var firstPoint = P0[0].clone().scale(factor1).add(P0[1].clone().scale(factor2)); 95 | smoothedPoints[0] = firstPoint; 96 | 97 | var lastPoint = P0[n0 - 1].clone().scale(factor2).add(P0[n0].clone().scale(factor1)); 98 | smoothedPoints[newLength - 1] = lastPoint; 99 | } 100 | 101 | var pow2_k = Std.int(Math.pow(2, k)); 102 | for (i in 0...n0 - 1) { 103 | for (j in 0...pow2_k) { 104 | var idx = pow2_k * i + j + 1; 105 | smoothedPoints[idx] = P0[i].clone().scale(F[j + 1][k]).add(P0[i + 1].clone().scale(G[j + 1][k])).add(P0[i + 2].clone().scale(H[j + 1][k])); 106 | } 107 | } 108 | 109 | if (close && smoothedPoints.length > 2) { 110 | smoothedPoints.splice(newLength - 2, 2); 111 | } 112 | 113 | return smoothedPoints; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | --------------------------------------------------------------------------------