├── .github └── workflows │ ├── build.yml │ └── docs.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── BruteSpace.png ├── HashSpace.png ├── KdSpace.png ├── QuadSpace.png ├── SortSpace.png └── example.nim ├── spacy.nimble ├── src └── spacy.nim ├── tests ├── bench.nim ├── config.nims └── test.nim └── tools └── gen_readme.nim /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Github Actions 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | os: [ubuntu-latest, windows-latest] 9 | 10 | runs-on: ${{ matrix.os }} 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: jiro4989/setup-nim-action@v1 15 | with: 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | - run: nimble test -y 18 | - run: nimble test --gc:orc -y 19 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | branches: 5 | - master 6 | env: 7 | nim-version: 'stable' 8 | nim-src: src/${{ github.event.repository.name }}.nim 9 | deploy-dir: .gh-pages 10 | jobs: 11 | docs: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: jiro4989/setup-nim-action@v1 16 | with: 17 | nim-version: ${{ env.nim-version }} 18 | - run: nimble install -Y 19 | - run: nimble doc --index:on --project --git.url:https://github.com/${{ github.repository }} --git.commit:master --out:${{ env.deploy-dir }} ${{ env.nim-src }} 20 | - name: "Copy to index.html" 21 | run: cp ${{ env.deploy-dir }}/${{ github.event.repository.name }}.html ${{ env.deploy-dir }}/index.html 22 | - name: Deploy documents 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ${{ env.deploy-dir }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore files with no extention: 2 | * 3 | !*/ 4 | !*.* 5 | 6 | # normal ignores: 7 | *.exe 8 | nimcache 9 | *.pdb 10 | *.ilk 11 | .* 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Andre von Houck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spatial data structures for Nim. 2 | 3 | `nimble install spacy` 4 | 5 | ![Github Actions](https://github.com/treeform/spacy/workflows/Github%20Actions/badge.svg) 6 | 7 | [API reference](https://treeform.github.io/spacy) 8 | 9 | ## About 10 | 11 | Spatial algorithms are used to find the "closest" things faster than simple brute force iteration would. They make your code run faster using smarter data structures. This library has different "Spaces" that you can use to speed up games and graphical applications. 12 | 13 | One key design decision is that all spaces have a very similar API and can be easily swapped. This way you can swap out spaces and see which one works best for your use case. 14 | 15 | ```nim 16 | import spacy, vmath, random 17 | 18 | var rand = initRand(2021) 19 | 20 | # Create one of the BruteSpace, SortSpace, HashSpace, QuadSpace or KdSpace. 21 | var space = newSortSpace() 22 | for i in 0 ..< 1000: 23 | # All entries should have a id and pos. 24 | let e = Entry(id: i.uint32, pos: rand.randVec2()) 25 | # Insert entries. 26 | space.insert e 27 | # Call finalize to start using the space. 28 | space.finalize() 29 | 30 | # Iterate N x N entires and get the closest. 31 | let distance = 0.001 32 | for a in space.all(): 33 | for b in space.findInRange(a, distance): 34 | echo a, " is close to ", b 35 | 36 | # Clear the space when you are done. 37 | space.clear() 38 | ``` 39 | 40 | ### Perf time (1000 vs 1000 at 0.001 distance): 41 | ``` 42 | BruteSpace ......................... 1.839 ms 1.869 ms ±0.052 x100 43 | SortSpace .......................... 1.511 ms 1.539 ms ±0.028 x100 44 | HashSpace .......................... 0.441 ms 0.457 ms ±0.022 x100 45 | QuadSpace .......................... 0.185 ms 0.195 ms ±0.010 x100 46 | KdSpace ............................ 0.519 ms 0.548 ms ±0.037 x100 47 | ``` 48 | 49 | You would usually pick the best one for your use case by profiling and tuning. There is a cost to generating each space, you need to make sure it justifies the lookup time savings. 50 | 51 | # BruteSpace 52 | 53 | BruteSpace is basically a brute force algorithm that takes every inserted element and compares to every other inserted element. 54 | 55 | ![examples/BruteSpace.png](examples/BruteSpace.png) 56 | 57 | The BruteSpace is faster when there are few elements in the space or you don't do many look ups. It’s a good baseline space that enables you to see how slow things actually can be. Don't discount it! Linear scans are pretty fast when you are just zipping through memory. Brute force might be all you need! 58 | 59 | # SortSpace 60 | 61 | SortSpace is probably the simplest spatial algorithm you can have. All it does is sorts all entries on one axis. Here it does the X axis. Then all it does is looks to the right and the left for matches. It’s very simple to code and produces good results when the radius is really small. 62 | 63 | ![examples/SortSpace.png](examples/SortSpace.png) 64 | 65 | You can see we are checking way less elements compared to BruteSpace. Instead of checking vs all elements we are only checking in the vertical slice. 66 | 67 | SortSpace draws its power from the underlying sorting algorithm n×log(n) nature. It’s really good for very small distances when you don’t expect many elements to appear in the vertical slice. SortSpace is really good at cache locality because you are searching things next to each other and are walking linearly in memory. 68 | 69 | # HashSpace 70 | 71 | HashSpace is a little more complex than SortSpace but it’s still pretty simple. Instead of drawing its power from a sorting algorithm it draws its power from hash tables. HashSpace has a resolution and every entry going in is put into a grid-bucket. To check for surrounding entries you simply look up closest grid buckets and then loop through their entries. 72 | 73 | ![examples/HashSpace.png](examples/HashSpace.png) 74 | 75 | HashSpaces are really good for when your entries are uniformly distributed with even density and things can’t really bunch up too much. They work even better when entries are really far apart. They are also really good when you are always searching the same distance in that you can make the grid size match your search radius. You can tune this space for your usecase. 76 | 77 | # QuadSpace 78 | 79 | QuadSpace is basically the same as "quad tree" (I just like the space theme). Quad trees are a little harder to make but usually winners in all kinds of spatial applications. They work by starting out with a single quad and as more elements are inserted into the quad they hit maximum elements in a quad and split into 4. The elements are redistributed. As those inner quads begin to fill up they are split as well. When looking up stuff you just have to walk into the closets quads. 80 | 81 | ![examples/QuadSpace.png](examples/QuadSpace.png) 82 | 83 | QuadSpaces are really good at almost everything. But they might miss out in some niche cases where SortSpaces (really small distances) or HashSpaces (uniform density) might win out. They are also bad at cache locality as many pointers or references might make you jump all over the place in memory. 84 | 85 | # KdSpace 86 | 87 | Just like QuadSpace is about Quad Trees, KdSpace is about kd-tree. Kd-Trees differ from quad trees in that they are binary and they sort their results as they divide. Potentially getting less nodes and less bounds to check. Quad trees build their nodes as new elements are inserted while kd-trees build all the nodes in one big final step. 88 | 89 | ![examples/KdSpace.png](examples/KdSpace.png) 90 | 91 | KdSpace trees take a long time to build. In theory KdSpace would be good when the entries are static, the tree is built once and used often. While QuadSpace might be better when the tree is rebuilt all the time. 92 | 93 | # Always be profiling. 94 | 95 | You can’t really say one Space is faster than the others, you always need to check. The hardware or your particular problem might drastically change the speed characteristics. This is why all spaces have a similar API and you can just swap them out when another space seems better for your use case. 96 | 97 | # API: spacy 98 | 99 | ```nim 100 | import spacy 101 | ``` 102 | 103 | ## **type** Entry 104 | 105 | 106 | ```nim 107 | Entry = object 108 | id*: uint32 109 | pos*: Vec2 110 | ``` 111 | 112 | ## **type** BruteSpace 113 | 114 | Brute-force space just compares every entry vs every other entry. Supposed to be good for very small number or large ranges. 115 | 116 | ```nim 117 | BruteSpace = ref object 118 | list*: seq[Entry] 119 | ``` 120 | 121 | ## **proc** newBruteSpace 122 | 123 | Creates a new brute-force space. 124 | 125 | ```nim 126 | proc newBruteSpace(): BruteSpace 127 | ``` 128 | 129 | ## **proc** insert 130 | 131 | Adds entry to the space. 132 | 133 | ```nim 134 | proc insert(bs: BruteSpace; e: Entry) {.inline.} 135 | ``` 136 | 137 | ## **proc** finalize 138 | 139 | Finishes the space and makes it ready for use. 140 | 141 | ```nim 142 | proc finalize(bs: BruteSpace) {.inline.} 143 | ``` 144 | 145 | ## **proc** clear 146 | 147 | Clears the spaces and makes it ready to be used again. 148 | 149 | ```nim 150 | proc clear(bs: BruteSpace) {.inline.} 151 | ``` 152 | 153 | ## **proc** len 154 | 155 | Number of entries inserted 156 | 157 | ```nim 158 | proc len(bs: BruteSpace): int {.inline.} 159 | ``` 160 | 161 | ## **iterator** all 162 | 163 | Iterates all entries in a space. 164 | 165 | ```nim 166 | iterator all(bs: BruteSpace): Entry 167 | ``` 168 | 169 | ## **iterator** findInRangeApprox 170 | 171 | Iterates all entries in range of an entry but does not cull them. Useful if you need distance anyways and will compute other computations. 172 | 173 | ```nim 174 | iterator findInRangeApprox(bs: BruteSpace; e: Entry; radius: float): Entry 175 | ``` 176 | 177 | ## **iterator** findInRange 178 | 179 | Iterates all entries in range of an entry. 180 | 181 | ```nim 182 | iterator findInRange(bs: BruteSpace; e: Entry; radius: float): Entry 183 | ``` 184 | 185 | ## **type** SortSpace 186 | 187 | Sort space sorts all entires on one axis X. Supposed to be good for very small ranges. 188 | 189 | ```nim 190 | SortSpace = ref object 191 | list*: seq[Entry] 192 | ``` 193 | 194 | ## **proc** newSortSpace 195 | 196 | Creates a new sorted space. 197 | 198 | ```nim 199 | proc newSortSpace(): SortSpace 200 | ``` 201 | 202 | ## **proc** insert 203 | 204 | Adds entry to the space. 205 | 206 | ```nim 207 | proc insert(ss: SortSpace; e: Entry) {.inline.} 208 | ``` 209 | 210 | ## **proc** finalize 211 | 212 | Finishes the space and makes it ready for use. 213 | 214 | ```nim 215 | proc finalize(ss: SortSpace) {.inline.} 216 | ``` 217 | 218 | ## **proc** clear 219 | 220 | Clears the spaces and makes it ready to be used again. 221 | 222 | ```nim 223 | proc clear(ss: SortSpace) {.inline.} 224 | ``` 225 | 226 | ## **proc** len 227 | 228 | Number of entries inserted. 229 | 230 | ```nim 231 | proc len(ss: SortSpace): int {.inline.} 232 | ``` 233 | 234 | ## **iterator** all 235 | 236 | Iterates all entries in a space. 237 | 238 | ```nim 239 | iterator all(ss: SortSpace): Entry 240 | ``` 241 | 242 | ## **iterator** findInRangeApprox 243 | 244 | Iterates all entries in range of an entry but does not cull them. Useful if you need distance anyways and will compute other computations. 245 | 246 | ```nim 247 | iterator findInRangeApprox(ss: SortSpace; e: Entry; radius: float): Entry 248 | ``` 249 | 250 | ## **iterator** findInRange 251 | 252 | Iterates all entries in range of an entry. 253 | 254 | ```nim 255 | iterator findInRange(ss: SortSpace; e: Entry; radius: float): Entry 256 | ``` 257 | 258 | ## **type** HashSpace 259 | 260 | Divides space into little tiles that objects are hashed too. Supposed to be good for very uniform filled space. 261 | 262 | ```nim 263 | HashSpace = ref object 264 | hash*: TableRef[(int32, int32), seq[Entry]] 265 | resolution*: float 266 | ``` 267 | 268 | ## **proc** newHashSpace 269 | 270 | Creates a hash table space. 271 | 272 | ```nim 273 | proc newHashSpace(resolution: float): HashSpace 274 | ``` 275 | 276 | ## **proc** insert 277 | 278 | Adds entry to the space. 279 | 280 | ```nim 281 | proc insert(hs: HashSpace; e: Entry) {.raises: [KeyError].} 282 | ``` 283 | 284 | ## **proc** finalize 285 | 286 | Finishes the space and makes it ready for use. 287 | 288 | ```nim 289 | proc finalize(hs: HashSpace) {.inline.} 290 | ``` 291 | 292 | ## **proc** clear 293 | 294 | Clears the spaces and makes it ready to be used again. 295 | 296 | ```nim 297 | proc clear(hs: HashSpace) {.inline.} 298 | ``` 299 | 300 | ## **proc** len 301 | 302 | Number of entries inserted 303 | 304 | ```nim 305 | proc len(hs: HashSpace): int {.inline.} 306 | ``` 307 | 308 | ## **iterator** all 309 | 310 | Iterates all entries in a space. 311 | 312 | ```nim 313 | iterator all(hs: HashSpace): Entry 314 | ``` 315 | 316 | ## **iterator** findInRangeApprox 317 | 318 | Iterates all entries in range of an entry but does not cull them. Useful if you need distance anyways and will compute other computations. 319 | 320 | ```nim 321 | iterator findInRangeApprox(hs: HashSpace; e: Entry; radius: float): Entry {.raises: [KeyError].} 322 | ``` 323 | 324 | ## **iterator** findInRange 325 | 326 | Iterates all entries in range of an entry. 327 | 328 | ```nim 329 | iterator findInRange(hs: HashSpace; e: Entry; radius: float): Entry {.raises: [KeyError].} 330 | ``` 331 | 332 | ## **type** QuadSpace 333 | 334 | QuadTree, divide each node down if there is many elements. Supposed to be for large amount of entries. 335 | 336 | ```nim 337 | QuadSpace = ref object 338 | root*: QuadNode 339 | maxThings*: int 340 | maxLevels*: int 341 | ``` 342 | 343 | ## **type** QuadNode 344 | 345 | 346 | ```nim 347 | QuadNode = ref object 348 | things*: seq[Entry] 349 | nodes*: seq[QuadNode] 350 | bounds*: Rect 351 | level*: int 352 | ``` 353 | 354 | ## **proc** newQuadSpace 355 | 356 | Creates a new quad-tree space. 357 | 358 | ```nim 359 | proc newQuadSpace(bounds: Rect; maxThings = 10; maxLevels = 10): QuadSpace 360 | ``` 361 | 362 | ## **proc** insert 363 | 364 | 365 | ```nim 366 | proc insert(qs: QuadSpace; qn: var QuadNode; e: Entry) {.raises: [Exception], tags: [RootEffect].} 367 | ``` 368 | 369 | ## **proc** insert 370 | 371 | Adds entry to the space. 372 | 373 | ```nim 374 | proc insert(qs: QuadSpace; e: Entry) {.raises: [Exception], tags: [RootEffect].} 375 | ``` 376 | 377 | ## **proc** finalize 378 | 379 | Finishes the space and makes it ready for use. 380 | 381 | ```nim 382 | proc finalize(qs: QuadSpace) {.inline.} 383 | ``` 384 | 385 | ## **proc** clear 386 | 387 | Clears the spaces and makes it ready to be used again. 388 | 389 | ```nim 390 | proc clear(qs: QuadSpace) {.inline.} 391 | ``` 392 | 393 | ## **proc** len 394 | 395 | Number of entries inserted. 396 | 397 | ```nim 398 | proc len(qs: QuadSpace): int {.inline.} 399 | ``` 400 | 401 | ## **iterator** all 402 | 403 | Iterates all entries in a space. 404 | 405 | ```nim 406 | iterator all(qs: QuadSpace): Entry 407 | ``` 408 | 409 | ## **iterator** findInRangeApprox 410 | 411 | Iterates all entries in range of an entry but does not cull them. Useful if you need distance anyways and will compute other computations. 412 | 413 | ```nim 414 | iterator findInRangeApprox(qs: QuadSpace; e: Entry; radius: float): Entry 415 | ``` 416 | 417 | ## **iterator** findInRange 418 | 419 | Iterates all entries in range of an entry. 420 | 421 | ```nim 422 | iterator findInRange(qs: QuadSpace; e: Entry; radius: float): Entry 423 | ``` 424 | 425 | ## **type** KdSpace 426 | 427 | KD-Tree, each cell is divided vertically or horizontally. Supposed to be good for large amount of entries. 428 | 429 | ```nim 430 | KdSpace = ref object 431 | root*: KdNode 432 | maxThings*: int 433 | ``` 434 | 435 | ## **type** KdNode 436 | 437 | 438 | ```nim 439 | KdNode = ref object 440 | things*: seq[Entry] 441 | nodes*: seq[KdNode] 442 | bounds*: Rect 443 | level*: int 444 | ``` 445 | 446 | ## **proc** newKdSpace 447 | 448 | Creates a new space based on kd-tree. 449 | 450 | ```nim 451 | proc newKdSpace(bounds: Rect; maxThings = 10; maxLevels = 10): KdSpace 452 | ``` 453 | 454 | ## **proc** insert 455 | 456 | Adds entry to the space. 457 | 458 | ```nim 459 | proc insert(ks: KdSpace; e: Entry) {.inline.} 460 | ``` 461 | 462 | ## **proc** finalize 463 | 464 | Finishes the space and makes it ready for use. 465 | 466 | ```nim 467 | proc finalize(ks: KdSpace) 468 | ``` 469 | 470 | ## **proc** clear 471 | 472 | Clears the spaces and makes it ready to be used again. 473 | 474 | ```nim 475 | proc clear(ks: KdSpace) {.inline.} 476 | ``` 477 | 478 | ## **proc** len 479 | 480 | Number of entries inserted. 481 | 482 | ```nim 483 | proc len(ks: KdSpace): int 484 | ``` 485 | 486 | ## **iterator** all 487 | 488 | Iterates all entries in a space. 489 | 490 | ```nim 491 | iterator all(ks: KdSpace): Entry 492 | ``` 493 | 494 | ## **iterator** findInRangeApprox 495 | 496 | Iterates all entries in range of an entry but does not cull them. Useful if you need distance anyways and will compute other computations. 497 | 498 | ```nim 499 | iterator findInRangeApprox(ks: KdSpace; e: Entry; radius: float): Entry 500 | ``` 501 | 502 | ## **iterator** findInRange 503 | 504 | Iterates all entries in range of an entry. 505 | 506 | ```nim 507 | iterator findInRange(ks: KdSpace; e: Entry; radius: float): Entry 508 | ``` 509 | -------------------------------------------------------------------------------- /examples/BruteSpace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/spacy/b978eb0891d8e17b4ffcd628c8bc4af2cd727e03/examples/BruteSpace.png -------------------------------------------------------------------------------- /examples/HashSpace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/spacy/b978eb0891d8e17b4ffcd628c8bc4af2cd727e03/examples/HashSpace.png -------------------------------------------------------------------------------- /examples/KdSpace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/spacy/b978eb0891d8e17b4ffcd628c8bc4af2cd727e03/examples/KdSpace.png -------------------------------------------------------------------------------- /examples/QuadSpace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/spacy/b978eb0891d8e17b4ffcd628c8bc4af2cd727e03/examples/QuadSpace.png -------------------------------------------------------------------------------- /examples/SortSpace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/spacy/b978eb0891d8e17b4ffcd628c8bc4af2cd727e03/examples/SortSpace.png -------------------------------------------------------------------------------- /examples/example.nim: -------------------------------------------------------------------------------- 1 | import spacy, vmath, random 2 | 3 | var rand = initRand(2021) 4 | 5 | # Create one of the BruteSpace, SortSpace, HashSpace, QuadSpace or KdSpace. 6 | var space = newSortSpace() 7 | for i in 0 ..< 1000: 8 | # All entries should have a id and pos. 9 | let e = Entry(id: i.uint32, pos: rand.randVec2()) 10 | # Insert entries. 11 | space.insert e 12 | # Call finalize to start using the space. 13 | space.finalize() 14 | 15 | # Iterate N x N entires and get the closest. 16 | let distance = 0.001 17 | for a in space.all(): 18 | for b in space.findInRange(a, distance): 19 | echo a, " is close to ", b 20 | 21 | # Clear the space when you are done. 22 | space.clear() 23 | -------------------------------------------------------------------------------- /spacy.nimble: -------------------------------------------------------------------------------- 1 | version = "0.0.4" 2 | author = "Andre von Houck" 3 | description = "Spatial data structures for Nim." 4 | license = "MIT" 5 | 6 | srcDir = "src" 7 | 8 | requires "nim >= 1.4.0" 9 | requires "vmath >= 1.1.4" 10 | requires "bumpy >= 1.1.1" 11 | -------------------------------------------------------------------------------- /src/spacy.nim: -------------------------------------------------------------------------------- 1 | import algorithm, bumpy, math, tables, vmath 2 | 3 | type Entry* = object 4 | id*: uint32 5 | pos*: Vec2 6 | 7 | type BruteSpace* = ref object 8 | ## Brute-force space just compares every entry vs every other entry. 9 | ## Supposed to be good for very small number or large ranges. 10 | list*: seq[Entry] 11 | 12 | proc newBruteSpace*(): BruteSpace = 13 | ## Creates a new brute-force space. 14 | result = BruteSpace() 15 | result.list = newSeq[Entry]() 16 | 17 | proc insert*(bs: BruteSpace, e: Entry) {.inline.} = 18 | ## Adds entry to the space. 19 | bs.list.add e 20 | 21 | proc finalize*(bs: BruteSpace) {.inline.} = 22 | ## Finishes the space and makes it ready for use. 23 | discard 24 | 25 | proc clear*(bs: BruteSpace) {.inline.} = 26 | ## Clears the spaces and makes it ready to be used again. 27 | bs.list.setLen(0) 28 | 29 | proc len*(bs: BruteSpace): int {.inline.} = 30 | ## Number of entries inserted 31 | bs.list.len 32 | 33 | iterator all*(bs: BruteSpace): Entry = 34 | ## Iterates all entries in a space. 35 | for e in bs.list: 36 | yield e 37 | 38 | iterator findInRangeApprox*(bs: BruteSpace, e: Entry, radius: float): Entry = 39 | ## Iterates all entries in range of an entry but does not cull them. 40 | ## Useful if you need distance anyways and will compute other computations. 41 | for thing in bs.list: 42 | yield thing 43 | 44 | iterator findInRange*(bs: BruteSpace, e: Entry, radius: float): Entry = 45 | ## Iterates all entries in range of an entry. 46 | let radiusSq = radius * radius 47 | for thing in bs.findInRangeApprox(e, radius): 48 | if e.id != thing.id and e.pos.distSq(thing.pos) < radiusSq: 49 | yield thing 50 | 51 | type SortSpace* = ref object 52 | ## Sort space sorts all entires on one axis X. 53 | ## Supposed to be good for very small ranges. 54 | list*: seq[Entry] 55 | 56 | proc newSortSpace*(): SortSpace = 57 | ## Creates a new sorted space. 58 | result = SortSpace() 59 | result.list = newSeq[Entry]() 60 | 61 | proc insert*(ss: SortSpace, e: Entry) {.inline.} = 62 | ## Adds entry to the space. 63 | ss.list.add e 64 | 65 | proc sortSpaceCmp(a, b: Entry): int = 66 | cmp(a.pos.x, b.pos.x) 67 | 68 | proc finalize*(ss: SortSpace) {.inline.} = 69 | ## Finishes the space and makes it ready for use. 70 | ss.list.sort(sortSpaceCmp) 71 | 72 | proc clear*(ss: SortSpace) {.inline.} = 73 | ## Clears the spaces and makes it ready to be used again. 74 | ss.list.setLen(0) 75 | 76 | proc len*(ss: SortSpace): int {.inline.} = 77 | ## Number of entries inserted. 78 | ss.list.len 79 | 80 | iterator all*(ss: SortSpace): Entry = 81 | ## Iterates all entries in a space. 82 | for e in ss.list: 83 | yield e 84 | 85 | iterator findInRangeApprox*(ss: SortSpace, e: Entry, radius: float): Entry = 86 | ## Iterates all entries in range of an entry but does not cull them. 87 | ## Useful if you need distance anyways and will compute other computations. 88 | let 89 | l = ss.list 90 | 91 | # find index of entry 92 | let index = ss.list.lowerBound(e, sortSpaceCmp) 93 | 94 | # scan to the right 95 | var right = index 96 | while right < l.len: 97 | let thing = l[right] 98 | if thing.pos.x - e.pos.x > radius: 99 | break 100 | if thing.id != e.id: 101 | yield thing 102 | inc right 103 | 104 | # scan to the left 105 | var left = index - 1 106 | while left >= 0: 107 | let thing = l[left] 108 | if e.pos.x - thing.pos.x > radius: 109 | break 110 | yield thing 111 | dec left 112 | 113 | iterator findInRange*(ss: SortSpace, e: Entry, radius: float): Entry = 114 | ## Iterates all entries in range of an entry. 115 | let radiusSq = radius * radius 116 | for thing in ss.findInRangeApprox(e, radius): 117 | if e.id != thing.id and e.pos.distSq(thing.pos) < radiusSq: 118 | yield thing 119 | 120 | type HashSpace* = ref object 121 | ## Divides space into little tiles that objects are hashed too. 122 | ## Supposed to be good for very uniform filled space. 123 | hash*: TableRef[(int32, int32), seq[Entry]] 124 | resolution*: float 125 | 126 | proc newHashSpace*(resolution: float): HashSpace = 127 | ## Creates a hash table space. 128 | result = HashSpace() 129 | result.hash = newTable[(int32, int32), seq[Entry]]() 130 | result.resolution = resolution 131 | 132 | proc hashSpaceKey(hs: HashSpace, e: Entry): (int32, int32) = 133 | (int32(floor(e.pos.x / hs.resolution)), int32(floor(e.pos.y / hs.resolution))) 134 | 135 | proc insert*(hs: HashSpace, e: Entry) = 136 | ## Adds entry to the space. 137 | let key = hs.hashSpaceKey(e) 138 | if key in hs.hash: 139 | hs.hash[key].add(e) 140 | else: 141 | hs.hash[key] = @[e] 142 | 143 | proc finalize*(hs: HashSpace) {.inline.} = 144 | ## Finishes the space and makes it ready for use. 145 | discard 146 | 147 | proc clear*(hs: HashSpace) {.inline.} = 148 | ## Clears the spaces and makes it ready to be used again. 149 | hs.hash.clear() 150 | 151 | proc len*(hs: HashSpace): int {.inline.} = 152 | ## Number of entries inserted 153 | for list in hs.hash.values: 154 | result += list.len 155 | 156 | iterator all*(hs: HashSpace): Entry = 157 | ## Iterates all entries in a space. 158 | for list in hs.hash.values: 159 | for e in list: 160 | yield e 161 | 162 | iterator findInRangeApprox*(hs: HashSpace, e: Entry, radius: float): Entry = 163 | ## Iterates all entries in range of an entry but does not cull them. 164 | ## Useful if you need distance anyways and will compute other computations. 165 | let 166 | d = int(radius / hs.resolution) + 1 167 | px = int(e.pos.x / hs.resolution) 168 | py = int(e.pos.y / hs.resolution) 169 | 170 | for x in -d .. d: 171 | for y in -d .. d: 172 | let 173 | rx = px + x 174 | ry = py + y 175 | if circle(e.pos, radius).overlaps(rect( 176 | float(rx) * hs.resolution, 177 | float(ry) * hs.resolution, 178 | hs.resolution, 179 | hs.resolution 180 | )): 181 | let posKey = (int32 rx, int32 ry) 182 | if posKey in hs.hash: 183 | for thing in hs.hash[posKey]: 184 | if thing.id != e.id: 185 | yield thing 186 | 187 | iterator findInRange*(hs: HashSpace, e: Entry, radius: float): Entry = 188 | ## Iterates all entries in range of an entry. 189 | let radiusSq = radius * radius 190 | for thing in hs.findInRangeApprox(e, radius): 191 | if e.id != thing.id and e.pos.distSq(thing.pos) < radiusSq: 192 | yield thing 193 | 194 | type 195 | QuadSpace* = ref object 196 | ## QuadTree, divide each node down if there is many elements. 197 | ## Supposed to be for large amount of entries. 198 | root*: QuadNode 199 | maxThings*: int # when should node divide 200 | maxLevels*: int # how many levels should node build 201 | 202 | QuadNode* = ref object 203 | things*: seq[Entry] 204 | nodes*: seq[QuadNode] 205 | bounds*: Rect 206 | level*: int 207 | 208 | proc newQuadNode(bounds: Rect, level: int): QuadNode = 209 | result = QuadNode() 210 | result.bounds = bounds 211 | result.level = level 212 | 213 | proc newQuadSpace*(bounds: Rect, maxThings = 10, maxLevels = 10): QuadSpace = 214 | ## Creates a new quad-tree space. 215 | result = QuadSpace() 216 | result.root = newQuadNode(bounds, 0) 217 | result.maxThings = maxThings 218 | result.maxLevels = maxLevels 219 | 220 | proc insert*(qs: QuadSpace, e: Entry) 221 | proc insert*(qs: QuadSpace, qn: var QuadNode, e: Entry) 222 | 223 | proc whichQuadrant(qs: QuadNode, e: Entry): int = 224 | let 225 | xMid = qs.bounds.x + qs.bounds.w/2 226 | yMid = qs.bounds.y + qs.bounds.h/2 227 | if e.pos.x < xMid: 228 | if e.pos.y < yMid: 229 | return 0 230 | else: 231 | return 1 232 | else: 233 | if e.pos.y < yMid: 234 | return 2 235 | else: 236 | return 3 237 | 238 | proc split(qs: QuadSpace, qn: var QuadNode) = 239 | let 240 | nextLevel = qn.level + 1 241 | x = qn.bounds.x 242 | y = qn.bounds.y 243 | w = qn.bounds.w/2 244 | h = qn.bounds.h/2 245 | qn.nodes = @[ 246 | newQuadNode(Rect(x: x, y: y, w: w, h: h), nextLevel), 247 | newQuadNode(Rect(x: x, y: y+h, w: w, h: h), nextLevel), 248 | newQuadNode(Rect(x: x+w, y: y, w: w, h: h), nextLevel), 249 | newQuadNode(Rect(x: x+w, y: y+h, w: w, h: h), nextLevel) 250 | ] 251 | for e in qn.things: 252 | let index = qn.whichQuadrant(e) 253 | qs.insert(qn.nodes[index], e) 254 | qn.things.setLen(0) 255 | 256 | proc insert(qs: QuadSpace, qn: var QuadNode, e: Entry) = 257 | if qn.nodes.len != 0: 258 | let index = qn.whichQuadrant(e) 259 | qs.insert(qn.nodes[index], e) 260 | else: 261 | qn.things.add e 262 | if qn.things.len > qs.maxThings and qn.level < qs.maxLevels: 263 | qs.split(qn) 264 | 265 | proc insert*(qs: QuadSpace, e: Entry) = 266 | ## Adds entry to the space. 267 | qs.insert(qs.root, e) 268 | 269 | proc finalize*(qs: QuadSpace) {.inline.} = 270 | ## Finishes the space and makes it ready for use. 271 | discard 272 | 273 | proc clear*(qs: QuadSpace) {.inline.} = 274 | ## Clears the spaces and makes it ready to be used again. 275 | qs.root.nodes.setLen(0) 276 | qs.root.things.setLen(0) 277 | 278 | proc len*(qs: QuadSpace): int {.inline.} = 279 | ## Number of entries inserted. 280 | var nodes = @[qs.root] 281 | while nodes.len > 0: 282 | var qs = nodes.pop() 283 | if qs.nodes.len == 4: 284 | for node in qs.nodes: 285 | nodes.add(node) 286 | else: 287 | result += qs.things.len 288 | 289 | iterator all*(qs: QuadSpace): Entry = 290 | ## Iterates all entries in a space. 291 | var nodes = @[qs.root] 292 | while nodes.len > 0: 293 | var qs = nodes.pop() 294 | if qs.nodes.len == 4: 295 | for node in qs.nodes: 296 | nodes.add(node) 297 | else: 298 | for e in qs.things: 299 | yield e 300 | 301 | iterator findInRangeApprox*(qs: QuadSpace, e: Entry, radius: float): Entry = 302 | ## Iterates all entries in range of an entry but does not cull them. 303 | ## Useful if you need distance anyways and will compute other computations. 304 | var nodes = @[qs.root] 305 | while nodes.len > 0: 306 | var qs = nodes.pop() 307 | if qs.nodes.len == 4: 308 | for node in qs.nodes: 309 | if circle(e.pos, radius).overlaps(node.bounds): 310 | nodes.add(node) 311 | else: 312 | for e in qs.things: 313 | yield e 314 | 315 | iterator findInRange*(qs: QuadSpace, e: Entry, radius: float): Entry = 316 | ## Iterates all entries in range of an entry. 317 | let radiusSq = radius * radius 318 | for thing in qs.findInRangeApprox(e, radius): 319 | if e.id != thing.id and e.pos.distSq(thing.pos) < radiusSq: 320 | yield thing 321 | 322 | type 323 | KdSpace* = ref object 324 | ## KD-Tree, each cell is divided vertically or horizontally. 325 | ## Supposed to be good for large amount of entries. 326 | root*: KdNode 327 | maxThings*: int # When should node divide? 328 | 329 | KdNode* = ref object 330 | things*: seq[Entry] 331 | nodes*: seq[KdNode] 332 | bounds*: Rect 333 | level*: int 334 | 335 | proc newKdNode(bounds: Rect, level: int): KdNode = 336 | result = KdNode() 337 | result.bounds = bounds 338 | result.level = level 339 | 340 | proc newKdSpace*(bounds: Rect, maxThings = 10, maxLevels = 10): KdSpace = 341 | ## Creates a new space based on kd-tree. 342 | result = KdSpace() 343 | result.root = newKdNode(bounds, 0) 344 | result.maxThings = maxThings 345 | 346 | proc insert*(ks: KdSpace, e: Entry) {.inline.} = 347 | ## Adds entry to the space. 348 | ks.root.things.add e 349 | 350 | proc finalize(ks: KdSpace, kn: KdNode) = 351 | if kn.things.len > ks.maxThings: 352 | var axis = 353 | if kn.bounds.w > kn.bounds.h: 0 354 | else: 1 355 | kn.things.sort proc(a, b: Entry): int = cmp(a.pos[axis], b.pos[axis]) 356 | let 357 | b = kn.bounds 358 | arr1 = kn.things[0 ..< kn.things.len div 2] 359 | arr2 = kn.things[kn.things.len div 2 .. ^1] 360 | mid = arr1[^1].pos[axis] 361 | var 362 | node1: KdNode 363 | node2: KdNode 364 | if axis == 0: 365 | let midW = mid - b.x 366 | node1 = newKdNode(rect(b.x, b.y, midW, b.h), kn.level + 1) 367 | node2 = newKdNode(rect(mid, b.y, b.w - midW, b.h), kn.level + 1) 368 | else: 369 | let midH = mid - b.y 370 | node1 = newKdNode(rect(b.x, b.y, b.w, midH), kn.level + 1) 371 | node2 = newKdNode(rect(b.x, mid, b.w, b.h - midH), kn.level + 1) 372 | node1.things = arr1 373 | node2.things = arr2 374 | ks.finalize(node1) 375 | ks.finalize(node2) 376 | kn.things.setLen(0) 377 | kn.nodes = @[node1, node2] 378 | 379 | proc finalize*(ks: KdSpace) = 380 | ## Finishes the space and makes it ready for use. 381 | ks.finalize(ks.root) 382 | 383 | proc clear*(ks: KdSpace) {.inline.} = 384 | ## Clears the spaces and makes it ready to be used again. 385 | ks.root.things.setLen(0) 386 | ks.root.nodes.setLen(0) 387 | 388 | proc len*(ks: KdSpace): int = 389 | ## Number of entries inserted. 390 | var nodes = @[ks.root] 391 | while nodes.len > 0: 392 | var kn = nodes.pop() 393 | if kn.nodes.len == 2: 394 | for node in kn.nodes: 395 | nodes.add(node) 396 | else: 397 | result += kn.things.len 398 | 399 | iterator all*(ks: KdSpace): Entry = 400 | ## Iterates all entries in a space. 401 | var nodes = @[ks.root] 402 | while nodes.len > 0: 403 | var kn = nodes.pop() 404 | if kn.nodes.len == 2: 405 | for node in kn.nodes: 406 | nodes.add(node) 407 | else: 408 | for e in kn.things: 409 | yield e 410 | 411 | iterator findInRangeApprox*(ks: KdSpace, e: Entry, radius: float): Entry = 412 | ## Iterates all entries in range of an entry but does not cull them. 413 | ## Useful if you need distance anyways and will compute other computations. 414 | var nodes = @[ks.root] 415 | while nodes.len > 0: 416 | var kn = nodes.pop() 417 | if kn.nodes.len == 2: 418 | for node in kn.nodes: 419 | if circle(e.pos, radius).overlaps(node.bounds): 420 | nodes.add(node) 421 | else: 422 | for e in kn.things: 423 | yield e 424 | 425 | iterator findInRange*(ks: KdSpace, e: Entry, radius: float): Entry = 426 | ## Iterates all entries in range of an entry. 427 | let radiusSq = radius * radius 428 | for thing in ks.findInRangeApprox(e, radius): 429 | if e.id != thing.id and e.pos.distSq(thing.pos) < radiusSq: 430 | yield thing 431 | -------------------------------------------------------------------------------- /tests/bench.nim: -------------------------------------------------------------------------------- 1 | import benchy, bumpy, random, spacy, vmath 2 | 3 | proc makeEntries(num: int): seq[Entry] = 4 | var rand = initRand(1988) 5 | for i in 0 ..< num: 6 | result.add Entry(id: uint32 i, pos: rand.randVec2()) 7 | 8 | template testSetup(num: int, name: string, newSpace: untyped) = 9 | var entries = makeEntries(num) 10 | timeIt $num & " " & name & " setup", 100: 11 | var space = newSpace 12 | for e in entries: 13 | space.insert(e) 14 | space.finalize() 15 | keep(space) 16 | 17 | for num in [10, 100, 1000]: 18 | testSetup(num, "BruteSpace", newBruteSpace()) 19 | testSetup(num, "SortSpace", newSortSpace()) 20 | testSetup(num, "HashSpace", newHashSpace(0.25)) 21 | testSetup(num, "QuadSpace", newQuadSpace(rect(-1.0, -1.0, 1.0, 1.0))) 22 | testSetup(num, "KdSpace", newKdSpace(rect(-1.0, -1.0, 1.0, 1.0))) 23 | 24 | template testScan(num: int, dist: float32, name: string, newSpace: untyped) = 25 | var entries = makeEntries(num) 26 | 27 | timeIt name, 100: 28 | var space = newSpace 29 | for e in entries: 30 | space.insert(e) 31 | space.finalize() 32 | 33 | for me in entries: 34 | for other in space.findInRange(me, dist): 35 | keep(other) 36 | 37 | for num in [10, 100, 1000]: 38 | for dist in [0.001, 0.1, 1.00]: 39 | echo num, " entries at ", dist, " distance:" 40 | testScan(num, dist, "BruteSpace", newBruteSpace()) 41 | testScan(num, dist, "SortSpace", newSortSpace()) 42 | testScan(num, dist, "HashSpace", newHashSpace(dist)) 43 | testScan(num, dist, "QuadSpace", newQuadSpace(rect(-1.0, -1.0, 1.0, 1.0))) 44 | testScan(num, dist, "KdSpace", newKdSpace(rect(-1.0, -1.0, 1.0, 1.0))) 45 | 46 | template benchApprox(num: int, dist: float32, name: string, newSpace: untyped) = 47 | var entries = makeEntries(num) 48 | var space = newSpace 49 | for e in entries: 50 | space.insert(e) 51 | space.finalize() 52 | 53 | timeIt name & " exact", 100: 54 | for me in entries: 55 | for other in space.findInRange(me, dist): 56 | keep(other) 57 | 58 | timeIt name & " approx", 100: 59 | for me in entries: 60 | for other in space.findInRangeApprox(me, dist): 61 | keep(other) 62 | 63 | block: 64 | let num = 1000 65 | let dist = 0.1 66 | echo num, " entries at ", dist, " distance:" 67 | benchApprox(num, dist, "BruteSpace", newBruteSpace()) 68 | benchApprox(num, dist, "SortSpace", newSortSpace()) 69 | benchApprox(num, dist, "HashSpace", newHashSpace(dist)) 70 | benchApprox(num, dist, "QuadSpace", newQuadSpace(rect(-1.0, -1.0, 1.0, 1.0))) 71 | benchApprox(num, dist, "KdSpace", newKdSpace(rect(-1.0, -1.0, 1.0, 1.0))) 72 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | --path:"../src" 2 | -------------------------------------------------------------------------------- /tests/test.nim: -------------------------------------------------------------------------------- 1 | import bumpy, random, spacy, vmath 2 | 3 | randomize(2021) 4 | 5 | proc randVec2*(r: var Rand): Vec2 = 6 | let a = r.rand(PI * 2) 7 | let v = r.rand(1.0) 8 | vec2(cos(a) * v, sin(a) * v) 9 | 10 | template testSpace(name: string, space: untyped) = 11 | let a = Entry(id: 1, pos: vec2(0, 0)) 12 | space.insert a 13 | space.insert Entry(id: 2, pos: vec2(0.01, 0)) 14 | space.insert Entry(id: 3, pos: vec2(0.1, 0)) 15 | space.insert Entry(id: 4, pos: vec2(0, 0.2)) 16 | space.finalize() 17 | doAssert space.len == 4 18 | 19 | block: 20 | # in range 0.02 21 | var numFinds = 0 22 | for other in space.findInRange(a, 0.02): 23 | doAssert other.id == 2 24 | inc numFinds 25 | doAssert numFinds == 1 26 | 27 | block: 28 | # in range 0.12 29 | var numFinds = 0 30 | for other in space.findInRange(a, 0.12): 31 | doAssert other.id in [2.uint32, 3] 32 | inc numFinds 33 | doAssert numFinds == 2 34 | 35 | var rand = initRand(1988) 36 | 37 | space.clear() 38 | 39 | var at: Entry 40 | for i in 0 .. 1000: 41 | let e = Entry(id: uint32 i, pos: rand.randVec2()) 42 | space.insert e 43 | if i == 0: 44 | at = e 45 | space.finalize() 46 | doAssert space.len == 1001 47 | 48 | block: 49 | # in range 0.02 50 | var numFinds = 0 51 | for other in space.findInRange(a, 0.02): 52 | inc numFinds 53 | doAssert numFinds == 23 54 | 55 | block: 56 | # in range 0.12 57 | var numFinds = 0 58 | for other in space.findInRange(a, 0.12): 59 | inc numFinds 60 | doAssert numFinds == 125 61 | 62 | var bs = newBruteSpace() 63 | testSpace("BruteSpace", bs) 64 | 65 | var ss = newSortSpace() 66 | testSpace("SortSpace", ss) 67 | 68 | var hs = newHashSpace(0.1) 69 | testSpace("HashSpace", hs) 70 | 71 | var qs = newQuadSpace(rect(-1.0, -1.0, 2.0, 2.0)) 72 | testSpace("QuadSpace", qs) 73 | 74 | var ks = newKdSpace(rect(-1.0, -1.0, 2.0, 2.0)) 75 | testSpace("KdSpace", ks) 76 | -------------------------------------------------------------------------------- /tools/gen_readme.nim: -------------------------------------------------------------------------------- 1 | import pixie, random, spacy, strformat, tables, times, vmath 2 | 3 | let radius = 100.0 4 | 5 | proc strokeCircle*( 6 | image: Image, 7 | center: Vec2, 8 | radius: float32, 9 | color: ColorRGBA, 10 | strokeWidth: float32 = 1.0, 11 | blendMode = bmNormal 12 | ) = 13 | var path: Path 14 | path.ellipse(center, radius, radius) 15 | image.strokePath(path, color, blendMode=blendMode, strokeWidth=strokeWidth) 16 | 17 | proc strokeRect*( 18 | image: Image, 19 | rect: Rect, 20 | color: ColorRGBA, 21 | strokeWidth: float32 = 1.0, 22 | blendMode = bmNormal 23 | ) = 24 | var path: Path 25 | path.rect(rect) 26 | image.strokePath(path, color, blendMode=blendMode, strokeWidth=strokeWidth) 27 | 28 | proc drawInternal(image: Image, bs: BruteSpace, at: Entry) = 29 | discard 30 | 31 | proc drawInternal(image: Image, ss: SortSpace, at: Entry) = 32 | image.drawRect( 33 | rect(at.pos.x - radius, 0, radius*2, 1000), 34 | rgba(255, 255, 255, 25).toPremultipliedAlpha() 35 | ) 36 | 37 | proc drawInternal(image: Image, qn: QuadNode, at: Entry) = 38 | for node in qn.nodes: 39 | let b = node.bounds 40 | if node.nodes.len == 0 and overlaps(circle(at.pos, radius), b): 41 | image.drawRect(b, rgba(255, 255, 255, 25).toPremultipliedAlpha()) 42 | else: 43 | image.strokeRect(b, rgba(255, 255, 255, 25).toPremultipliedAlpha()) 44 | image.drawInternal(node, at) 45 | 46 | proc drawInternal(image: Image, qs: QuadSpace, at: Entry) = 47 | image.drawInternal(qs.root, at) 48 | 49 | proc drawInternal(image: Image, kn: KdNode, at: Entry) = 50 | for node in kn.nodes: 51 | let b = node.bounds 52 | if node.nodes.len == 0 and overlaps(circle(at.pos, radius), b): 53 | image.drawRect(b, rgba(255, 255, 255, 25).toPremultipliedAlpha()) 54 | else: 55 | image.strokeRect(b, rgba(255, 255, 255, 25).toPremultipliedAlpha()) 56 | 57 | image.drawInternal(node, at) 58 | 59 | proc drawInternal(image: Image, ks: KdSpace, at: Entry) = 60 | image.drawInternal(ks.root, at) 61 | 62 | proc drawInternal(image: Image, hs: HashSpace, at: Entry) = 63 | let r = hs.resolution 64 | for key in hs.hash.keys: 65 | let b = rect(float32(key[0])*r, float32(key[1])*r, r, r) 66 | if overlaps(circle(at.pos, radius), b): 67 | image.drawRect(b, rgba(255, 255, 255, 25).toPremultipliedAlpha()) 68 | else: 69 | image.strokeRect(b, rgba(255, 255, 255, 25).toPremultipliedAlpha()) 70 | 71 | template testSpace(name: string, space: untyped) = 72 | echo "---------------- ", name 73 | 74 | var at: Entry 75 | 76 | var rand = initRand(2021) 77 | 78 | for i in 0 ..< 1000: 79 | let e = Entry(id: uint32 i, pos: rand.randVec2() * 500 + vec2(500, 500)) 80 | space.insert e 81 | if i == 0: 82 | at = e 83 | space.finalize() 84 | 85 | var image = newImage(1000, 1000) 86 | image.fill(color(0.11, 0.14, 0.42).rgba) 87 | 88 | image.drawInternal(space, at) 89 | 90 | for e in space.all: 91 | image.drawCircle(e.pos, 1, rgba(255, 255, 255, 255)) 92 | 93 | for e in space.findInRangeApprox(at, radius): 94 | image.drawCircle(e.pos, 2, rgba(255, 255, 255, 255)) 95 | 96 | for e in space.findInRange(at, radius): 97 | image.drawCircle(e.pos, 3, rgba(0, 255, 0, 255)) 98 | 99 | image.drawCircle(at.pos, 3, rgba(255, 0, 0, 255)) 100 | image.strokeCircle(at.pos, radius, rgba(255, 255, 255, 255)) 101 | 102 | image.writeFile("examples/" & name & ".png") 103 | 104 | var bs = newBruteSpace() 105 | testSpace("BruteSpace", bs) 106 | 107 | var ss = newSortSpace() 108 | testSpace("SortSpace", ss) 109 | 110 | var hs = newHashSpace(radius) 111 | testSpace("HashSpace", hs) 112 | 113 | var qs = newQuadSpace(rect(0, 0, 1000.0, 1000.0)) 114 | testSpace("QuadSpace", qs) 115 | 116 | var ks = newKdSpace(rect(0, 0, 1000.0, 1000.0)) 117 | testSpace("KdSpace", ks) 118 | --------------------------------------------------------------------------------