diff --git a/server/services/svg-generator.js b/server/services/svg-generator.js index 49a4454..36be81c 100644 --- a/server/services/svg-generator.js +++ b/server/services/svg-generator.js @@ -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; } diff --git a/web/map-preview.html b/web/map-preview.html new file mode 100644 index 0000000..0f5c8c0 --- /dev/null +++ b/web/map-preview.html @@ -0,0 +1,63 @@ + + + + + + 全网线路图预览 - FSE铁路运输票务系统 + + + + +
+

全网线路图预览 (正交布线)

+
+ 返回后台 + +
+
+
+
正在计算布局,请稍候...
+
+ + +