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" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Community Digital Twin</title>
|
||||
<script type="module" crossorigin src="/assets/index-BXWyap6F.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BqNgBlW1.css">
|
||||
<script type="module" crossorigin src="/assets/index-Bso7APm4.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D9Xb8bfs.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -4,8 +4,10 @@ import DigitalTwin from './components/DigitalTwin.vue'
|
||||
import BuildingInfoCanvas from './components/BuildingInfoCanvas.vue'
|
||||
import TimeControls from './components/TimeControls.vue'
|
||||
import SettingsModal from './components/SettingsModal.vue'
|
||||
import AddBuildingModal from './components/AddBuildingModal.vue'
|
||||
|
||||
const isSettingsOpen = ref(false);
|
||||
const isAddBuildingOpen = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -16,8 +18,13 @@ const isSettingsOpen = ref(false);
|
||||
<button class="settings-btn" @click="isSettingsOpen = true">
|
||||
⚙ Settings
|
||||
</button>
|
||||
|
||||
<button class="add-building-btn" @click="isAddBuildingOpen = true">
|
||||
+ Add Building
|
||||
</button>
|
||||
|
||||
<SettingsModal :isOpen="isSettingsOpen" @close="isSettingsOpen = false" />
|
||||
<AddBuildingModal :isOpen="isAddBuildingOpen" @close="isAddBuildingOpen = false" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@@ -58,4 +65,33 @@ html, body, #app {
|
||||
transform: translate(4px, 4px);
|
||||
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>
|
||||
|
||||
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">
|
||||
import { onMounted } from 'vue';
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { useSimulation } from '../composables/useSimulation';
|
||||
import { useThreeScene } from '../composables/three/useThreeScene';
|
||||
import { useSunSystem } from '../composables/three/useSunSystem';
|
||||
@@ -24,8 +24,16 @@ const init = () => {
|
||||
const renderer = getRenderer();
|
||||
|
||||
// 1. City Objects
|
||||
const { initCity, interactableObjects } = useCityObjects();
|
||||
initCity(scene);
|
||||
const { initCity, interactableObjects, clearCity } = useCityObjects();
|
||||
// @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
|
||||
const { initSun, updateSun } = useSunSystem(scene, state);
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as THREE from 'three';
|
||||
export function useCityObjects() {
|
||||
const buildings: THREE.Mesh[] = [];
|
||||
const interactableObjects: THREE.Object3D[] = [];
|
||||
const sceneObjects: THREE.Object3D[] = [];
|
||||
|
||||
// --- Assets & Materials ---
|
||||
const MATERIALS = {
|
||||
@@ -65,6 +66,7 @@ export function useCityObjects() {
|
||||
};
|
||||
interactableObjects.push(mesh);
|
||||
buildings.push(mesh);
|
||||
sceneObjects.push(mesh); // Track
|
||||
scene.add(mesh);
|
||||
};
|
||||
|
||||
@@ -82,29 +84,32 @@ export function useCityObjects() {
|
||||
};
|
||||
|
||||
const initGround = (scene: THREE.Scene) => {
|
||||
// Base Plane (Asphalt)
|
||||
// ... (Base creation)
|
||||
const baseGeo = new THREE.PlaneGeometry(300, 300);
|
||||
const base = new THREE.Mesh(baseGeo, MATERIALS.road);
|
||||
// ...
|
||||
base.rotation.x = -Math.PI / 2;
|
||||
base.position.y = -0.1;
|
||||
base.receiveShadow = true;
|
||||
scene.add(base);
|
||||
sceneObjects.push(base); // Track
|
||||
|
||||
const plots: THREE.Vector3[] = [];
|
||||
const offset = ((GRID_SIZE - 1) * BLOCK_SIZE) / 2;
|
||||
|
||||
// --- Instanced Meshes Setup ---
|
||||
// 1. Sidewalks
|
||||
// ... (Instanced Meshes)
|
||||
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;
|
||||
sceneObjects.push(sidewalkMesh); // Track
|
||||
|
||||
// 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;
|
||||
sceneObjects.push(grassMesh); // Track
|
||||
|
||||
// ... (Loop to fill instances and plots - remains same)
|
||||
const dummy = new THREE.Object3D();
|
||||
let instanceIdx = 0;
|
||||
|
||||
@@ -132,33 +137,11 @@ export function useCityObjects() {
|
||||
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, plots: THREE.Vector3[]) => {
|
||||
const buildingDefs = [
|
||||
{ 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 initBuildings = (scene: THREE.Scene, plots: THREE.Vector3[], buildingDefs: any[]) => {
|
||||
// ... (Trees setup)
|
||||
const trunkGeo = new THREE.CylinderGeometry(0.5, 0.5, 2, 6);
|
||||
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);
|
||||
trunkMesh.castShadow = true; trunkMesh.receiveShadow = true;
|
||||
leavesMesh.castShadow = true; leavesMesh.receiveShadow = true;
|
||||
sceneObjects.push(trunkMesh); // Track
|
||||
sceneObjects.push(leavesMesh); // Track
|
||||
|
||||
let treeCount = 0;
|
||||
const dummy = new THREE.Object3D();
|
||||
|
||||
const addTreeInstance = (pos: THREE.Vector3) => {
|
||||
// ... (remains same)
|
||||
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);
|
||||
@@ -195,7 +178,8 @@ export function useCityObjects() {
|
||||
|
||||
if (buildingIndex < buildingDefs.length) {
|
||||
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++;
|
||||
} else {
|
||||
// Empty plot filler: Trees
|
||||
@@ -206,18 +190,28 @@ export function useCityObjects() {
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize Instanced Meshes
|
||||
trunkMesh.count = treeCount;
|
||||
leavesMesh.count = treeCount;
|
||||
scene.add(trunkMesh);
|
||||
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 {
|
||||
interactableObjects,
|
||||
initCity: (scene: THREE.Scene) => {
|
||||
initCity: (scene: THREE.Scene, buildingDefs: any[]) => {
|
||||
const plots = initGround(scene);
|
||||
initBuildings(scene, plots);
|
||||
}
|
||||
initBuildings(scene, plots, buildingDefs);
|
||||
},
|
||||
clearCity
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,6 +57,41 @@ export function useInteraction(
|
||||
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) => {
|
||||
if (!target) {
|
||||
selectionRing.visible = false;
|
||||
@@ -71,11 +106,11 @@ export function useInteraction(
|
||||
const size = new THREE.Vector3();
|
||||
box.getSize(size);
|
||||
|
||||
const padding = 4; // Extra space around building
|
||||
selectionRing.scale.set(size.x + padding, size.z + padding, 1); // z is y in local space of plane
|
||||
const padding = 2; // Reduced padding for tighter fit
|
||||
selectionRing.scale.set(size.x + padding, size.z + padding, 1);
|
||||
|
||||
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 = () => {
|
||||
@@ -158,6 +193,11 @@ export function useInteraction(
|
||||
|
||||
const data = group.userData;
|
||||
|
||||
// Highlight Mesh
|
||||
if (group instanceof THREE.Mesh) {
|
||||
applySelectionHighlight(group);
|
||||
}
|
||||
|
||||
// Screen coords for Info Window
|
||||
const vector = group.position.clone();
|
||||
vector.project(camera);
|
||||
@@ -191,6 +231,7 @@ export function useInteraction(
|
||||
} else {
|
||||
// Only Deselect if we clicked on NOTHING, and we weren't dragging.
|
||||
selectedBuilding.value = null;
|
||||
clearSelectionHighlight();
|
||||
updateSelectionRing(null as any);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,8 +14,16 @@ interface SimulationState {
|
||||
simulationMode: 'simulated' | 'live';
|
||||
maxZoom: number;
|
||||
zoomSensitivity: number;
|
||||
buildings: BuildingDefinition[];
|
||||
}
|
||||
|
||||
export interface BuildingDefinition {
|
||||
id: string;
|
||||
type: string;
|
||||
csvPath: string;
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
interface BuildingData {
|
||||
id: string;
|
||||
@@ -30,6 +38,11 @@ interface BuildingData {
|
||||
uiY?: number;
|
||||
}
|
||||
|
||||
// (State initialization is already correct with buildings..., just ensure the types match)
|
||||
|
||||
// ...
|
||||
|
||||
|
||||
// Initialize with current real-world time
|
||||
const now = new Date();
|
||||
const startOfYear = new Date(now.getFullYear(), 0, 0);
|
||||
@@ -50,7 +63,23 @@ const state = ref<SimulationState>({
|
||||
brightness: 1.0,
|
||||
simulationMode: 'simulated',
|
||||
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);
|
||||
|
||||
@@ -157,6 +186,10 @@ export const useSimulation = () => {
|
||||
state.value.totalGeneration = Math.floor(gen);
|
||||
};
|
||||
|
||||
const addBuilding = (def: BuildingDefinition) => {
|
||||
state.value.buildings.push(def);
|
||||
};
|
||||
|
||||
return {
|
||||
state,
|
||||
selectedBuilding,
|
||||
@@ -168,6 +201,7 @@ export const useSimulation = () => {
|
||||
setBrightness,
|
||||
setSimulationMode,
|
||||
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