SRP
This commit is contained in:
5
web-app/build_log.txt
Normal file
5
web-app/build_log.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
> web-app@0.0.0 build
|
||||||
|
> vue-tsc -b && vite build
|
||||||
|
|
||||||
|
src/components/AddBuildingModal.vue(40,10): error TS6133: 'ref' is declared but its value is never read.
|
||||||
File diff suppressed because one or more lines are too long
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-BXWyap6F.js"></script>
|
<script type="module" crossorigin src="/assets/index-Bso7APm4.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BqNgBlW1.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D9Xb8bfs.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import DigitalTwin from './components/DigitalTwin.vue'
|
|||||||
import BuildingInfoCanvas from './components/BuildingInfoCanvas.vue'
|
import BuildingInfoCanvas from './components/BuildingInfoCanvas.vue'
|
||||||
import TimeControls from './components/TimeControls.vue'
|
import TimeControls from './components/TimeControls.vue'
|
||||||
import SettingsModal from './components/SettingsModal.vue'
|
import SettingsModal from './components/SettingsModal.vue'
|
||||||
|
import AddBuildingModal from './components/AddBuildingModal.vue'
|
||||||
|
|
||||||
const isSettingsOpen = ref(false);
|
const isSettingsOpen = ref(false);
|
||||||
|
const isAddBuildingOpen = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -17,7 +19,12 @@ const isSettingsOpen = ref(false);
|
|||||||
⚙ Settings
|
⚙ Settings
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button class="add-building-btn" @click="isAddBuildingOpen = true">
|
||||||
|
+ Add Building
|
||||||
|
</button>
|
||||||
|
|
||||||
<SettingsModal :isOpen="isSettingsOpen" @close="isSettingsOpen = false" />
|
<SettingsModal :isOpen="isSettingsOpen" @close="isSettingsOpen = false" />
|
||||||
|
<AddBuildingModal :isOpen="isAddBuildingOpen" @close="isAddBuildingOpen = false" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -58,4 +65,33 @@ html, body, #app {
|
|||||||
transform: translate(4px, 4px);
|
transform: translate(4px, 4px);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-building-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 160px; /* Positioned to the left of Settings */
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #000;
|
||||||
|
color: white;
|
||||||
|
border: 2px solid #00FF00; /* Green accent */
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
box-shadow: 4px 4px 0 rgba(0,0,0,0.5);
|
||||||
|
transition: all 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-building-btn:hover {
|
||||||
|
background: #111;
|
||||||
|
color: #00FF00;
|
||||||
|
transform: translate(1px, 1px);
|
||||||
|
box-shadow: 3px 3px 0 rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-building-btn:active {
|
||||||
|
transform: translate(4px, 4px);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
212
web-app/src/components/AddBuildingModal.vue
Normal file
212
web-app/src/components/AddBuildingModal.vue
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="isOpen" class="modal-overlay">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Add New Building</h3>
|
||||||
|
<button class="close-btn" @click="close">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="setting-group">
|
||||||
|
<label>Building ID</label>
|
||||||
|
<input v-model="form.id" type="text" class="input-field" placeholder="e.g. H15" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<label>Type</label>
|
||||||
|
<select v-model="form.type" class="input-field select-field">
|
||||||
|
<option value="house">House</option>
|
||||||
|
<option value="factory">Factory</option>
|
||||||
|
<option value="office">Office</option>
|
||||||
|
<option value="shop">Shop</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<label>Data Source (CSV Path)</label>
|
||||||
|
<input v-model="form.csvPath" type="text" class="input-field" placeholder="e.g. data/H01.csv" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn secondary" @click="close">Cancel</button>
|
||||||
|
<button class="btn primary" @click="confirm">Add Building</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { watch, reactive } from 'vue';
|
||||||
|
import { useSimulation } from '../composables/useSimulation';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
isOpen: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const { addBuilding, state } = useSimulation();
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
id: '',
|
||||||
|
type: 'house',
|
||||||
|
csvPath: 'data/H01.csv'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-generate ID when opened
|
||||||
|
watch(() => props.isOpen, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
const nextNum = state.value.buildings.length + 1;
|
||||||
|
form.id = `H${nextNum.toString().padStart(2, '0')}`;
|
||||||
|
form.type = 'house';
|
||||||
|
form.csvPath = 'data/H01.csv';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirm = () => {
|
||||||
|
addBuilding({ ...form });
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Reuse styles from SettingsModal to maintain consistency */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 3px solid #000;
|
||||||
|
width: 400px;
|
||||||
|
max-width: 90%;
|
||||||
|
color: white;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
box-shadow: 10px 10px 0px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background: #FFD700;
|
||||||
|
color: black;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 3px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: black;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
background: #000;
|
||||||
|
border: 2px solid #333;
|
||||||
|
color: white;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #FFD700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-field {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
border-top: 2px solid #333;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #000;
|
||||||
|
box-shadow: 2px 2px 0px #000;
|
||||||
|
transition: all 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary {
|
||||||
|
background: #FFD700;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary:hover {
|
||||||
|
background: #ffea70;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.secondary {
|
||||||
|
background: #444;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.secondary:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue';
|
import { onMounted, watch } from 'vue';
|
||||||
import { useSimulation } from '../composables/useSimulation';
|
import { useSimulation } from '../composables/useSimulation';
|
||||||
import { useThreeScene } from '../composables/three/useThreeScene';
|
import { useThreeScene } from '../composables/three/useThreeScene';
|
||||||
import { useSunSystem } from '../composables/three/useSunSystem';
|
import { useSunSystem } from '../composables/three/useSunSystem';
|
||||||
@@ -24,8 +24,16 @@ const init = () => {
|
|||||||
const renderer = getRenderer();
|
const renderer = getRenderer();
|
||||||
|
|
||||||
// 1. City Objects
|
// 1. City Objects
|
||||||
const { initCity, interactableObjects } = useCityObjects();
|
const { initCity, interactableObjects, clearCity } = useCityObjects();
|
||||||
initCity(scene);
|
// @ts-ignore - 'buildings' added to state dynamically
|
||||||
|
initCity(scene, state.value.buildings);
|
||||||
|
|
||||||
|
// Watch for new buildings
|
||||||
|
watch(() => state.value.buildings.length, () => {
|
||||||
|
clearCity(scene);
|
||||||
|
// @ts-ignore
|
||||||
|
initCity(scene, state.value.buildings);
|
||||||
|
});
|
||||||
|
|
||||||
// 2. Sun System
|
// 2. Sun System
|
||||||
const { initSun, updateSun } = useSunSystem(scene, state);
|
const { initSun, updateSun } = useSunSystem(scene, state);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as THREE from 'three';
|
|||||||
export function useCityObjects() {
|
export function useCityObjects() {
|
||||||
const buildings: THREE.Mesh[] = [];
|
const buildings: THREE.Mesh[] = [];
|
||||||
const interactableObjects: THREE.Object3D[] = [];
|
const interactableObjects: THREE.Object3D[] = [];
|
||||||
|
const sceneObjects: THREE.Object3D[] = [];
|
||||||
|
|
||||||
// --- Assets & Materials ---
|
// --- Assets & Materials ---
|
||||||
const MATERIALS = {
|
const MATERIALS = {
|
||||||
@@ -65,6 +66,7 @@ export function useCityObjects() {
|
|||||||
};
|
};
|
||||||
interactableObjects.push(mesh);
|
interactableObjects.push(mesh);
|
||||||
buildings.push(mesh);
|
buildings.push(mesh);
|
||||||
|
sceneObjects.push(mesh); // Track
|
||||||
scene.add(mesh);
|
scene.add(mesh);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,29 +84,32 @@ export function useCityObjects() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initGround = (scene: THREE.Scene) => {
|
const initGround = (scene: THREE.Scene) => {
|
||||||
// Base Plane (Asphalt)
|
// ... (Base creation)
|
||||||
const baseGeo = new THREE.PlaneGeometry(300, 300);
|
const baseGeo = new THREE.PlaneGeometry(300, 300);
|
||||||
const base = new THREE.Mesh(baseGeo, MATERIALS.road);
|
const base = new THREE.Mesh(baseGeo, MATERIALS.road);
|
||||||
|
// ...
|
||||||
base.rotation.x = -Math.PI / 2;
|
base.rotation.x = -Math.PI / 2;
|
||||||
base.position.y = -0.1;
|
base.position.y = -0.1;
|
||||||
base.receiveShadow = true;
|
base.receiveShadow = true;
|
||||||
scene.add(base);
|
scene.add(base);
|
||||||
|
sceneObjects.push(base); // Track
|
||||||
|
|
||||||
const plots: THREE.Vector3[] = [];
|
const plots: THREE.Vector3[] = [];
|
||||||
const offset = ((GRID_SIZE - 1) * BLOCK_SIZE) / 2;
|
const offset = ((GRID_SIZE - 1) * BLOCK_SIZE) / 2;
|
||||||
|
|
||||||
// --- Instanced Meshes Setup ---
|
// ... (Instanced Meshes)
|
||||||
// 1. Sidewalks
|
|
||||||
const sidewalkGeo = new THREE.BoxGeometry(BLOCK_SIZE - ROAD_WIDTH, 0.4, BLOCK_SIZE - ROAD_WIDTH);
|
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);
|
const sidewalkMesh = new THREE.InstancedMesh(sidewalkGeo, MATERIALS.sidewalk, GRID_AREA);
|
||||||
sidewalkMesh.receiveShadow = true;
|
sidewalkMesh.receiveShadow = true;
|
||||||
|
sceneObjects.push(sidewalkMesh); // Track
|
||||||
|
|
||||||
// 2. Grass
|
|
||||||
const grassSize = (BLOCK_SIZE - ROAD_WIDTH) - (SIDEWALK_WIDTH * 2);
|
const grassSize = (BLOCK_SIZE - ROAD_WIDTH) - (SIDEWALK_WIDTH * 2);
|
||||||
const grassGeo = new THREE.BoxGeometry(grassSize, 0.5, grassSize);
|
const grassGeo = new THREE.BoxGeometry(grassSize, 0.5, grassSize);
|
||||||
const grassMesh = new THREE.InstancedMesh(grassGeo, MATERIALS.grass, GRID_AREA);
|
const grassMesh = new THREE.InstancedMesh(grassGeo, MATERIALS.grass, GRID_AREA);
|
||||||
grassMesh.receiveShadow = true;
|
grassMesh.receiveShadow = true;
|
||||||
|
sceneObjects.push(grassMesh); // Track
|
||||||
|
|
||||||
|
// ... (Loop to fill instances and plots - remains same)
|
||||||
const dummy = new THREE.Object3D();
|
const dummy = new THREE.Object3D();
|
||||||
let instanceIdx = 0;
|
let instanceIdx = 0;
|
||||||
|
|
||||||
@@ -132,33 +137,11 @@ export function useCityObjects() {
|
|||||||
scene.add(sidewalkMesh);
|
scene.add(sidewalkMesh);
|
||||||
scene.add(grassMesh);
|
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;
|
return plots;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initBuildings = (scene: THREE.Scene, plots: THREE.Vector3[]) => {
|
const initBuildings = (scene: THREE.Scene, plots: THREE.Vector3[], buildingDefs: any[]) => {
|
||||||
const buildingDefs = [
|
// ... (Trees setup)
|
||||||
{ id: 'H01', type: 'house', csv: 'data/H01.csv' },
|
|
||||||
{ id: 'H02', type: 'factory', csv: 'data/H02.csv' },
|
|
||||||
{ id: 'H03', type: 'office', csv: 'data/H03.csv' },
|
|
||||||
{ id: 'H04', type: 'shop', csv: 'data/H04.csv' },
|
|
||||||
{ id: 'H05', type: 'house', csv: 'data/H05.csv' },
|
|
||||||
{ id: 'H06', type: 'house', csv: 'data/H06.csv' },
|
|
||||||
{ id: 'H07', type: 'house', csv: 'data/H07.csv' },
|
|
||||||
{ id: 'H08', type: 'shop', csv: 'data/H08.csv' },
|
|
||||||
{ id: 'H09', type: 'shop', csv: 'data/H09.csv' },
|
|
||||||
{ id: 'H10', type: 'house', csv: 'data/H10.csv' },
|
|
||||||
{ id: 'H11', type: 'house', csv: 'data/H11.csv' },
|
|
||||||
{ id: 'H12', type: 'house', csv: 'data/H12.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 trunkGeo = new THREE.CylinderGeometry(0.5, 0.5, 2, 6);
|
||||||
const leavesGeo = new THREE.ConeGeometry(2, 4, 8);
|
const leavesGeo = new THREE.ConeGeometry(2, 4, 8);
|
||||||
|
|
||||||
@@ -166,21 +149,21 @@ export function useCityObjects() {
|
|||||||
const leavesMesh = new THREE.InstancedMesh(leavesGeo, MATERIALS.treeLeaves, MAX_TREES);
|
const leavesMesh = new THREE.InstancedMesh(leavesGeo, MATERIALS.treeLeaves, MAX_TREES);
|
||||||
trunkMesh.castShadow = true; trunkMesh.receiveShadow = true;
|
trunkMesh.castShadow = true; trunkMesh.receiveShadow = true;
|
||||||
leavesMesh.castShadow = true; leavesMesh.receiveShadow = true;
|
leavesMesh.castShadow = true; leavesMesh.receiveShadow = true;
|
||||||
|
sceneObjects.push(trunkMesh); // Track
|
||||||
|
sceneObjects.push(leavesMesh); // Track
|
||||||
|
|
||||||
let treeCount = 0;
|
let treeCount = 0;
|
||||||
const dummy = new THREE.Object3D();
|
const dummy = new THREE.Object3D();
|
||||||
|
|
||||||
const addTreeInstance = (pos: THREE.Vector3) => {
|
const addTreeInstance = (pos: THREE.Vector3) => {
|
||||||
|
// ... (remains same)
|
||||||
if (treeCount >= MAX_TREES) return;
|
if (treeCount >= MAX_TREES) return;
|
||||||
|
|
||||||
// Trunk
|
|
||||||
dummy.position.copy(pos).add(new THREE.Vector3(0, 1, 0));
|
dummy.position.copy(pos).add(new THREE.Vector3(0, 1, 0));
|
||||||
dummy.rotation.set(0, 0, 0);
|
dummy.rotation.set(0, 0, 0);
|
||||||
dummy.scale.set(1, 1, 1);
|
dummy.scale.set(1, 1, 1);
|
||||||
dummy.updateMatrix();
|
dummy.updateMatrix();
|
||||||
trunkMesh.setMatrixAt(treeCount, dummy.matrix);
|
trunkMesh.setMatrixAt(treeCount, dummy.matrix);
|
||||||
|
|
||||||
// Leaves
|
|
||||||
dummy.position.copy(pos).add(new THREE.Vector3(0, 4, 0));
|
dummy.position.copy(pos).add(new THREE.Vector3(0, 4, 0));
|
||||||
dummy.updateMatrix();
|
dummy.updateMatrix();
|
||||||
leavesMesh.setMatrixAt(treeCount, dummy.matrix);
|
leavesMesh.setMatrixAt(treeCount, dummy.matrix);
|
||||||
@@ -195,7 +178,8 @@ export function useCityObjects() {
|
|||||||
|
|
||||||
if (buildingIndex < buildingDefs.length) {
|
if (buildingIndex < buildingDefs.length) {
|
||||||
const def = buildingDefs[buildingIndex];
|
const def = buildingDefs[buildingIndex];
|
||||||
createBuilding(scene, def.type, plot, def.id, def.csv, 0);
|
// Support both 'csv' and 'csvPath' keys for compatibility if needed, but new state uses csvPath
|
||||||
|
createBuilding(scene, def.type, plot, def.id, def.csvPath || def.csv, 0);
|
||||||
buildingIndex++;
|
buildingIndex++;
|
||||||
} else {
|
} else {
|
||||||
// Empty plot filler: Trees
|
// Empty plot filler: Trees
|
||||||
@@ -206,18 +190,28 @@ export function useCityObjects() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finalize Instanced Meshes
|
|
||||||
trunkMesh.count = treeCount;
|
trunkMesh.count = treeCount;
|
||||||
leavesMesh.count = treeCount;
|
leavesMesh.count = treeCount;
|
||||||
scene.add(trunkMesh);
|
scene.add(trunkMesh);
|
||||||
scene.add(leavesMesh);
|
scene.add(leavesMesh);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearCity = (scene: THREE.Scene) => {
|
||||||
|
sceneObjects.forEach(obj => {
|
||||||
|
scene.remove(obj);
|
||||||
|
// Optional: Dispose geometries/materials if strictly needed
|
||||||
|
});
|
||||||
|
sceneObjects.length = 0;
|
||||||
|
buildings.length = 0;
|
||||||
|
interactableObjects.length = 0;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
interactableObjects,
|
interactableObjects,
|
||||||
initCity: (scene: THREE.Scene) => {
|
initCity: (scene: THREE.Scene, buildingDefs: any[]) => {
|
||||||
const plots = initGround(scene);
|
const plots = initGround(scene);
|
||||||
initBuildings(scene, plots);
|
initBuildings(scene, plots, buildingDefs);
|
||||||
}
|
},
|
||||||
|
clearCity
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,41 @@ export function useInteraction(
|
|||||||
scene.add(selectionRing);
|
scene.add(selectionRing);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let selectedMesh: THREE.Mesh | null = null;
|
||||||
|
let originalMaterial: THREE.Material | THREE.Material[] | null = null;
|
||||||
|
|
||||||
|
const clearSelectionHighlight = () => {
|
||||||
|
if (selectedMesh && originalMaterial) {
|
||||||
|
// Dispose the cloned material to avoid memory leaks
|
||||||
|
if (Array.isArray(selectedMesh.material)) {
|
||||||
|
selectedMesh.material.forEach(m => m.dispose());
|
||||||
|
} else {
|
||||||
|
selectedMesh.material.dispose();
|
||||||
|
}
|
||||||
|
// Restore original
|
||||||
|
selectedMesh.material = originalMaterial;
|
||||||
|
}
|
||||||
|
selectedMesh = null;
|
||||||
|
originalMaterial = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applySelectionHighlight = (mesh: THREE.Mesh) => {
|
||||||
|
clearSelectionHighlight(); // Clear previous
|
||||||
|
|
||||||
|
selectedMesh = mesh;
|
||||||
|
if (Array.isArray(mesh.material)) {
|
||||||
|
originalMaterial = mesh.material;
|
||||||
|
// Clone isn't straightforward for array, skip for now or we could map clone
|
||||||
|
} else {
|
||||||
|
originalMaterial = mesh.material;
|
||||||
|
const newMat = mesh.material.clone();
|
||||||
|
if ('emissive' in newMat) {
|
||||||
|
(newMat as THREE.MeshStandardMaterial).emissive.setHex(0x555555);
|
||||||
|
}
|
||||||
|
mesh.material = newMat;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateSelectionRing = (target: THREE.Object3D) => {
|
const updateSelectionRing = (target: THREE.Object3D) => {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
selectionRing.visible = false;
|
selectionRing.visible = false;
|
||||||
@@ -71,11 +106,11 @@ export function useInteraction(
|
|||||||
const size = new THREE.Vector3();
|
const size = new THREE.Vector3();
|
||||||
box.getSize(size);
|
box.getSize(size);
|
||||||
|
|
||||||
const padding = 4; // Extra space around building
|
const padding = 2; // Reduced padding for tighter fit
|
||||||
selectionRing.scale.set(size.x + padding, size.z + padding, 1); // z is y in local space of plane
|
selectionRing.scale.set(size.x + padding, size.z + padding, 1);
|
||||||
|
|
||||||
selectionRing.position.copy(target.position);
|
selectionRing.position.copy(target.position);
|
||||||
selectionRing.position.y = 0.2; // Just above ground
|
selectionRing.position.y = 0.6; // Raised above grass (0.5)
|
||||||
};
|
};
|
||||||
|
|
||||||
const initInteraction = () => {
|
const initInteraction = () => {
|
||||||
@@ -158,6 +193,11 @@ export function useInteraction(
|
|||||||
|
|
||||||
const data = group.userData;
|
const data = group.userData;
|
||||||
|
|
||||||
|
// Highlight Mesh
|
||||||
|
if (group instanceof THREE.Mesh) {
|
||||||
|
applySelectionHighlight(group);
|
||||||
|
}
|
||||||
|
|
||||||
// Screen coords for Info Window
|
// Screen coords for Info Window
|
||||||
const vector = group.position.clone();
|
const vector = group.position.clone();
|
||||||
vector.project(camera);
|
vector.project(camera);
|
||||||
@@ -191,6 +231,7 @@ export function useInteraction(
|
|||||||
} else {
|
} else {
|
||||||
// Only Deselect if we clicked on NOTHING, and we weren't dragging.
|
// Only Deselect if we clicked on NOTHING, and we weren't dragging.
|
||||||
selectedBuilding.value = null;
|
selectedBuilding.value = null;
|
||||||
|
clearSelectionHighlight();
|
||||||
updateSelectionRing(null as any);
|
updateSelectionRing(null as any);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,8 +14,16 @@ interface SimulationState {
|
|||||||
simulationMode: 'simulated' | 'live';
|
simulationMode: 'simulated' | 'live';
|
||||||
maxZoom: number;
|
maxZoom: number;
|
||||||
zoomSensitivity: number;
|
zoomSensitivity: number;
|
||||||
|
buildings: BuildingDefinition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BuildingDefinition {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
csvPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
interface BuildingData {
|
interface BuildingData {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -30,6 +38,11 @@ interface BuildingData {
|
|||||||
uiY?: number;
|
uiY?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// (State initialization is already correct with buildings..., just ensure the types match)
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
|
||||||
// Initialize with current real-world time
|
// Initialize with current real-world time
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const startOfYear = new Date(now.getFullYear(), 0, 0);
|
const startOfYear = new Date(now.getFullYear(), 0, 0);
|
||||||
@@ -50,7 +63,23 @@ const state = ref<SimulationState>({
|
|||||||
brightness: 1.0,
|
brightness: 1.0,
|
||||||
simulationMode: 'simulated',
|
simulationMode: 'simulated',
|
||||||
maxZoom: 120,
|
maxZoom: 120,
|
||||||
zoomSensitivity: 5
|
zoomSensitivity: 5,
|
||||||
|
buildings: [
|
||||||
|
{ id: 'H01', type: 'house', csvPath: 'data/H01.csv' },
|
||||||
|
{ id: 'H02', type: 'factory', csvPath: 'data/H02.csv' },
|
||||||
|
{ id: 'H03', type: 'office', csvPath: 'data/H03.csv' },
|
||||||
|
{ id: 'H04', type: 'shop', csvPath: 'data/H04.csv' },
|
||||||
|
{ id: 'H05', type: 'house', csvPath: 'data/H05.csv' },
|
||||||
|
{ id: 'H06', type: 'house', csvPath: 'data/H06.csv' },
|
||||||
|
{ id: 'H07', type: 'house', csvPath: 'data/H07.csv' },
|
||||||
|
{ id: 'H08', type: 'shop', csvPath: 'data/H08.csv' },
|
||||||
|
{ id: 'H09', type: 'shop', csvPath: 'data/H09.csv' },
|
||||||
|
{ id: 'H10', type: 'house', csvPath: 'data/H10.csv' },
|
||||||
|
{ id: 'H11', type: 'house', csvPath: 'data/H11.csv' },
|
||||||
|
{ id: 'H12', type: 'house', csvPath: 'data/H12.csv' },
|
||||||
|
{ id: 'H13', type: 'house', csvPath: 'data/H13.csv' },
|
||||||
|
{ id: 'H14', type: 'house', csvPath: 'data/H14.csv' },
|
||||||
|
]
|
||||||
});
|
});
|
||||||
const selectedBuilding = ref<BuildingData | null>(null);
|
const selectedBuilding = ref<BuildingData | null>(null);
|
||||||
|
|
||||||
@@ -157,6 +186,10 @@ export const useSimulation = () => {
|
|||||||
state.value.totalGeneration = Math.floor(gen);
|
state.value.totalGeneration = Math.floor(gen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addBuilding = (def: BuildingDefinition) => {
|
||||||
|
state.value.buildings.push(def);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
selectedBuilding,
|
selectedBuilding,
|
||||||
@@ -168,6 +201,7 @@ export const useSimulation = () => {
|
|||||||
setBrightness,
|
setBrightness,
|
||||||
setSimulationMode,
|
setSimulationMode,
|
||||||
setMaxZoom,
|
setMaxZoom,
|
||||||
setZoomSensitivity
|
setZoomSensitivity,
|
||||||
|
addBuilding
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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/SettingsModal.vue","./src/components/TimeControls.vue"],"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/AddBuildingModal.vue","./src/components/BuildingInfoCanvas.vue","./src/components/DigitalTwin.vue","./src/components/SettingsModal.vue","./src/components/TimeControls.vue"],"version":"5.6.3"}
|
||||||
Reference in New Issue
Block a user