Packages/MoonriseKit/Sources/SkyRenderer/HitTest.swift
45 lines · 1730 bytes
1import CoreGraphics
2import SkyEngine
3
4public enum SkyObject: Sendable, Equatable {
5 case body(Body)
6 /// IAU abbreviation.
7 case constellation(String)
8 /// Messier number.
9 case deepSky(Int)
10}
11
12extension SkyScene {
13 /// The object under a tap: the nearest visible sun, moon or planet within `radius` points,
14 /// else the nearest Messier object within 0.6 × `radius` (when `deepSky`),
15 /// else the constellation whose figure has a star nearest the tap.
16 public func object(at point: CGPoint, projection p: Projection, radius: CGFloat = 28, deepSky: Bool = true) -> SkyObject? {
17 func distance(_ h: Horizontal) -> CGFloat? {
18 guard h.altitude > 0, let q = p.point(h) else { return nil }
19 return hypot(q.x - point.x, q.y - point.y)
20 }
21 let bodyHit = bodies
22 .filter { $0.magnitude <= 6 }
23 .compactMap { b in distance(b.position).map { (b.body, $0) } }
24 .filter { $0.1 <= radius }
25 .min { $0.1 < $1.1 }
26 if let bodyHit { return .body(bodyHit.0) }
27
28 if deepSky {
29 let hit = self.deepSky
30 .compactMap { d in distance(d.position).map { (d.object.messier, $0) } }
31 .filter { $0.1 <= radius * 0.6 }
32 .min { $0.1 < $1.1 }
33 if let hit { return .deepSky(hit.0) }
34 }
35
36 var best: (String, CGFloat)?
37 for c in Catalog.constellations {
38 for hr in c.lines.joined() {
39 guard let h = starsByHR[hr], let d = distance(h), d <= radius * 1.5 else { continue }
40 if best == nil || d < best!.1 { best = (c.id, d) }
41 }
42 }
43 return best.map { .constellation($0.0) }
44 }
45}