very basic UI
This commit is contained in:
33
index.html
33
index.html
@@ -5,15 +5,36 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>3D City Sim</title>
|
<title>3D City Sim</title>
|
||||||
<style>
|
<link rel="stylesheet" href="style.css">
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<!-- The UI Overlay -->
|
||||||
|
<div id="ui-container">
|
||||||
|
<div class="header">
|
||||||
|
<h2>Route Planner</h2>
|
||||||
|
<p>Left Click: Add Point<br>Drag: Move Point</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Current Draft</h3>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>Length:</span>
|
||||||
|
<span id="current-length">0 m</span>
|
||||||
|
</div>
|
||||||
|
<div class="button-row">
|
||||||
|
<button id="btn-save" class="primary">Save Route</button>
|
||||||
|
<button id="btn-discard" class="danger">Discard</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Saved Routes</h3>
|
||||||
|
<ul id="route-list">
|
||||||
|
<!-- List items will be injected here -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -7,37 +7,38 @@ export class RouteManager {
|
|||||||
|
|
||||||
this.graphData = null;
|
this.graphData = null;
|
||||||
|
|
||||||
// State: A route is an ordered list of Node IDs
|
// -- State --
|
||||||
this.currentRouteNodes = [];
|
this.currentRouteNodes = [];
|
||||||
|
this.savedRoutes = []; // { nodes: [], length: number, mesh: THREE.Mesh }
|
||||||
|
|
||||||
// Visuals
|
// -- Visuals --
|
||||||
this.markers = []; // Array of Meshes. index matches currentRouteNodes
|
this.markers = []; // Draggable spheres
|
||||||
this.pathMesh = null;
|
this.currentPathMesh = null; // The tube being edited
|
||||||
|
|
||||||
this.ROAD_OFFSET = 2.5; // Meters
|
this.ROAD_OFFSET = 2.5;
|
||||||
|
|
||||||
|
// -- Callbacks --
|
||||||
|
this.onRouteChanged = null; // function(lengthInMeters)
|
||||||
}
|
}
|
||||||
|
|
||||||
initGraph(data) {
|
initGraph(data) {
|
||||||
this.graphData = data;
|
this.graphData = data;
|
||||||
this.graphData.adjacency = {};
|
this.graphData.adjacency = {};
|
||||||
|
|
||||||
// 1. Flip Coordinates (Data is +Y North, 3D is -Z North)
|
// 1. Flip Coordinates
|
||||||
for (let key in this.graphData.nodes) {
|
for (let key in this.graphData.nodes) {
|
||||||
this.graphData.nodes[key].y = -this.graphData.nodes[key].y;
|
this.graphData.nodes[key].y = -this.graphData.nodes[key].y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Build Adjacency
|
// 2. Build Adjacency
|
||||||
this.graphData.edges.forEach((edge, index) => {
|
this.graphData.edges.forEach((edge, index) => {
|
||||||
// Flip edge geometry
|
|
||||||
if (edge.points) edge.points.forEach(p => { p[1] = -p[1]; });
|
if (edge.points) edge.points.forEach(p => { p[1] = -p[1]; });
|
||||||
|
|
||||||
// Forward
|
|
||||||
if (!this.graphData.adjacency[edge.u]) this.graphData.adjacency[edge.u] = [];
|
if (!this.graphData.adjacency[edge.u]) this.graphData.adjacency[edge.u] = [];
|
||||||
this.graphData.adjacency[edge.u].push({
|
this.graphData.adjacency[edge.u].push({
|
||||||
to: edge.v, cost: edge.length || 1, edgeIndex: index
|
to: edge.v, cost: edge.length || 1, edgeIndex: index
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reverse (if not oneway)
|
|
||||||
if (!edge.oneway) {
|
if (!edge.oneway) {
|
||||||
if (!this.graphData.adjacency[edge.v]) this.graphData.adjacency[edge.v] = [];
|
if (!this.graphData.adjacency[edge.v]) this.graphData.adjacency[edge.v] = [];
|
||||||
this.graphData.adjacency[edge.v].push({
|
this.graphData.adjacency[edge.v].push({
|
||||||
@@ -48,7 +49,7 @@ export class RouteManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Interaction Methods
|
// API Methods (For UI/Input)
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
addNodeByWorldPosition(vector3) {
|
addNodeByWorldPosition(vector3) {
|
||||||
@@ -56,126 +57,172 @@ export class RouteManager {
|
|||||||
const nodeId = this.findNearestNode(vector3.x, vector3.z);
|
const nodeId = this.findNearestNode(vector3.x, vector3.z);
|
||||||
if (nodeId === null) return;
|
if (nodeId === null) return;
|
||||||
|
|
||||||
// Don't add duplicate adjacent nodes
|
|
||||||
if (this.currentRouteNodes.length > 0 &&
|
if (this.currentRouteNodes.length > 0 &&
|
||||||
this.currentRouteNodes[this.currentRouteNodes.length - 1] === nodeId) {
|
this.currentRouteNodes[this.currentRouteNodes.length - 1] === nodeId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentRouteNodes.push(nodeId);
|
this.currentRouteNodes.push(nodeId);
|
||||||
|
|
||||||
// Add new marker
|
|
||||||
this.addMarkerVisual(nodeId);
|
this.addMarkerVisual(nodeId);
|
||||||
|
|
||||||
// Update path
|
|
||||||
this.updatePathVisuals();
|
this.updatePathVisuals();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called while dragging a marker.
|
|
||||||
* Updates the node at markerIndex to the nearest graph node at worldPoint.
|
|
||||||
*/
|
|
||||||
dragNode(markerObject, worldPoint) {
|
dragNode(markerObject, worldPoint) {
|
||||||
if (!this.graphData) return;
|
if (!this.graphData) return;
|
||||||
|
|
||||||
// 1. Identify which node index this marker represents
|
|
||||||
const index = this.markers.indexOf(markerObject);
|
const index = this.markers.indexOf(markerObject);
|
||||||
if (index === -1) return;
|
if (index === -1) return;
|
||||||
|
|
||||||
// 2. Find nearest node to new mouse position
|
|
||||||
const newNodeId = this.findNearestNode(worldPoint.x, worldPoint.z);
|
const newNodeId = this.findNearestNode(worldPoint.x, worldPoint.z);
|
||||||
|
|
||||||
// 3. Optimization: Only update if the node ID actually changed
|
|
||||||
if (this.currentRouteNodes[index] !== newNodeId) {
|
if (this.currentRouteNodes[index] !== newNodeId) {
|
||||||
|
|
||||||
this.currentRouteNodes[index] = newNodeId;
|
this.currentRouteNodes[index] = newNodeId;
|
||||||
|
|
||||||
// Update Marker Visual Position
|
|
||||||
const nodeData = this.graphData.nodes[newNodeId];
|
const nodeData = this.graphData.nodes[newNodeId];
|
||||||
markerObject.position.set(nodeData.x, 2, nodeData.y);
|
markerObject.position.set(nodeData.x, 2, nodeData.y);
|
||||||
markerObject.userData.nodeId = newNodeId; // Keep sync
|
markerObject.userData.nodeId = newNodeId;
|
||||||
|
|
||||||
// Recalculate Path
|
|
||||||
this.updatePathVisuals();
|
this.updatePathVisuals();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveCurrentRoute() {
|
||||||
|
if (this.currentRouteNodes.length < 2 || !this.currentPathMesh) return;
|
||||||
|
|
||||||
|
// 1. Calculate final length
|
||||||
|
const totalLength = this.currentPathMesh.userData.length || 0;
|
||||||
|
|
||||||
|
// 2. Freeze the mesh (Change color to indicate saved state)
|
||||||
|
this.currentPathMesh.material.color.setHex(0x10B981); // Emerald Green
|
||||||
|
|
||||||
|
// 3. Store in saved list
|
||||||
|
this.savedRoutes.push({
|
||||||
|
nodes: [...this.currentRouteNodes],
|
||||||
|
length: totalLength,
|
||||||
|
mesh: this.currentPathMesh
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Detach mesh from "current" reference so we don't delete it on reset
|
||||||
|
this.currentPathMesh = null;
|
||||||
|
|
||||||
|
// 5. Clear drafting state (remove markers, clear node list)
|
||||||
|
this.resetDraftingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCurrentRoute() {
|
||||||
|
// 1. Remove current mesh from scene
|
||||||
|
if (this.currentPathMesh) {
|
||||||
|
this.scene.remove(this.currentPathMesh);
|
||||||
|
this.currentPathMesh.geometry.dispose();
|
||||||
|
this.currentPathMesh = null;
|
||||||
|
}
|
||||||
|
// 2. Reset state
|
||||||
|
this.resetDraftingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetDraftingState() {
|
||||||
|
this.currentRouteNodes = [];
|
||||||
|
// Remove all markers
|
||||||
|
this.markers.forEach(m => this.scene.remove(m));
|
||||||
|
this.markers = [];
|
||||||
|
|
||||||
|
// Notify UI
|
||||||
|
if (this.onRouteChanged) this.onRouteChanged(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSavedRoute(index) {
|
||||||
|
if (index < 0 || index >= this.savedRoutes.length) return;
|
||||||
|
|
||||||
|
const route = this.savedRoutes[index];
|
||||||
|
|
||||||
|
// Remove mesh from scene
|
||||||
|
if (route.mesh) {
|
||||||
|
this.scene.remove(route.mesh);
|
||||||
|
route.mesh.geometry.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.savedRoutes.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSavedRoutes() {
|
||||||
|
return this.savedRoutes;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Visual Logic
|
// Visuals & Logic
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
updatePathVisuals() {
|
updatePathVisuals() {
|
||||||
// Need 2+ nodes to make a path
|
// Needs 2+ nodes
|
||||||
if (this.currentRouteNodes.length < 2) {
|
if (this.currentRouteNodes.length < 2) {
|
||||||
if (this.pathMesh) {
|
if (this.currentPathMesh) {
|
||||||
this.scene.remove(this.pathMesh);
|
this.scene.remove(this.currentPathMesh);
|
||||||
this.pathMesh = null;
|
this.currentPathMesh = null;
|
||||||
}
|
}
|
||||||
|
if (this.onRouteChanged) this.onRouteChanged(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Calculate Geometry
|
|
||||||
let fullPathPoints = [];
|
let fullPathPoints = [];
|
||||||
|
let totalDist = 0;
|
||||||
|
|
||||||
for (let i = 0; i < this.currentRouteNodes.length - 1; i++) {
|
for (let i = 0; i < this.currentRouteNodes.length - 1; i++) {
|
||||||
const start = this.currentRouteNodes[i];
|
const start = this.currentRouteNodes[i];
|
||||||
const end = this.currentRouteNodes[i + 1];
|
const end = this.currentRouteNodes[i + 1];
|
||||||
|
|
||||||
// Run A* for this segment
|
|
||||||
const segmentEdges = this.computePathAStar(start, end);
|
const segmentEdges = this.computePathAStar(start, end);
|
||||||
|
|
||||||
if (!segmentEdges) {
|
if (!segmentEdges) continue;
|
||||||
// No path found (disconnected graph?), just draw straight line or skip
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process Geometry
|
|
||||||
segmentEdges.forEach(step => {
|
segmentEdges.forEach(step => {
|
||||||
|
totalDist += step.edgeData.cost || 0; // Accumulate distance
|
||||||
|
|
||||||
const rawPoints = step.edgeData.points;
|
const rawPoints = step.edgeData.points;
|
||||||
let segmentPoints = rawPoints.map(p => new THREE.Vector2(p[0], p[1]));
|
let segmentPoints = rawPoints.map(p => new THREE.Vector2(p[0], p[1]));
|
||||||
if (step.isReverse) segmentPoints.reverse();
|
if (step.isReverse) segmentPoints.reverse();
|
||||||
|
|
||||||
// Offset
|
|
||||||
const offsetSegment = this.getOffsetPath(segmentPoints, this.ROAD_OFFSET);
|
const offsetSegment = this.getOffsetPath(segmentPoints, this.ROAD_OFFSET);
|
||||||
offsetSegment.forEach(p => fullPathPoints.push(new THREE.Vector3(p.x, 0.5, p.y)));
|
offsetSegment.forEach(p => fullPathPoints.push(new THREE.Vector3(p.x, 0.5, p.y)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Update/Create Mesh
|
// Update Mesh
|
||||||
if (this.pathMesh) {
|
if (this.currentPathMesh) {
|
||||||
this.scene.remove(this.pathMesh);
|
this.scene.remove(this.currentPathMesh);
|
||||||
this.pathMesh.geometry.dispose();
|
this.currentPathMesh.geometry.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullPathPoints.length < 2) return;
|
if (fullPathPoints.length < 2) return;
|
||||||
|
|
||||||
const curve = new THREE.CatmullRomCurve3(fullPathPoints);
|
const curve = new THREE.CatmullRomCurve3(fullPathPoints);
|
||||||
// Low tension = smoother corners
|
|
||||||
const tubeGeom = new THREE.TubeGeometry(curve, fullPathPoints.length, 1.5, 6, false);
|
const tubeGeom = new THREE.TubeGeometry(curve, fullPathPoints.length, 1.5, 6, false);
|
||||||
const tubeMat = new THREE.MeshBasicMaterial({ color: this.settings.colors.route });
|
const tubeMat = new THREE.MeshBasicMaterial({ color: this.settings.colors.route });
|
||||||
|
|
||||||
this.pathMesh = new THREE.Mesh(tubeGeom, tubeMat);
|
this.currentPathMesh = new THREE.Mesh(tubeGeom, tubeMat);
|
||||||
this.scene.add(this.pathMesh);
|
// Store length on the mesh for easy access later
|
||||||
|
this.currentPathMesh.userData.length = totalDist;
|
||||||
|
|
||||||
|
this.scene.add(this.currentPathMesh);
|
||||||
|
|
||||||
|
// Update markers color (First=Green, Last=Red, Others=Yellow)
|
||||||
|
this.updateMarkerColors();
|
||||||
|
|
||||||
|
// Trigger Callback
|
||||||
|
if (this.onRouteChanged) this.onRouteChanged(totalDist);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMarkerColors() {
|
||||||
|
this.markers.forEach((marker, i) => {
|
||||||
|
let color = 0xFFFF00; // Default Yellow (Waypoint)
|
||||||
|
if (i === 0) color = this.settings.colors.pathStart; // Green
|
||||||
|
else if (i === this.markers.length - 1) color = this.settings.colors.pathEnd; // Red
|
||||||
|
marker.material.color.setHex(color);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addMarkerVisual(nodeId) {
|
addMarkerVisual(nodeId) {
|
||||||
const node = this.graphData.nodes[nodeId];
|
const node = this.graphData.nodes[nodeId];
|
||||||
const geom = new THREE.SphereGeometry(4);
|
const geom = new THREE.SphereGeometry(4);
|
||||||
|
const mat = new THREE.MeshBasicMaterial({ color: this.settings.colors.pathEnd });
|
||||||
// Color Logic: Start(Green) -> End(Red). Intermediate? Yellow.
|
|
||||||
let color = this.settings.colors.pathStart;
|
|
||||||
if (this.markers.length > 0) color = this.settings.colors.pathEnd; // Default to End color
|
|
||||||
|
|
||||||
// If we are adding a new end, turn the PREVIOUS end into a waypoint (Yellow)
|
|
||||||
if (this.markers.length > 0) {
|
|
||||||
// Change the previous last marker to yellow (waypoint)
|
|
||||||
// Unless it was the start marker (index 0)
|
|
||||||
if (this.markers.length > 1) {
|
|
||||||
this.markers[this.markers.length - 1].material.color.setHex(0xFFFF00);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mat = new THREE.MeshBasicMaterial({ color: color });
|
|
||||||
const mesh = new THREE.Mesh(geom, mat);
|
const mesh = new THREE.Mesh(geom, mat);
|
||||||
|
|
||||||
mesh.position.set(node.x, 2, node.y);
|
mesh.position.set(node.x, 2, node.y);
|
||||||
@@ -183,10 +230,12 @@ export class RouteManager {
|
|||||||
|
|
||||||
this.scene.add(mesh);
|
this.scene.add(mesh);
|
||||||
this.markers.push(mesh);
|
this.markers.push(mesh);
|
||||||
|
|
||||||
|
this.updateMarkerColors();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Algorithms (A* & Math)
|
// Algorithms (A* & Helpers)
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
findNearestNode(x, z) {
|
findNearestNode(x, z) {
|
||||||
@@ -206,12 +255,10 @@ export class RouteManager {
|
|||||||
|
|
||||||
computePathAStar(start, end) {
|
computePathAStar(start, end) {
|
||||||
if (start === end) return [];
|
if (start === end) return [];
|
||||||
|
|
||||||
const openSet = new Set([start]);
|
const openSet = new Set([start]);
|
||||||
const cameFrom = {};
|
const cameFrom = {};
|
||||||
const gScore = {};
|
const gScore = {};
|
||||||
const fScore = {};
|
const fScore = {};
|
||||||
|
|
||||||
gScore[start] = 0;
|
gScore[start] = 0;
|
||||||
fScore[start] = this.heuristic(start, end);
|
fScore[start] = this.heuristic(start, end);
|
||||||
|
|
||||||
@@ -222,9 +269,7 @@ export class RouteManager {
|
|||||||
const score = fScore[node] !== undefined ? fScore[node] : Infinity;
|
const score = fScore[node] !== undefined ? fScore[node] : Infinity;
|
||||||
if (score < minF) { minF = score; current = node; }
|
if (score < minF) { minF = score; current = node; }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current === end) return this.reconstructPath(cameFrom, current);
|
if (current === end) return this.reconstructPath(cameFrom, current);
|
||||||
|
|
||||||
openSet.delete(current);
|
openSet.delete(current);
|
||||||
|
|
||||||
const neighbors = this.graphData.adjacency[current] || [];
|
const neighbors = this.graphData.adjacency[current] || [];
|
||||||
|
|||||||
70
src/UIManager.js
Normal file
70
src/UIManager.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
export class UIManager {
|
||||||
|
constructor(routeManager) {
|
||||||
|
this.routeManager = routeManager;
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
this.elCurrentLength = document.getElementById('current-length');
|
||||||
|
this.elRouteList = document.getElementById('route-list');
|
||||||
|
this.btnSave = document.getElementById('btn-save');
|
||||||
|
this.btnDiscard = document.getElementById('btn-discard');
|
||||||
|
|
||||||
|
this.initListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
initListeners() {
|
||||||
|
this.btnSave.addEventListener('click', () => {
|
||||||
|
this.routeManager.saveCurrentRoute();
|
||||||
|
this.renderRouteList();
|
||||||
|
this.updateStats(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.btnDiscard.addEventListener('click', () => {
|
||||||
|
this.routeManager.clearCurrentRoute();
|
||||||
|
this.updateStats(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the text display for current route length
|
||||||
|
* @param {number} lengthInMeters
|
||||||
|
*/
|
||||||
|
updateStats(lengthInMeters) {
|
||||||
|
// Format: If > 1000m, show km. Else meters.
|
||||||
|
let text = "";
|
||||||
|
if (lengthInMeters > 1000) {
|
||||||
|
text = (lengthInMeters / 1000).toFixed(2) + " km";
|
||||||
|
} else {
|
||||||
|
text = Math.round(lengthInMeters) + " m";
|
||||||
|
}
|
||||||
|
this.elCurrentLength.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRouteList() {
|
||||||
|
this.elRouteList.innerHTML = '';
|
||||||
|
const routes = this.routeManager.getSavedRoutes();
|
||||||
|
|
||||||
|
routes.forEach((route, index) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
|
||||||
|
// Format Length
|
||||||
|
let lenStr = route.length > 1000
|
||||||
|
? (route.length / 1000).toFixed(2) + " km"
|
||||||
|
: Math.round(route.length) + " m";
|
||||||
|
|
||||||
|
li.innerHTML = `
|
||||||
|
<span><strong>Route ${index + 1}</strong> (${lenStr})</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Delete Button
|
||||||
|
const btnDel = document.createElement('button');
|
||||||
|
btnDel.textContent = "✕";
|
||||||
|
btnDel.onclick = () => {
|
||||||
|
this.routeManager.deleteSavedRoute(index);
|
||||||
|
this.renderRouteList();
|
||||||
|
};
|
||||||
|
|
||||||
|
li.appendChild(btnDel);
|
||||||
|
this.elRouteList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main.js
24
src/main.js
@@ -2,9 +2,10 @@ import * as THREE from 'three';
|
|||||||
import { MapControls } from 'three/addons/controls/MapControls.js';
|
import { MapControls } from 'three/addons/controls/MapControls.js';
|
||||||
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
|
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
|
||||||
|
|
||||||
// Import our Classes
|
|
||||||
import { InputManager } from './InputManager.js';
|
import { InputManager } from './InputManager.js';
|
||||||
import { RouteManager } from './RouteManager.js';
|
import { RouteManager } from './RouteManager.js';
|
||||||
|
import { UIManager } from './UIManager.js';
|
||||||
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 1. Configuration
|
// 1. Configuration
|
||||||
@@ -28,34 +29,35 @@ const SETTINGS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let scene, camera, renderer, controls;
|
let scene, camera, renderer, controls;
|
||||||
let inputManager, routeManager;
|
let inputManager, routeManager, uiManager;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
setupScene();
|
setupScene();
|
||||||
|
|
||||||
// -- INITIALIZATION --
|
// 1. Managers
|
||||||
// 1. Create Route Manager
|
|
||||||
routeManager = new RouteManager(scene, SETTINGS);
|
routeManager = new RouteManager(scene, SETTINGS);
|
||||||
|
|
||||||
// 2. Create Input Manager (Pass controls so we can disable them during drag)
|
|
||||||
inputManager = new InputManager(camera, renderer.domElement, scene, controls);
|
inputManager = new InputManager(camera, renderer.domElement, scene, controls);
|
||||||
|
uiManager = new UIManager(routeManager); // Wire UI to Route Logic
|
||||||
|
|
||||||
|
// 2. Events
|
||||||
inputManager.init();
|
inputManager.init();
|
||||||
|
|
||||||
// 3. Wire Events
|
|
||||||
|
|
||||||
// Handle Click (Add Node)
|
|
||||||
inputManager.onClick = (point, object) => {
|
inputManager.onClick = (point, object) => {
|
||||||
if (object.name === "GROUND") {
|
if (object.name === "GROUND") {
|
||||||
routeManager.addNodeByWorldPosition(point);
|
routeManager.addNodeByWorldPosition(point);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle Drag (Move Node)
|
|
||||||
inputManager.onDrag = (markerObject, newPoint) => {
|
inputManager.onDrag = (markerObject, newPoint) => {
|
||||||
routeManager.dragNode(markerObject, newPoint);
|
routeManager.dragNode(markerObject, newPoint);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. Load Data
|
// Wire RouteManager back to UI (to update stats when dragging)
|
||||||
|
routeManager.onRouteChanged = (dist) => {
|
||||||
|
uiManager.updateStats(dist);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Data Load
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch(SETTINGS.files.visual).then(r => r.json()),
|
fetch(SETTINGS.files.visual).then(r => r.json()),
|
||||||
fetch(SETTINGS.files.routing).then(r => r.json())
|
fetch(SETTINGS.files.routing).then(r => r.json())
|
||||||
|
|||||||
66
style.css
Normal file
66
style.css
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ui-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
width: 280px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||||
|
user-select: none;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h2 { margin: 0 0 5px 0; font-size: 18px; color: #333; }
|
||||||
|
.header p { margin: 0; font-size: 12px; color: #666; }
|
||||||
|
|
||||||
|
.section { margin-top: 20px; border-top: 1px solid #eee; padding-top: 15px; }
|
||||||
|
.section h3 { margin: 0 0 10px 0; font-size: 14px; text-transform: uppercase; color: #888; letter-spacing: 0.5px; }
|
||||||
|
|
||||||
|
.stat-row { display: flex; justify-content: space-between; font-weight: bold; margin-bottom: 10px; font-size: 16px; }
|
||||||
|
|
||||||
|
.button-row { display: flex; gap: 10px; }
|
||||||
|
button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
button:hover { opacity: 0.8; }
|
||||||
|
|
||||||
|
.primary { background-color: #2563EB; color: white; }
|
||||||
|
.danger { background-color: #ef4444; color: white; }
|
||||||
|
.secondary { background-color: #e5e7eb; color: #374151; }
|
||||||
|
|
||||||
|
/* Route List */
|
||||||
|
#route-list { list-style: none; padding: 0; margin: 0; }
|
||||||
|
#route-list li {
|
||||||
|
background: #f3f4f6;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
#route-list li button {
|
||||||
|
flex: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user