/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * Sun & Sky panel — one place for sky, lighting and the sun-path study, * aware of which rendering path is active: * * • Standalone (WebGPU): lighting preset + exposure shape the model's * shading, Sky draws the procedural sky, and the sun study (when the * model is georeferenced) drives the real sun direction. * • World context (Cesium): the model is composited into Cesium, which * lights the scene from its sun and atmosphere — so preset/exposure * hide and Sky toggles the atmosphere instead. The study adds the * sun-path dome and real cast shadows. * * The whole panel collapses to its header row; the study keeps running * (sweep animation lives in useSolarSweep at the viewport level). */ import { useRef, useState } from 'react'; import { Play, Pause, ChevronDown, ChevronUp, GripVertical } from 'lucide-react'; import { useViewerStore } from '@/store'; import { useDraggablePanel } from '@/hooks/useDraggablePanel'; import { cn } from '@/lib/utils'; import type { CesiumDataSource } from '@/store/slices/cesiumSlice'; import type { SolarSweepMode } from '@/store/slices/solarSlice'; import { LIGHTING_PRESETS, LIGHTING_PRESET_ORDER, isLightingPresetId } from '@/lib/lighting-presets'; import { posthog } from '@/lib/analytics'; import { solarDisplayOffsetMinutes, toSolarDateInputValue, solarMinutesOfDay, composeSolarMs, formatSolarTime, } from '@/lib/solar-time'; const CONTEXT_SOURCES: Array<{ value: CesiumDataSource; label: string; hint: string }> = [ { value: 'osm-buildings', label: 'OSM Buildings', hint: 'Extruded footprints over the satellite base map' }, { value: 'google-photorealistic', label: 'Photorealistic', hint: 'Google 3D Tiles — textured real-world context' }, ]; const SWEEP_MODES: Array<{ value: SolarSweepMode; label: string; hint: string }> = [ { value: 'day', label: 'Day', hint: 'Sweep the time of day' }, { value: 'year', label: 'Year', hint: 'Sweep the date across the year' }, ]; export function SunSkyPanel() { const open = useViewerStore((s) => s.envPanelOpen); const skyEnabled = useViewerStore((s) => s.envSkyEnabled); const setSkyEnabled = useViewerStore((s) => s.setEnvSkyEnabled); const preset = useViewerStore((s) => s.envPreset); const setPreset = useViewerStore((s) => s.setEnvPreset); const exposure = useViewerStore((s) => s.envExposure); const setExposure = useViewerStore((s) => s.setEnvExposure); const cesiumAvailable = useViewerStore((s) => s.cesiumAvailable); const cesiumEnabled = useViewerStore((s) => s.cesiumEnabled); const setCesiumEnabled = useViewerStore((s) => s.setCesiumEnabled); const dataSource = useViewerStore((s) => s.cesiumDataSource); const setDataSource = useViewerStore((s) => s.setCesiumDataSource); const solarEnabled = useViewerStore((s) => s.solarEnabled); const setSolarEnabled = useViewerStore((s) => s.setSolarEnabled); const dateMs = useViewerStore((s) => s.solarDateMs); const setDateMs = useViewerStore((s) => s.setSolarDateMs); const showSunPath = useViewerStore((s) => s.solarShowSunPath); const setShowSunPath = useViewerStore((s) => s.setSolarShowSunPath); const showShadows = useViewerStore((s) => s.solarShowShadows); const setShowShadows = useViewerStore((s) => s.setSolarShowShadows); const sunInfo = useViewerStore((s) => s.solarSunInfo); const useLocalTime = useViewerStore((s) => s.solarUseLocalTime); const setUseLocalTime = useViewerStore((s) => s.setSolarUseLocalTime); const playing = useViewerStore((s) => s.solarPlaying); const togglePlaying = useViewerStore((s) => s.toggleSolarPlaying); const sweepMode = useViewerStore((s) => s.solarSweepMode); const setSweepMode = useViewerStore((s) => s.setSolarSweepMode); const [collapsed, setCollapsed] = useState(false); // Hooks must run unconditionally — keep these ABOVE the `!open` early return // (a conditional hook is React error #310). const panelRef = useRef(null); const drag = useDraggablePanel(panelRef); if (!open) return null; const offsetMin = solarDisplayOffsetMinutes(useLocalTime, sunInfo?.longitude); const minutes = solarMinutesOfDay(dateMs, offsetMin); const tzLabel = useLocalTime ? `Site${sunInfo ? ` (UTC${offsetMin >= 0 ? '+' : '−'}${Math.abs(offsetMin / 60).toFixed(1)})` : ''}` : 'UTC'; return (
{/* Header: the grip drags (issue #1107); the rest toggles collapse — kept separate so the two affordances don't collide. */}
{!collapsed && ( <> {/* Environment — the preset IS the whole look: every preset except Default brings its own sky. In the world context the model is lit by Cesium's sun instead, so the choice becomes a single Atmosphere switch. */} {cesiumEnabled ? (
setSkyEnabled(!skyEnabled)} title="Sky, sun disc and haze in the world context" /> Lighting follows the sun & atmosphere
) : ( )} {/* Exposure — WebGPU shading only, hidden in world-context mode */} {!cesiumEnabled && ( )} {/* Sun study — needs a georeferenced model for the real sun */} {cesiumAvailable && ( <>
Sun study
{solarEnabled && ( <> {/* Date + play/pause */}
{/* Not a
{/* Time of day */} {/* Sweep mode */}
{SWEEP_MODES.map((m) => ( ))}
{/* World-context extras: dome + shadows + context source */} {cesiumEnabled ? ( <>
setShowSunPath(!showSunPath)} /> setShowShadows(!showShadows)} />
{CONTEXT_SOURCES.map((src) => ( ))}
) : ( )} {!sunInfo && (

Site location unavailable — the model's projected CRS could not be resolved, so the real sun position can't be computed.

)} {/* Readout */}
)} )} )}
); } /** Small pill toggle button. */ function ToggleChip({ label, active, onClick, title, className }: { label: string; active: boolean; onClick: () => void; title?: string; className?: string; }) { return ( ); } /** One label/value cell in the sun readout grid. */ function Readout({ label, value }: { label: string; value: string }) { return ( <> {label} {value} ); }