├── .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 | 
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 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
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 |
56 |
57 |
58 |
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 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
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 |
53 |
54 |
55 |
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 |
--------------------------------------------------------------------------------
| |