optimization: spatial hash grid
All checks were successful
Deploy to GitHub Pages / deploy (push) Has been skipped

This commit is contained in:
Evan Scamehorn
2025-12-16 21:15:25 -06:00
parent e99c7e624e
commit 267a733153

View File

@@ -8,14 +8,14 @@ export class RouteManager {
this.graphData = null; this.graphData = null;
// -- State -- // -- State --
this.isDrafting = false; // New flag this.isDrafting = false;
this.currentRouteNodes = []; this.currentRouteNodes = [];
this.savedRoutes = []; this.savedRoutes = [];
// -- Visuals -- // -- Visuals --
this.markers = []; this.markers = [];
this.currentPathMesh = null; this.currentPathMesh = null;
this.ghostMarker = null; // Transparent sphere this.ghostMarker = null;
this.servedNodes = new Set(); this.servedNodes = new Set();
this.servedCoordinates = []; this.servedCoordinates = [];
@@ -28,6 +28,10 @@ export class RouteManager {
// Draft state // Draft state
this.latestPathPoints = []; this.latestPathPoints = [];
// -- Spatial Optimization --
this.spatialGrid = {};
this.cellSize = 200; // Tune this: Larger = more nodes per cell, Smaller = more empty cells
this.initGhostMarker(); this.initGhostMarker();
} }
@@ -42,9 +46,13 @@ export class RouteManager {
initGraph(data) { initGraph(data) {
this.graphData = data; this.graphData = data;
this.graphData.adjacency = {}; this.graphData.adjacency = {};
// 1. Fix 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
this.graphData.edges.forEach((edge, index) => { this.graphData.edges.forEach((edge, index) => {
if (edge.points) edge.points.forEach(p => { p[1] = -p[1]; }); if (edge.points) edge.points.forEach(p => { p[1] = -p[1]; });
if (!this.graphData.adjacency[edge.u]) this.graphData.adjacency[edge.u] = []; if (!this.graphData.adjacency[edge.u]) this.graphData.adjacency[edge.u] = [];
@@ -54,6 +62,90 @@ export class RouteManager {
this.graphData.adjacency[edge.v].push({ to: edge.u, cost: edge.length || 1, edgeIndex: index, isReverse: true }); this.graphData.adjacency[edge.v].push({ to: edge.u, cost: edge.length || 1, edgeIndex: index, isReverse: true });
} }
}); });
// 3. Build Spatial Index (The Performance Fix)
this.buildSpatialIndex();
}
// ============================
// Spatial Optimization
// ============================
buildSpatialIndex() {
this.spatialGrid = {};
// Iterate over all nodes once
for (const [id, node] of Object.entries(this.graphData.nodes)) {
const key = this.getGridKey(node.x, node.y);
if (!this.spatialGrid[key]) {
this.spatialGrid[key] = [];
}
// Store simple object for fast iteration
this.spatialGrid[key].push({ id: parseInt(id), x: node.x, y: node.y });
}
}
getGridKey(x, y) {
const cx = Math.floor(x / this.cellSize);
const cy = Math.floor(y / this.cellSize);
return `${cx}:${cy}`;
}
// Optimized Nearest Node Search
findNearestNode(x, z) {
if (!this.graphData) return null;
const centerCx = Math.floor(x / this.cellSize);
const centerCy = Math.floor(z / this.cellSize);
let closestId = null;
let minDist = Infinity;
// Check center cell and immediate 8 neighbors
// This reduces checks from ~5000 to ~20
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
const key = `${centerCx + i}:${centerCy + j}`;
const cellNodes = this.spatialGrid[key];
if (cellNodes) {
for (let k = 0; k < cellNodes.length; k++) {
const node = cellNodes[k];
const dx = node.x - x;
const dz = node.y - z; // graph node.y is actually z in 3D space
const d2 = dx * dx + dz * dz;
if (d2 < minDist) {
minDist = d2;
closestId = node.id;
}
}
}
}
}
// Fallback: If no node was found in the local grid (e.g. sparse area),
// do a global search. This rarely happens if cellSize is reasonable.
if (closestId === null) {
return this.findNearestNodeBruteForce(x, z);
}
return closestId;
}
findNearestNodeBruteForce(x, z) {
let closestId = null;
let minDist = Infinity;
for (const [id, node] of Object.entries(this.graphData.nodes)) {
const dx = node.x - x;
const dz = node.y - z;
const d2 = dx * dx + dz * dz;
if (d2 < minDist) {
minDist = d2;
closestId = parseInt(id);
}
}
return closestId;
} }
// ============================ // ============================
@@ -69,7 +161,7 @@ export class RouteManager {
}); });
this.ghostMarker = new THREE.Mesh(geom, mat); this.ghostMarker = new THREE.Mesh(geom, mat);
this.ghostMarker.visible = false; this.ghostMarker.visible = false;
this.ghostMarker.name = "GHOST_MARKER"; // Ignore in raycasting this.ghostMarker.name = "GHOST_MARKER";
this.scene.add(this.ghostMarker); this.scene.add(this.ghostMarker);
} }
@@ -81,10 +173,9 @@ export class RouteManager {
stopDrafting() { stopDrafting() {
this.isDrafting = false; this.isDrafting = false;
this.ghostMarker.visible = false; this.ghostMarker.visible = false;
this.clearCurrentRoute(); // Clean up visuals this.clearCurrentRoute();
} }
// Called by Main.js input listener on mouse move
updateGhostMarker(worldPoint) { updateGhostMarker(worldPoint) {
if (!this.isDrafting || !this.graphData) { if (!this.isDrafting || !this.graphData) {
this.ghostMarker.visible = false; this.ghostMarker.visible = false;
@@ -202,9 +293,7 @@ export class RouteManager {
color: color color: color
}); });
// We do NOT call stopDrafting here, UIManager handles the logic to call stopDrafting this.currentPathMesh = null;
// We just return success
this.currentPathMesh = null; // Detach mesh from manager so it stays in scene
this.refreshServedNodes(); this.refreshServedNodes();
this.gameManager.recalculateApproval(); this.gameManager.recalculateApproval();
this.gameManager.updateUI(); this.gameManager.updateUI();
@@ -247,14 +336,12 @@ export class RouteManager {
} }
editSavedRoute(index) { editSavedRoute(index) {
// Delete and pull back to draft
if (index < 0 || index >= this.savedRoutes.length) return; if (index < 0 || index >= this.savedRoutes.length) return;
const route = this.savedRoutes[index]; const route = this.savedRoutes[index];
this.currentRouteNodes = [...route.nodes]; this.currentRouteNodes = [...route.nodes];
this.deleteSavedRoute(index); this.deleteSavedRoute(index);
// Visualize draft immediately
this.currentRouteNodes.forEach(nodeId => this.addMarkerVisual(nodeId)); this.currentRouteNodes.forEach(nodeId => this.addMarkerVisual(nodeId));
this.updatePathVisuals(); this.updatePathVisuals();
} }
@@ -319,7 +406,7 @@ export class RouteManager {
} }
addNodeByWorldPosition(vector3) { addNodeByWorldPosition(vector3) {
if (!this.isDrafting) return; // BLOCK INPUT IF NOT DRAFTING if (!this.isDrafting) return;
if (!this.graphData) return; if (!this.graphData) return;
const nodeId = this.findNearestNode(vector3.x, vector3.z); const nodeId = this.findNearestNode(vector3.x, vector3.z);
@@ -331,7 +418,7 @@ export class RouteManager {
} }
dragNode(markerObject, worldPoint) { dragNode(markerObject, worldPoint) {
if (!this.isDrafting) return; // BLOCK DRAG IF NOT DRAFTING if (!this.isDrafting) return;
if (!this.graphData) return; if (!this.graphData) return;
const index = this.markers.indexOf(markerObject); const index = this.markers.indexOf(markerObject);
if (index === -1) return; if (index === -1) return;
@@ -400,7 +487,7 @@ export class RouteManager {
updateMarkerColors() { updateMarkerColors() {
this.markers.forEach((marker, i) => { this.markers.forEach((marker, i) => {
let color = 0xFFFF00; // Yellow let color = 0xFFFF00;
if (i === 0) color = this.settings.colors.pathStart; if (i === 0) color = this.settings.colors.pathStart;
else if (i === this.markers.length - 1) color = this.settings.colors.pathEnd; else if (i === this.markers.length - 1) color = this.settings.colors.pathEnd;
marker.material.color.setHex(color); marker.material.color.setHex(color);
@@ -451,21 +538,6 @@ export class RouteManager {
return Math.sqrt(minSq); return Math.sqrt(minSq);
} }
findNearestNode(x, z) {
let closestId = null;
let minDist = Infinity;
for (const [id, node] of Object.entries(this.graphData.nodes)) {
const dx = node.x - x;
const dz = node.y - z;
const d2 = dx * dx + dz * dz;
if (d2 < minDist) {
minDist = d2;
closestId = parseInt(id);
}
}
return closestId;
}
computePathAStar(start, end) { computePathAStar(start, end) {
if (start === end) return []; if (start === end) return [];
const openSet = new Set([start]); const openSet = new Set([start]);
@@ -530,4 +602,3 @@ export class RouteManager {
return newPath; return newPath;
} }
} }