import os, datetime, math, random
import numpy as np
import customtkinter as ctk
import tkinter as tk
from tkinter import messagebox
from skyfield.api import Loader
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
class Menu16App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Sistem Tata Surya 3D Geosentris (Modul 16)")
self.geometry("1100x700")
ctk.set_appearance_mode("Dark")
self.load_obj = Loader(BASE_DIR)
self.eph = self.load_obj('de421.bsp')
self.ts = self.load_obj.timescale()
self.setup_ui()
self.is_viewing_3d = True
self.update_3d_animation()
def setup_ui(self):
self.sidebar = ctk.CTkFrame(self, width=250)
self.sidebar.pack(side="left", fill="y", padx=10, pady=10)
now = datetime.datetime.now()
ctk.CTkLabel(self.sidebar, text="SIMULASI 3D\n(Geosentris)", font=("Segoe UI", 16, "bold"), text_color="#00E5FF").pack(pady=20)
self.entry_y = ctk.CTkEntry(self.sidebar, placeholder_text="Tahun"); self.entry_y.insert(0, str(now.year)); self.entry_y.pack(pady=5)
self.entry_m = ctk.CTkEntry(self.sidebar, placeholder_text="Bulan"); self.entry_m.insert(0, str(now.month)); self.entry_m.pack(pady=5)
self.entry_d = ctk.CTkEntry(self.sidebar, placeholder_text="Tanggal"); self.entry_d.insert(0, str(now.day)); self.entry_d.pack(pady=5)
ctk.CTkButton(self.sidebar, text="▶ TERAPKAN WAKTU", command=lambda: messagebox.showinfo("Update", "Waktu 3D telah diubah.")).pack(pady=20)
ctk.CTkLabel(self.sidebar, text="Navigasi Mouse:\n- Klik Kiri & Geser: Rotasi\n- Scroll Wheel: Zoom", text_color="yellow").pack(pady=20)
# Main 3D Canvas
self.main_frame = ctk.CTkFrame(self, fg_color="#020205")
self.main_frame.pack(side="right", fill="both", expand=True, padx=10, pady=10)
self.anim_3d_canvas = tk.Canvas(self.main_frame, bg='#010105', highlightthickness=0)
self.anim_3d_canvas.pack(fill="both", expand=True, padx=15, pady=15)
# Inisialisasi Kamera
self.cam_angle_x = 0.8
self.cam_angle_y = 0.3
self.cam_zoom = 1.0
self.anim_3d_canvas.bind("<B1-Motion>", self.rotate_3d_view)
self.anim_3d_canvas.bind("<MouseWheel>", self.on_mouse_wheel_3d)
self.anim_3d_canvas.bind("<Button-4>", self.on_mouse_wheel_3d)
self.anim_3d_canvas.bind("<Button-5>", self.on_mouse_wheel_3d)
def rotate_3d_view(self, event):
w, h = self.anim_3d_canvas.winfo_width(), self.anim_3d_canvas.winfo_height()
if w > 0 and h > 0:
self.cam_angle_x = (event.x / w) * 2 * math.pi
self.cam_angle_y = (event.y / h) * math.pi
def on_mouse_wheel_3d(self, event):
if str(event.type) == 'MouseWheel':
if event.delta > 0: self.cam_zoom *= 1.2
else: self.cam_zoom *= 0.8
else:
if getattr(event, 'num', 0) == 4: self.cam_zoom *= 1.2
elif getattr(event, 'num', 0) == 5: self.cam_zoom *= 0.8
self.cam_zoom = max(0.1, min(12.0, self.cam_zoom)) # Batasi Zoom
def project_3d(self, x, y, z, width, height):
# Rotasi Matriks X dan Y
x1 = x * math.cos(self.cam_angle_x) - z * math.sin(self.cam_angle_x)
z1 = x * math.sin(self.cam_angle_x) + z * math.cos(self.cam_angle_x)
y2 = y * math.cos(self.cam_angle_y) - z1 * math.sin(self.cam_angle_y)
z2 = y * math.sin(self.cam_angle_y) + z1 * math.cos(self.cam_angle_y)
factor = 500 / (500 + z2) if (500 + z2) != 0 else 1
px = x1 * factor + (width / 2)
py = -y2 * factor + (height / 2)
return px, py, z2
def update_3d_animation(self):
if not self.is_viewing_3d: return
canvas = self.anim_3d_canvas
w, h = canvas.winfo_width(), canvas.winfo_height()
if w <= 1:
self.after(200, self.update_3d_animation)
return
canvas.delete("all")
# Bintang statis background
random.seed(42)
for _ in range(100):
rx, ry = random.randint(0, w), random.randint(0, h)
canvas.create_oval(rx, ry, rx+1, ry+1, fill="#555555", outline="")
try:
y, m, d = int(self.entry_y.get()), int(self.entry_m.get()), int(self.entry_d.get())
now = datetime.datetime.now()
t_sim = self.ts.utc(y, m, d, now.hour, now.minute, now.second)
earth_obj = self.eph['earth']
celestial_bodies = [
('moon', "#ECEFF1", 15, 180, "BULAN"),
('mercury', "#B0BEC5", 10, 230, "MERKURIUS"),
('venus', "#FFCC80", 18, 290, "VENUS"),
('sun', "#FFD600", 50, 350, "MATAHARI"),
('mars', "#EF5350", 16, 450, "MARS"),
('jupiter', "#FFB74D", 35, 600, "JUPITER")
]
draw_list = []
# Bumi di tengah
p_earth = self.project_3d(0, 0, 0, w, h)
draw_list.append(('earth', p_earth, "#2196F3", 30 * self.cam_zoom, "BUMI"))
# Benda Langit Lainnya
for name, color, size, vis_dist, label in celestial_bodies:
try:
target_key = f'{name} barycenter' if name not in ['sun', 'moon'] else name
target_obj = self.eph[target_key]
pos_km = earth_obj.at(t_sim).observe(target_obj).position.km
dist_km = np.linalg.norm(pos_km)
px, py, pz = (pos_km / dist_km) * (vis_dist * self.cam_zoom)
proj_p = self.project_3d(px, py, pz, w, h)
draw_list.append((name, proj_p, color, size * self.cam_zoom, label))
except Exception: pass
# Algoritma Z-Buffer: Urutkan objek dari yang paling belakang (pz terbesar)
draw_list.sort(key=lambda x: x[1][2], reverse=True)
for tag, p, color, size, label in draw_list:
canvas.create_oval(p[0]-size, p[1]-size, p[0]+size, p[1]+size, fill=color, outline="white" if tag=='earth' else "")
if label and size > 4:
font_size = max(7, int(9 * min(self.cam_zoom, 1.5)))
canvas.create_text(p[0], p[1]+size+12, text=label, fill="white", font=("Consolas", font_size, "bold"))
# Garis Orbit
r_moon, r_sun = 180 * self.cam_zoom, 350 * self.cam_zoom
canvas.create_oval(w/2-r_moon, h/2-r_moon, w/2+r_moon, h/2+r_moon, outline="#222")
canvas.create_oval(w/2-r_sun, h/2-r_sun, w/2+r_sun, h/2+r_sun, outline="#222")
info = f"3D GEOCENTRIC (LIVE)\nZoom Level: {self.cam_zoom:.1f}x\nTime: {now.strftime('%H:%M:%S')}"
canvas.create_text(20, 20, text=info, fill="#00E5FF", font=("Consolas", 12, "bold"), anchor="nw")
except Exception as e:
canvas.create_text(w/2, h/2, text=f"Sedang mengkalkulasi vektor ruang... {e}", fill="gray")
self.after(50, self.update_3d_animation)
if __name__ == "__main__":
app = Menu16App()
app.mainloop()