SRP
This commit is contained in:
1
web-app/dist/assets/index-1evUyPMc.css
vendored
Normal file
1
web-app/dist/assets/index-1evUyPMc.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}.canvas-container[data-v-0c7f17d8]{width:100%;height:100vh;display:block}.building-panel[data-v-7c86670f]{position:absolute;width:300px;background:#141419f2;border:3px solid #000;border-radius:0;box-shadow:6px 6px #0006;padding:0;overflow:hidden;font-family:Courier New,monospace;z-index:1000;pointer-events:auto;color:#fff;transform:translate(-50%,-110%);transition:left .05s linear,top .05s linear}.building-panel.flipped[data-v-7c86670f]{transform:translate(-50%,10%)}.header[data-v-7c86670f]{background:gold;color:#000;padding:12px;display:flex;justify-content:space-between;align-items:center;border-bottom:3px solid #000;text-transform:uppercase}.header h3[data-v-7c86670f]{margin:0;font-size:1rem;font-weight:900}.close-btn[data-v-7c86670f]{background:#000;border:2px solid black;color:#fff;font-size:1.2rem;cursor:pointer;line-height:1;padding:2px 8px;font-weight:700}.close-btn[data-v-7c86670f]:hover{background:#f44}.content[data-v-7c86670f]{padding:16px}.info-row[data-v-7c86670f]{display:flex;justify-content:space-between;margin-bottom:8px;border-bottom:2px dashed #444;padding-bottom:4px}.label[data-v-7c86670f]{color:#aaa;font-weight:700;text-transform:uppercase;font-size:.8rem}.value[data-v-7c86670f]{font-weight:600;color:#fff;font-family:monospace}.section-title[data-v-7c86670f]{margin-top:16px;margin-bottom:8px;font-size:.8rem;text-transform:uppercase;background:#333;color:#fff;padding:4px;text-align:center;font-weight:700;border:2px solid #000}.iot-text[data-v-7c86670f]{font-size:.85rem;color:#ccc;background:#111;padding:8px;border:2px solid #333;margin:0}.actions[data-v-7c86670f]{margin-top:20px;display:flex;gap:8px}.action-btn[data-v-7c86670f]{flex:1;padding:8px;border:3px solid #000;border-radius:0;background:#fc0;color:#000;font-weight:900;cursor:pointer;text-transform:uppercase;box-shadow:3px 3px #000;transition:all .1s}.action-btn[data-v-7c86670f]:hover{transform:translate(2px,2px);box-shadow:1px 1px #000}.action-btn[data-v-7c86670f]:active{transform:translate(3px,3px);box-shadow:0 0 #000}.dashboard-bar[data-v-99b9fc73]{position:absolute;bottom:0;left:0;width:100%;height:80px;background:#111;border-top:4px solid #000;display:flex;justify-content:space-between;align-items:center;padding:0 40px;box-sizing:border-box;color:#fff;font-family:Courier New,monospace;z-index:900;box-shadow:0 -4px 20px #00000080}.stat-group[data-v-99b9fc73]{display:flex;gap:30px}.stat-item[data-v-99b9fc73]{display:flex;flex-direction:column}.label[data-v-99b9fc73]{font-size:.75rem;color:#888;font-weight:700}.value[data-v-99b9fc73]{font-size:1.4rem;font-weight:700}.value.green[data-v-99b9fc73]{color:#0f0;text-shadow:0 0 10px rgba(0,255,0,.3)}.value.red[data-v-99b9fc73]{color:#f33;text-shadow:0 0 10px rgba(255,0,0,.3)}.time-display[data-v-99b9fc73]{font-size:1.8rem;font-weight:900;color:gold;background:#000;padding:5px 20px;border:2px solid #333;letter-spacing:2px}.controls[data-v-99b9fc73]{display:flex;gap:20px;align-items:center}.speed-controls[data-v-99b9fc73]{display:flex;gap:5px;background:#222;padding:4px;border:1px solid #444}button[data-v-99b9fc73]{background:transparent;border:2px solid transparent;color:#888;padding:6px 12px;cursor:pointer;font-family:Courier New,monospace;font-weight:700;text-transform:uppercase;transition:all .1s}button[data-v-99b9fc73]:hover{color:#fff;background:#333}button.active[data-v-99b9fc73]{background:gold;color:#000;box-shadow:0 0 10px gold}.play-btn[data-v-99b9fc73]{border:2px solid #444;padding:10px 24px;font-size:1.1rem;background:#222;color:#fff}.play-btn[data-v-99b9fc73]:hover{border-color:#fff}.play-btn.active[data-v-99b9fc73]{background:#f33;color:#fff;box-shadow:none;border-color:red}html,body,#app{margin:0;padding:0;width:100%;height:100%;overflow:hidden}
|
||||||
3858
web-app/dist/assets/index-BDdF_E2G.js
vendored
3858
web-app/dist/assets/index-BDdF_E2G.js
vendored
File diff suppressed because one or more lines are too long
1
web-app/dist/assets/index-CSR7bLu2.css
vendored
1
web-app/dist/assets/index-CSR7bLu2.css
vendored
@@ -1 +0,0 @@
|
|||||||
:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}.canvas-container[data-v-3dc03414]{width:100%;height:100vh;display:block}.building-panel[data-v-2da88f20]{position:absolute;transform:translate(-50%,-110%);width:300px;background:#fffffff2;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:12px;box-shadow:0 4px 20px #00000026;padding:0;overflow:hidden;font-family:Inter,sans-serif;animation:popIn-2da88f20 .2s cubic-bezier(.175,.885,.32,1.275);z-index:1000;pointer-events:auto}@keyframes popIn-2da88f20{0%{opacity:0;transform:translate(-50%,-100%) scale(.9)}to{opacity:1;transform:translate(-50%,-110%) scale(1)}}.header[data-v-2da88f20]{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:16px;display:flex;justify-content:space-between;align-items:center}.header h3[data-v-2da88f20]{margin:0;font-size:1.1rem;font-weight:600}.close-btn[data-v-2da88f20]{background:none;border:none;color:#fff;font-size:1.5rem;cursor:pointer;line-height:1;padding:0 5px}.content[data-v-2da88f20]{padding:20px}.info-row[data-v-2da88f20]{display:flex;justify-content:space-between;margin-bottom:12px;border-bottom:1px solid #eee;padding-bottom:8px}.label[data-v-2da88f20]{color:#666;font-weight:500}.value[data-v-2da88f20]{font-weight:600;color:#333}.section-title[data-v-2da88f20]{margin-top:20px;margin-bottom:10px;font-size:.9rem;text-transform:uppercase;letter-spacing:1px;color:#888;font-weight:700}.iot-text[data-v-2da88f20]{font-size:.9rem;color:#555;background:#f5f5f7;padding:10px;border-radius:6px;margin:0}.actions[data-v-2da88f20]{margin-top:20px;display:flex;gap:10px}.action-btn[data-v-2da88f20]{flex:1;padding:10px;border:none;border-radius:6px;background:#f0f0f0;color:#333;font-weight:600;cursor:pointer;transition:all .2s}.action-btn[data-v-2da88f20]:hover{background:#e0e0e0;transform:translateY(-1px)}.time-controls[data-v-fa1ceac6]{position:absolute;bottom:20px;left:50%;transform:translate(-50%);background:#141419d9;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);padding:12px 24px;border-radius:30px;display:flex;flex-direction:column;align-items:center;gap:10px;color:#fff;box-shadow:0 4px 15px #0000004d}.time-display[data-v-fa1ceac6]{font-family:Monaco,Consolas,monospace;font-size:1.1rem;font-weight:600;color:#4facfe}.controls[data-v-fa1ceac6]{display:flex;gap:15px;align-items:center}button[data-v-fa1ceac6]{background:transparent;border:1px solid rgba(255,255,255,.2);color:#fff;padding:6px 12px;border-radius:15px;cursor:pointer;font-size:.9rem;transition:all .2s}button[data-v-fa1ceac6]:hover{background:#ffffff1a}button.active[data-v-fa1ceac6]{background:#4facfe;color:#fff;border-color:#4facfe}.speed-controls[data-v-fa1ceac6]{display:flex;gap:5px}.speed-controls button[data-v-fa1ceac6]{padding:4px 8px;font-size:.8rem}html,body,#app{margin:0;padding:0;width:100%;height:100%;overflow:hidden}
|
|
||||||
3858
web-app/dist/assets/index-CwCwvZ0Q.js
vendored
Normal file
3858
web-app/dist/assets/index-CwCwvZ0Q.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
web-app/dist/index.html
vendored
4
web-app/dist/index.html
vendored
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Community Digital Twin</title>
|
<title>Community Digital Twin</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BDdF_E2G.js"></script>
|
<script type="module" crossorigin src="/assets/index-CwCwvZ0Q.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CSR7bLu2.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-1evUyPMc.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import { useSimulation } from '../composables/useSimulation';
|
|||||||
import { computed, ref, onMounted, onUnmounted } from 'vue';
|
import { computed, ref, onMounted, onUnmounted } from 'vue';
|
||||||
|
|
||||||
const { selectedBuilding } = useSimulation();
|
const { selectedBuilding } = useSimulation();
|
||||||
const panelRef = ref<HTMLElement | null>(null);
|
|
||||||
const windowWidth = ref(window.innerWidth);
|
const windowWidth = ref(window.innerWidth);
|
||||||
const windowHeight = ref(window.innerHeight);
|
const windowHeight = ref(window.innerHeight);
|
||||||
|
|
||||||
@@ -104,20 +103,20 @@ const panelStyle = computed(() => {
|
|||||||
.building-panel {
|
.building-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(20, 20, 25, 0.95); /* Dark HUD background */
|
||||||
backdrop-filter: blur(10px);
|
border: 3px solid #000; /* Bold Outline */
|
||||||
border-radius: 12px;
|
border-radius: 0; /* Sharp corners */
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
box-shadow: 6px 6px 0px rgba(0,0,0,0.4); /* Hard drop shadow */
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Courier New', monospace; /* Tech/Game font */
|
||||||
animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
color: white;
|
||||||
|
|
||||||
/* Default: Above the point */
|
/* Default: Above the point */
|
||||||
transform: translate(-50%, -110%);
|
transform: translate(-50%, -110%);
|
||||||
transition: left 0.1s ease-out, top 0.1s ease-out, transform 0.2s;
|
transition: left 0.05s linear, top 0.05s linear; /* Snappier movement */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Flip to below the point if active */
|
/* Flip to below the point if active */
|
||||||
@@ -125,97 +124,111 @@ const panelStyle = computed(() => {
|
|||||||
transform: translate(-50%, 10%); /* 10% gap below point */
|
transform: translate(-50%, 10%); /* 10% gap below point */
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes popIn {
|
|
||||||
from { opacity: 0; transform: scale(0.9); } /* simplified generic pop */
|
|
||||||
to { opacity: 1; transform: scale(1); } /* overridden by class transforms */
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: #FFD700; /* Vibrant Gold */
|
||||||
color: white;
|
color: black;
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border-bottom: 3px solid #000;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header h3 {
|
.header h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.1rem;
|
font-size: 1.0rem;
|
||||||
font-weight: 600;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-btn {
|
.close-btn {
|
||||||
background: none;
|
background: black;
|
||||||
border: none;
|
border: 2px solid black;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 1.5rem;
|
font-size: 1.2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 0 5px;
|
padding: 2px 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
background: #ff4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 20px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-row {
|
.info-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 8px;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 2px dashed #444;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: #666;
|
color: #aaa;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #fff;
|
||||||
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
margin-top: 20px;
|
margin-top: 16px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.8rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
background: #333;
|
||||||
color: #888;
|
color: #fff;
|
||||||
font-weight: 700;
|
padding: 4px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 2px solid #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iot-text {
|
.iot-text {
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
color: #555;
|
color: #ccc;
|
||||||
background: #f5f5f7;
|
background: #111;
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
border-radius: 6px;
|
border: 2px solid #333;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
border: none;
|
border: 3px solid #000;
|
||||||
border-radius: 6px;
|
border-radius: 0;
|
||||||
background: #f0f0f0;
|
background: #ffcc00;
|
||||||
color: #333;
|
color: black;
|
||||||
font-weight: 600;
|
font-weight: 900;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
text-transform: uppercase;
|
||||||
|
box-shadow: 3px 3px 0px #000;
|
||||||
|
transition: all 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn:hover {
|
.action-btn:hover {
|
||||||
background: #e0e0e0;
|
transform: translate(2px, 2px); /* Click effect */
|
||||||
transform: translateY(-1px);
|
box-shadow: 1px 1px 0px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:active {
|
||||||
|
transform: translate(3px, 3px);
|
||||||
|
box-shadow: 0px 0px 0px #000;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,85 +7,162 @@ const { state, togglePlay, setSpeed } = useSimulation();
|
|||||||
const formattedTime = computed(() => {
|
const formattedTime = computed(() => {
|
||||||
const hours = Math.floor(state.value.currentTime);
|
const hours = Math.floor(state.value.currentTime);
|
||||||
const minutes = Math.floor((state.value.currentTime - hours) * 60);
|
const minutes = Math.floor((state.value.currentTime - hours) * 60);
|
||||||
return `Day ${state.value.day}, ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
return `Day ${state.value.day} - ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const netEnergy = computed(() => {
|
||||||
|
return (state.value.totalGeneration || 0) - (state.value.totalConsumption || 0);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="time-controls">
|
<div class="dashboard-bar">
|
||||||
<div class="time-display">{{ formattedTime }}</div>
|
|
||||||
<div class="controls">
|
<!-- Left: Energy Stats -->
|
||||||
<button @click="togglePlay" :class="{ active: state.isPlaying }">
|
<div class="stat-group">
|
||||||
{{ state.isPlaying ? 'Pause' : 'Play' }}
|
<div class="stat-item">
|
||||||
</button>
|
<span class="label">CONSUMPTION</span>
|
||||||
<div class="speed-controls">
|
<span class="value red">{{ Math.round(state.totalConsumption || 0) }} kW</span>
|
||||||
<button @click="setSpeed(1)" :class="{ active: state.speed === 1 }">1x</button>
|
</div>
|
||||||
<button @click="setSpeed(5)" :class="{ active: state.speed === 5 }">5x</button>
|
<div class="stat-item">
|
||||||
<button @click="setSpeed(20)" :class="{ active: state.speed === 20 }">20x</button>
|
<span class="label">GENERATION</span>
|
||||||
|
<span class="value green">{{ Math.round(state.totalGeneration || 0) }} kW</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="label">NET GRID</span>
|
||||||
|
<span class="value" :class="netEnergy >= 0 ? 'green' : 'red'">
|
||||||
|
{{ netEnergy > 0 ? '+' : ''}}{{ Math.round(netEnergy) }} kW
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Center: Time -->
|
||||||
|
<div class="time-display">
|
||||||
|
{{ formattedTime }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Controls -->
|
||||||
|
<div class="controls">
|
||||||
|
<div class="speed-controls">
|
||||||
|
<button @click="setSpeed(1)" :class="{ active: state.speed === 1 }">1x</button>
|
||||||
|
<button @click="setSpeed(200)" :class="{ active: state.speed === 200 }">200x</button>
|
||||||
|
</div>
|
||||||
|
<button class="play-btn" @click="togglePlay" :class="{ active: state.isPlaying }">
|
||||||
|
{{ state.isPlaying ? 'PAUSE' : 'PLAY' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.time-controls {
|
.dashboard-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 20px;
|
bottom: 0px;
|
||||||
left: 50%;
|
left: 0;
|
||||||
transform: translateX(-50%);
|
width: 100%;
|
||||||
background: rgba(20, 20, 25, 0.85);
|
height: 80px;
|
||||||
backdrop-filter: blur(8px);
|
background: #111;
|
||||||
padding: 12px 24px;
|
border-top: 4px solid #000;
|
||||||
border-radius: 30px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
padding: 0 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
color: white;
|
color: white;
|
||||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
font-family: 'Courier New', monospace;
|
||||||
|
z-index: 900;
|
||||||
|
box-shadow: 0 -4px 20px rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value.green { color: #00ff00; text-shadow: 0 0 10px rgba(0,255,0,0.3); }
|
||||||
|
.value.red { color: #ff3333; text-shadow: 0 0 10px rgba(255,0,0,0.3); }
|
||||||
|
|
||||||
.time-display {
|
.time-display {
|
||||||
font-family: 'Monaco', 'Consolas', monospace;
|
font-size: 1.8rem;
|
||||||
font-size: 1.1rem;
|
font-weight: 900;
|
||||||
font-weight: 600;
|
color: #FFD700; /* Gold */
|
||||||
color: #4facfe;
|
background: #000;
|
||||||
|
padding: 5px 20px;
|
||||||
|
border: 2px solid #333;
|
||||||
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid rgba(255,255,255,0.2);
|
|
||||||
color: white;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 15px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.active {
|
|
||||||
background: #4facfe;
|
|
||||||
color: white;
|
|
||||||
border-color: #4facfe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speed-controls {
|
.speed-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
background: #222;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.speed-controls button {
|
button {
|
||||||
padding: 4px 8px;
|
background: transparent;
|
||||||
font-size: 0.8rem;
|
border: 2px solid transparent;
|
||||||
|
color: #888;
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition: all 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
color: white;
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.active {
|
||||||
|
background: #FFD700;
|
||||||
|
color: black;
|
||||||
|
box-shadow: 0 0 10px #FFD700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn {
|
||||||
|
border: 2px solid #444;
|
||||||
|
padding: 10px 24px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
background: #222;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn:hover {
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn.active {
|
||||||
|
background: #ff3333; /* Stop/Pause color */
|
||||||
|
color: white;
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ export function useCameraControls(camera: THREE.Camera, renderer: THREE.WebGLRen
|
|||||||
|
|
||||||
const initControls = () => {
|
const initControls = () => {
|
||||||
controls = new OrbitControls(camera, renderer.domElement);
|
controls = new OrbitControls(camera, renderer.domElement);
|
||||||
controls.enableDamping = true;
|
controls.enableDamping = false;
|
||||||
controls.dampingFactor = 0.05;
|
controls.dampingFactor = 0.05;
|
||||||
controls.enableRotate = false;
|
controls.enableRotate = false;
|
||||||
controls.enablePan = true;
|
controls.enablePan = true;
|
||||||
controls.enableZoom = false;
|
controls.enableZoom = false;
|
||||||
|
controls.screenSpacePanning = false; // Fix: Pan on XZ plane only
|
||||||
// Strictly match legacy controls.js
|
// Strictly match legacy controls.js
|
||||||
controls.mouseButtons = {
|
controls.mouseButtons = {
|
||||||
LEFT: THREE.MOUSE.PAN
|
LEFT: THREE.MOUSE.PAN
|
||||||
@@ -38,19 +39,22 @@ export function useCameraControls(camera: THREE.Camera, renderer: THREE.WebGLRen
|
|||||||
renderer.domElement.addEventListener('wheel', (event: WheelEvent) => {
|
renderer.domElement.addEventListener('wheel', (event: WheelEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!moveAnimating) {
|
if (!moveAnimating) {
|
||||||
if (event.deltaY < 0 && camera.position.y <= 35) {
|
const zoomAmount = 5;
|
||||||
|
if (event.deltaY < 0 && camera.position.y <= 120) { // Increased Zoom Out limit
|
||||||
|
// Zoom Out (Up)
|
||||||
moveStart = camera.position.clone();
|
moveStart = camera.position.clone();
|
||||||
moveEnd = camera.position.clone().add(new THREE.Vector3(2, 10, 2));
|
moveEnd = camera.position.clone().add(new THREE.Vector3(0, zoomAmount, 0));
|
||||||
contStart = controls.target.clone();
|
contStart = controls.target.clone();
|
||||||
contEnd = controls.target.clone().add(new THREE.Vector3(2, 0, 2));
|
contEnd = controls.target.clone(); // Target stays same
|
||||||
moveProgress = 0;
|
moveProgress = 0;
|
||||||
moveStartTime = performance.now() / 1000;
|
moveStartTime = performance.now() / 1000;
|
||||||
moveAnimating = true;
|
moveAnimating = true;
|
||||||
} else if (event.deltaY > 0 && camera.position.y >= 30) {
|
} else if (event.deltaY > 0 && camera.position.y >= 10) {
|
||||||
|
// Zoom In (Down)
|
||||||
moveStart = camera.position.clone();
|
moveStart = camera.position.clone();
|
||||||
moveEnd = camera.position.clone().add(new THREE.Vector3(-2, -10, -2));
|
moveEnd = camera.position.clone().add(new THREE.Vector3(0, -zoomAmount, 0));
|
||||||
contStart = controls.target.clone();
|
contStart = controls.target.clone();
|
||||||
contEnd = controls.target.clone().add(new THREE.Vector3(-2, 0, -2));
|
contEnd = controls.target.clone();
|
||||||
moveProgress = 0;
|
moveProgress = 0;
|
||||||
moveStartTime = performance.now() / 1000;
|
moveStartTime = performance.now() / 1000;
|
||||||
moveAnimating = true;
|
moveAnimating = true;
|
||||||
@@ -78,7 +82,6 @@ export function useCameraControls(camera: THREE.Camera, renderer: THREE.WebGLRen
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
controls, // exposed if needed
|
|
||||||
initControls,
|
initControls,
|
||||||
updateControls,
|
updateControls,
|
||||||
getControls: () => controls
|
getControls: () => controls
|
||||||
|
|||||||
@@ -4,32 +4,49 @@ export function useCityObjects() {
|
|||||||
const buildings: THREE.Mesh[] = [];
|
const buildings: THREE.Mesh[] = [];
|
||||||
const interactableObjects: THREE.Object3D[] = [];
|
const interactableObjects: THREE.Object3D[] = [];
|
||||||
|
|
||||||
|
// --- Assets & Materials ---
|
||||||
|
const MATERIALS = {
|
||||||
|
road: new THREE.MeshLambertMaterial({ color: 0x333333 }),
|
||||||
|
roadLine: new THREE.MeshBasicMaterial({ color: 0xffffff }),
|
||||||
|
sidewalk: new THREE.MeshLambertMaterial({ color: 0xcccccc }),
|
||||||
|
grass: new THREE.MeshLambertMaterial({ color: 0x7cfc00 }),
|
||||||
|
treeTrunk: new THREE.MeshLambertMaterial({ color: 0x8b4513 }),
|
||||||
|
treeLeaves: new THREE.MeshLambertMaterial({ color: 0x228b22 }),
|
||||||
|
};
|
||||||
|
|
||||||
const buildingTypes: Record<string, any> = {
|
const buildingTypes: Record<string, any> = {
|
||||||
house: {
|
house: {
|
||||||
geometry: new THREE.BoxGeometry(4, 6, 4),
|
geometry: new THREE.BoxGeometry(6, 6, 6),
|
||||||
material: new THREE.MeshLambertMaterial({ color: 0x87CEEB }),
|
material: new THREE.MeshLambertMaterial({ color: 0xffaa00 }), // Vibrant Orange
|
||||||
description: "Residential House"
|
description: "Residential House"
|
||||||
},
|
},
|
||||||
factory: {
|
factory: {
|
||||||
geometry: (() => {
|
geometry: new THREE.BoxGeometry(8, 6, 10),
|
||||||
const shape = new THREE.Shape().moveTo(0, 0).lineTo(0, 6).lineTo(8, 6).lineTo(8, 8).lineTo(10, 8).lineTo(10, 0).lineTo(0, 0);
|
material: new THREE.MeshLambertMaterial({ color: 0xe74c3c }), // Red
|
||||||
return new THREE.ExtrudeGeometry(shape, { depth: 12, bevelEnabled: false });
|
|
||||||
})(),
|
|
||||||
material: new THREE.MeshLambertMaterial({ color: 0xB22222 }),
|
|
||||||
description: "Manufacturing Plant"
|
description: "Manufacturing Plant"
|
||||||
},
|
},
|
||||||
office: {
|
office: {
|
||||||
geometry: new THREE.BoxGeometry(6, 10, 6),
|
geometry: new THREE.BoxGeometry(8, 14, 8),
|
||||||
material: new THREE.MeshLambertMaterial({ color: 0xFFFFFF }),
|
material: new THREE.MeshLambertMaterial({ color: 0x3498db }), // Blue
|
||||||
description: "Office Building"
|
description: "Office Building"
|
||||||
},
|
},
|
||||||
shop: {
|
shop: {
|
||||||
geometry: new THREE.BoxGeometry(8, 4, 6),
|
geometry: new THREE.BoxGeometry(8, 5, 8),
|
||||||
material: new THREE.MeshLambertMaterial({ color: 0xd8d400 }),
|
material: new THREE.MeshLambertMaterial({ color: 0xf1c40f }), // Yellow
|
||||||
description: "Retail Shop"
|
description: "Retail Shop"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- City Generation Config ---
|
||||||
|
const BLOCK_SIZE = 30; // Size of one grid cell (including road half-width)
|
||||||
|
const ROAD_WIDTH = 8;
|
||||||
|
const SIDEWALK_WIDTH = 1.5;
|
||||||
|
const GRID_SIZE = 6; // 6x6 grid
|
||||||
|
|
||||||
|
// --- Instanced Mesh Config ---
|
||||||
|
const MAX_TREES = 200; // Safe upper bound for 6x6 grid with max 3 trees per plot
|
||||||
|
const GRID_AREA = GRID_SIZE * GRID_SIZE; // 36
|
||||||
|
|
||||||
const createGenericBuilding = (scene: THREE.Scene, buildingData: any, position: THREE.Vector3, id: string, csvPath: string, rotation: number = 0) => {
|
const createGenericBuilding = (scene: THREE.Scene, buildingData: any, position: THREE.Vector3, id: string, csvPath: string, rotation: number = 0) => {
|
||||||
const mesh = new THREE.Mesh(buildingData.geometry, buildingData.material);
|
const mesh = new THREE.Mesh(buildingData.geometry, buildingData.material);
|
||||||
mesh.position.copy(position);
|
mesh.position.copy(position);
|
||||||
@@ -51,46 +68,156 @@ export function useCityObjects() {
|
|||||||
scene.add(mesh);
|
scene.add(mesh);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createBuilding = (scene: THREE.Scene, type: string, position: THREE.Vector3, id: string, csvPath: string, rotation?: number) => {
|
const createBuilding = (scene: THREE.Scene, type: string, position: THREE.Vector3, id: string, csvPath: string, rotation: number = 0) => {
|
||||||
const buildingData = buildingTypes[type];
|
const buildingData = buildingTypes[type];
|
||||||
if (!buildingData) {
|
if (!buildingData) {
|
||||||
console.warn(`Building type ${type} not found`);
|
console.warn(`Building type ${type} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
createGenericBuilding(scene, buildingData, position, id, csvPath, rotation);
|
// Adjust position to sit on sidewalk/grass (y=0.5)
|
||||||
|
const pos = position.clone();
|
||||||
|
pos.y = 0.5;
|
||||||
|
|
||||||
|
createGenericBuilding(scene, buildingData, pos, id, csvPath, rotation);
|
||||||
};
|
};
|
||||||
|
|
||||||
const initGround = (scene: THREE.Scene) => {
|
const initGround = (scene: THREE.Scene) => {
|
||||||
const groundGeometry = new THREE.PlaneGeometry(150, 150);
|
// Base Plane (Asphalt)
|
||||||
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x91ca49, side: THREE.DoubleSide });
|
const baseGeo = new THREE.PlaneGeometry(300, 300);
|
||||||
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
const base = new THREE.Mesh(baseGeo, MATERIALS.road);
|
||||||
ground.rotation.x = -Math.PI / 2;
|
base.rotation.x = -Math.PI / 2;
|
||||||
ground.receiveShadow = true;
|
base.position.y = -0.1;
|
||||||
scene.add(ground);
|
base.receiveShadow = true;
|
||||||
|
scene.add(base);
|
||||||
|
|
||||||
|
const plots: THREE.Vector3[] = [];
|
||||||
|
const offset = ((GRID_SIZE - 1) * BLOCK_SIZE) / 2;
|
||||||
|
|
||||||
|
// --- Instanced Meshes Setup ---
|
||||||
|
// 1. Sidewalks
|
||||||
|
const sidewalkGeo = new THREE.BoxGeometry(BLOCK_SIZE - ROAD_WIDTH, 0.4, BLOCK_SIZE - ROAD_WIDTH);
|
||||||
|
const sidewalkMesh = new THREE.InstancedMesh(sidewalkGeo, MATERIALS.sidewalk, GRID_AREA);
|
||||||
|
sidewalkMesh.receiveShadow = true;
|
||||||
|
|
||||||
|
// 2. Grass
|
||||||
|
const grassSize = (BLOCK_SIZE - ROAD_WIDTH) - (SIDEWALK_WIDTH * 2);
|
||||||
|
const grassGeo = new THREE.BoxGeometry(grassSize, 0.5, grassSize);
|
||||||
|
const grassMesh = new THREE.InstancedMesh(grassGeo, MATERIALS.grass, GRID_AREA);
|
||||||
|
grassMesh.receiveShadow = true;
|
||||||
|
|
||||||
|
const dummy = new THREE.Object3D();
|
||||||
|
let instanceIdx = 0;
|
||||||
|
|
||||||
|
for (let ix = 0; ix < GRID_SIZE; ix++) {
|
||||||
|
for (let iz = 0; iz < GRID_SIZE; iz++) {
|
||||||
|
const x = (ix * BLOCK_SIZE) - offset;
|
||||||
|
const z = (iz * BLOCK_SIZE) - offset;
|
||||||
|
|
||||||
|
// Position Helper
|
||||||
|
dummy.position.set(x, 0.2, z);
|
||||||
|
dummy.rotation.set(0, 0, 0);
|
||||||
|
dummy.scale.set(1, 1, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
sidewalkMesh.setMatrixAt(instanceIdx, dummy.matrix);
|
||||||
|
|
||||||
|
dummy.position.set(x, 0.25, z);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
grassMesh.setMatrixAt(instanceIdx, dummy.matrix);
|
||||||
|
|
||||||
|
plots.push(new THREE.Vector3(x, 0, z));
|
||||||
|
instanceIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.add(sidewalkMesh);
|
||||||
|
scene.add(grassMesh);
|
||||||
|
|
||||||
|
// Grid Lines
|
||||||
|
const gridHelper = new THREE.GridHelper(GRID_SIZE * BLOCK_SIZE, GRID_SIZE, 0xffffff, 0xffffff);
|
||||||
|
gridHelper.position.y = 0.05;
|
||||||
|
// scene.add(gridHelper); // Optional, kept off for clean look
|
||||||
|
|
||||||
|
return plots;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initBuildings = (scene: THREE.Scene) => {
|
const initBuildings = (scene: THREE.Scene, plots: THREE.Vector3[]) => {
|
||||||
createBuilding(scene, 'house', new THREE.Vector3(-9, 0, 24), 'H01', 'data/H01.csv');
|
const buildingDefs = [
|
||||||
createBuilding(scene, 'factory', new THREE.Vector3(48, 0, -49), 'H02', 'data/H02.csv');
|
{ id: 'H01', type: 'house', csv: 'data/H01.csv' },
|
||||||
createBuilding(scene, 'office', new THREE.Vector3(-38, 0, -12), 'H03', 'data/H03.csv');
|
{ id: 'H02', type: 'factory', csv: 'data/H02.csv' },
|
||||||
createBuilding(scene, 'shop', new THREE.Vector3(-5, 0, -5), 'H04', 'data/H04.csv', Math.PI / 2);
|
{ id: 'H03', type: 'office', csv: 'data/H03.csv' },
|
||||||
createBuilding(scene, 'house', new THREE.Vector3(-5, 0, 40), 'H05', 'data/H05.csv');
|
{ id: 'H04', type: 'shop', csv: 'data/H04.csv' },
|
||||||
createBuilding(scene, 'house', new THREE.Vector3(6, 0, 24), 'H06', 'data/H06.csv');
|
{ id: 'H05', type: 'house', csv: 'data/H05.csv' },
|
||||||
createBuilding(scene, 'house', new THREE.Vector3(-37, 0, 30), 'H07', 'data/H07.csv');
|
{ id: 'H06', type: 'house', csv: 'data/H06.csv' },
|
||||||
createBuilding(scene, 'house', new THREE.Vector3(-34, 0, 9), 'H08', 'data/H08.csv', -Math.PI / 2);
|
{ id: 'H07', type: 'house', csv: 'data/H07.csv' },
|
||||||
createBuilding(scene, 'house', new THREE.Vector3(19, 0, -20), 'H09', 'data/H09.csv', Math.PI / 2);
|
{ id: 'H08', type: 'shop', csv: 'data/H08.csv' },
|
||||||
createBuilding(scene, 'house', new THREE.Vector3(36, 0, 6), 'H10', 'data/H10.csv');
|
{ id: 'H09', type: 'shop', csv: 'data/H09.csv' },
|
||||||
createBuilding(scene, 'house', new THREE.Vector3(34, 0, -19), 'H11', 'data/H11.csv', Math.PI);
|
{ id: 'H10', type: 'house', csv: 'data/H10.csv' },
|
||||||
createBuilding(scene, 'house', new THREE.Vector3(-21, 0, -18), 'H12', 'data/H12.csv');
|
{ id: 'H11', type: 'house', csv: 'data/H11.csv' },
|
||||||
createBuilding(scene, 'house', new THREE.Vector3(-5, 0, -20), 'H13', 'data/H13.csv');
|
{ id: 'H12', type: 'house', csv: 'data/H12.csv' },
|
||||||
createBuilding(scene, 'house', new THREE.Vector3(-7, 0, -48), 'H14', 'data/H14.csv');
|
{ id: 'H13', type: 'house', csv: 'data/H13.csv' },
|
||||||
|
{ id: 'H14', type: 'house', csv: 'data/H14.csv' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Tree Instancing Setup ---
|
||||||
|
const trunkGeo = new THREE.CylinderGeometry(0.5, 0.5, 2, 6);
|
||||||
|
const leavesGeo = new THREE.ConeGeometry(2, 4, 8);
|
||||||
|
|
||||||
|
const trunkMesh = new THREE.InstancedMesh(trunkGeo, MATERIALS.treeTrunk, MAX_TREES);
|
||||||
|
const leavesMesh = new THREE.InstancedMesh(leavesGeo, MATERIALS.treeLeaves, MAX_TREES);
|
||||||
|
trunkMesh.castShadow = true; trunkMesh.receiveShadow = true;
|
||||||
|
leavesMesh.castShadow = true; leavesMesh.receiveShadow = true;
|
||||||
|
|
||||||
|
let treeCount = 0;
|
||||||
|
const dummy = new THREE.Object3D();
|
||||||
|
|
||||||
|
const addTreeInstance = (pos: THREE.Vector3) => {
|
||||||
|
if (treeCount >= MAX_TREES) return;
|
||||||
|
|
||||||
|
// Trunk
|
||||||
|
dummy.position.copy(pos).add(new THREE.Vector3(0, 1, 0));
|
||||||
|
dummy.rotation.set(0, 0, 0);
|
||||||
|
dummy.scale.set(1, 1, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
trunkMesh.setMatrixAt(treeCount, dummy.matrix);
|
||||||
|
|
||||||
|
// Leaves
|
||||||
|
dummy.position.copy(pos).add(new THREE.Vector3(0, 4, 0));
|
||||||
|
dummy.updateMatrix();
|
||||||
|
leavesMesh.setMatrixAt(treeCount, dummy.matrix);
|
||||||
|
|
||||||
|
treeCount++;
|
||||||
|
};
|
||||||
|
|
||||||
|
let buildingIndex = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < plots.length; i++) {
|
||||||
|
const plot = plots[i];
|
||||||
|
|
||||||
|
if (buildingIndex < buildingDefs.length) {
|
||||||
|
const def = buildingDefs[buildingIndex];
|
||||||
|
createBuilding(scene, def.type, plot, def.id, def.csv, 0);
|
||||||
|
buildingIndex++;
|
||||||
|
} else {
|
||||||
|
// Empty plot filler: Trees
|
||||||
|
addTreeInstance(plot.clone().add(new THREE.Vector3(-5, 0.5, -5)));
|
||||||
|
if (Math.random() > 0.5) {
|
||||||
|
addTreeInstance(plot.clone().add(new THREE.Vector3(6, 0.5, 4)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize Instanced Meshes
|
||||||
|
trunkMesh.count = treeCount;
|
||||||
|
leavesMesh.count = treeCount;
|
||||||
|
scene.add(trunkMesh);
|
||||||
|
scene.add(leavesMesh);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
interactableObjects,
|
interactableObjects,
|
||||||
initCity: (scene: THREE.Scene) => {
|
initCity: (scene: THREE.Scene) => {
|
||||||
initGround(scene);
|
const plots = initGround(scene);
|
||||||
initBuildings(scene);
|
initBuildings(scene, plots);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,11 @@ export function useInteraction(
|
|||||||
if (controls) controls.enabled = !event.value;
|
if (controls) controls.enabled = !event.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
scene.add(transformControls as unknown as THREE.Object3D);
|
try {
|
||||||
|
scene.add(transformControls.getHelper());
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to add TransformControls helper to scene:", e);
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('keydown', (event) => {
|
window.addEventListener('keydown', (event) => {
|
||||||
if (event.key === 'm') {
|
if (event.key === 'm') {
|
||||||
@@ -109,9 +113,26 @@ export function useInteraction(
|
|||||||
});
|
});
|
||||||
|
|
||||||
renderer.domElement.addEventListener('pointerdown', onPointerDown);
|
renderer.domElement.addEventListener('pointerdown', onPointerDown);
|
||||||
|
renderer.domElement.addEventListener('pointerup', onPointerUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
|
||||||
const onPointerDown = (event: MouseEvent) => {
|
const onPointerDown = (event: MouseEvent) => {
|
||||||
|
startX = event.clientX;
|
||||||
|
startY = event.clientY;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = (event: MouseEvent) => {
|
||||||
|
const diffX = Math.abs(event.clientX - startX);
|
||||||
|
const diffY = Math.abs(event.clientY - startY);
|
||||||
|
|
||||||
|
// Click Threshold (pixels)
|
||||||
|
if (diffX > 5 || diffY > 5) {
|
||||||
|
return; // Dragged, ignore
|
||||||
|
}
|
||||||
|
|
||||||
const rect = renderer.domElement.getBoundingClientRect();
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||||
@@ -128,8 +149,6 @@ export function useInteraction(
|
|||||||
} else {
|
} else {
|
||||||
transformControls.detach();
|
transformControls.detach();
|
||||||
}
|
}
|
||||||
// Fall through to allow selection info/ring even in move mode,
|
|
||||||
// matching legacy behavior where listeners likely co-existed.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selection Mode
|
// Selection Mode
|
||||||
@@ -139,7 +158,7 @@ export function useInteraction(
|
|||||||
|
|
||||||
const data = group.userData;
|
const data = group.userData;
|
||||||
|
|
||||||
// Screen coords
|
// Screen coords for Info Window
|
||||||
const vector = group.position.clone();
|
const vector = group.position.clone();
|
||||||
vector.project(camera);
|
vector.project(camera);
|
||||||
const x = (vector.x * .5 + .5) * rect.width;
|
const x = (vector.x * .5 + .5) * rect.width;
|
||||||
@@ -170,6 +189,7 @@ export function useInteraction(
|
|||||||
}
|
}
|
||||||
updateSelectionRing(group);
|
updateSelectionRing(group);
|
||||||
} else {
|
} else {
|
||||||
|
// Only Deselect if we clicked on NOTHING, and we weren't dragging.
|
||||||
selectedBuilding.value = null;
|
selectedBuilding.value = null;
|
||||||
updateSelectionRing(null as any);
|
updateSelectionRing(null as any);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function useSunSystem(scene: THREE.Scene, state: Ref<{ currentTime: numbe
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initSun = () => {
|
const initSun = () => {
|
||||||
ambientLight = new THREE.AmbientLight(0x90bbbd, 0.9);
|
ambientLight = new THREE.AmbientLight(0x90bbbd, 1.2); // Boosted Ambient
|
||||||
scene.add(ambientLight);
|
scene.add(ambientLight);
|
||||||
|
|
||||||
directionalLight = new THREE.DirectionalLight(0xffffff);
|
directionalLight = new THREE.DirectionalLight(0xffffff);
|
||||||
@@ -52,7 +52,7 @@ export function useSunSystem(scene: THREE.Scene, state: Ref<{ currentTime: numbe
|
|||||||
sun: COLORS.daySun,
|
sun: COLORS.daySun,
|
||||||
ambient: COLORS.dayAmbient,
|
ambient: COLORS.dayAmbient,
|
||||||
bg: COLORS.day,
|
bg: COLORS.day,
|
||||||
intensity: 1.0,
|
intensity: 1.5, // Boosted Sun Intensity
|
||||||
shadow: true
|
shadow: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ export function useSunSystem(scene: THREE.Scene, state: Ref<{ currentTime: numbe
|
|||||||
sun: COLORS.tempSun,
|
sun: COLORS.tempSun,
|
||||||
ambient: COLORS.tempAmbient,
|
ambient: COLORS.tempAmbient,
|
||||||
bg: COLORS.tempBg, // We might want a more "orange" sky here, but lerp is safe
|
bg: COLORS.tempBg, // We might want a more "orange" sky here, but lerp is safe
|
||||||
intensity: 0.5 + (0.5 * t), // 0.5 to 1.0
|
intensity: 0.5 + (1.0 * t), // 0.5 to 1.5
|
||||||
shadow: true
|
shadow: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -102,14 +102,7 @@ export function useSunSystem(scene: THREE.Scene, state: Ref<{ currentTime: numbe
|
|||||||
const updateSun = () => {
|
const updateSun = () => {
|
||||||
if (!directionalLight || !ambientLight) return;
|
if (!directionalLight || !ambientLight) return;
|
||||||
|
|
||||||
sunPosition();
|
// Calculate Position
|
||||||
|
|
||||||
// Get elevation from the already calculated position
|
|
||||||
// We can re-calculate or just extract it.
|
|
||||||
// We have `sunPosition` function but it updates the light directly.
|
|
||||||
// Let's rely on state.
|
|
||||||
// Actually, we need the elevation.
|
|
||||||
// Ideally `sunPosition` returns it.
|
|
||||||
const latitude = 41.17873;
|
const latitude = 41.17873;
|
||||||
const longitude = -8.60835;
|
const longitude = -8.60835;
|
||||||
const currentHour = state.value.currentTime;
|
const currentHour = state.value.currentTime;
|
||||||
@@ -117,8 +110,25 @@ export function useSunSystem(scene: THREE.Scene, state: Ref<{ currentTime: numbe
|
|||||||
now.setHours(Math.floor(currentHour));
|
now.setHours(Math.floor(currentHour));
|
||||||
now.setMinutes(Math.floor((currentHour % 1) * 60));
|
now.setMinutes(Math.floor((currentHour % 1) * 60));
|
||||||
now.setSeconds(0);
|
now.setSeconds(0);
|
||||||
|
|
||||||
const sunPos = SunCalc.getPosition(now, latitude, longitude);
|
const sunPos = SunCalc.getPosition(now, latitude, longitude);
|
||||||
|
|
||||||
|
// Position the light
|
||||||
|
const sunRadius = 100;
|
||||||
|
const azimuth = sunPos.azimuth;
|
||||||
|
const elevation = sunPos.altitude;
|
||||||
|
|
||||||
|
const sunX = sunRadius * Math.cos(elevation) * Math.cos(azimuth);
|
||||||
|
const sunY = sunRadius * Math.sin(elevation);
|
||||||
|
const sunZ = sunRadius * Math.cos(elevation) * Math.sin(azimuth);
|
||||||
|
|
||||||
|
if (directionalLight) {
|
||||||
|
directionalLight.position.set(sunX, sunY, sunZ);
|
||||||
|
directionalLight.target.position.set(0, 0, 0);
|
||||||
|
directionalLight.target.updateMatrixWorld();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Colors based on elevation
|
||||||
const colors = getSunColors(sunPos.altitude);
|
const colors = getSunColors(sunPos.altitude);
|
||||||
|
|
||||||
directionalLight.color.copy(colors.sun);
|
directionalLight.color.copy(colors.sun);
|
||||||
|
|||||||
@@ -7,8 +7,12 @@ interface SimulationState {
|
|||||||
speed: number;
|
speed: number;
|
||||||
day: number;
|
day: number;
|
||||||
totalSeconds: number;
|
totalSeconds: number;
|
||||||
|
totalConsumption: number;
|
||||||
|
totalGeneration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface BuildingData {
|
interface BuildingData {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -32,9 +36,11 @@ const dayOfYear = Math.floor(diff / oneDay);
|
|||||||
const state = ref<SimulationState>({
|
const state = ref<SimulationState>({
|
||||||
currentTime: now.getHours() + now.getMinutes() / 60,
|
currentTime: now.getHours() + now.getMinutes() / 60,
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
speed: 1, // 1 real sec = 1 game min (default)
|
speed: 1,
|
||||||
day: dayOfYear,
|
day: dayOfYear,
|
||||||
totalSeconds: 0 // Will be calculated
|
totalSeconds: 0,
|
||||||
|
totalConsumption: 0,
|
||||||
|
totalGeneration: 0
|
||||||
});
|
});
|
||||||
const selectedBuilding = ref<BuildingData | null>(null);
|
const selectedBuilding = ref<BuildingData | null>(null);
|
||||||
|
|
||||||
@@ -48,9 +54,8 @@ export const useSimulation = () => {
|
|||||||
state.value.speed = speed;
|
state.value.speed = speed;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadBuildingData = (buildingId: string, csvPath: string) => {
|
const loadBuildingData = (_buildingId: string, csvPath: string) => {
|
||||||
console.log(`Loading data for ${buildingId} from ${csvPath}`);
|
// console.log(`Loading data for ${_buildingId} from ${csvPath}`);
|
||||||
// In a real app we might cache this
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Papa.parse(csvPath, {
|
Papa.parse(csvPath, {
|
||||||
download: true,
|
download: true,
|
||||||
@@ -68,12 +73,39 @@ export const useSimulation = () => {
|
|||||||
|
|
||||||
const updateTime = (delta: number) => {
|
const updateTime = (delta: number) => {
|
||||||
if (!state.value.isPlaying) return;
|
if (!state.value.isPlaying) return;
|
||||||
// Simple time progression mock
|
|
||||||
state.value.currentTime += delta * state.value.speed;
|
state.value.currentTime += delta * state.value.speed;
|
||||||
if (state.value.currentTime >= 24) {
|
if (state.value.currentTime >= 24) {
|
||||||
state.value.currentTime = 0;
|
state.value.currentTime = 0;
|
||||||
state.value.day++;
|
state.value.day++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/composables/useSimulation.ts","./src/composables/three/useCameraControls.ts","./src/composables/three/useCityObjects.ts","./src/composables/three/useInteraction.ts","./src/composables/three/useSunSystem.ts","./src/composables/three/useThreeScene.ts","./src/App.vue","./src/components/BuildingInfoCanvas.vue","./src/components/DigitalTwin.vue","./src/components/TimeControls.vue"],"errors":true,"version":"5.6.3"}
|
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/composables/useSimulation.ts","./src/composables/three/useCameraControls.ts","./src/composables/three/useCityObjects.ts","./src/composables/three/useInteraction.ts","./src/composables/three/useSunSystem.ts","./src/composables/three/useThreeScene.ts","./src/App.vue","./src/components/BuildingInfoCanvas.vue","./src/components/DigitalTwin.vue","./src/components/TimeControls.vue"],"version":"5.6.3"}
|
||||||
Reference in New Issue
Block a user