Improve routing quality heuristics and expose detour/bend metrics
This commit is contained in:
parent
f2d48cee85
commit
6c5431a6e5
@ -1150,7 +1150,7 @@ async function compileModel(model, opts = {}) {
|
|||||||
|
|
||||||
const m = result.layout_metrics;
|
const m = result.layout_metrics;
|
||||||
setStatus(
|
setStatus(
|
||||||
`Compiled (${result.errors.length}E, ${result.warnings.length}W | ${m.crossings} crossings, ${m.overlap_edges} overlaps)`
|
`Compiled (${result.errors.length}E, ${result.warnings.length}W | ${m.crossings} crossings, ${m.overlap_edges} overlaps, ${m.total_bends ?? 0} bends, ${(m.detour_ratio ?? 1).toFixed(2)}x detour)`
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus(`Compile failed: ${err.message}`, false);
|
setStatus(`Compile failed: ${err.message}`, false);
|
||||||
@ -1564,7 +1564,7 @@ async function runLayoutAction(path) {
|
|||||||
fitView(out.compile.layout);
|
fitView(out.compile.layout);
|
||||||
saveSnapshot();
|
saveSnapshot();
|
||||||
setStatus(
|
setStatus(
|
||||||
`Compiled (${out.compile.errors.length}E, ${out.compile.warnings.length}W | ${out.compile.layout_metrics.crossings} crossings)`
|
`Compiled (${out.compile.errors.length}E, ${out.compile.warnings.length}W | ${out.compile.layout_metrics.crossings} crossings, ${out.compile.layout_metrics.overlap_edges} overlaps, ${out.compile.layout_metrics.total_bends ?? 0} bends)`
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus(`Layout action failed: ${err.message}`, false);
|
setStatus(`Layout action failed: ${err.message}`, false);
|
||||||
|
|||||||
@ -255,7 +255,12 @@ export function compile(payload, options = {}) {
|
|||||||
crossings: 0,
|
crossings: 0,
|
||||||
label_collisions: 0,
|
label_collisions: 0,
|
||||||
tie_points_used: 0,
|
tie_points_used: 0,
|
||||||
bus_groups: 0
|
bus_groups: 0,
|
||||||
|
total_bends: 0,
|
||||||
|
total_length: 0,
|
||||||
|
direct_length: 0,
|
||||||
|
detour_ratio: 1,
|
||||||
|
label_tie_fallbacks: 0
|
||||||
},
|
},
|
||||||
bus_groups: [],
|
bus_groups: [],
|
||||||
focus_map: {},
|
focus_map: {},
|
||||||
|
|||||||
247
src/layout.js
247
src/layout.js
@ -18,6 +18,7 @@ const NET_CLASS_PRIORITY = {
|
|||||||
const LABEL_TIE_CLASSES = new Set(["power", "ground", "bus"]);
|
const LABEL_TIE_CLASSES = new Set(["power", "ground", "bus"]);
|
||||||
const DEFAULT_RENDER_MODE = "schematic_stub";
|
const DEFAULT_RENDER_MODE = "schematic_stub";
|
||||||
const ROTATION_STEPS = [0, 90, 180, 270];
|
const ROTATION_STEPS = [0, 90, 180, 270];
|
||||||
|
const MIN_CHANNEL_SPACING_STEPS = 2;
|
||||||
|
|
||||||
function toGrid(value) {
|
function toGrid(value) {
|
||||||
return Math.round(value / GRID) * GRID;
|
return Math.round(value / GRID) * GRID;
|
||||||
@ -733,6 +734,65 @@ function hasForeignPointUsage(pointUsage, netName, point) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function foreignUsageCount(usageMap, key, ownNet) {
|
||||||
|
const usage = usageMap.get(key);
|
||||||
|
if (!usage) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
for (const [net, n] of usage.entries()) {
|
||||||
|
if (net !== ownNet) {
|
||||||
|
count += n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function channelCrowdingPenalty(current, next, context) {
|
||||||
|
const { netName, hLineUsage, vLineUsage } = context;
|
||||||
|
if (current.x !== next.x && current.y !== next.y) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHorizontal = current.y === next.y;
|
||||||
|
const lineUsage = isHorizontal ? hLineUsage : vLineUsage;
|
||||||
|
const lineCoord = isHorizontal ? current.y : current.x;
|
||||||
|
|
||||||
|
let penalty = 0;
|
||||||
|
for (let step = -MIN_CHANNEL_SPACING_STEPS; step <= MIN_CHANNEL_SPACING_STEPS; step += 1) {
|
||||||
|
const coord = lineCoord + step * GRID;
|
||||||
|
const foreign = foreignUsageCount(lineUsage, coord, netName);
|
||||||
|
if (!foreign) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = Math.abs(step);
|
||||||
|
const weight = distance === 0 ? 34 : distance === 1 ? 17 : 7;
|
||||||
|
penalty += foreign * weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return penalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointCongestionPenalty(point, context) {
|
||||||
|
const { pointUsage, netName } = context;
|
||||||
|
let penalty = 0;
|
||||||
|
for (const nb of [
|
||||||
|
{ x: point.x, y: point.y },
|
||||||
|
{ x: point.x + GRID, y: point.y },
|
||||||
|
{ x: point.x - GRID, y: point.y },
|
||||||
|
{ x: point.x, y: point.y + GRID },
|
||||||
|
{ x: point.x, y: point.y - GRID }
|
||||||
|
]) {
|
||||||
|
const foreign = foreignUsageCount(pointUsage, pointKey(nb), netName);
|
||||||
|
if (foreign) {
|
||||||
|
penalty += foreign * 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return penalty;
|
||||||
|
}
|
||||||
|
|
||||||
function proximityPenalty(point, obstacles, allowedRefs) {
|
function proximityPenalty(point, obstacles, allowedRefs) {
|
||||||
let penalty = 0;
|
let penalty = 0;
|
||||||
for (const box of obstacles) {
|
for (const box of obstacles) {
|
||||||
@ -743,8 +803,8 @@ function proximityPenalty(point, obstacles, allowedRefs) {
|
|||||||
const dx = Math.max(box.x - point.x, 0, point.x - (box.x + box.w));
|
const dx = Math.max(box.x - point.x, 0, point.x - (box.x + box.w));
|
||||||
const dy = Math.max(box.y - point.y, 0, point.y - (box.y + box.h));
|
const dy = Math.max(box.y - point.y, 0, point.y - (box.y + box.h));
|
||||||
const dist = dx + dy;
|
const dist = dx + dy;
|
||||||
if (dist < GRID * 2) {
|
if (dist < GRID * 4) {
|
||||||
penalty += (GRID * 2 - dist) * 0.35;
|
penalty += (GRID * 4 - dist) * 0.7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return penalty;
|
return penalty;
|
||||||
@ -816,9 +876,11 @@ function aStar(start, goal, context) {
|
|||||||
|
|
||||||
const nbKey = pointKey(nb);
|
const nbKey = pointKey(nb);
|
||||||
const prevCost = gScore.get(currentKey) ?? Number.POSITIVE_INFINITY;
|
const prevCost = gScore.get(currentKey) ?? Number.POSITIVE_INFINITY;
|
||||||
const turnPenalty = current.dir && current.dir !== nb.dir ? 16 : 0;
|
const turnPenalty = current.dir && current.dir !== nb.dir ? 24 : 0;
|
||||||
const obstaclePenalty = proximityPenalty(nb, obstacles, allowedRefs);
|
const obstaclePenalty = proximityPenalty(nb, obstacles, allowedRefs);
|
||||||
const tentative = prevCost + GRID + turnPenalty + obstaclePenalty;
|
const channelPenalty = channelCrowdingPenalty(current, nb, context);
|
||||||
|
const congestionPenalty = pointCongestionPenalty(nb, context);
|
||||||
|
const tentative = prevCost + GRID + turnPenalty + obstaclePenalty + channelPenalty + congestionPenalty;
|
||||||
|
|
||||||
if (tentative >= (gScore.get(nbKey) ?? Number.POSITIVE_INFINITY)) {
|
if (tentative >= (gScore.get(nbKey) ?? Number.POSITIVE_INFINITY)) {
|
||||||
continue;
|
continue;
|
||||||
@ -898,7 +960,13 @@ function segmentStepPoints(a, b) {
|
|||||||
return [a, b];
|
return [a, b];
|
||||||
}
|
}
|
||||||
|
|
||||||
function addUsageForSegments(edgeUsage, pointUsage, netName, segments) {
|
function addLineUsage(lineUsage, coord, netName) {
|
||||||
|
const usage = lineUsage.get(coord) ?? new Map();
|
||||||
|
usage.set(netName, (usage.get(netName) ?? 0) + 1);
|
||||||
|
lineUsage.set(coord, usage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUsageForSegments(edgeUsage, pointUsage, hLineUsage, vLineUsage, netName, segments) {
|
||||||
for (const seg of segments) {
|
for (const seg of segments) {
|
||||||
const stepPoints = segmentStepPoints(seg.a, seg.b);
|
const stepPoints = segmentStepPoints(seg.a, seg.b);
|
||||||
|
|
||||||
@ -912,6 +980,12 @@ function addUsageForSegments(edgeUsage, pointUsage, netName, segments) {
|
|||||||
edgeUsage.set(eKey, edge);
|
edgeUsage.set(eKey, edge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (seg.a.y === seg.b.y) {
|
||||||
|
addLineUsage(hLineUsage, seg.a.y, netName);
|
||||||
|
} else if (seg.a.x === seg.b.x) {
|
||||||
|
addLineUsage(vLineUsage, seg.a.x, netName);
|
||||||
|
}
|
||||||
|
|
||||||
for (const p of stepPoints) {
|
for (const p of stepPoints) {
|
||||||
const pKey = pointKey(p);
|
const pKey = pointKey(p);
|
||||||
const usage = pointUsage.get(pKey) ?? new Map();
|
const usage = pointUsage.get(pKey) ?? new Map();
|
||||||
@ -1007,7 +1081,14 @@ function routeLabelTieNet(net, pinNodes, context) {
|
|||||||
const stub = pointsToSegments([pin.point, pin.exit]);
|
const stub = pointsToSegments([pin.point, pin.exit]);
|
||||||
if (stub.length) {
|
if (stub.length) {
|
||||||
routes.push(stub);
|
routes.push(stub);
|
||||||
addUsageForSegments(context.edgeUsage, context.pointUsage, net.name, stub);
|
addUsageForSegments(
|
||||||
|
context.edgeUsage,
|
||||||
|
context.pointUsage,
|
||||||
|
context.hLineUsage,
|
||||||
|
context.vLineUsage,
|
||||||
|
net.name,
|
||||||
|
stub
|
||||||
|
);
|
||||||
}
|
}
|
||||||
tiePoints.push({ x: pin.exit.x, y: pin.exit.y });
|
tiePoints.push({ x: pin.exit.x, y: pin.exit.y });
|
||||||
}
|
}
|
||||||
@ -1034,7 +1115,15 @@ function routeLabelTieNet(net, pinNodes, context) {
|
|||||||
routes,
|
routes,
|
||||||
labelPoints,
|
labelPoints,
|
||||||
tiePoints,
|
tiePoints,
|
||||||
junctionPoints: []
|
junctionPoints: [],
|
||||||
|
route_stats: {
|
||||||
|
total_length: routeLengthFromSegments(routes),
|
||||||
|
direct_length: 0,
|
||||||
|
total_bends: 0,
|
||||||
|
detour_ratio: 1,
|
||||||
|
used_label_tie: true,
|
||||||
|
fallback_reason: null
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1046,6 +1135,32 @@ function pathLength(points) {
|
|||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function routeLengthFromSegments(routes) {
|
||||||
|
let length = 0;
|
||||||
|
for (const route of routes) {
|
||||||
|
for (const seg of route) {
|
||||||
|
length += Math.abs(seg.a.x - seg.b.x) + Math.abs(seg.a.y - seg.b.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countBendsInRoute(routes) {
|
||||||
|
let bends = 0;
|
||||||
|
for (const route of routes) {
|
||||||
|
for (let i = 1; i < route.length; i += 1) {
|
||||||
|
const prev = route[i - 1];
|
||||||
|
const curr = route[i];
|
||||||
|
const prevH = prev.a.y === prev.b.y;
|
||||||
|
const currH = curr.a.y === curr.b.y;
|
||||||
|
if (prevH !== currH) {
|
||||||
|
bends += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bends;
|
||||||
|
}
|
||||||
|
|
||||||
function routePointToPointNet(net, pinNodes, context) {
|
function routePointToPointNet(net, pinNodes, context) {
|
||||||
if (pinNodes.length < 2) {
|
if (pinNodes.length < 2) {
|
||||||
return {
|
return {
|
||||||
@ -1053,7 +1168,15 @@ function routePointToPointNet(net, pinNodes, context) {
|
|||||||
routes: [],
|
routes: [],
|
||||||
labelPoints: [],
|
labelPoints: [],
|
||||||
tiePoints: [],
|
tiePoints: [],
|
||||||
junctionPoints: []
|
junctionPoints: [],
|
||||||
|
route_stats: {
|
||||||
|
total_length: 0,
|
||||||
|
direct_length: 0,
|
||||||
|
total_bends: 0,
|
||||||
|
detour_ratio: 1,
|
||||||
|
used_label_tie: false,
|
||||||
|
fallback_reason: null
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1071,7 +1194,14 @@ function routePointToPointNet(net, pinNodes, context) {
|
|||||||
const sourceStub = pointsToSegments([source.point, source.exit]);
|
const sourceStub = pointsToSegments([source.point, source.exit]);
|
||||||
if (sourceStub.length) {
|
if (sourceStub.length) {
|
||||||
routes.push(sourceStub);
|
routes.push(sourceStub);
|
||||||
addUsageForSegments(context.edgeUsage, context.pointUsage, net.name, sourceStub);
|
addUsageForSegments(
|
||||||
|
context.edgeUsage,
|
||||||
|
context.pointUsage,
|
||||||
|
context.hLineUsage,
|
||||||
|
context.vLineUsage,
|
||||||
|
net.name,
|
||||||
|
sourceStub
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const treePoints = new Map();
|
const treePoints = new Map();
|
||||||
@ -1079,6 +1209,8 @@ function routePointToPointNet(net, pinNodes, context) {
|
|||||||
|
|
||||||
const allowedRefs = new Set(sorted.map((p) => p.ref));
|
const allowedRefs = new Set(sorted.map((p) => p.ref));
|
||||||
const remaining = sorted.slice(1);
|
const remaining = sorted.slice(1);
|
||||||
|
let accumulatedPath = 0;
|
||||||
|
let accumulatedDirect = 0;
|
||||||
|
|
||||||
for (const target of remaining) {
|
for (const target of remaining) {
|
||||||
const candidates = uniquePoints([...treePoints.values()])
|
const candidates = uniquePoints([...treePoints.values()])
|
||||||
@ -1103,19 +1235,48 @@ function routePointToPointNet(net, pinNodes, context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!best) {
|
if (!best) {
|
||||||
return routeLabelTieNet(net, sorted, context);
|
return {
|
||||||
|
...routeLabelTieNet(net, sorted, context),
|
||||||
|
route_stats: {
|
||||||
|
total_length: 0,
|
||||||
|
direct_length: 0,
|
||||||
|
total_bends: 0,
|
||||||
|
detour_ratio: 1,
|
||||||
|
used_label_tie: true,
|
||||||
|
fallback_reason: "no_path"
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const direct = heuristic(target.exit, best.attach);
|
const direct = heuristic(target.exit, best.attach);
|
||||||
if (context.renderMode === "schematic_stub" && best.cost > direct * 1.65) {
|
accumulatedPath += best.cost;
|
||||||
return routeLabelTieNet(net, sorted, context);
|
accumulatedDirect += Math.max(GRID, direct);
|
||||||
|
if (context.renderMode === "schematic_stub" && best.cost > direct * 1.6) {
|
||||||
|
return {
|
||||||
|
...routeLabelTieNet(net, sorted, context),
|
||||||
|
route_stats: {
|
||||||
|
total_length: accumulatedPath,
|
||||||
|
direct_length: accumulatedDirect,
|
||||||
|
total_bends: 0,
|
||||||
|
detour_ratio: accumulatedPath / Math.max(GRID, accumulatedDirect),
|
||||||
|
used_label_tie: true,
|
||||||
|
fallback_reason: "branch_detour"
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const branchPoints = [target.point, target.exit, ...best.path.slice(1)];
|
const branchPoints = [target.point, target.exit, ...best.path.slice(1)];
|
||||||
const branchSegments = pointsToSegments(branchPoints);
|
const branchSegments = pointsToSegments(branchPoints);
|
||||||
if (branchSegments.length) {
|
if (branchSegments.length) {
|
||||||
routes.push(branchSegments);
|
routes.push(branchSegments);
|
||||||
addUsageForSegments(context.edgeUsage, context.pointUsage, net.name, branchSegments);
|
addUsageForSegments(
|
||||||
|
context.edgeUsage,
|
||||||
|
context.pointUsage,
|
||||||
|
context.hLineUsage,
|
||||||
|
context.vLineUsage,
|
||||||
|
net.name,
|
||||||
|
branchSegments
|
||||||
|
);
|
||||||
for (const seg of branchSegments) {
|
for (const seg of branchSegments) {
|
||||||
for (const p of segmentStepPoints(seg.a, seg.b)) {
|
for (const p of segmentStepPoints(seg.a, seg.b)) {
|
||||||
treePoints.set(pointKey(p), p);
|
treePoints.set(pointKey(p), p);
|
||||||
@ -1141,12 +1302,42 @@ function routePointToPointNet(net, pinNodes, context) {
|
|||||||
junctionPoints.push({ x: source.exit.x, y: source.exit.y });
|
junctionPoints.push({ x: source.exit.x, y: source.exit.y });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalLength = routeLengthFromSegments(routes);
|
||||||
|
const totalBends = countBendsInRoute(routes);
|
||||||
|
const directLength = Math.max(GRID, accumulatedDirect || heuristic(source.exit, sorted[sorted.length - 1].exit));
|
||||||
|
const detourRatio = totalLength / directLength;
|
||||||
|
const maxAllowedBends = Math.max(6, sorted.length * 3);
|
||||||
|
if (
|
||||||
|
context.renderMode === "schematic_stub" &&
|
||||||
|
(detourRatio > 1.9 || totalBends > maxAllowedBends || totalLength > GRID * 180)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...routeLabelTieNet(net, sorted, context),
|
||||||
|
route_stats: {
|
||||||
|
total_length: totalLength,
|
||||||
|
direct_length: directLength,
|
||||||
|
total_bends: totalBends,
|
||||||
|
detour_ratio: detourRatio,
|
||||||
|
used_label_tie: true,
|
||||||
|
fallback_reason: "global_quality"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mode: "routed",
|
mode: "routed",
|
||||||
routes,
|
routes,
|
||||||
labelPoints,
|
labelPoints,
|
||||||
tiePoints: [],
|
tiePoints: [],
|
||||||
junctionPoints
|
junctionPoints,
|
||||||
|
route_stats: {
|
||||||
|
total_length: totalLength,
|
||||||
|
direct_length: directLength,
|
||||||
|
total_bends: totalBends,
|
||||||
|
detour_ratio: detourRatio,
|
||||||
|
used_label_tie: false,
|
||||||
|
fallback_reason: null
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1194,6 +1385,8 @@ function routeAllNets(model, placed, placedMap, bounds, options) {
|
|||||||
const obstacles = buildObstacles(model, placed);
|
const obstacles = buildObstacles(model, placed);
|
||||||
const edgeUsage = new Map();
|
const edgeUsage = new Map();
|
||||||
const pointUsage = new Map();
|
const pointUsage = new Map();
|
||||||
|
const hLineUsage = new Map();
|
||||||
|
const vLineUsage = new Map();
|
||||||
const busGroups = detectBusGroups(model.nets);
|
const busGroups = detectBusGroups(model.nets);
|
||||||
const busNetNames = new Set(busGroups.flatMap((g) => g.nets));
|
const busNetNames = new Set(busGroups.flatMap((g) => g.nets));
|
||||||
|
|
||||||
@ -1216,6 +1409,8 @@ function routeAllNets(model, placed, placedMap, bounds, options) {
|
|||||||
obstacles,
|
obstacles,
|
||||||
edgeUsage,
|
edgeUsage,
|
||||||
pointUsage,
|
pointUsage,
|
||||||
|
hLineUsage,
|
||||||
|
vLineUsage,
|
||||||
renderMode: options.renderMode,
|
renderMode: options.renderMode,
|
||||||
busNetNames
|
busNetNames
|
||||||
};
|
};
|
||||||
@ -1239,7 +1434,15 @@ function routeAllNets(model, placed, placedMap, bounds, options) {
|
|||||||
routes: [],
|
routes: [],
|
||||||
labelPoints: [],
|
labelPoints: [],
|
||||||
tiePoints: [],
|
tiePoints: [],
|
||||||
junctionPoints: []
|
junctionPoints: [],
|
||||||
|
route_stats: {
|
||||||
|
total_length: 0,
|
||||||
|
direct_length: 0,
|
||||||
|
total_bends: 0,
|
||||||
|
detour_ratio: 1,
|
||||||
|
used_label_tie: false,
|
||||||
|
fallback_reason: null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1367,6 +1570,13 @@ function computeLayoutMetrics(routed, busGroups) {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
const tiePoints = routed.reduce((total, rn) => total + rn.tiePoints.length, 0);
|
const tiePoints = routed.reduce((total, rn) => total + rn.tiePoints.length, 0);
|
||||||
|
const totalBends = routed.reduce((total, rn) => total + Number(rn.route_stats?.total_bends ?? 0), 0);
|
||||||
|
const totalLength = routed.reduce((total, rn) => total + Number(rn.route_stats?.total_length ?? 0), 0);
|
||||||
|
const totalDirect = routed.reduce((total, rn) => total + Number(rn.route_stats?.direct_length ?? 0), 0);
|
||||||
|
const labelTieFallbacks = routed.reduce(
|
||||||
|
(total, rn) => total + (rn.route_stats?.used_label_tie && rn.route_stats?.fallback_reason ? 1 : 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
segment_count: segmentCount,
|
segment_count: segmentCount,
|
||||||
@ -1374,7 +1584,12 @@ function computeLayoutMetrics(routed, busGroups) {
|
|||||||
crossings: countCrossings(routed),
|
crossings: countCrossings(routed),
|
||||||
label_collisions: countLabelCollisions(routed),
|
label_collisions: countLabelCollisions(routed),
|
||||||
tie_points_used: tiePoints,
|
tie_points_used: tiePoints,
|
||||||
bus_groups: busGroups.length
|
bus_groups: busGroups.length,
|
||||||
|
total_bends: totalBends,
|
||||||
|
total_length: totalLength,
|
||||||
|
direct_length: totalDirect,
|
||||||
|
detour_ratio: totalDirect > 0 ? totalLength / totalDirect : 1,
|
||||||
|
label_tie_fallbacks: labelTieFallbacks
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user