# Neighbors

a SpatialIndex over four thousand points answers three queries each frame: which point is nearest, which points are within a given distance, and which are inside a sliding box. Every point is also joined to its nearest neighbor

Run it with `swift run --package-path Examples Example-Shapes-Neighbors`.

```swift
import Ollin

/// Four thousand points, and three questions asked of them every frame: which
/// one is nearest, which ones are within reach, and which ones are inside a box.
/// A `SpatialIndex` answers all three without looking at the other 3,999.
///
/// The faint web is the same idea at rest: every point joined to its own nearest
/// neighbor, four thousand searches done once in `setup()`.
///
/// ```sh
/// swift run Example-Shapes-Neighbors --export-loop /tmp/neighbors.gif
/// ```
///
/// Hold the mouse down to drive the probe yourself.
@main
final class Neighbors_Example: Sketch {
    private let period = 14.0
    override var loopDuration: Double? { period }

    private var points: [Vector2] = []
    private var index = SpatialIndex([])
    private var web: [(Vector2, Vector2)] = []

    override func setup() {
        seed(11)
        points = poissonDisk(radius: 14)
        index = SpatialIndex(points)

        // Every point joined to its nearest neighbor. Four thousand searches,
        // each reading a handful of cells instead of the whole scatter.
        for i in points.indices {
            // Two nearest, because the nearest point to a point is itself.
            let near = index.kNearest(2, to: points[i])
            if let other = near.first(where: { $0 != i }) {
                web.append((points[i], points[other]))
            }
        }
    }

    override func draw() {
        background(Color(hex: 0x0E1117))

        let probe = mouseIsPressed
            ? mouse
            : center + Vector2(cos(time * 0.7) * 300, sin(time * 0.9) * 260)
        let reach = 92 + sin(time * 1.3) * 42

        // The resting answer: each point to its nearest neighbor.
        stroke(Color(hex: 0x232B3A))
        strokeWeight(1 * scale)
        for (a, b) in web { drawLine(a, b) }

        // A box sliding across, holding whatever is inside it.
        let boxWidth = 250.0, boxHeight = 170.0
        let slide = smoothstep(0, 1, pingPong(over: period))
        let box = Rectangle(x: 70 + slide * (width - boxWidth - 140),
                            y: height - boxHeight - 90,
                            width: boxWidth, height: boxHeight)
        noFill()
        stroke(Color(hex: 0x3E5C7E))
        strokeWeight(1.5 * scale)
        drawRect(corner: box.corner, width: box.width, height: box.height)
        let inBox = index.indices(in: box)
        noStroke()
        fill(Color(hex: 0x5B8FB9))
        for i in inBox { drawCircle(center: points[i], radius: 3.4 * scale) }

        // Everything within reach of the probe, each one drawn back to it.
        let within = index.neighbors(of: probe, within: reach)
        let reached = Set(within)
        stroke(Color(hex: 0xE8B44A).withAlpha(0.5))
        strokeWeight(1 * scale)
        for i in within { drawLine(probe, points[i]) }
        noStroke()
        fill(Color(hex: 0xE8B44A))
        for i in within { drawCircle(center: points[i], radius: 3.6 * scale) }

        // The reach itself.
        noFill()
        stroke(Color(hex: 0x4A5568))
        strokeWeight(1 * scale)
        drawCircle(center: probe, radius: reach)

        // The scatter that is left over, so the answers read against something.
        noStroke()
        fill(Color(hex: 0x39404E))
        for i in points.indices where !reached.contains(i) {
            drawCircle(center: points[i], radius: 2.2 * scale)
        }

        // The single nearest point, ringed last so the fan cannot hide it.
        if let nearest = index.nearest(to: probe) {
            noFill()
            stroke(Color(hex: 0xF2EFE8))
            strokeWeight(2 * scale)
            drawCircle(center: points[nearest], radius: 11 * scale)
        }
        noStroke()
        fill(Color(hex: 0xF2EFE8))
        drawCircle(center: probe, radius: 5 * scale)

        drawCaption("\(points.count) points: the nearest one, "
                    + "\(within.count) within reach, \(inBox.count) in the box")
    }
}
```
