diff --git a/README.md b/README.md new file mode 100644 index 0000000..38da485 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# Community Energy Simulation + +A 3D Digital Twin application for simulating and visualizing community energy systems. This application provides an interactive 3D interface to monitor energy consumption and generation across various building types in a community. + +## Features + +### 3D Digital Twin +- **Interactive Scene**: Explore the community with full camera controls (pan, rotate, zoom). +- **Building Selection**: Click on buildings to view detailed information and highlight them in the scene. +- **Visual Feedback**: Emissive highlighting and selection rings for selected objects. + +### Simulation Modes +- **Simulated Mode**: Runs an internal simulation with a day/night cycle, calculating energy loads based on time of day (solar generation curves, evening consumption peaks). +- **Live Mode**: Connects to an external backend (via Socket.io) to visualize real-time data streaming from Redis Pub/Sub. + +### Dynamic Configuration +- **Add Buildings**: Dynamically add new buildings to the scene by specifying ID, Type (house, factory, etc.), and CSV Data path. +- **Adjustable Settings**: + - **Simulation Speed**: Control how fast time passes in simulated mode. + - **Time of Day**: Visual day/night cycle. + - **Brightness & Zoom**: Customize the visual experience. + - **Data Origin**: Configure a base URL for loading remote CSV datasets. + - **Backend URL**: Configure the Socket.io server connection for Live Mode. + +## Tech Stack + +- **Frontend Framework**: Vue.js 3 + Vite +- **Language**: TypeScript +- **3D Library**: Three.js +- **State Management**: Vue Composition API (Reactivity) +- **Data Handling**: PapaParse (CSV), Socket.io-client (Real-time) + +## Getting Started + +### Prerequisites + +- Node.js (v18 or higher) +- npm or yarn + +### Installation + +1. Navigate to the web application directory: + ```bash + cd web-app + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +### Running Locally + +Start the development server: + +```bash +npm run dev +``` + +The application will be available at `http://localhost:5173`. + +### Building for Production + +Build the application for deployment: + +```bash +npm run build +``` + +The output will be in the `dist` directory. + +## Docker Deployment + +A `Dockerfile` and `docker.sh` script are provided for containerization. + +To build and push the image (requires Docker login): +```bash +./docker.sh +``` + +Or run locally: +```bash +docker build -t community-simulation . +docker run -p 8080:8080 community-simulation +``` + +## Usage + +### Connecting to Live Data +1. Ensure your backend service (Redis + Socket.io bridge) is running. +2. Open the application settings (Gear icon). +3. Select **Live (Real Time)** mode. +4. Enter your **Backend URL** (default: `http://localhost:8000`). +5. Save settings. The app will connect and listen for `simulation:update` events. + +### Adding Custom Buildings +1. Click the **+ Add Building** button. +2. Enter a unique **ID** (e.g., "H99"). +3. Select a **Type** (Determines the 3D model used). +4. Provide a **CSV Path** relative to the `public/` folder or your configured Data Origin (e.g., `data/my-building.csv`). +5. Click **Add**. + +## Project Structure + +- `web-app/`: Main Vue.js application source code. + - `src/components/`: UI components (Modals, Overlay). + - `src/composables/`: Game logic and state management (`useSimulation`, `useCityObjects`, etc.). + - `public/`: Static assets (3D models, textures, CSV data). +- `js/`: Legacy/Prototype JavaScript files. diff --git a/web-app/src/components/SettingsModal.vue b/web-app/src/components/SettingsModal.vue index 73bebd7..b8ee0f2 100644 --- a/web-app/src/components/SettingsModal.vue +++ b/web-app/src/components/SettingsModal.vue @@ -60,6 +60,19 @@ /> +
+ +
+ WebSocket server address for real-time updates. +
+ +
+
@@ -92,8 +105,9 @@ const props = defineProps<{ const emit = defineEmits(['close']); -const { state, setDataBaseUrl, setBrightness, setSimulationMode, setMaxZoom, setZoomSensitivity } = useSimulation(); +const { state, setDataBaseUrl, setBackendUrl, setBrightness, setSimulationMode, setMaxZoom, setZoomSensitivity } = useSimulation(); const baseUrlInput = ref(''); +const backendUrlInput = ref(''); const brightnessInput = ref(1.0); const simulationModeInput = ref<'simulated' | 'live'>('simulated'); const maxZoomInput = ref(120); @@ -103,6 +117,7 @@ const zoomSensitivityInput = ref(5); watch(() => props.isOpen, (newVal) => { if (newVal) { baseUrlInput.value = state.value.dataBaseUrl || ''; + backendUrlInput.value = state.value.backendUrl || 'http://localhost:8000'; brightnessInput.value = state.value.brightness ?? 1.0; simulationModeInput.value = state.value.simulationMode ?? 'simulated'; maxZoomInput.value = state.value.maxZoom ?? 120; @@ -116,6 +131,7 @@ const close = () => { const save = () => { setDataBaseUrl(baseUrlInput.value); + setBackendUrl(backendUrlInput.value); setBrightness(Number(brightnessInput.value)); setSimulationMode(simulationModeInput.value); setMaxZoom(Number(maxZoomInput.value)); diff --git a/web-app/src/composables/useSimulation.ts b/web-app/src/composables/useSimulation.ts index 76d1c6c..fc31535 100644 --- a/web-app/src/composables/useSimulation.ts +++ b/web-app/src/composables/useSimulation.ts @@ -1,5 +1,6 @@ import { ref } from 'vue'; import Papa from 'papaparse'; +import { io, type Socket } from 'socket.io-client'; interface SimulationState { currentTime: number; // 0 to 24 (hours) @@ -10,6 +11,7 @@ interface SimulationState { totalConsumption: number; totalGeneration: number; dataBaseUrl: string; + backendUrl: string; brightness: number; simulationMode: 'simulated' | 'live'; maxZoom: number; @@ -60,6 +62,7 @@ const state = ref({ totalConsumption: 0, totalGeneration: 0, dataBaseUrl: '', + backendUrl: 'http://localhost:8000', brightness: 1.0, simulationMode: 'simulated', maxZoom: 120, @@ -83,6 +86,8 @@ const state = ref({ }); const selectedBuilding = ref(null); +let socket: Socket | null = null; + export const useSimulation = () => { const togglePlay = () => { @@ -97,10 +102,50 @@ export const useSimulation = () => { state.value.brightness = val; }; + const initSocket = () => { + if (socket) return; + + console.log(`Connecting to backend: ${state.value.backendUrl}`); + socket = io(state.value.backendUrl); + + socket.on('connect', () => { + console.log('Socket connected'); + }); + + socket.on('disconnect', () => { + console.log('Socket disconnected'); + }); + + socket.on('simulation:update', (data: any) => { + // Expected data format: { totalConsumption: number, totalGeneration: number, buildings: { [id]: { consumption: number, generation: number } } } + // OR individual updates. Let's assume a snapshot for now or aggregate. + // If data contains totals: + if (data.totalConsumption !== undefined) state.value.totalConsumption = data.totalConsumption; + if (data.totalGeneration !== undefined) state.value.totalGeneration = data.totalGeneration; + + // Update selected building if present + if (selectedBuilding.value && data.buildings && data.buildings[selectedBuilding.value.id]) { + const bUpdate = data.buildings[selectedBuilding.value.id]; + selectedBuilding.value.consumption = bUpdate.consumption; + selectedBuilding.value.generation = bUpdate.generation; + } + }); + }; + + const disconnectSocket = () => { + if (socket) { + socket.disconnect(); + socket = null; + } + }; + const setSimulationMode = (mode: 'simulated' | 'live') => { state.value.simulationMode = mode; if (mode === 'live') { state.value.isPlaying = true; // Auto-play in live? Or just simple update. + initSocket(); + } else { + disconnectSocket(); } }; @@ -116,6 +161,15 @@ export const useSimulation = () => { state.value.dataBaseUrl = url; }; + const setBackendUrl = (url: string) => { + state.value.backendUrl = url; + // Reconnect if live + if (state.value.simulationMode === 'live' && socket) { + disconnectSocket(); + initSocket(); + } + }; + const loadBuildingData = (_buildingId: string, csvPath: string) => { // Construct full URL let fullPath = csvPath; @@ -156,34 +210,34 @@ export const useSimulation = () => { state.value.currentTime = 0; state.value.day++; } - } - // --- Simulated Community Energy Logic --- - const time = state.value.currentTime; + // --- Simulated Community Energy Logic --- + const time = state.value.currentTime; - // Consumption Curve: Base load + Evening Peak (18-22) - // Base Community Load ~4000kW - let load = 4000; - // Peak - if (time > 17 && time < 23) { - load += 2000 * Math.sin(((time - 17) / 6) * Math.PI); - } - // Morning spike - if (time > 6 && time < 9) { - load += 1000 * Math.sin(((time - 6) / 3) * Math.PI); - } + // Consumption Curve: Base load + Evening Peak (18-22) + // Base Community Load ~4000kW + let load = 4000; + // Peak + if (time > 17 && time < 23) { + load += 2000 * Math.sin(((time - 17) / 6) * Math.PI); + } + // Morning spike + if (time > 6 && time < 9) { + load += 1000 * Math.sin(((time - 6) / 3) * Math.PI); + } - // Generation Curve: Based on simplified Sun Elevation - // Simple Parabola from 6am to 6pm - let gen = 0; - if (time > 6 && time < 18) { - // Peak at 12 - const sunFactor = Math.sin(((time - 6) / 12) * Math.PI); - gen = 3500 * sunFactor; // Max 3500kW generation - } + // Generation Curve: Based on simplified Sun Elevation + // Simple Parabola from 6am to 6pm + let gen = 0; + if (time > 6 && time < 18) { + // Peak at 12 + const sunFactor = Math.sin(((time - 6) / 12) * Math.PI); + gen = 3500 * sunFactor; // Max 3500kW generation + } - state.value.totalConsumption = Math.floor(load); - state.value.totalGeneration = Math.floor(gen); + state.value.totalConsumption = Math.floor(load); + state.value.totalGeneration = Math.floor(gen); + } }; const addBuilding = (def: BuildingDefinition) => { @@ -198,6 +252,7 @@ export const useSimulation = () => { loadBuildingData, updateTime, setDataBaseUrl, + setBackendUrl, setBrightness, setSimulationMode, setMaxZoom,