package main
import (
"encoding/json"
"fmt"
"html/template"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
var currentParams = TorusParams{
MajorRadius: 2.0,
MinorRadius: 0.5,
MajorSegments: 50,
MinorSegments: 30,
RotationSpeedX: 0.3,
RotationSpeedY: 0.5,
}
var currentAsteroidParams = AsteroidParams{
SpawnRate: 1.0,
MaxAsteroids: 500,
MinSize: 0.05,
MaxSize: 0.3,
MinSpeed: 0.5,
MaxSpeed: 3.0,
RotationSpeed: 1.0,
}
var currentScene = "donut" // "donut" or "asteroids"
// Global CUDA status for WebSocket broadcasting
var globalCudaEnabled *bool
// Status WebSocket broadcaster
type StatusBroadcaster struct {
clients map[*websocket.Conn]bool
register chan *websocket.Conn
unregister chan *websocket.Conn
broadcast chan StatusData
mutex sync.RWMutex
}
type StatusData struct {
CudaEnabled bool `json:"cudaEnabled"`
Params TorusParams `json:"params"`
AsteroidParams AsteroidParams `json:"asteroidParams"`
CurrentScene string `json:"currentScene"`
FPS float64 `json:"fps"`
WindowWidth int `json:"windowWidth"`
WindowHeight int `json:"windowHeight"`
}
var statusBroadcaster *StatusBroadcaster
var statusUpgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
return origin == "" || origin == "http://localhost:8080" || origin == "http://127.0.0.1:8080"
},
}
func newStatusBroadcaster() *StatusBroadcaster {
return &StatusBroadcaster{
clients: make(map[*websocket.Conn]bool),
register: make(chan *websocket.Conn),
unregister: make(chan *websocket.Conn),
broadcast: make(chan StatusData),
}
}
func (sb *StatusBroadcaster) run() {
ticker := time.NewTicker(100 * time.Millisecond) // Update 10 times per second
defer ticker.Stop()
for {
select {
case client := <-sb.register:
sb.mutex.Lock()
sb.clients[client] = true
sb.mutex.Unlock()
LogInfo("StatusWS", "New client connected to status stream")
// Send initial status to new client
sb.sendStatusToClient(client)
case client := <-sb.unregister:
sb.mutex.Lock()
if _, ok := sb.clients[client]; ok {
delete(sb.clients, client)
client.Close()
}
sb.mutex.Unlock()
LogInfo("StatusWS", "Client disconnected from status stream")
case <-ticker.C:
// Broadcast status updates to all clients
sb.broadcastStatus()
}
}
}
func (sb *StatusBroadcaster) sendStatusToClient(client *websocket.Conn) {
status := StatusData{
CudaEnabled: globalCudaEnabled != nil && *globalCudaEnabled,
Params: currentParams,
AsteroidParams: currentAsteroidParams,
CurrentScene: currentScene,
FPS: GetCurrentFPS(),
WindowWidth: func() int { w, _ := GetWindowSize(); return w }(),
WindowHeight: func() int { _, h := GetWindowSize(); return h }(),
}
if err := client.WriteJSON(status); err != nil {
sb.unregister <- client
}
}
func (sb *StatusBroadcaster) broadcastStatus() {
status := StatusData{
CudaEnabled: globalCudaEnabled != nil && *globalCudaEnabled,
Params: currentParams,
AsteroidParams: currentAsteroidParams,
CurrentScene: currentScene,
FPS: GetCurrentFPS(),
WindowWidth: func() int { w, _ := GetWindowSize(); return w }(),
WindowHeight: func() int { _, h := GetWindowSize(); return h }(),
}
sb.mutex.RLock()
defer sb.mutex.RUnlock()
for client := range sb.clients {
if err := client.WriteJSON(status); err != nil {
go func(c *websocket.Conn) {
sb.unregister <- c
}(client)
}
}
}
const htmlTemplate = `
CUDA-Accelerated 3D Visualization Control Panel
🎮 3D Visualization Control Panel
🍩 Torus Parameters
🌌 Asteroid Field Parameters
Info: This control panel adjusts the 3D visualization in real-time.
Switch between the torus scene and asteroid field using the buttons above or press 1/2 in the 3D viewport.
Current Mode:{{if .CudaEnabled}}CUDA Accelerated{{else}}CPU Fallback{{end}} Current Scene:{{.CurrentScene}} Performance:-- FPS Viewport:800 x 600 (resizable)
📋 System Logs
Connecting...
[Starting]WebUIControl panel loaded, connecting to log stream...
`
// StartWebServer starts the web interface server
func StartWebServer(params *TorusParams, cudaEnabled *bool) {
// Set global CUDA status for WebSocket broadcasting
globalCudaEnabled = cudaEnabled
// Initialize and start status broadcaster
statusBroadcaster = newStatusBroadcaster()
go statusBroadcaster.run()
// Serve static files for the web interface
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.New("index").Parse(htmlTemplate))
data := struct {
TorusParams
AsteroidParams
CudaEnabled bool
CurrentScene string
}{
TorusParams: currentParams,
AsteroidParams: currentAsteroidParams,
CudaEnabled: cudaEnabled != nil && *cudaEnabled,
CurrentScene: currentScene,
}
tmpl.Execute(w, data)
})
// WebSocket endpoint for real-time logging
http.HandleFunc("/ws", wsLogHandler)
// WebSocket endpoint for real-time status updates
http.HandleFunc("/ws-status", func(w http.ResponseWriter, r *http.Request) {
conn, err := statusUpgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Status WebSocket upgrade error:", err)
return
}
statusBroadcaster.register <- conn
// Keep connection alive and handle disconnection
defer func() {
statusBroadcaster.unregister <- conn
conn.Close()
}()
// Read messages from client (ping/pong)
for {
_, _, err := conn.ReadMessage()
if err != nil {
break
}
}
})
// API endpoint for scene switching
http.HandleFunc("/api/scene", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1048576) // 1MB limit
var sceneData map[string]string
if err := json.NewDecoder(r.Body).Decode(&sceneData); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if scene, ok := sceneData["scene"]; ok {
currentScene = scene
// Update the actual scene in main.go
SetCurrentScene(scene)
LogSuccess("Scene", fmt.Sprintf("Scene switched to: %s", scene))
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "scene changed"})
})
// API endpoint to update torus parameters
http.HandleFunc("/api/params", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1048576) // 1MB limit
var updates map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Log parameter changes
for key, value := range updates {
LogInfo("UI", fmt.Sprintf("Torus parameter changed: %s = %v", key, value))
}
// Update parameters
for key, value := range updates {
switch key {
case "majorRadius":
if v, ok := value.(float64); ok {
oldValue := currentParams.MajorRadius
currentParams.MajorRadius = float32(v)
LogSuccess("Parameters", fmt.Sprintf("Major Radius: %.2f → %.2f", oldValue, float32(v)))
}
case "minorRadius":
if v, ok := value.(float64); ok {
oldValue := currentParams.MinorRadius
currentParams.MinorRadius = float32(v)
LogSuccess("Parameters", fmt.Sprintf("Minor Radius: %.3f → %.3f", oldValue, float32(v)))
}
case "majorSegments":
if v, ok := value.(float64); ok {
oldValue := currentParams.MajorSegments
currentParams.MajorSegments = int(v)
LogSuccess("Parameters", fmt.Sprintf("Major Segments: %d → %d", oldValue, int(v)))
}
case "minorSegments":
if v, ok := value.(float64); ok {
oldValue := currentParams.MinorSegments
currentParams.MinorSegments = int(v)
LogSuccess("Parameters", fmt.Sprintf("Minor Segments: %d → %d", oldValue, int(v)))
}
case "rotationSpeedX":
if v, ok := value.(float64); ok {
oldValue := currentParams.RotationSpeedX
currentParams.RotationSpeedX = float32(v)
LogSuccess("Parameters", fmt.Sprintf("X Rotation Speed: %.1f → %.1f", oldValue, float32(v)))
}
case "rotationSpeedY":
if v, ok := value.(float64); ok {
oldValue := currentParams.RotationSpeedY
currentParams.RotationSpeedY = float32(v)
LogSuccess("Parameters", fmt.Sprintf("Y Rotation Speed: %.1f → %.1f", oldValue, float32(v)))
}
}
}
// Copy back to the main params if provided
if params != nil {
*params = currentParams
}
LogInfo("System", "Torus parameters updated successfully")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "updated"})
})
// API endpoint to update asteroid parameters
http.HandleFunc("/api/asteroid-params", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1048576) // 1MB limit
var updates map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Log parameter changes
for key, value := range updates {
LogInfo("UI", fmt.Sprintf("Asteroid parameter changed: %s = %v", key, value))
}
// Update parameters
for key, value := range updates {
switch key {
case "spawnRate":
if v, ok := value.(float64); ok {
oldValue := currentAsteroidParams.SpawnRate
currentAsteroidParams.SpawnRate = float32(v)
LogSuccess("Asteroids", fmt.Sprintf("Spawn Rate: %.1f → %.1f", oldValue, float32(v)))
}
case "maxAsteroids":
if v, ok := value.(float64); ok {
oldValue := currentAsteroidParams.MaxAsteroids
currentAsteroidParams.MaxAsteroids = int(v)
LogSuccess("Asteroids", fmt.Sprintf("Max Asteroids: %d → %d", oldValue, int(v)))
}
case "minSize":
if v, ok := value.(float64); ok {
oldValue := currentAsteroidParams.MinSize
currentAsteroidParams.MinSize = float32(v)
LogSuccess("Asteroids", fmt.Sprintf("Min Size: %.3f → %.3f", oldValue, float32(v)))
}
case "maxSize":
if v, ok := value.(float64); ok {
oldValue := currentAsteroidParams.MaxSize
currentAsteroidParams.MaxSize = float32(v)
LogSuccess("Asteroids", fmt.Sprintf("Max Size: %.3f → %.3f", oldValue, float32(v)))
}
case "minSpeed":
if v, ok := value.(float64); ok {
oldValue := currentAsteroidParams.MinSpeed
currentAsteroidParams.MinSpeed = float32(v)
LogSuccess("Asteroids", fmt.Sprintf("Min Speed: %.1f → %.1f", oldValue, float32(v)))
}
case "maxSpeed":
if v, ok := value.(float64); ok {
oldValue := currentAsteroidParams.MaxSpeed
currentAsteroidParams.MaxSpeed = float32(v)
LogSuccess("Asteroids", fmt.Sprintf("Max Speed: %.1f → %.1f", oldValue, float32(v)))
}
case "rotationSpeed":
if v, ok := value.(float64); ok {
oldValue := currentAsteroidParams.RotationSpeed
currentAsteroidParams.RotationSpeed = float32(v)
LogSuccess("Asteroids", fmt.Sprintf("Rotation Speed: %.1f → %.1f", oldValue, float32(v)))
}
}
}
// Update the actual asteroid config in main.go
UpdateAsteroidConfig(currentAsteroidParams)
LogInfo("System", "Asteroid parameters updated successfully")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "updated"})
})
// API endpoint to clear asteroids
http.HandleFunc("/api/clear-asteroids", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Call the actual clear function in main.go
ClearAllAsteroids()
LogInfo("Asteroids", "Clearing all asteroids from field")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "asteroids cleared"})
})
// API endpoint for status
http.HandleFunc("/api/status", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"cudaEnabled": cudaEnabled != nil && *cudaEnabled,
"params": currentParams,
"asteroidParams": currentAsteroidParams,
"currentScene": currentScene,
"fps": GetCurrentFPS(),
"windowWidth": func() int { w, _ := GetWindowSize(); return w }(),
"windowHeight": func() int { _, h := GetWindowSize(); return h }(),
})
})
LogInfo("WebServer", "Starting web interface on http://localhost:8080")
LogInfo("WebServer", "Access the control panel and logs in your browser")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// GetCurrentParams returns the current torus parameters
func GetCurrentParams() TorusParams {
return currentParams
}
// GetCurrentAsteroidParams returns the current asteroid parameters
func GetCurrentAsteroidParams() AsteroidParams {
return currentAsteroidParams
}
// GetCurrentScene returns the current scene name
func GetCurrentScene() string {
return currentScene
}