App/Vision/MoonriseVisionApp.swift
103 lines · 4461 bytes
4 symbols in this file
1import Library
2import RealityKit
3import SkyEngine
4import SkyRenderer
5import SwiftUI
6
7@main
8struct MoonriseVisionApp: App {
9 var body: some SwiftUI.Scene {
10 WindowGroup { VisionHome() }
11
12 WindowGroup(id: "orrery") { OrreryVolume() }
13 .windowStyle(.volumetric)
14 .defaultSize(width: 0.8, height: 0.4, depth: 0.8, in: .meters)
15
16 ImmersiveSpace(id: "sky") { ImmersiveSky() }
17 }
18}
19
20/// Library globes, plus buttons for the orrery volume and the immersive sky.
21struct VisionHome: View {
22 @Environment(\.openWindow) private var openWindow
23 @Environment(\.openImmersiveSpace) private var openImmersiveSpace
24 @Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace
25 @State private var immersed = false
26
27 private var bodies: [LibraryPage] {
28 [LibrarySection.sun, .planets, .dwarfPlanets, .moons].flatMap { Library.pages(in: $0) }
29 }
30
31 var body: some View {
32 NavigationSplitView {
33 List(bodies) { page in
34 NavigationLink(page.title) { GlobeView(page: page, credit: GlobeMaps.credit[page.id]) }
35 }
36 .navigationTitle("Moonrise")
37 .toolbar {
38 ToolbarItem(placement: .bottomOrnament) {
39 HStack {
40 Button("Orrery") { openWindow(id: "orrery") }
41 Button(immersed ? "Leave the sky" : "Tonight's sky") {
42 Task {
43 if immersed { await dismissImmersiveSpace() } else { _ = await openImmersiveSpace(id: "sky") }
44 immersed.toggle()
45 }
46 }
47 }
48 }
49 }
50 } detail: {
51 Text("Choose a body to see it as a globe.").foregroundStyle(.secondary)
52 }
53 }
54}
55
56/// Outside the view, where `Body` is SkyEngine's rather than the view's body type.
57private let orreryPlanets = Body.planets
58
59/// The Sun and planets at today's heliocentric positions. Distances are square-rooted so Neptune fits.
60struct OrreryVolume: View {
61 var body: some View {
62 RealityView { content in
63 let sun = ModelEntity(mesh: .generateSphere(radius: 0.025), materials: [UnlitMaterial(color: .yellow)])
64 content.add(sun)
65 for body in orreryPlanets {
66 guard let p = try? SkyEngine.heliocentricEcliptic(body, at: .now) else { continue }
67 let au = (p * p).sum().squareRoot()
68 let scale = 0.065 * au.squareRoot() / au
69 let page = Library.page(body: body.rawValue)
70 let colour = UIColor(Color(hex: page?.drawing?.color) ?? .white)
71 let planet = ModelEntity(mesh: .generateSphere(radius: 0.008), materials: [SimpleMaterial(color: colour, isMetallic: false)])
72 // Ecliptic x, y in the horizontal plane; ecliptic north up.
73 planet.position = [Float(p.x * scale), Float(p.z * scale), Float(-p.y * scale)]
74 content.add(planet)
75 }
76 }
77 }
78}
79
80/// Stars brighter than magnitude 4.5 on a 20 m sphere, placed for now from Chicago,
81/// with the horizon as a faint disc.
82struct ImmersiveSky: View {
83 var body: some View {
84 RealityView { content in
85 let observer = Observer(latitude: 41.8781, longitude: -87.6298)
86 let stars = Catalog.stars.filter { $0.magnitude < 4.5 }
87 let positions = SkyEngine.horizontal(j2000: stars.map { $0.position(at: .now) }, at: .now, from: observer)
88 let mesh = MeshResource.generateSphere(radius: 0.04)
89 for (star, h) in zip(stars, positions) where h.altitude > -2 {
90 let alt = Float(h.altitude * .pi / 180), az = Float(h.azimuth * .pi / 180)
91 let r: Float = 20
92 let s = ModelEntity(mesh: mesh, materials: [UnlitMaterial(color: .white)])
93 s.scale = .init(repeating: max(0.3, Float(2.2 - 0.4 * star.magnitude)))
94 // Azimuth from north (−z) towards east (+x).
95 s.position = [r * cos(alt) * sin(az), r * sin(alt), -r * cos(alt) * cos(az)]
96 content.add(s)
97 }
98 let ground = ModelEntity(mesh: .generatePlane(width: 40, depth: 40), materials: [UnlitMaterial(color: UIColor(white: 0.05, alpha: 0.9))])
99 ground.position.y = -0.01
100 content.add(ground)
101 }
102 }
103}