feat(route-map): 新增线路预览页面并优化八向布线布局

- 新增全网线路图预览前端页面 `/web/map-preview.html`
- 调整布局物理参数与迭代次数优化排布效果
- 新增正交八向布线强制对齐逻辑
- 实现节点网格对齐与优化线路路径转角处理
- 调整锚点固定力度提升布局稳定性
This commit is contained in:
2026-07-15 02:54:11 +08:00
parent 620289e836
commit 4ea58772d3
2 changed files with 143 additions and 10 deletions
+80 -10
View File
@@ -380,12 +380,12 @@ function optimizeLayout(nodeMap, lineSegments, referenceMeta) {
neighbors.get(edge.to)?.add(edge.from);
}
const iterations = 220;
const repulsionBase = 18000;
const springLength = 92;
const springStrength = 0.012;
const bendStrength = 0.018;
const anchorStrength = 0.018;
const iterations = 450;
const repulsionBase = 45000;
const springLength = 120;
const springStrength = 0.015;
const bendStrength = 0.025;
const anchorStrength = 0.02;
const damping = 0.82;
for (let iteration = 0; iteration < iterations; iteration += 1) {
@@ -393,6 +393,7 @@ function optimizeLayout(nodeMap, lineSegments, referenceMeta) {
const forces = {};
for (const node of nodes) forces[node.canonical] = { x: 0, y: 0 };
// 1. Repulsion to untangle graph
for (let i = 0; i < nodes.length; i += 1) {
const a = nodes[i];
const pa = positions[a.canonical];
@@ -418,6 +419,7 @@ function optimizeLayout(nodeMap, lineSegments, referenceMeta) {
}
}
// 2. Spring forces for edges & bend smoothing
for (const segment of lineSegments.segments) {
for (let index = 0; index < segment.stops.length - 1; index += 1) {
const from = segment.stops[index];
@@ -448,14 +450,46 @@ function optimizeLayout(nodeMap, lineSegments, referenceMeta) {
}
}
// 3. Orthogonal snapping force (Octilinear bias)
const orthoStrength = 0.03 * alpha;
for (const edge of lineSegments.edges) {
const pa = positions[edge.from];
const pb = positions[edge.to];
const dx = pb.x - pa.x;
const dy = pb.y - pa.y;
const adx = Math.abs(dx);
const ady = Math.abs(dy);
if (adx < ady && adx < ady * 0.4) {
// pull towards vertical
forces[edge.from].x += dx * orthoStrength;
forces[edge.to].x -= dx * orthoStrength;
} else if (ady < adx && ady < adx * 0.4) {
// pull towards horizontal
forces[edge.from].y += dy * orthoStrength;
forces[edge.to].y -= dy * orthoStrength;
} else {
// pull towards diagonal (45 degrees)
const avg = (adx + ady) / 2;
const targetDx = avg * Math.sign(dx || 1);
const targetDy = avg * Math.sign(dy || 1);
forces[edge.from].x += (dx - targetDx) * orthoStrength;
forces[edge.to].x -= (dx - targetDx) * orthoStrength;
forces[edge.from].y += (dy - targetDy) * orthoStrength;
forces[edge.to].y -= (dy - targetDy) * orthoStrength;
}
}
// 4. Anchor forces
for (const node of nodes) {
const anchor = seed[node.canonical];
if (!anchor) continue;
const strength = anchor.locked ? 0.14 : anchorStrength;
const strength = anchor.locked ? 0.2 : anchorStrength;
forces[node.canonical].x += (anchor.x - positions[node.canonical].x) * strength;
forces[node.canonical].y += (anchor.y - positions[node.canonical].y) * strength;
}
// 5. Update velocities and positions
for (const node of nodes) {
const velocity = velocities[node.canonical];
const force = forces[node.canonical];
@@ -466,6 +500,7 @@ function optimizeLayout(nodeMap, lineSegments, referenceMeta) {
}
}
// Final alignment to Grid to ensure perfect octilinear rendering
const xs = nodes.map((node) => positions[node.canonical].x);
const ys = nodes.map((node) => positions[node.canonical].y);
const minX = Math.min(...xs);
@@ -480,10 +515,16 @@ function optimizeLayout(nodeMap, lineSegments, referenceMeta) {
const offsetX = 130;
const offsetY = 110;
const GRID_SIZE = 24;
for (const node of nodes) {
const point = positions[node.canonical];
point.x = round(((point.x - minX) * scale) + offsetX, 2);
point.y = round(((point.y - minY) * scale) + offsetY, 2);
let rawX = ((point.x - minX) * scale) + offsetX;
let rawY = ((point.y - minY) * scale) + offsetY;
// Snap to grid
point.x = Math.round(rawX / GRID_SIZE) * GRID_SIZE;
point.y = Math.round(rawY / GRID_SIZE) * GRID_SIZE;
}
return positions;
@@ -492,8 +533,37 @@ function optimizeLayout(nodeMap, lineSegments, referenceMeta) {
function buildLinePath(points) {
if (!Array.isArray(points) || points.length === 0) return '';
let d = `M ${points[0].x} ${points[0].y}`;
for (let index = 1; index < points.length; index += 1) {
d += ` L ${points[index].x} ${points[index].y}`;
const p1 = points[index - 1];
const p2 = points[index];
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const adx = Math.abs(dx);
const ady = Math.abs(dy);
if (adx === 0 || ady === 0 || Math.abs(adx - ady) < 2) {
// Already orthogonal or 45-degree diagonal
d += ` L ${p2.x} ${p2.y}`;
} else {
// Need an octilinear chamfer (a single bend)
const signX = Math.sign(dx);
const signY = Math.sign(dy);
if (adx > ady) {
// Horizontal is longer: straight horizontal, then 45-degree
const hDist = adx - ady;
const midX = p1.x + hDist * signX;
const midY = p1.y;
d += ` L ${midX} ${midY} L ${p2.x} ${p2.y}`;
} else {
// Vertical is longer: straight vertical, then 45-degree
const vDist = ady - adx;
const midX = p1.x;
const midY = p1.y + vDist * signY;
d += ` L ${midX} ${midY} L ${p2.x} ${p2.y}`;
}
}
}
return d;
}