├── .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 | [](https://github.com/Nycto/DelaunayNim/actions/workflows/build.yml)
5 | [](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 | 
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 ""
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 |
--------------------------------------------------------------------------------