optimization: spatial hash grid
All checks were successful
Deploy to GitHub Pages / deploy (push) Has been skipped
All checks were successful
Deploy to GitHub Pages / deploy (push) Has been skipped
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user