Implement A* pathfinding for topology wire routing

This commit is contained in:
2026-02-11 16:48:23 -06:00
parent 3711c24ed0
commit c75c051c5d

View File

@@ -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);