├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── bin ├── config.nims └── createSvg.nim ├── delaunay.nimble ├── nim.cfg ├── src ├── delaunay.nim └── delaunay │ └── private │ ├── anglesort.nim │ ├── edge.nim │ ├── point.nim │ └── triangle.nim └── tests ├── config.nims ├── helpers.nim ├── point_test.nim ├── t_anglesort.nim ├── t_delaunay.nim ├── t_edge.nim ├── t_largedata.nim └── t_triangle.nim /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | jobs: 4 | 5 | test: 6 | runs-on: ubuntu-latest 7 | container: nimlang/choosenim 8 | strategy: 9 | matrix: 10 | threads: [on, off] 11 | nim: [ 2.0.0, 1.6.14 ] 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Choose Nim 15 | run: choosenim update -y ${{ matrix.nim }} 16 | - name: Safe git directory 17 | run: git config --global --add safe.directory "$(pwd)" 18 | - name: Test 19 | run: nimble --threads:${{ matrix.threads }} test -y 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all 2 | * 3 | 4 | # Unignore all with extensions 5 | !*.* 6 | 7 | # Unignore all dirs 8 | !*/ 9 | 10 | build 11 | .DS_Store 12 | *.svg 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2015 James Frasca 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DelaunayNim 2 | =========== 3 | 4 | [![Build](https://github.com/Nycto/DelaunayNim/actions/workflows/build.yml/badge.svg)](https://github.com/Nycto/DelaunayNim/actions/workflows/build.yml) 5 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/Nycto/DelaunayNim/blob/main/LICENSE.md) 6 | 7 | A Nim library for calculating the Delaunay Triangulation of a set of points. 8 | This is accomplished using a divide and conquer algorithm, as described here: 9 | 10 | http://www.geom.uiuc.edu/~samuelp/del_project.html 11 | 12 | ![Delaunay Triangulation](http://nycto.github.io/DelaunayNim/delaunay.svg) 13 | 14 | 15 | Quick Example 16 | ------------- 17 | 18 | ```nimrod 19 | import delaunay 20 | 21 | # Points can be any object with an `x` and `y` field 22 | let points: seq[tuple[x, y: float]] = @[ 23 | (x: 25.0, y: 183.0), 24 | (x: 189.0, y: 187.0), 25 | (x: 34.0, y: 169.0), 26 | (x: 149.0, y: 136.0), 27 | (x: 78.0, y: 105.0), 28 | ] 29 | 30 | for edge in triangulate(points): 31 | echo edge 32 | ``` 33 | 34 | That app outputs the following: 35 | 36 | ``` 37 | (a: (x: 25.0, y: 183.0), b: (x: 34.0, y: 169.0)) 38 | (a: (x: 25.0, y: 183.0), b: (x: 189.0, y: 187.0)) 39 | (a: (x: 34.0, y: 169.0), b: (x: 78.0, y: 105.0)) 40 | (a: (x: 34.0, y: 169.0), b: (x: 149.0, y: 136.0)) 41 | (a: (x: 34.0, y: 169.0), b: (x: 189.0, y: 187.0)) 42 | (a: (x: 78.0, y: 105.0), b: (x: 149.0, y: 136.0)) 43 | (a: (x: 149.0, y: 136.0), b: (x: 189.0, y: 187.0)) 44 | ``` 45 | 46 | Full Example 47 | ------------ 48 | 49 | A full example can be found here: 50 | https://github.com/Nycto/DelaunayNim/blob/master/bin/createSvg.nim 51 | 52 | That little binary accepts a list of points and outputs an SVG of the 53 | triangulated grid. You can use it like this: 54 | 55 | ``` 56 | seq 100 \ 57 | | awk 'BEGIN { srand(); } { print int(rand() * 500) " " int(rand() * 500) }' \ 58 | | ./bin/createSvg \ 59 | > example.svg 60 | ``` 61 | 62 | License 63 | ------- 64 | 65 | This library is released under the MIT License, which is pretty spiffy. You 66 | should have received a copy of the MIT License along with this program. If 67 | not, see http://www.opensource.org/licenses/mit-license.php 68 | -------------------------------------------------------------------------------- /bin/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /bin/createSvg.nim: -------------------------------------------------------------------------------- 1 | import strutils, delaunay, options 2 | 3 | when defined(profiler) or defined(memProfiler): 4 | import nimprof 5 | 6 | proc getOrElse[T]( opt: Option[T], default: T ): T = 7 | if opt.isSome: opt.get else: default 8 | 9 | var points: seq[tuple[x, y: float]] = @[] 10 | 11 | var width: float = 0 12 | var height: float = 0 13 | var minX: Option[float] = none(float) 14 | var minY: Option[float] = none(float) 15 | 16 | for line in stdin.lines: 17 | let parts = line.split({' ', ','}) 18 | 19 | if parts.len mod 2 != 0: 20 | stderr.writeLine "Line contains an odd number of inputs:\n" & line 21 | quit(QuitFailure) 22 | 23 | for i in countup(0, parts.len - 1, 2): 24 | try: 25 | let point = (x: parseFloat(parts[i]), y: parseFloat(parts[i + 1])) 26 | points.add(point) 27 | 28 | # Track the highest and lowest `X` to snug in the viewport 29 | width = if point.x > width: point.x else: width 30 | if minY.isNone or point.x < minX.get: 31 | minX = some(point.x) 32 | 33 | # Track the highest and lowest `Y` to snug in the viewport 34 | height = if point.y > height: point.y else: height 35 | if minY.isNone or point.y < minY.get: 36 | minY = some(point.y) 37 | 38 | except ValueError: 39 | stderr.writeLine "Point can not be parsed as numbers:\n" & line 40 | quit(QuitFailure) 41 | 42 | 43 | echo "" 44 | echo "" 47 | 48 | for a, b in triangulate(points): 49 | echo " " 54 | 55 | echo "" 56 | 57 | -------------------------------------------------------------------------------- /delaunay.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.3.0" 4 | author = "Nycto" 5 | description = "Delaunay triangulator" 6 | license = "MIT" 7 | srcDir = "src" 8 | skipDirs = @[ "bin" ] 9 | 10 | # Deps 11 | requires "nim >= 1.2.0" 12 | 13 | -------------------------------------------------------------------------------- /nim.cfg: -------------------------------------------------------------------------------- 1 | --hint[Processing]:off 2 | -------------------------------------------------------------------------------- /src/delaunay.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Runs a delaunay triangulation on a set of points 3 | # 4 | 5 | import options 6 | import delaunay/private/point 7 | import delaunay/private/edge 8 | import delaunay/private/anglesort 9 | import delaunay/private/triangle 10 | 11 | 12 | iterator pairIter[T]( 13 | inner: AngleSorted[T] 14 | ): tuple[current: T, next: Option[T]] = 15 | ## Present two points at a time instead of one 16 | 17 | var first = true 18 | var current: T 19 | 20 | for next in items(inner): 21 | if first: 22 | first = false 23 | else: 24 | let tup = (current: current, next: some[T](next)) 25 | yield tup 26 | 27 | current = next 28 | 29 | if not first: 30 | let tup = (current: current, next: none(T)) 31 | yield tup 32 | 33 | 34 | proc findCandidate[T: Point]( 35 | group: var EdgeGroup[T], anchor: T, reference: T, points: AngleSorted[T] 36 | ): Option[T] = 37 | ## Searches the given list of points for an acceptable candidate for 38 | ## the next edge 39 | 40 | for point, next in pairIter( points ): 41 | if next.isNone: 42 | return some[T](point) 43 | 44 | let triangle = newTriangle(anchor, reference, point) 45 | 46 | # Kill this edge if the next point is in the circumcircle 47 | if triangle.isInCircumcircle(next.get): 48 | group.remove(anchor, point) 49 | else: 50 | return some(point) 51 | 52 | return none(T) 53 | 54 | 55 | proc mergeUsingBase[T: Point]( 56 | left: var EdgeGroup[T], right: var EdgeGroup[T], 57 | baseEdge: tuple[left, right: T] 58 | ) = 59 | ## Merges together two edge groups using the given edge as the 60 | ## base for the merge 61 | 62 | let leftPoints = sort( 63 | connected[T](left, baseEdge.left), 64 | counterclockwise, baseEdge.left, baseEdge.right ) 65 | 66 | let leftCandidate = findCandidate( 67 | left, baseEdge.left, baseEdge.right, leftPoints ) 68 | 69 | let rightPoints = sort( 70 | connected[T](right, baseEdge.right), 71 | clockwise, baseEdge.right, baseEdge.left ) 72 | 73 | let rightCandidate = findCandidate( 74 | right, baseEdge.right, baseEdge.left, rightPoints) 75 | 76 | left.add( baseEdge.left, baseEdge.right ); 77 | 78 | # Without candidates, there is nothing to merge 79 | if rightCandidate.isNone and leftCandidate.isNone: 80 | return 81 | 82 | # If there are candidates on the left but not the right 83 | elif rightCandidate.isNone: 84 | let newBase = (left: leftCandidate.get, right: baseEdge.right) 85 | mergeUsingBase[T]( left, right, newBase ) 86 | return 87 | 88 | # If there are candidates on the right but not the left 89 | elif leftCandidate.isNone: 90 | let newBase = (left: baseEdge.left, right: rightCandidate.get) 91 | mergeUsingBase[T]( left, right, newBase ) 92 | return 93 | 94 | let triangle = newTriangle(baseEdge.left, baseEdge.right, leftCandidate.get) 95 | 96 | # If the right candidate is within the circumcircle of the left 97 | # candidate, then the right candidate is the one we choose 98 | if triangle.isInCircumcircle(rightCandidate.get): 99 | let newBase = (left: baseEdge.left, right: rightCandidate.get) 100 | mergeUsingBase( left, right, newBase ) 101 | 102 | # The only remaining option is that the left candidate is the one 103 | else: 104 | let newBase = (left: leftCandidate.get, right: baseEdge.right) 105 | mergeUsingBase[T]( left, right, newBase ) 106 | 107 | proc chooseBase[T: Point]( 108 | group: EdgeGroup[T], direction: Direction, 109 | examine: T, reference: T 110 | ): T = 111 | ## Chooses the base point for a new merge from a single group 112 | ## * direction - The direction to sort pulled edges 113 | ## * best - The best point seen so far 114 | ## * examine - The next point to examine 115 | ## * reference - The reference point from the other side of the merge 116 | 117 | # If we see a horizontal line, both right and left are on even ground 118 | if reference.y == examine.y: 119 | return examine 120 | 121 | let connected = sort( 122 | connected(group, examine), 123 | direction, examine, reference 124 | ).first 125 | 126 | # No more options? Guess this is it... 127 | if connected.isNone: 128 | return examine 129 | else: 130 | return chooseBase( group, direction, connected.get, reference ) 131 | 132 | proc chooseBases[T: Point]( 133 | left: EdgeGroup[T], right: EdgeGroup[T] 134 | ): tuple[left, right: T] = 135 | ## Chooses base points and invokes a function with them 136 | 137 | let baseRight = chooseBase( 138 | right, counterclockwise, right.bottomLeft, left.bottomRight) 139 | 140 | let baseLeft = chooseBase( 141 | left, clockwise, left.bottomRight, baseRight) 142 | 143 | # Walk the right side back towards the right to confirm that we made 144 | # the correct choice before. This can fix situations where we chose 145 | # the wrong baseLeft in the first pass 146 | let verifiedRight = chooseBase( 147 | right, counterclockwise, baseRight, baseLeft) 148 | 149 | return (left: baseLeft, right: verifiedRight) 150 | 151 | 152 | proc merge[T: Point]( 153 | left: var EdgeGroup[T], right: var EdgeGroup[T] 154 | ): EdgeGroup[T] = 155 | ## Merges together sets of edges into the left edge 156 | mergeUsingBase(left, right, chooseBases(left, right)) 157 | left.add(right) 158 | return left 159 | 160 | 161 | proc calculate[T: Point]( points: PointList[T] ): EdgeGroup[T] = 162 | ## Calculates the edges for a list of points 163 | 164 | case points.len 165 | of 0..1: 166 | return newEdgeGroup[T]() 167 | 168 | of 2: 169 | return newEdgeGroup[T]( `[]`(points, 0) -> `[]`(points, 1) ) 170 | 171 | of 3: 172 | let tri = newTriangle(`[]`(points, 0), `[]`(points, 1), `[]`(points, 2)) 173 | let ab = tri.a -> tri.b 174 | let bc = tri.b -> tri.c 175 | 176 | # If it isn't a triangle, it's a line. And because we can rely on 177 | # the sort order of a PointList, we know this would be a duplicate 178 | # edge unless its a triangle 179 | if tri.isTriangle: 180 | return newEdgeGroup[T]( ab, bc, (tri.a -> tri.c) ) 181 | else: 182 | return newEdgeGroup[T]( ab, bc ) 183 | 184 | else: 185 | let (left, right) = points.split 186 | var leftEdges = calculate(left) 187 | var rightEdges = calculate(right) 188 | return merge( leftEdges, rightEdges ) 189 | 190 | 191 | iterator triangulate*[T: Point]( rawPoints: openArray[T] ): tuple[a, b: T] = 192 | ## Iterates through the edges formed by running a delaunay triangulation 193 | ## on a set of points 194 | let points = newPointList(rawPoints) 195 | for edge in edges( calculate(points) ): 196 | yield edge 197 | 198 | -------------------------------------------------------------------------------- /src/delaunay/private/anglesort.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Sort points based on their angle from a line 3 | # 4 | 5 | import delaunay/private/point 6 | import options, algorithm, math, tables, sequtils 7 | 8 | type AngleCache = Table[tuple[x, y: float], float] 9 | ## Cache calculated angles 10 | 11 | proc angle[T: Point]( cache: var AngleCache, base: T, a: T, b: T ): float = 12 | ## Returns the angle between two vectors that share a base point in radians 13 | 14 | let point = (x: b.x, y: b.y) 15 | if cache.hasKey(point): 16 | return cache[point] 17 | 18 | let v1 = (x: a.x - base.x, y: a.y - base.y) 19 | let v2 = (x: b.x - base.x, y: b.y - base.y) 20 | 21 | # @TODO: This could probably be made faster by using a heuristic 22 | # instead of calling arctan2. These pages have a few options: 23 | # https://stackoverflow.com/questions/6989100/sort-points-in-clockwise-order 24 | # https://stackoverflow.com/questions/16542042/fastest-way-to-sort-vectors-by-angle-without-actually-computing-that-angle 25 | let radians = arctan2(v2.y, v2.x) - arctan2(v1.y, v1.x) 26 | let angle = if radians >= 0: radians else: 2 * PI + radians 27 | 28 | # Cache the result to reduce overhead for future lookups 29 | cache.add(point, angle) 30 | 31 | return angle 32 | 33 | 34 | proc cmpDistance[T]( base, a, b: T ): int = 35 | ## Compares the distance between two points 36 | 37 | proc dist (p: T): float = 38 | ## The squared distance of a point from 'base' 39 | return (p.x - base.x) * (p.x - base.x) + (p.y - base.y) * (p.y - base.y) 40 | 41 | let compared = dist(a) - dist(b) 42 | 43 | return if compared == 0: 0 elif compared < 0: -1 else: 1 44 | 45 | 46 | 47 | type AngleSorted*[T] = distinct seq[T] ## \ 48 | ## A list of points that have been sorted by angle 49 | 50 | proc `==`*[T]( actual: AngleSorted[T], expected: openArray[T] ): bool = 51 | ## Compare an angle sorted value to an array 52 | return system.`==`( seq[T](actual), @expected ) 53 | 54 | proc first*[T]( actual: AngleSorted[T] ): Option[T] = 55 | ## Returns the first item 56 | let asSeq = seq[T](actual) 57 | return if asSeq.len == 0: none(T) else: some(asSeq[0]) 58 | 59 | proc `$`*[T: Point]( points: AngleSorted[T] ): string = 60 | ## Convert to a string 61 | result = "AngleSorted(" 62 | var first = true 63 | for point in items(seq[T](points)): 64 | if first: 65 | first = false 66 | else: 67 | result.add(", ") 68 | result.add( toStr(point) ) 69 | result.add(")") 70 | 71 | type Direction* = enum ## \ 72 | ## A rotation direction 73 | clockwise, counterclockwise 74 | 75 | proc sort*[T: Point]( 76 | points: openArray[T], direction: Direction, 77 | center: T, reference: T 78 | ): AngleSorted[T] = 79 | ## Sorts a list of points in the given direction, relative to the edge 80 | ## formed by drawing a line from `center` to `reference` 81 | 82 | # Start by making a copy so we can do an in place sort 83 | var output: seq[T] = `@`[T](points) 84 | 85 | # Track angles that have already been calculated to reduce the trig 86 | var angles = initTable[tuple[x, y: float], float]() 87 | 88 | output.sort do (a, b: T) -> int: 89 | 90 | let angleToA = angles.angle(center, reference, a) 91 | let angleToB = angles.angle(center, reference, b) 92 | 93 | if angleToA == angleToB: 94 | return cmpDistance(center, a, b) 95 | elif angleToA == 0: 96 | return -1 97 | elif angleToB == 0: 98 | return 1 99 | elif direction == clockwise: 100 | return if angleToA < angleToB: 1 else: -1 101 | else: 102 | return if angleToA < angleToB: -1 else: 1 103 | 104 | ## Now filter down to everything under 180 degrees 105 | for i in 0.. PI: 112 | discard 113 | elif direction == counterclockwise and angle < PI: 114 | discard 115 | else: 116 | # Trim the result once we see the first point that is beyond 180 117 | # degrees. The points are sorted, so the rest are guaranteed to be 118 | # over 180 too 119 | output.setLen(i) 120 | break 121 | 122 | return AngleSorted(output) 123 | 124 | iterator items*[T]( points: AngleSorted[T] ): T = 125 | ## Iterate over each point 126 | for point in items( seq[T](points) ): 127 | yield point 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/delaunay/private/edge.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Edges 3 | # 4 | 5 | import delaunay/private/anglesort, delaunay/private/point 6 | import tables, sets, options, sequtils 7 | 8 | type Edge*[T] = tuple[a, b: T] ## \ 9 | ## Two connected points 10 | 11 | proc `->`*[T: Point]( a, b: T ): Edge[T] {.inline.} = 12 | ## Creates an edge from two points 13 | return if (a <=> b) > 0: (a: b, b: a) else: (a: a, b: b) 14 | 15 | proc `<=>`*[T: Point]( a, b: Edge[T] ): int = 16 | ## Sorts two edges 17 | let aCompared = a.a <=> b.a 18 | if aCompared == 0: 19 | return a.b <=> b.b 20 | else: 21 | return aCompared 22 | 23 | 24 | type EdgeGroup*[T] = object ## \ 25 | ## A group of edges 26 | 27 | # A map of points to the points they are connected to 28 | connections: Table[T, HashSet[T]] 29 | 30 | # Tracks the bottom-left point in this group of edges 31 | lowerLeft: Option[T] 32 | 33 | # Tracks the bottom-right point in this group of edges 34 | lowerRight: Option[T] 35 | 36 | 37 | type EmptyGroupError* = object of Exception ## \ 38 | ## Thrown when trying to read something out of a group without edges 39 | 40 | 41 | proc add*[T: Point] ( group: var EdgeGroup[T], one, two: T ) 42 | 43 | 44 | proc newEdgeGroup*[T: Point]( edges: varargs[Edge[T]] ): EdgeGroup[T] = 45 | ## Creates a new, empty edge group 46 | result = EdgeGroup[T]( 47 | connections: initTable[T, HashSet[T]](), 48 | lowerLeft: none(T), 49 | lowerRight: none(T) 50 | ) 51 | 52 | for edge in edges: 53 | add( result, edge.a, edge.b ) 54 | 55 | 56 | proc potentialBottom[T: Point]( group: var EdgeGroup[T], point: T ) = 57 | ## Compares a point to the bottom most points already tracked. Replaces 58 | ## those points with this one if this is lower. Adds it if it is on the 59 | ## same level. 60 | 61 | if isNone(group.lowerLeft) or point.y < group.lowerLeft.get.y: 62 | group.lowerLeft = some(point) 63 | group.lowerRight = some(point) 64 | 65 | elif point.y == group.lowerLeft.get.y: 66 | if point.x < group.lowerLeft.get.x: 67 | group.lowerLeft = some(point) 68 | if point.x > group.lowerRight.get.x: 69 | group.lowerRight = some(point) 70 | 71 | proc connect[T: Point]( group: var EdgeGroup[T], base, other: T ) = 72 | ## Adds a point and its connection to his group 73 | 74 | if group.connections.hasKey(base): 75 | group.connections[base].incl(other) 76 | else: 77 | group.connections.add(base, toHashSet([ other ])) 78 | 79 | 80 | proc add*[T: Point] ( group: var EdgeGroup[T], one, two: T ) = 81 | ## Adds an edge to this group 82 | group.potentialBottom( one ) 83 | group.potentialBottom( two ) 84 | group.connect( one, two ) 85 | group.connect( two, one ) 86 | 87 | proc add*[T: Point] ( group: var EdgeGroup[T], other: EdgeGroup[T] ) = 88 | ## Adds an entire EdgeGroup to this one 89 | for point, edges in pairs( other.connections ): 90 | if group.connections.hasKey(point): 91 | for other in items( edges ): 92 | group.connections[point].incl(other) 93 | else: 94 | group.connections.add(point, edges) 95 | 96 | if other.lowerLeft.isSome: 97 | group.potentialBottom( other.lowerLeft.get ) 98 | 99 | if other.lowerRight.isSome: 100 | group.potentialBottom( other.lowerRight.get ) 101 | 102 | 103 | proc remove*[T: Point] ( group: var EdgeGroup[T], one, two: T ) = 104 | ## Removes an edge from this group. Note that the two points are still 105 | ## considered as part of this EdgeGroup when considering the bottom left 106 | ## and bottom right points 107 | if group.connections.hasKey(one): 108 | group.connections[one].excl(two) 109 | if group.connections.hasKey(two): 110 | group.connections[two].excl(one) 111 | 112 | proc bottomRight*[T: Point]( group: EdgeGroup[T] ): T = 113 | ## Returns the bottom right point in this edge group 114 | if isNone group.lowerRight: 115 | raise newException(EmptyGroupError, "EdgeGroup is empty") 116 | return group.lowerRight.get 117 | 118 | proc bottomLeft*[T: Point]( group: EdgeGroup[T] ): T = 119 | ## Returns the bottom left point in this edge group 120 | if isNone group.lowerLeft: 121 | raise newException(EmptyGroupError, "EdgeGroup is empty") 122 | return group.lowerLeft.get 123 | 124 | iterator edges*[T: Point]( group: EdgeGroup[T] ): Edge[T] = 125 | ## Iterates over all the edges in a group 126 | 127 | var seen = initHashSet[T]() 128 | for key in group.connections.keys: 129 | seen.incl(key) 130 | for point in `[]`(group.connections, key).items: 131 | if not seen.contains(point): 132 | yield (key -> point) 133 | 134 | 135 | proc `$`*[T: Point]( group: EdgeGroup[T] ): string = 136 | ## Creates a readable string from an edge group 137 | result = "EdgeGroup( " 138 | var first = true 139 | for edge in edges( group ): 140 | if first: 141 | first = false 142 | else: 143 | result.add(", ") 144 | result.add( toStr(edge.a) & " -> " & toStr(edge.b) ) 145 | result.add(" )") 146 | 147 | 148 | type MissingPointError* = object of Exception ## \ 149 | ## Thrown when trying to read connections of a point that isn't in a group 150 | 151 | proc connected*[T: Point]( group: EdgeGroup[T], point: T ): seq[T] = 152 | ## Returns the points connected to a specific point 153 | 154 | if not group.connections.hasKey(point): 155 | raise newException(MissingPointError, "Point isnt in group: " & $point) 156 | 157 | return toSeq( items( `[]`(group.connections, point) ) ) 158 | 159 | 160 | -------------------------------------------------------------------------------- /src/delaunay/private/point.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Functions for interacting with points 3 | # 4 | 5 | import algorithm, sets, math 6 | 7 | # 8 | # Points represent simple x,y coordinates. The goal is to allow library 9 | # consumers to use their own objects as points, hence implement them as 10 | # generic type classes 11 | # 12 | 13 | type Point* = concept p ## \ 14 | ## A type interface for dealing with points 15 | p.x is float 16 | p.y is float 17 | 18 | proc `<=>`*[T: Point]( a, b: T ): int = 19 | ## Compares two points, sorting left to right, bottom to top 20 | if a.x < b.x: 21 | return -1 22 | elif a.x > b.x: 23 | return 1 24 | else: 25 | return cmp(a.y, b.y) 26 | 27 | proc toStr*[T: Point]( point: T ): string = 28 | ## Converts an edge to a readable string 29 | result = "(" 30 | result.add( if floor(point.x) == point.x: $(int(point.x)) else: $point.x ) 31 | result.add(", ") 32 | result.add( if floor(point.y) == point.y: $(int(point.y)) else: $point.y ) 33 | result.add(")") 34 | 35 | 36 | # 37 | # PointList is a phantom type that guarantees a list of points is unique and 38 | # in sorted order 39 | # 40 | 41 | type PointList*[T] = object ## \ 42 | ## A list of sorted, unique points 43 | 44 | # The sequence that backs this point list. Passed by reference to avoid 45 | # making copies every time a slice is extracted 46 | points: seq[T] 47 | 48 | # The starting offset within the backing sequence for this slice 49 | start: int 50 | 51 | # The number of points in this slice 52 | length: int 53 | 54 | proc newPointList*[T: Point]( list: openArray[T] ): PointList[T] = 55 | ## Creates a point list from a list of points 56 | 57 | var output: seq[T] = @[] 58 | setlen(output, list.len) 59 | 60 | # Keep track of the points that have been seen 61 | var seen = initHashSet[tuple[x, y: float]]() 62 | 63 | # Dedupe, which also copies the input 64 | for point in items(list): 65 | let pair = (x: point.x, y: point.y) 66 | 67 | if not seen.contains(pair): 68 | # `add(seq)` pushes on to the end of a sequence, meaning our 69 | # `setlen` below would remove the wrong values. We use assignment 70 | # instead to avoid that 71 | output[seen.len] = point 72 | seen.incl(pair) 73 | 74 | setlen(output, seen.len) 75 | when not defined(gcArc) and not defined(gcOrc): 76 | shallow(output) 77 | 78 | # Sort points left to right, bottom to top 79 | output.sort(`<=>`) 80 | 81 | result = PointList[T](points: output, start: 0, length: output.len) 82 | 83 | iterator items*[T]( points: PointList[T] ): T = 84 | ## Iterates over each point in this list 85 | for i in points.start .. <(points.start + points.length): 86 | yield points.points[i] 87 | 88 | proc `@`*[T]( points: PointList[T] ): seq[T] = 89 | ## Convert a Point list back to a sequence 90 | result = @[] 91 | for point in points: 92 | result.add(point) 93 | 94 | proc len*[T]( points: PointList[T] ): int = 95 | ## Returns the number of points in a point list 96 | points.length 97 | 98 | proc `[]`*[T]( points: PointList[T], i: int ): T {.inline.} = 99 | ## Returns a specific point at the given offset 100 | if i >= points.length: 101 | raise newException(IndexError, "Index is out of bounds") 102 | points.points[i + points.start] 103 | 104 | proc `$`*[T]( points: PointList[T] ): string = 105 | ## Return a point list as a string 106 | result = "Points(" 107 | var first = true 108 | for point in points: 109 | if first: 110 | first = false 111 | else: 112 | result.add(", ") 113 | result.add( toStr(point) ) 114 | result.add(")") 115 | 116 | type CantSplitError* = object of Exception ## \ 117 | ## Thrown when trying to split a list of points that is too small 118 | 119 | proc split*[T]( 120 | points: PointList[T] 121 | ): tuple[left, right: PointList[T]] = 122 | ## Divides this PointList evenly in to two smaller lists 123 | if len(points) < 4: 124 | raise newException(CantSplitError, "PointList is too small to split") 125 | 126 | let halfway: float = points.len / 2 127 | let leftLength = toInt(ceil(halfway)) 128 | let rightLength = toInt(floor(halfway)) 129 | 130 | let left = PointList[T]( 131 | points: points.points, 132 | start: points.start, 133 | length: leftLength) 134 | 135 | let right = PointList[T]( 136 | points: points.points, 137 | start: points.start + leftLength, 138 | length: rightLength) 139 | 140 | return (left: left, right: right) 141 | 142 | 143 | -------------------------------------------------------------------------------- /src/delaunay/private/triangle.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Triangle procs and objects 3 | # 4 | 5 | import delaunay/private/point 6 | 7 | 8 | type Triangle*[T] = object 9 | ## This may come as a suprise, but a triangle is made up of three points 10 | a*, b*, c*: T 11 | 12 | proc newTriangle*[T: Point]( a: T, b: T, c: T ): Triangle[T] = 13 | ## Constructor 14 | result = Triangle[T](a: a, b: b, c: c) 15 | 16 | proc `$`*[T: Point]( tri: Triangle[T] ): string = 17 | result = "Triangle(" & 18 | toStr(tri.a) & ", " & 19 | toStr(tri.b) & ", " & 20 | toStr(tri.c) & ")" 21 | 22 | proc isTriangle*[T: Point]( tri: Triangle[T] ): bool = 23 | ## Determines whether three points actualy form a valid triangle. The only 24 | ## way this returns false is if they form a line 25 | return (tri.b.y - tri.a.y) * (tri.c.x - tri.b.x) != 26 | (tri.c.y - tri.b.y) * (tri.b.x - tri.a.x) 27 | 28 | proc isInCircumcircle*[T: Point]( tri: Triangle[T], point: T ): bool = 29 | ## Returns whether a point exists within the circumcircle of a triangle 30 | 31 | if not isTriangle[T](tri): 32 | raise newException( 33 | AssertionError, 34 | "Three given points don't form a triangle: " & $tri 35 | ) 36 | 37 | let a = tri.a 38 | let b = tri.b 39 | let c = tri.c 40 | 41 | # If we are dealing with any horizontal lines, they cause the slope 42 | # to be infinity. The easy solution is to just use different edges 43 | if a.y == b.y or b.y == c.y: 44 | return isInCircumCircle[T]( newTriangle[T](tri.c, tri.a, tri.b), point ) 45 | 46 | # Calculate the slope of each of the perpendicular lines. In 47 | # the equation `y = mx + b`, this is the `m` 48 | let slopeAB = -1 * ( (b.x - a.x) / (b.y - a.y) ) 49 | let slopeBC = -1 * ( (c.x - b.x) / (c.y - b.y) ) 50 | 51 | # Calculate the y-intercept of each of the perpendicular lines. In 52 | # the equation `y = mx + b`, this is the `b` 53 | let yinterceptAB = ( -1 * slopeAB * (a.x + b.x) + a.y + b.y ) / 2 54 | let yinterceptBC = ( -1 * slopeBC * (b.x + c.x) + b.y + c.y ) / 2 55 | 56 | # The centroid of the circumcircle 57 | let centerX = (yinterceptBC - yinterceptAB) / (slopeAB - slopeBC) 58 | let centerY = (slopeAB * centerX) + yinterceptAB 59 | 60 | # The radius of the circumcircle 61 | let radius = ( (centerX - a.x) * (centerX - a.x) ) + 62 | ( (centerY - a.y) * (centerY - a.y) ) 63 | 64 | # The distance of the point being checked from the centroid 65 | let distance = ( (centerX - point.x) * (centerX - point.x) ) + 66 | ( (centerY - point.y) * (centerY - point.y) ) 67 | 68 | # If the distance is less than the radius, the point is in the circle. 69 | # Note that we consider points on the circumference to be outside 70 | # of the circumcircle 71 | return distance < radius 72 | 73 | 74 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /tests/helpers.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Helpers to make testing easier 3 | # 4 | 5 | import sequtils, sets 6 | import delaunay 7 | import delaunay/private/edge 8 | import delaunay/private/point 9 | 10 | proc p*( x, y: float ): tuple[x, y: float] = 11 | ## Creates a point 12 | result = (x, y) 13 | 14 | proc `->`*( a, b: tuple[x, y: float] ): Edge[tuple[x, y: float]] = 15 | ## Creates an edge from two tuples 16 | result = edge.`->`(a, b) 17 | 18 | proc `==`*[T]( actual: seq[T], expected: openArray[T] ): bool = 19 | ## Allows you to compare a sequence to an array 20 | result = system.`==`( actual, @expected ) 21 | 22 | proc `==`*[T]( actual: iterator: T, expected: openArray[T] ): bool = 23 | ## Allows you to compare an iterator to an array 24 | let iterated = toseq( actual() ) 25 | let sequence = @expected 26 | result = iterated == sequence 27 | 28 | proc `$`*[T]( iter: iterator: T ): string = 29 | ## Coverts an iterator to a string 30 | result = `$`(toseq(iter())) 31 | 32 | 33 | type EdgeList* = seq[Edge[tuple[x, y: float]]] 34 | 35 | proc edges*( expected: varargs[Edge[tuple[x, y: float]]] ): EdgeList = 36 | return EdgeList(@expected) 37 | 38 | proc `$`*( edges: EdgeList ): string = 39 | result = "EdgeList(" 40 | var first = true 41 | for edge in edges: 42 | if first: 43 | first = false 44 | else: 45 | result.add(", ") 46 | result.add( toStr(edge.a) & " -> " & toStr(edge.b) ) 47 | result.add(")") 48 | 49 | proc points*( edges: EdgeList ): seq[tuple[x, y: float]] = 50 | # Pull all the points from all the edges into a list 51 | result = @[] 52 | for edge in seq[Edge[tuple[x, y: float]]](edges): 53 | result.add(edge.a) 54 | result.add(edge.b) 55 | 56 | proc triangulate*( expected: EdgeList ): EdgeList = 57 | ## Extract the points from an edge list and run a triangulation 58 | let pointList = points(expected) 59 | let asSeq = toSeq( triangulate(pointList) ) 60 | return EdgeList( asSeq ) 61 | 62 | template `==`*( expected, actual: EdgeList ): bool = 63 | ## Compare two edge lists 64 | var expectedSet = toHashSet(expected) 65 | var actualSet = toHashSet(actual) 66 | 67 | for point in difference(expectedSet, actualSet): 68 | checkpoint("Missing: " & toStr(point.a) & " -> " & toStr(point.b)) 69 | 70 | for extra in difference(actualSet, expectedSet): 71 | checkpoint("Extra: " & toStr(extra.a) & " -> " & toStr(extra.b) ) 72 | 73 | actualSet == expectedSet 74 | 75 | -------------------------------------------------------------------------------- /tests/point_test.nim: -------------------------------------------------------------------------------- 1 | import unittest, helpers 2 | import delaunay/private/point 3 | 4 | suite "PointLists should ": 5 | 6 | test "Remove duplicates": 7 | require( 8 | @[ p(1, 2), p(2, 1) ] == 9 | @( newPointList([ p(1, 2), p(2, 1) ]) ) 10 | ) 11 | 12 | require( 13 | @[ p(1, 2), p(2, 1) ] == 14 | @( newPointList([ p(1, 2), p(2, 1), p(1, 2), p(2, 1) ]) ) 15 | ) 16 | 17 | test "Sort from left to right, bottom to top": 18 | require( 19 | @[ p(0, 5), p(3, 3), p(5, 1) ] == 20 | @(newPointList([ p(5, 1), p(3, 3), p(0, 5) ])) 21 | ) 22 | 23 | require( 24 | @[ p(0, 1), p(0, 3), p(0, 5) ] == 25 | @(newPointList([ p(0, 5), p(0, 1), p(0, 3) ])) 26 | ) 27 | 28 | require( 29 | @[ 30 | p(0, 1), p(1, 0), p(1, 2), p(1, 3), p(2, 1), 31 | p(3, 3), p(4, 2), p(5, 0), p(5, 1), p(5, 3) 32 | ] == 33 | @(newPointList([ 34 | p(4, 2), p(5, 3), p(2, 1), p(0, 1), p(5, 0), 35 | p(3, 3), p(5, 1), p(1, 2), p(1, 0), p(1, 3) 36 | ])) 37 | ) 38 | 39 | test "Calculate the length of a point list": 40 | let points = newPointList([ p(1, 2), p(2, 1) ]) 41 | require( points.len == 2 ) 42 | 43 | test "Allow array access to individual values": 44 | let points = newPointList([ p(1, 2), p(2, 1) ]) 45 | require( points[0] == p(1, 2) ) 46 | require( points[1] == p(2, 1) ) 47 | expect(IndexError): 48 | discard points[2] 49 | 50 | test "Split an even point list in half": 51 | let points = newPointList([ 52 | p(1, 2), p(2, 1), p(4, 5), p(6, 3) ]) 53 | let (left, right) = points.split 54 | require( @left == @[ p(1, 2), p(2, 1) ] ) 55 | require( @right == @[ p(4, 5), p(6, 3) ] ) 56 | 57 | test "Split an odd point list in half": 58 | let points = newPointList([ 59 | p(1, 2), p(2, 1), p(4, 5), p(6, 3), p(7, 8) ]) 60 | let (left, right) = points.split 61 | require( @left == @[ p(1, 2), p(2, 1), p(4, 5) ] ) 62 | require( @right == @[ p(6, 3), p(7, 8) ] ) 63 | 64 | test "Allow splitting a split": 65 | let points = newPointList([ 66 | p(0, 1), p(1, 0), p(1, 2), p(1, 3), p(2, 1), 67 | p(3, 3), p(4, 2), p(5, 0), p(5, 1), p(5, 3) ]) 68 | 69 | let (left, right) = points.split 70 | 71 | let (rLeft, rRight) = right.split 72 | require( @rRight == @[ p(5, 1), p(5, 3) ] ) 73 | require( @rLeft == @[ p(3, 3), p(4, 2), p(5, 0) ] ) 74 | 75 | let (lLeft, lRight) = left.split 76 | require( @lRight == @[ p(1, 3), p(2, 1) ] ) 77 | require( @lLeft == @[ p(0, 1), p(1, 0), p(1, 2) ] ) 78 | 79 | test "Throw when trying to split a small list": 80 | let points = newPointList([ p(1, 2), p(2, 1), p(4, 5) ]) 81 | expect(CantSplitError): 82 | discard points.split 83 | 84 | test "Allow array access after a split": 85 | let points = newPointList([ 86 | p(1, 2), p(2, 1), p(4, 5), p(6, 3) ]) 87 | let (left, right) = points.split 88 | 89 | require( left[0] == p(1, 2) ) 90 | require( left[1] == p(2, 1) ) 91 | expect(IndexError): 92 | discard left[2] 93 | 94 | require( right[0] == p(4, 5) ) 95 | require( right[1] == p(6, 3) ) 96 | expect(IndexError): 97 | discard right[2] 98 | 99 | -------------------------------------------------------------------------------- /tests/t_anglesort.nim: -------------------------------------------------------------------------------- 1 | import unittest, helpers 2 | import delaunay/private/anglesort 3 | import delaunay/private/point 4 | import algorithm 5 | 6 | 7 | suite "AngleSort should ": 8 | 9 | test "Sort points in clockwise order": 10 | let iter = [ p(3, 10), p(3, 5), p(8, 4), p(1, 2) ] 11 | .sort( clockwise, p(4, 0), p(1, 1) ) 12 | 13 | require( iter == [ p(1, 2), p(3, 5), p(3, 10), p(8, 4) ] ) 14 | 15 | test "Sort points in counter clockwise order": 16 | let iter = [ p(1, 2), p(0, 1), p(2, 1) ] 17 | .sort( counterclockwise, p(1, 0), p(5, 1) ) 18 | 19 | require( iter == [ p(2, 1), p(1, 2), p(0, 1) ] ) 20 | 21 | 22 | test "Sort by distance when the angle is the same": 23 | let iter = 24 | [ p(1, 2), p(1, 1), p(2, 2), p(1, 1), p(2, 1), p(3, 3) ] 25 | .sort( counterclockwise, p(0, 0), p(5, 1) ) 26 | 27 | require( iter == [ 28 | p(2, 1), p(1, 1), p(1, 1), p(2, 2), p(3, 3), p(1, 2) 29 | ] ) 30 | 31 | 32 | test "Handle points with the same slope": 33 | 34 | test "Clockwise": 35 | let iter = [ p(1, 1), p(5, 0) ] 36 | .sort( clockwise, p(0, 0), p(4, 4) ) 37 | 38 | require( iter == [ p(1, 1), p(5, 0) ] ) 39 | 40 | test "CounterClockwise": 41 | let iter = [ p(1, 1), p(0, 5) ] 42 | .sort( counterclockwise, p(0, 0), p(4, 4) ) 43 | 44 | require( iter == [ p(1, 1), p(0, 5) ] ) 45 | 46 | test "Remove points greater than 180 degrees": 47 | 48 | let points = [ 49 | p( 5, 1 ), p( 5, 5 ), p( 3, 5 ), 50 | p( 0, 5 ), 51 | p( -3, 5 ), p( -5, 5 ), p( -5, 1 ), 52 | p( -5, 0 ), 53 | p( -5, -1 ), p(-5, -5), p(-3, -5), 54 | p( 0, -5 ), 55 | p( 1, -5 ), p( 5, -5 ), p( 5, -3 ) 56 | ] 57 | 58 | test "CounterClockwise": 59 | let iter = points.sort(counterclockwise, p(0, 0), p(5, 0)) 60 | require( iter == points[0..6] ) 61 | 62 | test "Clockwise": 63 | let iter = points.sort(clockwise, p(0, 0), p(5, 0)) 64 | require( iter == points.reversed[0..6] ) 65 | 66 | test "Include 0 degree angles when filtering": 67 | 68 | let points = [ p(2, 1), p(3, 3), p(4, 2), p(-1, 2) ] 69 | let iter = points.sort(clockwise, p(5, 0), p(2, 1)) 70 | require( iter == [ p(2, 1), p(-1, 2), p(3, 3), p(4, 2) ] ) 71 | 72 | -------------------------------------------------------------------------------- /tests/t_delaunay.nim: -------------------------------------------------------------------------------- 1 | import unittest, helpers, sets, sequtils 2 | import delaunay 3 | 4 | 5 | suite "Delaunay triangulation should ": 6 | let emptyEdges: seq[tuple[a, b: tuple[x, y: float]]] = @[] 7 | 8 | test "Return empty for empty input": 9 | let emptyPoints: seq[tuple[x, y: float]] = @[] 10 | let edges = toSeq(triangulate(emptyPoints)) 11 | require( edges == emptyEdges ) 12 | 13 | test "Return empty for a single point": 14 | let edges = toSeq( triangulate(@[ p(1, 1) ]) ) 15 | require( edges == emptyEdges ) 16 | 17 | test "Return a single edge with two points": 18 | let expected = edges( p(1, 1) -> p(4, 4) ) 19 | let triangulated = triangulate(expected) 20 | require( expected == triangulated ) 21 | 22 | test "Return three edges for a triangle": 23 | let expected = edges( 24 | p(0, 0) -> p(2, 2), p(0, 0) -> p(4, 0), p(2, 2) -> p(4, 0) ) 25 | let triangulated = triangulate(expected) 26 | require( expected == triangulated ) 27 | 28 | test "Return two edges for a line": 29 | block: 30 | let expected = edges( p(0, 0) -> p(2, 2), p(2, 2) -> p(4, 4) ) 31 | let triangulated = toSeq(triangulate(@[p(0, 0), p(2, 2), p(4, 4)])) 32 | require( expected == triangulated ) 33 | 34 | block: 35 | let expected = edges( p(0, 0) -> p(2, 2), p(2, 2) -> p(4, 4) ) 36 | let triangulated = toSeq(triangulate(@[p(0, 0), p(4, 4), p(2, 2)])) 37 | require( expected == triangulated ) 38 | 39 | block: 40 | let expected = edges( p(0, 0) -> p(2, 2), p(2, 2) -> p(4, 4) ) 41 | let triangulated = toSeq(triangulate(@[p(4, 4), p(0, 0), p(2, 2) ])) 42 | require( expected == triangulated ) 43 | 44 | test "Four points": 45 | # Edges for the following grid: 46 | # 47 | # 2 | * 48 | # 1 | * * 49 | # 0 | * 50 | # ------------- 51 | # 0 1 2 3 52 | 53 | let expected = edges( 54 | p(0, 1) -> p(1, 0), p(0, 1) -> p(1, 2), 55 | p(1, 0) -> p(1, 2), p(1, 0) -> p(3, 1), 56 | p(1, 2) -> p(3, 1) ) 57 | let triangulated = triangulate(expected) 58 | require( expected == triangulated ) 59 | 60 | test "One Merge": 61 | # Edges for the following grid: 62 | # 63 | # 3 | * 64 | # 2 | * 65 | # 1 | * * 66 | # 0 | * 67 | # ---------- 68 | # 0 1 2 69 | 70 | let expected = edges( 71 | p(0, 1) -> p(1, 0), p(0, 1) -> p(1, 2), p(0, 1) -> p(1, 3), 72 | p(1, 0) -> p(2, 1), p(1, 0) -> p(1, 2), 73 | p(1, 2) -> p(2, 1), p(1, 2) -> p(1, 3), 74 | p(1, 3) -> p(2, 1) ) 75 | let triangulated = triangulate(expected) 76 | require( expected == triangulated ) 77 | 78 | test "Right half of the complex grid": 79 | # Edges for the following grid: 80 | # 81 | # 3 | * * 82 | # 2 | * 83 | # 1 | * 84 | # 0 | * 85 | # ------------------- 86 | # 0 1 2 3 4 5 87 | 88 | let expected = edges( 89 | p(3, 3) -> p(5, 0), p(3, 3) -> p(4, 2), p(3, 3) -> p(5, 3), 90 | p(4, 2) -> p(5, 0), p(4, 2) -> p(5, 1), p(4, 2) -> p(5, 3), 91 | p(5, 0) -> p(5, 1), 92 | p(5, 1) -> p(5, 3) ) 93 | let triangulated = triangulate(expected) 94 | require( expected == triangulated ) 95 | 96 | test "Complex grid": 97 | # Edges for the following grid: 98 | # 99 | # 3 | * * * 100 | # 2 | * * 101 | # 1 | * * * 102 | # 0 | * * 103 | # ------------------- 104 | # 0 1 2 3 4 5 105 | 106 | let expected = edges( 107 | p(0, 1) -> p(1, 0), p(0, 1) -> p(1, 2), p(0, 1) -> p(1, 3), 108 | p(1, 0) -> p(1, 2), p(1, 0) -> p(2, 1), p(1, 0) -> p(5, 0), 109 | p(1, 2) -> p(2, 1), p(1, 2) -> p(3, 3), p(1, 2) -> p(1, 3), 110 | p(1, 3) -> p(3, 3), 111 | p(2, 1) -> p(5, 0), p(2, 1) -> p(4, 2), p(2, 1) -> p(3, 3), 112 | p(3, 3) -> p(4, 2), p(3, 3) -> p(5, 3), 113 | p(4, 2) -> p(5, 0), p(4, 2) -> p(5, 1), p(4, 2) -> p(5, 3), 114 | p(5, 0) -> p(5, 1), 115 | p(5, 1) -> p(5, 3) ) 116 | let triangulated = triangulate(expected) 117 | require( expected == triangulated ) 118 | 119 | test "Left higher than right": 120 | # Edges for the following grid: 121 | # 122 | # 2 | * * 123 | # 1 | 124 | # 0 | * * 125 | # ------------- 126 | # 0 1 2 3 127 | 128 | let expected = edges( 129 | p(0, 2) -> p(1, 2), p(0, 2) -> p(2, 0), 130 | p(1, 2) -> p(2, 0), p(1, 2) -> p(3, 0), 131 | p(2, 0) -> p(3, 0) ) 132 | let triangulated = triangulate(expected) 133 | require( expected == triangulated ) 134 | 135 | test "Tie for bottom": 136 | ## Edges for the following grid: 137 | ## 138 | ## 2 | * * * 139 | ## 1 | 140 | ## 0 | * * * 141 | ## ---------------- 142 | ## 0 1 2 3 4 143 | 144 | let expected = edges( 145 | p(0, 0) -> p(1, 0), p(0, 0) -> p(2, 2), 146 | p(1, 0) -> p(2, 0), p(1, 0) -> p(2, 2), 147 | p(2, 0) -> p(2, 2), p(2, 0) -> p(3, 2), p(2, 0) -> p(4, 2), 148 | p(2, 2) -> p(3, 2), 149 | p(3, 2) -> p(4, 2) ) 150 | let triangulated = triangulate(expected) 151 | require( expected == triangulated ) 152 | 153 | test "Horizontal grid": 154 | # Edges for the following grid: 155 | # 156 | # 0 | * * * * 157 | # ------------- 158 | # 0 1 2 3 159 | 160 | let expected = edges( 161 | p(0, 0) -> p(1, 0), 162 | p(1, 0) -> p(2, 0), 163 | p(2, 0) -> p(3, 0) ) 164 | let triangulated = triangulate(expected) 165 | require( expected == triangulated ) 166 | 167 | test "Right higher than left": 168 | # Edges for the following grid: 169 | # 170 | # 2 | * * 171 | # 1 | 172 | # 0 | * * 173 | # ------------- 174 | # 0 1 2 3 175 | 176 | let expected = edges( 177 | p(0, 0) -> p(1, 0), p(0, 0) -> p(2, 2), 178 | p(1, 0) -> p(2, 2), p(1, 0) -> p(3, 2), 179 | p(2, 2) -> p(3, 2) ) 180 | let triangulated = triangulate(expected) 181 | require( expected == triangulated ) 182 | 183 | test "Deceptive base edge": 184 | # Edges for the following grid: 185 | # 186 | # 2 | * * 187 | # 1 | * 188 | # 0 | * 189 | # ------------- 190 | # 0 1 2 3 191 | 192 | let expected = edges( 193 | p(0, 0) -> p(0, 1), p(0, 0) -> p(2, 2), 194 | p(0, 1) -> p(0, 2), p(0, 1) -> p(2, 2), 195 | p(0, 2) -> p(2, 2) ) 196 | let triangulated = triangulate(expected) 197 | require( expected == triangulated ) 198 | 199 | test "Merge from non-bottom": 200 | 201 | # Edges for the following grid: 202 | # 203 | # 3 | * 204 | # 2 | * 205 | # 1 | * 206 | # 0 | * 207 | # ---------------- 208 | # 0 1 2 3 4 209 | 210 | let expected = edges( 211 | p(0, 0) -> p(2, 1), p(0, 0) -> p(2, 2), 212 | p(2, 1) -> p(2, 2), p(2, 1) -> p(4, 3), 213 | p(2, 2) -> p(4, 3) ) 214 | let triangulated = triangulate(expected) 215 | require( expected == triangulated ) 216 | 217 | test "Reconsider right base edge": 218 | # Edges for the following grid: 219 | # 220 | # 4 | * 221 | # 3 | 222 | # 2 | * 223 | # 1 | * 224 | # 0 | * 225 | # ---------------- 226 | # 0 1 2 3 4 227 | 228 | let expected = edges( 229 | p(0, 0) -> p(3, 1), p(0, 0) -> p(3, 2), p(0, 0) -> p(4, 4), 230 | p(3, 1) -> p(3, 2), p(3, 1) -> p(4, 4), 231 | p(3, 2) -> p(4, 4) ) 232 | let triangulated = triangulate(expected) 233 | require( expected == triangulated ) 234 | 235 | test "Vertical line": 236 | # Edges for the following grid: 237 | # 238 | # 3 | * 239 | # 2 | * 240 | # 1 | * 241 | # 0 | * 242 | # ------------- 243 | # 0 1 2 3 244 | 245 | let expected = edges( 246 | p(1, 0) -> p(1, 1), 247 | p(1, 1) -> p(1, 2), 248 | p(1, 2) -> p(1, 3) ) 249 | let triangulated = triangulate(expected) 250 | require( expected == triangulated ) 251 | 252 | test "Diagonal line": 253 | # Edges for the following grid: 254 | # 255 | # 3 | * 256 | # 2 | * 257 | # 1 | * 258 | # 0 | * 259 | # ------------- 260 | # 0 1 2 3 261 | 262 | let expected = edges( 263 | p(0, 0) -> p(1, 1), 264 | p(1, 1) -> p(2, 2), 265 | p(2, 2) -> p(3, 3) ) 266 | let triangulated = triangulate(expected) 267 | require( expected == triangulated ) 268 | 269 | 270 | -------------------------------------------------------------------------------- /tests/t_edge.nim: -------------------------------------------------------------------------------- 1 | import unittest, sequtils, algorithm, helpers 2 | import delaunay/private/edge 3 | import delaunay/private/point 4 | import delaunay/private/anglesort 5 | 6 | proc `==`[T]( actual: EdgeGroup[T], expected: seq[Edge[T]] ): bool = 7 | var edges = toSeq(actual.edges) 8 | 9 | edges.sort do (a, b: Edge[T]) -> int: 10 | return a <=> b 11 | 12 | # This is kind of a crappy test in that it depends on potentially 13 | # non-deterministic sorting within Maps and Sets, but its enough for now 14 | system.`==`(edges, expected) 15 | 16 | 17 | suite "Edge Groups should ": 18 | 19 | test "Track bottom left and right": 20 | var group = newEdgeGroup[tuple[x, y: float]]() 21 | 22 | expect(EmptyGroupError): 23 | discard group.bottomRight 24 | 25 | expect(EmptyGroupError): 26 | discard group.bottomLeft 27 | 28 | group.add( p(1, 1), p(4, 5) ) 29 | require( p(1, 1) == group.bottomRight ) 30 | require( p(1, 1) == group.bottomLeft ) 31 | 32 | group.add( p(1, 0), p(10, 4) ) 33 | require( p(1, 0) == group.bottomRight ) 34 | require( p(1, 0) == group.bottomLeft ) 35 | 36 | group.add( p(2, 0), p(1, 9) ) 37 | require( p(2, 0) == group.bottomRight ) 38 | require( p(1, 0) == group.bottomLeft ) 39 | 40 | group.add( p(0, 0), p(1, 9) ) 41 | require( p(2, 0) == group.bottomRight ) 42 | require( p(0, 0) == group.bottomLeft ) 43 | 44 | test "Iterate over all edges": 45 | var group = newEdgeGroup[tuple[x, y: float]]() 46 | 47 | group.add( p(1, 1), p(4, 5) ) 48 | group.add( p(1, 1), p(2, 2) ) 49 | group.add( p(4, 5), p(2, 2) ) 50 | 51 | require(group == @[ 52 | p(1, 1) -> p(2, 2), 53 | p(1, 1) -> p(4, 5), 54 | p(2, 2) -> p(4, 5) 55 | ]) 56 | 57 | test "Allow edges to be removed": 58 | var group = newEdgeGroup[tuple[x, y: float]]() 59 | 60 | group.add( p(1, 1), p(4, 5) ) 61 | group.add( p(1, 1), p(2, 2) ) 62 | group.add( p(4, 5), p(2, 2) ) 63 | group.remove( p(1, 1), p(2, 2) ) 64 | 65 | require(group == @[ 66 | p(1, 1) -> p(4, 5), 67 | p(2, 2) -> p(4, 5) 68 | ]) 69 | 70 | test "Return connected points": 71 | 72 | var group = newEdgeGroup[tuple[x, y: float]]() 73 | group.add( p(1, 1), p(4, 5) ) 74 | group.add( p(1, 1), p(2, 2) ) 75 | group.add( p(4, 5), p(2, 2) ) 76 | group.add( p(4, 5), p(6, 6) ) 77 | 78 | let connections = group.connected( p(1, 1) ) 79 | 80 | require( connections == @[ p(2, 2), p(4, 5) ] ) 81 | 82 | test "Add edge groups together": 83 | 84 | var one = newEdgeGroup[tuple[x, y: float]]() 85 | one.add( p(1, 1), p(4, 5) ) 86 | one.add( p(1, 1), p(2, 2) ) 87 | one.add( p(4, 5), p(2, 2) ) 88 | 89 | var two = newEdgeGroup[tuple[x, y: float]]() 90 | two.add( p(1, 1), p(6, 3) ) 91 | two.add( p(6, 3), p(8, 8) ) 92 | 93 | one.add(two) 94 | 95 | require( one == @[ 96 | p(1, 1) -> p(2, 2), 97 | p(1, 1) -> p(4, 5), 98 | p(1, 1) -> p(6, 3), 99 | p(2, 2) -> p(4, 5), 100 | p(6, 3) -> p(8, 8) 101 | ]) 102 | 103 | 104 | -------------------------------------------------------------------------------- /tests/t_largedata.nim: -------------------------------------------------------------------------------- 1 | import unittest, helpers, sets, sequtils 2 | import delaunay 3 | 4 | suite "Large Data Sets": 5 | 6 | test "10 points": 7 | # These points were taken from here: 8 | # http://www.geom.uiuc.edu/locate/user/samuelp/rnd_10.ps 9 | 10 | let expected = edges( 11 | p(240.84, 0.0) -> p(293.758, 222.503), 12 | p(270.047, 326.4888) -> p(293.758, 222.503), 13 | p(222.766, 330.1085) -> p(293.758, 222.503), 14 | p(164.199, 206.129) -> p(293.758, 222.503), 15 | p(0.0, 209.774) -> p(240.84, 0.0), 16 | p(164.199, 206.129) -> p(240.84, 0.0), 17 | p(217.152, 355.0742) -> p(270.047, 326.4888), 18 | p(217.152, 355.0742) -> p(222.766, 330.1085), 19 | p(124.179, 257.139) -> p(217.152, 355.0742), 20 | p(52.9105, 288.189) -> p(217.152, 355.0742), 21 | p(222.766, 330.1085) -> p(270.047, 326.4888), 22 | p(124.179, 257.139) -> p(222.766, 330.1085), 23 | p(164.199, 206.129) -> p(222.766, 330.1085), 24 | p(124.179, 257.139) -> p(164.199, 206.129), 25 | p(9.1228, 224.344) -> p(124.179, 257.139), 26 | p(52.9105, 288.189) -> p(124.179, 257.139), 27 | p(9.1228, 224.344) -> p(164.199, 206.129), 28 | p(0.0, 209.774) -> p(9.1228, 224.344), 29 | p(9.1228, 224.344) -> p(52.9105, 288.189), 30 | p(0.0, 209.774) -> p(164.199, 206.129) ) 31 | let triangulated = triangulate(expected) 32 | require( expected == triangulated ) 33 | 34 | test "15 points": 35 | # These points were taken from here: 36 | # http://www.mathworks.com/help/matlab/math/voronoi-diagrams.html 37 | 38 | let expected = edges( 39 | p(-4.0, -1.0) -> p(-2.3, -0.7), p(-4.0, -1.0) -> p(-3.5, -2.9), 40 | p(-4.0, -1.0) -> p(-3.7, 1.5), p(0.0, -0.5) -> p(2.0, -1.5), 41 | p(-0.9, -3.9) -> p(0.0, -0.5), p(0.0, -0.5) -> p(0.8, 1.2), 42 | p(-1.5, 1.3) -> p(0.0, -0.5), p(-2.3, -0.7) -> p(0.0, -0.5), 43 | p(2.0, -3.5) -> p(2.0, -1.5), p(2.0, -3.5) -> p(3.5, -2.25), 44 | p(-0.9, -3.9) -> p(2.0, -3.5), p(-0.9, -3.9) -> p(2.0, -1.5), 45 | p(2.0, -1.5) -> p(3.5, -2.25), p(2.0, -1.5) -> p(3.7, -0.8), 46 | p(0.8, 1.2) -> p(2.0, -1.5), p(2.0, -1.5) -> p(3.3, 1.5), 47 | p(3.5, -2.25) -> p(3.7, -0.8), p(3.3, 1.5) -> p(3.7, -0.8), 48 | p(1.8, 3.3) -> p(3.3, 1.5), p(0.8, 1.2) -> p(3.3, 1.5), 49 | p(-1.5, 1.3) -> p(0.8, 1.2), p(0.8, 1.2) -> p(1.8, 3.3), 50 | p(-1.5, 3.2) -> p(0.8, 1.2), p(-3.5, -2.9) -> p(-0.9, -3.9), 51 | p(-3.5, -2.9) -> p(-2.3, -0.7), p(-2.3, -0.7) -> p(-0.9, -3.9), 52 | p(-2.3, -0.7) -> p(-1.5, 1.3), p(-3.7, 1.5) -> p(-2.3, -0.7), 53 | p(-3.7, 1.5) -> p(-1.5, 1.3), p(-1.5, 1.3) -> p(-1.5, 3.2), 54 | p(-1.5, 3.2) -> p(1.8, 3.3), p(-3.7, 1.5) -> p(-1.5, 3.2) ) 55 | let triangulated = triangulate(expected) 56 | require( expected == triangulated ) 57 | 58 | 59 | test "100 points": 60 | # These points were taken from here: 61 | # http://www.geom.uiuc.edu/locate/user/samuelp/rnd_100.ps 62 | 63 | let expected = edges( 64 | p(10.4936, 274.9466) -> p(19.4267, 280.9228), 65 | p(19.4267, 280.9228) -> p(40.328, 277.9291), 66 | p(19.4267, 280.9228) -> p(79.1631, 292.8664), 67 | p(19.4267, 280.9228) -> p(26.9061, 240.594), 68 | p(1.5104, 273.4452) -> p(19.4267, 280.9228), 69 | p(255.4231, 228.623) -> p(259.8991, 243.558), 70 | p(243.4611, 243.575) -> p(259.8991, 243.558), 71 | p(259.8991, 243.558) -> p(280.7981, 248.006), 72 | p(253.9251, 267.48) -> p(259.8991, 243.558), 73 | p(134.4261, 231.63) -> p(191.1831, 245.055), 74 | p(134.4261, 231.63) -> p(134.5251, 210.681), 75 | p(122.4761, 238.93) -> p(134.4261, 231.63), 76 | p(134.4261, 231.63) -> p(135.9231, 239.102), 77 | p(0.0, 161.435) -> p(22.4041, 207.729), 78 | p(16.4795, 171.882) -> p(22.4041, 207.729), 79 | p(22.4041, 207.729) -> p(22.4241, 224.166), 80 | p(0.00189, 228.647) -> p(22.4041, 207.729), 81 | p(22.4041, 207.729) -> p(34.3621, 204.749), 82 | p(22.4041, 207.729) -> p(37.3561, 200.25), 83 | p(271.8371, 180.841) -> p(273.3331, 182.313), 84 | p(259.8911, 136.044) -> p(271.8371, 180.841), 85 | p(256.9551, 195.767) -> p(271.8371, 180.841), 86 | p(271.8371, 180.841) -> p(286.7741, 134.55), 87 | p(237.5361, 162.922) -> p(271.8371, 180.841), 88 | p(41.8301, 40.452) -> p(52.2941, 74.802), 89 | p(28.379, 71.809) -> p(52.2941, 74.802), 90 | p(52.2941, 74.802) -> p(61.239, 98.517), 91 | p(52.2941, 74.802) -> p(91.1271, 88.103), 92 | p(52.2941, 74.802) -> p(76.184, 37.466), 93 | p(215.2231, 270.445) -> p(235.9941, 259.993), 94 | p(210.6281, 290.8921) -> p(215.2231, 270.445), 95 | p(215.2231, 270.445) -> p(219.5621, 271.92), 96 | p(191.1831, 245.055) -> p(215.2231, 270.445), 97 | p(186.7011, 262.999) -> p(215.2231, 270.445), 98 | p(204.6271, 292.4606) -> p(215.2231, 270.445), 99 | p(210.6281, 290.8921) -> p(291.2541, 268.966), 100 | p(210.6281, 290.8921) -> p(253.9251, 267.48), 101 | p(210.6281, 290.8921) -> p(219.5621, 271.92), 102 | p(204.6271, 292.4606) -> p(210.6281, 290.8921), 103 | p(40.328, 277.9291) -> p(79.1631, 292.8664), 104 | p(79.1631, 292.8664) -> p(82.1751, 246.561), 105 | p(79.1631, 292.8664) -> p(101.5841, 274.9452), 106 | p(79.1631, 292.8664) -> p(204.6271, 292.4606), 107 | p(255.4231, 228.623) -> p(280.7981, 248.006), 108 | p(273.3331, 182.313) -> p(280.7981, 248.006), 109 | p(280.7981, 248.006) -> p(291.2541, 268.966), 110 | p(253.9251, 267.48) -> p(280.7981, 248.006), 111 | p(22.4241, 224.166) -> p(28.385, 237.591), 112 | p(28.385, 237.591) -> p(34.3621, 204.749), 113 | p(26.9061, 240.594) -> p(28.385, 237.591), 114 | p(8.9758, 240.317) -> p(28.385, 237.591), 115 | p(28.385, 237.591) -> p(76.1891, 236.112), 116 | p(97.0881, 254.034) -> p(101.5841, 274.9452), 117 | p(101.5841, 274.9452) -> p(113.5151, 260.002), 118 | p(82.1751, 246.561) -> p(101.5841, 274.9452), 119 | p(101.5841, 274.9452) -> p(186.7011, 262.999), 120 | p(101.5841, 274.9452) -> p(204.6271, 292.4606), 121 | p(41.855, 5.8604) -> p(86.6391, 0.0), 122 | p(86.6391, 0.0) -> p(164.3001, 47.901), 123 | p(68.709, 10.581) -> p(86.6391, 0.0), 124 | p(76.1761, 9.081) -> p(86.6391, 0.0), 125 | p(86.6391, 0.0) -> p(206.1281, 12.0639), 126 | p(86.6391, 0.0) -> p(230.0171, 0.077453), 127 | p(86.6391, 0.0) -> p(91.1091, 19.541), 128 | p(113.5151, 260.002) -> p(186.7011, 262.999), 129 | p(186.7011, 262.999) -> p(191.1831, 245.055), 130 | p(186.7011, 262.999) -> p(204.6271, 292.4606), 131 | p(135.9231, 239.102) -> p(186.7011, 262.999), 132 | p(235.9941, 259.993) -> p(243.4611, 243.575), 133 | p(224.0441, 215.202) -> p(243.4611, 243.575), 134 | p(243.4611, 243.575) -> p(253.9251, 267.48), 135 | p(191.1831, 245.055) -> p(243.4611, 243.575), 136 | p(243.4611, 243.575) -> p(255.4231, 228.623), 137 | p(88.1281, 180.848) -> p(134.5251, 210.681), 138 | p(88.1281, 180.848) -> p(143.3901, 194.294), 139 | p(74.6871, 198.77) -> p(88.1281, 180.848), 140 | p(88.1281, 180.848) -> p(94.1221, 161.428), 141 | p(82.2601, 174.877) -> p(88.1281, 180.848), 142 | p(0.0, 161.435) -> p(16.4795, 171.882), 143 | p(0.0, 161.435) -> p(0.00189, 228.647), 144 | p(0.0, 161.435) -> p(5.9912, 146.495), 145 | p(0.0, 161.435) -> p(10.4615, 162.91), 146 | p(0.0, 161.435) -> p(7.4694, 16.541), 147 | p(0.0, 161.435) -> p(11.9564, 53.875), 148 | p(28.379, 164.312) -> p(32.879, 171.888), 149 | p(22.4091, 168.899) -> p(28.379, 164.312), 150 | p(28.379, 164.312) -> p(46.3081, 153.954), 151 | p(10.4615, 162.91) -> p(28.379, 164.312), 152 | p(19.4183, 137.123) -> p(28.379, 164.312), 153 | p(242.4821, 10.536) -> p(286.7741, 134.55), 154 | p(264.3721, 122.596) -> p(286.7741, 134.55), 155 | p(286.7741, 134.55) -> p(291.2541, 268.966), 156 | p(262.8751, 94.204) -> p(286.7741, 134.55), 157 | p(273.3331, 182.313) -> p(286.7741, 134.55), 158 | p(259.8911, 136.044) -> p(286.7741, 134.55), 159 | p(70.2231, 31.486) -> p(91.1091, 19.541), 160 | p(91.1091, 19.541) -> p(164.3001, 47.901), 161 | p(76.1761, 9.081) -> p(91.1091, 19.541), 162 | p(76.184, 37.466) -> p(91.1091, 19.541), 163 | p(91.1091, 19.541) -> p(91.1271, 88.103), 164 | p(38.8451, 122.578) -> p(40.3291, 134.55), 165 | p(40.3291, 134.55) -> p(46.3081, 153.954), 166 | p(40.3291, 134.55) -> p(46.3031, 129.851), 167 | p(19.4183, 137.123) -> p(40.3291, 134.55), 168 | p(46.3031, 129.851) -> p(61.239, 98.517), 169 | p(46.3031, 129.851) -> p(61.333, 155.455), 170 | p(38.8451, 122.578) -> p(46.3031, 129.851), 171 | p(46.3031, 129.851) -> p(80.6701, 142.001), 172 | p(46.3031, 129.851) -> p(46.3081, 153.954), 173 | p(41.8301, 40.452) -> p(41.855, 5.8604), 174 | p(41.855, 5.8604) -> p(70.2231, 31.486), 175 | p(19.4691, 35.954) -> p(41.855, 5.8604), 176 | p(41.855, 5.8604) -> p(68.709, 10.581), 177 | p(7.4694, 16.541) -> p(41.855, 5.8604), 178 | p(0.00189, 228.647) -> p(22.4241, 224.166), 179 | p(0.00189, 228.647) -> p(8.9758, 240.317), 180 | p(0.00189, 228.647) -> p(1.5104, 273.4452), 181 | p(16.4795, 171.882) -> p(37.3561, 200.25), 182 | p(37.3561, 200.25) -> p(40.3471, 183.821), 183 | p(34.3621, 204.749) -> p(37.3561, 200.25), 184 | p(37.3561, 200.25) -> p(74.6871, 198.77), 185 | p(203.1471, 83.759) -> p(210.6901, 149.486), 186 | p(186.7091, 156.932) -> p(210.6901, 149.486), 187 | p(207.6181, 192.8) -> p(210.6901, 149.486), 188 | p(189.6871, 162.872) -> p(210.6901, 149.486), 189 | p(210.6901, 149.486) -> p(244.9611, 112.133), 190 | p(210.6901, 149.486) -> p(259.8911, 136.044), 191 | p(210.6901, 149.486) -> p(237.5361, 162.922), 192 | p(242.4821, 10.536) -> p(262.8751, 94.204), 193 | p(235.9901, 34.408) -> p(242.4821, 10.536), 194 | p(234.5041, 7.4159) -> p(242.4821, 10.536), 195 | p(230.0171, 0.077453) -> p(242.4821, 10.536), 196 | p(273.3331, 182.313) -> p(291.2541, 268.966), 197 | p(253.9251, 267.48) -> p(291.2541, 268.966), 198 | p(159.8371, 164.402) -> p(186.7091, 156.932), 199 | p(186.7091, 156.932) -> p(203.1471, 83.759), 200 | p(167.2841, 168.903) -> p(186.7091, 156.932), 201 | p(186.7091, 156.932) -> p(189.6871, 162.872), 202 | p(206.1281, 12.0639) -> p(235.9901, 34.408), 203 | p(225.5341, 43.438) -> p(235.9901, 34.408), 204 | p(234.5041, 7.4159) -> p(235.9901, 34.408), 205 | p(235.9901, 34.408) -> p(262.8751, 94.204), 206 | p(255.4231, 228.623) -> p(256.9551, 195.767), 207 | p(224.0441, 215.202) -> p(255.4231, 228.623), 208 | p(255.4231, 228.623) -> p(273.3331, 182.313), 209 | p(22.4241, 224.166) -> p(34.3621, 204.749), 210 | p(34.3621, 204.749) -> p(74.6871, 198.77), 211 | p(34.3621, 204.749) -> p(76.1891, 236.112), 212 | p(8.9758, 240.317) -> p(10.4936, 274.9466), 213 | p(8.9758, 240.317) -> p(22.4241, 224.166), 214 | p(8.9758, 240.317) -> p(26.9061, 240.594), 215 | p(1.5104, 273.4452) -> p(8.9758, 240.317), 216 | p(7.4694, 16.541) -> p(19.4691, 35.954), 217 | p(7.4694, 16.541) -> p(11.9564, 53.875), 218 | p(61.239, 98.517) -> p(91.1271, 88.103), 219 | p(91.1271, 88.103) -> p(106.0851, 109.12), 220 | p(91.1271, 88.103) -> p(164.3001, 47.901), 221 | p(76.184, 37.466) -> p(91.1271, 88.103), 222 | p(10.4936, 274.9466) -> p(26.9061, 240.594), 223 | p(1.5104, 273.4452) -> p(10.4936, 274.9466), 224 | p(206.1281, 12.0639) -> p(230.0171, 0.077453), 225 | p(230.0171, 0.077453) -> p(234.5041, 7.4159), 226 | p(203.1471, 83.759) -> p(262.8751, 94.204), 227 | p(262.8751, 94.204) -> p(264.3721, 122.596), 228 | p(225.5341, 43.438) -> p(262.8751, 94.204), 229 | p(244.9611, 112.133) -> p(262.8751, 94.204), 230 | p(206.1281, 12.0639) -> p(225.5341, 43.438), 231 | p(203.1471, 83.759) -> p(225.5341, 43.438), 232 | p(176.2521, 47.916) -> p(225.5341, 43.438), 233 | p(10.4615, 162.91) -> p(19.4183, 137.123), 234 | p(10.4615, 162.91) -> p(16.4795, 171.882), 235 | p(10.4615, 162.91) -> p(22.4091, 168.899), 236 | p(5.9912, 146.495) -> p(10.4615, 162.91), 237 | p(11.9564, 53.875) -> p(41.8301, 40.452), 238 | p(11.9564, 53.875) -> p(19.4691, 35.954), 239 | p(11.9564, 53.875) -> p(28.379, 71.809), 240 | p(5.9912, 146.495) -> p(11.9564, 53.875), 241 | p(259.8911, 136.044) -> p(264.3721, 122.596), 242 | p(244.9611, 112.133) -> p(259.8911, 136.044), 243 | p(237.5361, 162.922) -> p(259.8911, 136.044), 244 | p(40.328, 277.9291) -> p(76.1891, 236.112), 245 | p(76.1891, 236.112) -> p(103.1051, 246.563), 246 | p(26.9061, 240.594) -> p(76.1891, 236.112), 247 | p(76.1891, 236.112) -> p(82.1751, 246.561), 248 | p(74.6871, 198.77) -> p(76.1891, 236.112), 249 | p(16.4795, 171.882) -> p(32.879, 171.888), 250 | p(16.4795, 171.882) -> p(22.4091, 168.899), 251 | p(16.4795, 171.882) -> p(40.3471, 183.821), 252 | p(32.879, 171.888) -> p(46.3081, 153.954), 253 | p(22.4091, 168.899) -> p(32.879, 171.888), 254 | p(32.879, 171.888) -> p(40.3471, 183.821), 255 | p(256.9551, 195.767) -> p(273.3331, 182.313), 256 | p(113.5151, 260.002) -> p(122.4761, 238.93), 257 | p(103.1051, 246.563) -> p(122.4761, 238.93), 258 | p(122.4761, 238.93) -> p(134.5251, 210.681), 259 | p(74.6871, 198.77) -> p(122.4761, 238.93), 260 | p(122.4761, 238.93) -> p(135.9231, 239.102), 261 | p(113.5151, 260.002) -> p(135.9231, 239.102), 262 | p(135.9231, 239.102) -> p(191.1831, 245.055), 263 | p(191.1831, 245.055) -> p(235.9941, 259.993), 264 | p(219.5621, 271.92) -> p(235.9941, 259.993), 265 | p(235.9941, 259.993) -> p(253.9251, 267.48), 266 | p(207.6181, 192.8) -> p(237.5361, 162.922), 267 | p(237.5361, 162.922) -> p(256.9551, 195.767), 268 | p(221.0641, 195.777) -> p(237.5361, 162.922), 269 | p(224.0441, 215.202) -> p(256.9551, 195.767), 270 | p(221.0641, 195.777) -> p(256.9551, 195.767), 271 | p(97.0881, 254.034) -> p(113.5151, 260.002), 272 | p(103.1051, 246.563) -> p(113.5151, 260.002), 273 | p(97.0881, 254.034) -> p(103.1051, 246.563), 274 | p(82.1751, 246.561) -> p(103.1051, 246.563), 275 | p(74.6871, 198.77) -> p(103.1051, 246.563), 276 | p(82.1751, 246.561) -> p(97.0881, 254.034), 277 | p(68.709, 10.581) -> p(70.2231, 31.486), 278 | p(68.709, 10.581) -> p(76.1761, 9.081), 279 | p(203.1471, 83.759) -> p(244.9611, 112.133), 280 | p(244.9611, 112.133) -> p(264.3721, 122.596), 281 | p(82.2601, 174.877) -> p(83.6771, 158.446), 282 | p(80.6701, 142.001) -> p(83.6771, 158.446), 283 | p(61.333, 155.455) -> p(83.6771, 158.446), 284 | p(83.6771, 158.446) -> p(94.1221, 161.428), 285 | p(206.1281, 12.0639) -> p(234.5041, 7.4159), 286 | p(19.4691, 35.954) -> p(41.8301, 40.452), 287 | p(94.1221, 161.428) -> p(159.8371, 164.402), 288 | p(94.1221, 161.428) -> p(106.0851, 109.12), 289 | p(80.6701, 142.001) -> p(94.1221, 161.428), 290 | p(94.1221, 161.428) -> p(143.3901, 194.294), 291 | p(82.2601, 174.877) -> p(94.1221, 161.428), 292 | p(164.3001, 47.901) -> p(203.1471, 83.759), 293 | p(106.0851, 109.12) -> p(164.3001, 47.901), 294 | p(164.3001, 47.901) -> p(176.2521, 47.916), 295 | p(164.3001, 47.901) -> p(206.1281, 12.0639), 296 | p(28.379, 71.809) -> p(41.8301, 40.452), 297 | p(28.379, 71.809) -> p(61.239, 98.517), 298 | p(28.379, 71.809) -> p(38.8451, 122.578), 299 | p(19.4183, 137.123) -> p(28.379, 71.809), 300 | p(5.9912, 146.495) -> p(28.379, 71.809), 301 | p(41.8301, 40.452) -> p(70.2231, 31.486), 302 | p(41.8301, 40.452) -> p(76.184, 37.466), 303 | p(46.3081, 153.954) -> p(61.333, 155.455), 304 | p(40.3471, 183.821) -> p(61.333, 155.455), 305 | p(61.333, 155.455) -> p(80.6701, 142.001), 306 | p(61.333, 155.455) -> p(82.2601, 174.877), 307 | p(38.8451, 122.578) -> p(61.239, 98.517), 308 | p(19.4183, 137.123) -> p(38.8451, 122.578), 309 | p(70.2231, 31.486) -> p(76.1761, 9.081), 310 | p(159.8371, 164.402) -> p(167.2841, 168.903), 311 | p(167.2841, 168.903) -> p(201.6851, 203.229), 312 | p(143.3901, 194.294) -> p(167.2841, 168.903), 313 | p(167.2841, 168.903) -> p(189.6871, 162.872), 314 | p(26.9061, 240.594) -> p(40.328, 277.9291), 315 | p(19.4183, 137.123) -> p(46.3081, 153.954), 316 | p(40.3471, 183.821) -> p(46.3081, 153.954), 317 | p(5.9912, 146.495) -> p(19.4183, 137.123), 318 | p(61.239, 98.517) -> p(80.6701, 142.001), 319 | p(80.6701, 142.001) -> p(106.0851, 109.12), 320 | p(134.5251, 210.681) -> p(191.1831, 245.055), 321 | p(134.5251, 210.681) -> p(143.3901, 194.294), 322 | p(74.6871, 198.77) -> p(134.5251, 210.681), 323 | p(159.8371, 164.402) -> p(203.1471, 83.759), 324 | p(106.0851, 109.12) -> p(159.8371, 164.402), 325 | p(143.3901, 194.294) -> p(159.8371, 164.402), 326 | p(201.6851, 203.229) -> p(207.6181, 192.8), 327 | p(189.6871, 162.872) -> p(201.6851, 203.229), 328 | p(191.1831, 245.055) -> p(201.6851, 203.229), 329 | p(201.6851, 203.229) -> p(224.0441, 215.202), 330 | p(201.6851, 203.229) -> p(221.0641, 195.777), 331 | p(143.3901, 194.294) -> p(201.6851, 203.229), 332 | p(61.239, 98.517) -> p(106.0851, 109.12), 333 | p(106.0851, 109.12) -> p(203.1471, 83.759), 334 | p(219.5621, 271.92) -> p(253.9251, 267.48), 335 | p(189.6871, 162.872) -> p(207.6181, 192.8), 336 | p(191.1831, 245.055) -> p(224.0441, 215.202), 337 | p(143.3901, 194.294) -> p(191.1831, 245.055), 338 | p(176.2521, 47.916) -> p(206.1281, 12.0639), 339 | p(221.0641, 195.777) -> p(224.0441, 215.202), 340 | p(40.3471, 183.821) -> p(74.6871, 198.77), 341 | p(74.6871, 198.77) -> p(82.2601, 174.877), 342 | p(40.3471, 183.821) -> p(82.2601, 174.877), 343 | p(70.2231, 31.486) -> p(76.184, 37.466), 344 | p(40.328, 277.9291) -> p(82.1751, 246.561), 345 | p(176.2521, 47.916) -> p(203.1471, 83.759), 346 | p(207.6181, 192.8) -> p(221.0641, 195.777) ) 347 | let triangulated = triangulate(expected) 348 | require( expected == triangulated ) 349 | 350 | 351 | -------------------------------------------------------------------------------- /tests/t_triangle.nim: -------------------------------------------------------------------------------- 1 | import unittest, helpers 2 | import delaunay/private/triangle 3 | import delaunay/private/point 4 | 5 | suite "Triangles should ": 6 | 7 | test "determine whether three points are a triangle": 8 | let tri = newTriangle( p(0, 0), p(1, 1), p(2, 0) ) 9 | require( isTriangle(tri) ) 10 | 11 | test "determine that points in a line aren't a triangle": 12 | let tri = newTriangle( p(0, 0), p(3, 2), p(6, 4) ) 13 | require( not isTriangle(tri) ) 14 | 15 | test "determine that a triangle contains duplicate points": 16 | require( not isTriangle( 17 | newTriangle( p(0, 0), p(3, 2), p(3, 2) ) 18 | ) ) 19 | 20 | require( not isTriangle( 21 | newTriangle( p(0, 0), p(3, 2), p(0, 0) ) 22 | ) ) 23 | 24 | require( not isTriangle( 25 | newTriangle( p(3, 2), p(3, 2), p(0, 0) ) 26 | ) ) 27 | 28 | test "return whether a point is within a circumcirlcle": 29 | 30 | # Points in a line can't form a triangle 31 | expect(AssertionError): 32 | discard isInCircumcircle( 33 | newTriangle( p(0, 0), p(5, 5), p(10, 10) ), 34 | p(5, 2) 35 | ) 36 | 37 | # Points in a line can't form a triangle 38 | expect(AssertionError): 39 | discard isInCircumcircle( 40 | newTriangle( p(0, 0), p(0, 5), p(0, 10) ), 41 | p(5, 2) 42 | ) 43 | 44 | # Throw when two points are shared 45 | expect(AssertionError): 46 | discard isInCircumcircle( 47 | newTriangle( p(0, 0), p(5, 0), p(5, 0) ), 48 | p(3, 3) 49 | ) 50 | 51 | require( isInCircumcircle( 52 | newTriangle( p(0, 0), p(5, 0), p(0, 5) ), 53 | p(3, 3) 54 | )) 55 | 56 | require( isInCircumcircle( 57 | newTriangle( p(2, 7), p(0, 0), p(5, 0) ), 58 | p(3, 3) 59 | )) 60 | 61 | require( isInCircumcircle( 62 | newTriangle( p(0, 0), p(5, 5), p(10, 0) ), 63 | p(5, 2) 64 | )) 65 | 66 | require( not isInCircumcircle( 67 | newTriangle( p(0, 0), p(5, 5), p(10, 0) ), 68 | p(50, 2) 69 | )) 70 | 71 | require( not isInCircumcircle( 72 | newTriangle( p(0, 0), p(100, 1), p(200, -10.0) ), 73 | p(5, 2) 74 | )) 75 | 76 | require( isInCircumcircle( 77 | newTriangle( p(0, 0), p(100, 1), p(200, -10.0) ), 78 | p(100, -300.0) 79 | )) 80 | 81 | # A point that lies ON the circumcircle is not within it 82 | require( not isInCircumcircle( 83 | newTriangle( p(0, 0), p(0, 1), p(1, 1) ), 84 | p(1, 0) 85 | )) 86 | 87 | 88 | --------------------------------------------------------------------------------