Implement A* pathfinding for topology wire routing
This commit is contained in:
370
static/app.js
370
static/app.js
@@ -1065,8 +1065,19 @@ function renderTopology() {
|
||||
const dcDeviceCandidates = new Map();
|
||||
const dcGateByID = new Map();
|
||||
const rackGateByAreaID = new Map();
|
||||
const routingObstacles = [];
|
||||
const mapLayouts = [];
|
||||
|
||||
function addRoutingObstacle(x, y, w, h, padding = 7) {
|
||||
if (![x, y, w, h].every(Number.isFinite)) return;
|
||||
routingObstacles.push({
|
||||
left: x - padding,
|
||||
top: y - padding,
|
||||
right: x + w + padding,
|
||||
bottom: y + h + padding
|
||||
});
|
||||
}
|
||||
|
||||
function deviceMatchesFilter(device) {
|
||||
const kind = deviceKind(device.kind);
|
||||
return matchesTopologyDeviceFilter({
|
||||
@@ -1277,6 +1288,7 @@ function renderTopology() {
|
||||
class: 'topo-node wan'
|
||||
});
|
||||
layerNodes.appendChild(rect);
|
||||
addRoutingObstacle(x, y, wanW, wanH, 8);
|
||||
|
||||
const nameText = createSvg('text', {
|
||||
x: x + 10,
|
||||
@@ -1322,6 +1334,7 @@ function renderTopology() {
|
||||
class: 'topo-dc-box'
|
||||
});
|
||||
layerNodes.appendChild(dcRect);
|
||||
addRoutingObstacle(dcX, dcY, dcLayout.w, dcLayout.h, 6);
|
||||
|
||||
const dcTitle = createSvg('text', {
|
||||
x: dcX + 12,
|
||||
@@ -1381,6 +1394,7 @@ function renderTopology() {
|
||||
class: 'topo-rack-box'
|
||||
});
|
||||
layerNodes.appendChild(rackRect);
|
||||
addRoutingObstacle(rackX, rackY, rackLayout.w, rackLayout.h, 5);
|
||||
|
||||
const rackTitle = createSvg('text', {
|
||||
x: rackX + 9,
|
||||
@@ -1441,6 +1455,7 @@ function renderTopology() {
|
||||
class: classNames.join(' ')
|
||||
});
|
||||
layerNodes.appendChild(devRect);
|
||||
addRoutingObstacle(devX, devY, devW, deviceH, 4);
|
||||
|
||||
const label = createSvg('text', {
|
||||
x: devX + 8,
|
||||
@@ -1620,97 +1635,297 @@ function renderTopology() {
|
||||
return { scope: 'global', key: 'global' };
|
||||
}
|
||||
|
||||
function routeConnection(from, to, laneInfo, endpointKeys, slots = { from: 0, to: 0 }) {
|
||||
const fromSlot = Number.isFinite(slots.from) ? slots.from : 0;
|
||||
const toSlot = Number.isFinite(slots.to) ? slots.to : 0;
|
||||
function endpointBorderPoint(endpoint, towardX) {
|
||||
const left = Number.isFinite(endpoint.left) ? endpoint.left : endpoint.x - 8;
|
||||
const right = Number.isFinite(endpoint.right) ? endpoint.right : endpoint.x + 8;
|
||||
const x = towardX >= endpoint.x ? right : left;
|
||||
return [x, endpoint.y];
|
||||
}
|
||||
|
||||
function endpointBorderPoint(endpoint, towardX) {
|
||||
const left = Number.isFinite(endpoint.left) ? endpoint.left : endpoint.x - 8;
|
||||
const right = Number.isFinite(endpoint.right) ? endpoint.right : endpoint.x + 8;
|
||||
const x = towardX >= endpoint.x ? right : left;
|
||||
return [x, endpoint.y];
|
||||
}
|
||||
function buildGatewayPath(endpoint, endpointKey, slot, scope) {
|
||||
const points = [];
|
||||
let netX = endpoint.x;
|
||||
let netY = endpoint.y;
|
||||
|
||||
function buildGatewayPath(endpoint, endpointKey, slot, scope) {
|
||||
const points = [];
|
||||
let netX = endpoint.x;
|
||||
let netY = endpoint.y;
|
||||
if (endpoint.kind === 'device') {
|
||||
const gate = rackGateByAreaID.get(endpoint.rackId || '');
|
||||
const dcGate = dcGateByID.get(endpoint.dcId || '');
|
||||
const rackGateX = gate ? gate.x + slot * 4 : endpoint.x + (10 + slot * 4);
|
||||
const rackGateY = gate ? gate.y + slot * 4 : endpoint.y;
|
||||
const border = endpointBorderPoint(endpoint, rackGateX);
|
||||
points.push(border, [rackGateX, border[1]], [rackGateX, rackGateY]);
|
||||
netX = rackGateX;
|
||||
netY = rackGateY;
|
||||
|
||||
if (endpoint.kind === 'device') {
|
||||
const gate = rackGateByAreaID.get(endpoint.rackId || '');
|
||||
const dcGate = dcGateByID.get(endpoint.dcId || '');
|
||||
const rackGateX = gate ? gate.x + slot * 4 : endpoint.x + (10 + slot * 4);
|
||||
const rackGateY = gate ? gate.y + slot * 4 : endpoint.y;
|
||||
const border = endpointBorderPoint(endpoint, rackGateX);
|
||||
points.push(border, [rackGateX, border[1]], [rackGateX, rackGateY]);
|
||||
netX = rackGateX;
|
||||
netY = rackGateY;
|
||||
|
||||
if (dcGate && scope !== 'rack') {
|
||||
const dcSlot = slot;
|
||||
const dcNetX = dcGate.x + dcSlot * 6;
|
||||
const dcNetY = dcGate.y - dcSlot * 2;
|
||||
points.push([rackGateX, dcNetY], [dcNetX, dcNetY]);
|
||||
netX = dcNetX;
|
||||
netY = dcNetY;
|
||||
}
|
||||
} else if (endpoint.kind === 'datacenter') {
|
||||
const dcID = endpointKey.replace(/^datacenter:/, '');
|
||||
const dcGate = dcGateByID.get(dcID);
|
||||
if (dcGate) {
|
||||
netX = dcGate.x + slot * 6;
|
||||
netY = dcGate.y - slot * 2;
|
||||
points.push([endpoint.x, endpoint.y], [endpoint.x, netY], [netX, netY]);
|
||||
} else {
|
||||
points.push([endpoint.x, endpoint.y]);
|
||||
}
|
||||
} else if (endpoint.kind === 'wan_input') {
|
||||
const top = Number.isFinite(endpoint.top) ? endpoint.top : endpoint.y;
|
||||
const netYOffset = 16 + slot * 6;
|
||||
netX = endpoint.x + slot * 5;
|
||||
netY = top - netYOffset;
|
||||
if (dcGate && scope !== 'rack') {
|
||||
const dcSlot = slot;
|
||||
const dcNetX = dcGate.x + dcSlot * 6;
|
||||
const dcNetY = dcGate.y - dcSlot * 2;
|
||||
points.push([rackGateX, dcNetY], [dcNetX, dcNetY]);
|
||||
netX = dcNetX;
|
||||
netY = dcNetY;
|
||||
}
|
||||
} else if (endpoint.kind === 'datacenter') {
|
||||
const dcID = endpointKey.replace(/^datacenter:/, '');
|
||||
const dcGate = dcGateByID.get(dcID);
|
||||
if (dcGate) {
|
||||
netX = dcGate.x + slot * 6;
|
||||
netY = dcGate.y - slot * 2;
|
||||
points.push([endpoint.x, endpoint.y], [endpoint.x, netY], [netX, netY]);
|
||||
} else {
|
||||
points.push([endpoint.x, endpoint.y]);
|
||||
}
|
||||
|
||||
if (!points.length) points.push([endpoint.x, endpoint.y]);
|
||||
return {
|
||||
points: normalizedPathPoints(points),
|
||||
netX,
|
||||
netY
|
||||
};
|
||||
} else if (endpoint.kind === 'wan_input') {
|
||||
const top = Number.isFinite(endpoint.top) ? endpoint.top : endpoint.y;
|
||||
const netYOffset = 16 + slot * 6;
|
||||
netX = endpoint.x + slot * 5;
|
||||
netY = top - netYOffset;
|
||||
points.push([endpoint.x, endpoint.y], [endpoint.x, netY], [netX, netY]);
|
||||
} else {
|
||||
points.push([endpoint.x, endpoint.y]);
|
||||
}
|
||||
|
||||
const scope = laneInfo?.scope || 'global';
|
||||
const laneRank = Number.isFinite(laneInfo?.rank) ? laneInfo.rank : 0;
|
||||
const fromPath = buildGatewayPath(from, endpointKeys.from, fromSlot, scope);
|
||||
const toPath = buildGatewayPath(to, endpointKeys.to, toSlot, scope);
|
||||
if (!points.length) points.push([endpoint.x, endpoint.y]);
|
||||
return {
|
||||
points: normalizedPathPoints(points),
|
||||
netX,
|
||||
netY
|
||||
};
|
||||
}
|
||||
|
||||
const routeGridSize = 14;
|
||||
const routeGridCols = Math.max(2, Math.ceil(totalWidth / routeGridSize));
|
||||
const routeGridRows = Math.max(2, Math.ceil(totalHeight / routeGridSize));
|
||||
const routeStaticBlocked = new Uint8Array(routeGridCols * routeGridRows);
|
||||
const routeWireReserved = new Uint8Array(routeGridCols * routeGridRows);
|
||||
|
||||
function routeCellIndex(x, y) {
|
||||
return y * routeGridCols + x;
|
||||
}
|
||||
|
||||
function routeCellCenter(x, y) {
|
||||
return [x * routeGridSize + routeGridSize / 2, y * routeGridSize + routeGridSize / 2];
|
||||
}
|
||||
|
||||
function clampRouteCellX(x) {
|
||||
return Math.max(0, Math.min(routeGridCols - 1, x));
|
||||
}
|
||||
|
||||
function clampRouteCellY(y) {
|
||||
return Math.max(0, Math.min(routeGridRows - 1, y));
|
||||
}
|
||||
|
||||
function pointToRouteCell(point) {
|
||||
const x = clampRouteCellX(Math.round((point[0] - routeGridSize / 2) / routeGridSize));
|
||||
const y = clampRouteCellY(Math.round((point[1] - routeGridSize / 2) / routeGridSize));
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function blockRouteRect(rect) {
|
||||
const minX = clampRouteCellX(Math.floor(rect.left / routeGridSize));
|
||||
const maxX = clampRouteCellX(Math.floor(rect.right / routeGridSize));
|
||||
const minY = clampRouteCellY(Math.floor(rect.top / routeGridSize));
|
||||
const maxY = clampRouteCellY(Math.floor(rect.bottom / routeGridSize));
|
||||
for (let cy = minY; cy <= maxY; cy += 1) {
|
||||
for (let cx = minX; cx <= maxX; cx += 1) {
|
||||
routeStaticBlocked[routeCellIndex(cx, cy)] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const obstacle of routingObstacles) {
|
||||
blockRouteRect(obstacle);
|
||||
}
|
||||
|
||||
function heapPush(heap, item) {
|
||||
heap.push(item);
|
||||
let idx = heap.length - 1;
|
||||
while (idx > 0) {
|
||||
const parent = Math.floor((idx - 1) / 2);
|
||||
if (heap[parent].score <= heap[idx].score) break;
|
||||
[heap[parent], heap[idx]] = [heap[idx], heap[parent]];
|
||||
idx = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function heapPop(heap) {
|
||||
if (!heap.length) return null;
|
||||
const first = heap[0];
|
||||
const tail = heap.pop();
|
||||
if (heap.length && tail) {
|
||||
heap[0] = tail;
|
||||
let idx = 0;
|
||||
while (true) {
|
||||
const left = idx * 2 + 1;
|
||||
const right = left + 1;
|
||||
let smallest = idx;
|
||||
if (left < heap.length && heap[left].score < heap[smallest].score) smallest = left;
|
||||
if (right < heap.length && heap[right].score < heap[smallest].score) smallest = right;
|
||||
if (smallest === idx) break;
|
||||
[heap[idx], heap[smallest]] = [heap[smallest], heap[idx]];
|
||||
idx = smallest;
|
||||
}
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
function findRouteCells(startCell, endCell, allowReserved) {
|
||||
const nodeCount = routeGridCols * routeGridRows;
|
||||
const gScore = new Float64Array(nodeCount);
|
||||
const fScore = new Float64Array(nodeCount);
|
||||
const cameFrom = new Int32Array(nodeCount);
|
||||
gScore.fill(Number.POSITIVE_INFINITY);
|
||||
fScore.fill(Number.POSITIVE_INFINITY);
|
||||
cameFrom.fill(-1);
|
||||
|
||||
const startIdx = routeCellIndex(startCell.x, startCell.y);
|
||||
const endIdx = routeCellIndex(endCell.x, endCell.y);
|
||||
const openHeap = [];
|
||||
const heuristic = (x, y) => Math.abs(x - endCell.x) + Math.abs(y - endCell.y);
|
||||
const neighbors = [[1, 0], [0, 1], [-1, 0], [0, -1]];
|
||||
|
||||
gScore[startIdx] = 0;
|
||||
fScore[startIdx] = heuristic(startCell.x, startCell.y);
|
||||
heapPush(openHeap, { idx: startIdx, score: fScore[startIdx] });
|
||||
|
||||
while (openHeap.length) {
|
||||
const current = heapPop(openHeap);
|
||||
if (!current) break;
|
||||
const currentIdx = current.idx;
|
||||
if (current.score > fScore[currentIdx] + 1e-9) continue;
|
||||
if (currentIdx === endIdx) break;
|
||||
|
||||
const cx = currentIdx % routeGridCols;
|
||||
const cy = (currentIdx - cx) / routeGridCols;
|
||||
for (const [dx, dy] of neighbors) {
|
||||
const nx = cx + dx;
|
||||
const ny = cy + dy;
|
||||
if (nx < 0 || ny < 0 || nx >= routeGridCols || ny >= routeGridRows) continue;
|
||||
|
||||
const nIdx = routeCellIndex(nx, ny);
|
||||
const isEndpoint = nIdx === startIdx || nIdx === endIdx;
|
||||
if (!isEndpoint && routeStaticBlocked[nIdx]) continue;
|
||||
|
||||
const onReservedWire = !isEndpoint && routeWireReserved[nIdx] === 1;
|
||||
if (onReservedWire && !allowReserved) continue;
|
||||
|
||||
const stepCost = 1 + (onReservedWire ? 11 : 0);
|
||||
const tentative = gScore[currentIdx] + stepCost;
|
||||
if (tentative + 1e-9 >= gScore[nIdx]) continue;
|
||||
|
||||
cameFrom[nIdx] = currentIdx;
|
||||
gScore[nIdx] = tentative;
|
||||
fScore[nIdx] = tentative + heuristic(nx, ny);
|
||||
heapPush(openHeap, { idx: nIdx, score: fScore[nIdx] });
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isFinite(gScore[endIdx])) return null;
|
||||
const path = [];
|
||||
let cursor = endIdx;
|
||||
while (cursor !== -1) {
|
||||
const x = cursor % routeGridCols;
|
||||
const y = (cursor - x) / routeGridCols;
|
||||
path.push({ x, y });
|
||||
if (cursor === startIdx) break;
|
||||
cursor = cameFrom[cursor];
|
||||
}
|
||||
path.reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
function withTemporaryEndpointClearance(startCell, endCell, action) {
|
||||
const touched = [];
|
||||
function clearAround(cell, radius = 1) {
|
||||
for (let dy = -radius; dy <= radius; dy += 1) {
|
||||
for (let dx = -radius; dx <= radius; dx += 1) {
|
||||
const cx = cell.x + dx;
|
||||
const cy = cell.y + dy;
|
||||
if (cx < 0 || cy < 0 || cx >= routeGridCols || cy >= routeGridRows) continue;
|
||||
const idx = routeCellIndex(cx, cy);
|
||||
if (routeStaticBlocked[idx] === 1) {
|
||||
routeStaticBlocked[idx] = 0;
|
||||
touched.push(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearAround(startCell, 1);
|
||||
clearAround(endCell, 1);
|
||||
const result = action();
|
||||
for (const idx of touched) {
|
||||
routeStaticBlocked[idx] = 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function orthBridgePoints(fromPoint, toPoint) {
|
||||
const fx = fromPoint[0];
|
||||
const fy = fromPoint[1];
|
||||
const tx = toPoint[0];
|
||||
const ty = toPoint[1];
|
||||
if (fx === tx && fy === ty) return [];
|
||||
if (fx === tx || fy === ty) return [[tx, ty]];
|
||||
return [[fx, ty], [tx, ty]];
|
||||
}
|
||||
|
||||
function reserveRoutePoints(points) {
|
||||
const normalized = normalizedPathPoints(points);
|
||||
for (let idx = 1; idx < normalized.length; idx += 1) {
|
||||
const a = normalized[idx - 1];
|
||||
const b = normalized[idx];
|
||||
const dx = b[0] - a[0];
|
||||
const dy = b[1] - a[1];
|
||||
const steps = Math.max(1, Math.ceil(Math.max(Math.abs(dx), Math.abs(dy)) / Math.max(1, routeGridSize / 2)));
|
||||
for (let step = 0; step <= steps; step += 1) {
|
||||
const t = step / steps;
|
||||
const px = a[0] + dx * t;
|
||||
const py = a[1] + dy * t;
|
||||
const cell = pointToRouteCell([px, py]);
|
||||
routeWireReserved[routeCellIndex(cell.x, cell.y)] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function routeConnection(from, to, endpointKeys, slots = { from: 0, to: 0 }, scope = 'global') {
|
||||
const fromSlot = Number.isFinite(slots.from) ? slots.from : 0;
|
||||
const toSlot = Number.isFinite(slots.to) ? slots.to : 0;
|
||||
const resolvedScope = String(scope || 'global');
|
||||
const fromPath = buildGatewayPath(from, endpointKeys.from, fromSlot, resolvedScope);
|
||||
const toPath = buildGatewayPath(to, endpointKeys.to, toSlot, resolvedScope);
|
||||
|
||||
const startPoint = [fromPath.netX, fromPath.netY];
|
||||
const endPoint = [toPath.netX, toPath.netY];
|
||||
const startCell = pointToRouteCell(startPoint);
|
||||
const endCell = pointToRouteCell(endPoint);
|
||||
|
||||
const routedCells = withTemporaryEndpointClearance(startCell, endCell, () => {
|
||||
const strict = findRouteCells(startCell, endCell, false);
|
||||
if (strict && strict.length) return strict;
|
||||
const relaxed = findRouteCells(startCell, endCell, true);
|
||||
return relaxed || [];
|
||||
});
|
||||
|
||||
const trunkPoints = routedCells.length
|
||||
? routedCells.map((cell) => routeCellCenter(cell.x, cell.y))
|
||||
: [];
|
||||
|
||||
const route = [...fromPath.points];
|
||||
if (scope === 'rack') {
|
||||
const laneX = Math.max(fromPath.netX, toPath.netX) + 28 + laneRank * 10;
|
||||
route.push([laneX, fromPath.netY], [laneX, toPath.netY]);
|
||||
if (trunkPoints.length) {
|
||||
route.push(...orthBridgePoints(startPoint, trunkPoints[0]));
|
||||
if (trunkPoints.length > 1) route.push(...trunkPoints.slice(1));
|
||||
route.push(...orthBridgePoints(trunkPoints[trunkPoints.length - 1], endPoint));
|
||||
} else {
|
||||
const anchors = [fromPath.netY, toPath.netY];
|
||||
if (scope === 'dc') {
|
||||
if (Number.isFinite(from.dcTop)) anchors.push(from.dcTop);
|
||||
if (Number.isFinite(to.dcTop)) anchors.push(to.dcTop);
|
||||
} else {
|
||||
if (Number.isFinite(from.mapTop)) anchors.push(from.mapTop);
|
||||
if (Number.isFinite(to.mapTop)) anchors.push(to.mapTop);
|
||||
}
|
||||
|
||||
const topAnchor = Math.min(...anchors);
|
||||
const topOffset = scope === 'dc' ? 24 : (scope === 'map' ? 32 : 40);
|
||||
const laneY = Math.max(12, topAnchor - topOffset - laneRank * routeLaneStep);
|
||||
route.push([fromPath.netX, laneY], [toPath.netX, laneY]);
|
||||
route.push(...orthBridgePoints(startPoint, endPoint));
|
||||
}
|
||||
|
||||
const back = [...toPath.points].reverse();
|
||||
if (back.length > 1) route.push(...back.slice(1));
|
||||
|
||||
return normalizedPathPoints(route);
|
||||
const normalized = normalizedPathPoints(route);
|
||||
reserveRoutePoints(normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (appState.topologyShowContainment) {
|
||||
@@ -1728,7 +1943,6 @@ function renderTopology() {
|
||||
}
|
||||
|
||||
const endpointSlotCounters = new Map();
|
||||
const connectionLaneCounters = new Map();
|
||||
function takeEndpointSlot(key) {
|
||||
const next = endpointSlotCounters.get(key) || 0;
|
||||
endpointSlotCounters.set(key, next + 1);
|
||||
@@ -1774,14 +1988,12 @@ function renderTopology() {
|
||||
const { conn, fromPos, toPos, fromEndpointKey, toEndpointKey, lane } = entry;
|
||||
const fromSlot = takeEndpointSlot(fromEndpointKey);
|
||||
const toSlot = takeEndpointSlot(toEndpointKey);
|
||||
const laneRank = connectionLaneCounters.get(lane.key) || 0;
|
||||
connectionLaneCounters.set(lane.key, laneRank + 1);
|
||||
const routePoints = routeConnection(
|
||||
fromPos,
|
||||
toPos,
|
||||
{ scope: lane.scope, rank: laneRank },
|
||||
{ from: fromEndpointKey, to: toEndpointKey },
|
||||
{ from: fromSlot, to: toSlot }
|
||||
{ from: fromSlot, to: toSlot },
|
||||
lane.scope
|
||||
);
|
||||
const wirePath = pathFromPoints(routePoints, 18);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user