""" Environment Tools Skybox, particles, and other environmental effects """ from typing import Dict, Any, Optional from backend.storage import storage # ============================================================================= # Skybox Tools # ============================================================================= def add_skybox( scene_id: str, preset: str = "day", turbidity: float = 10.0, rayleigh: float = 2.0, sun_elevation: float = 45.0, sun_azimuth: float = 180.0 ) -> Dict[str, Any]: """ Add a procedural sky to the scene using Three.js Sky shader. The sky simulates atmospheric scattering for realistic outdoor environments. Args: scene_id: ID of the scene preset: Quick preset - "day", "sunset", "noon", "dawn", "night" (overrides other params) turbidity: Haziness of the atmosphere (2.0=clear, 10.0=hazy, 20.0=foggy) rayleigh: Amount of Rayleigh scattering (affects blue color of sky) sun_elevation: Sun angle from horizon in degrees (0=horizon, 90=overhead) sun_azimuth: Sun compass direction in degrees (0=north, 90=east, 180=south) Returns: Dictionary with skybox settings and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "environment" not in scene: scene["environment"] = {} # Apply presets presets = { "day": {"turbidity": 10.0, "rayleigh": 2.0, "sun_elevation": 45.0, "sun_azimuth": 180.0}, "noon": {"turbidity": 8.0, "rayleigh": 1.5, "sun_elevation": 85.0, "sun_azimuth": 180.0}, "sunset": {"turbidity": 4.0, "rayleigh": 4.0, "sun_elevation": 5.0, "sun_azimuth": 180.0}, "dawn": {"turbidity": 4.0, "rayleigh": 4.0, "sun_elevation": 5.0, "sun_azimuth": 0.0}, "night": {"turbidity": 20.0, "rayleigh": 0.1, "sun_elevation": -10.0, "sun_azimuth": 180.0}, } if preset in presets: params = presets[preset] turbidity = params["turbidity"] rayleigh = params["rayleigh"] sun_elevation = params["sun_elevation"] sun_azimuth = params["sun_azimuth"] # Validate ranges turbidity = max(0.1, min(20.0, turbidity)) rayleigh = max(0.0, min(4.0, rayleigh)) sun_elevation = max(-90.0, min(90.0, sun_elevation)) sun_azimuth = sun_azimuth % 360.0 skybox = { "enabled": True, "type": "procedural", "turbidity": turbidity, "rayleigh": rayleigh, "sun_elevation": sun_elevation, "sun_azimuth": sun_azimuth, "preset": preset } scene["environment"]["skybox"] = skybox storage.save(scene) return { "scene_id": scene_id, "message": f"Added {preset} sky (sun at {sun_elevation}° elevation)", "skybox": skybox } def remove_skybox(scene_id: str) -> Dict[str, Any]: """ Remove the skybox from the scene, reverting to solid background color. Args: scene_id: ID of the scene Returns: Dictionary with confirmation message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "environment" not in scene: scene["environment"] = {} # Check if skybox exists had_skybox = scene["environment"].get("skybox", {}).get("enabled", False) scene["environment"]["skybox"] = {"enabled": False} storage.save(scene) if had_skybox: return { "scene_id": scene_id, "message": "Removed skybox, reverted to solid background color" } else: return { "scene_id": scene_id, "message": "No skybox was active" } # ============================================================================= # Particle System Tools # ============================================================================= def add_particles( scene_id: str, preset: str, position: Optional[Dict[str, float]] = None, particle_id: Optional[str] = None ) -> Dict[str, Any]: """ Add a particle effect to the scene using preset configurations. Args: scene_id: ID of the scene preset: Effect type - "fire", "smoke", "sparkle", "rain", "snow" position: {x, y, z} position for localized effects (fire, smoke, sparkle) For weather effects (rain, snow), position is ignored (covers world) particle_id: Optional unique identifier for this particle system Returns: Dictionary with particle system info and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") valid_presets = ["fire", "smoke", "sparkle", "rain", "snow"] if preset not in valid_presets: raise ValueError(f"Invalid preset '{preset}'. Must be one of: {valid_presets}") if "particles" not in scene: scene["particles"] = [] # Generate ID if not provided if not particle_id: particle_id = f"{preset}_{len(scene['particles'])}" # Check for duplicate ID for p in scene["particles"]: if p.get("id") == particle_id: raise ValueError(f"Particle system with id '{particle_id}' already exists") # Default position for localized effects if position is None: position = {"x": 0, "y": 1, "z": 0} # Preset configurations preset_configs = { "fire": { "count": 200, "size": 0.3, "color_start": "#ff6600", "color_end": "#ff0000", "velocity": {"x": 0, "y": 2, "z": 0}, "lifetime": 1.5, "spread": 0.5, "localized": True }, "smoke": { "count": 100, "size": 0.8, "color_start": "#666666", "color_end": "#333333", "velocity": {"x": 0, "y": 1, "z": 0}, "lifetime": 3.0, "spread": 1.0, "localized": True }, "sparkle": { "count": 50, "size": 0.1, "color_start": "#ffffff", "color_end": "#ffff00", "velocity": {"x": 0, "y": 0.5, "z": 0}, "lifetime": 2.0, "spread": 2.0, "localized": True }, "rain": { "count": 1000, "size": 0.05, "color_start": "#aaccff", "color_end": "#88aadd", "velocity": {"x": 0, "y": -15, "z": 0}, "lifetime": 2.0, "spread": 50.0, "localized": False }, "snow": { "count": 500, "size": 0.15, "color_start": "#ffffff", "color_end": "#eeeeff", "velocity": {"x": 0.5, "y": -2, "z": 0.3}, "lifetime": 8.0, "spread": 50.0, "localized": False } } config = preset_configs[preset] particle_system = { "id": particle_id, "preset": preset, "position": position, "enabled": True, **config } scene["particles"].append(particle_system) storage.save(scene) if config["localized"]: pos_str = f" at ({position['x']}, {position['y']}, {position['z']})" else: pos_str = " (world-wide weather effect)" return { "scene_id": scene_id, "particle_id": particle_id, "message": f"Added {preset} particle effect{pos_str}", "particle_system": particle_system } def remove_particles(scene_id: str, particle_id: str) -> Dict[str, Any]: """ Remove a particle system from the scene. Args: scene_id: ID of the scene particle_id: ID of the particle system to remove Returns: Dictionary with confirmation message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "particles" not in scene or not scene["particles"]: raise ValueError("Scene has no particle systems") original_count = len(scene["particles"]) scene["particles"] = [p for p in scene["particles"] if p.get("id") != particle_id] if len(scene["particles"]) == original_count: raise ValueError(f"Particle system '{particle_id}' not found") storage.save(scene) return { "scene_id": scene_id, "message": f"Removed particle system '{particle_id}'", "particle_id": particle_id }