|
1 | | - |
2 | 1 | import ForceGraph2D, { ForceGraphMethods } from 'react-force-graph-2d'; |
3 | 2 | import { Graph, GraphData, Link, Node } from './model'; |
4 | 3 | import { Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from 'react'; |
@@ -232,38 +231,66 @@ export default function GraphView({ |
232 | 231 |
|
233 | 232 | if (!start.x || !start.y || !end.x || !end.y) return |
234 | 233 |
|
| 234 | + let textX, textY, angle; |
| 235 | + |
235 | 236 | if (start.id === end.id) { |
236 | 237 | const radius = NODE_SIZE * link.curve * 6.2; |
237 | 238 | const angleOffset = -Math.PI / 4; // 45 degrees offset for text alignment |
238 | | - const textX = start.x + radius * Math.cos(angleOffset); |
239 | | - const textY = start.y + radius * Math.sin(angleOffset); |
240 | | - |
241 | | - ctx.save(); |
242 | | - ctx.translate(textX, textY); |
243 | | - ctx.rotate(-angleOffset); |
| 239 | + textX = start.x + radius * Math.cos(angleOffset); |
| 240 | + textY = start.y + radius * Math.sin(angleOffset); |
| 241 | + angle = -angleOffset; |
244 | 242 | } else { |
245 | | - const midX = (start.x + end.x) / 2 + (end.y - start.y) * (link.curve / 2); |
246 | | - const midY = (start.y + end.y) / 2 + (start.x - end.x) * (link.curve / 2); |
247 | | - |
248 | | - let textAngle = Math.atan2(end.y - start.y, end.x - start.x) |
249 | | - |
| 243 | + const midX = (start.x + end.x) / 2; |
| 244 | + const midY = (start.y + end.y) / 2; |
| 245 | + const offset = link.curve / 2; |
| 246 | + |
| 247 | + angle = Math.atan2(end.y - start.y, end.x - start.x); |
| 248 | + |
250 | 249 | // maintain label vertical orientation for legibility |
251 | | - if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle); |
252 | | - if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle); |
253 | | - |
254 | | - ctx.save(); |
255 | | - ctx.translate(midX, midY); |
256 | | - ctx.rotate(textAngle); |
| 250 | + if (angle > Math.PI / 2) angle = -(Math.PI - angle); |
| 251 | + if (angle < -Math.PI / 2) angle = -(-Math.PI - angle); |
| 252 | + |
| 253 | + // Calculate perpendicular offset |
| 254 | + const perpX = -Math.sin(angle) * offset; |
| 255 | + const perpY = Math.cos(angle) * offset; |
| 256 | + |
| 257 | + // Adjust position to compensate for rotation around origin |
| 258 | + const cos = Math.cos(angle); |
| 259 | + const sin = Math.sin(angle); |
| 260 | + textX = midX + perpX; |
| 261 | + textY = midY + perpY; |
| 262 | + const rotatedX = textX * cos + textY * sin; |
| 263 | + const rotatedY = -textX * sin + textY * cos; |
| 264 | + textX = rotatedX; |
| 265 | + textY = rotatedY; |
257 | 266 | } |
258 | 267 |
|
259 | | - // add label |
| 268 | + // Setup text properties to measure background size |
| 269 | + ctx.font = '2px Arial'; |
| 270 | + const padding = 0.5; |
| 271 | + const textWidth = ctx.measureText(link.label).width; |
| 272 | + const textHeight = 2; // Approximate height for 2px font |
| 273 | + |
| 274 | + // add label with background and rotation |
| 275 | + ctx.rotate(angle); |
| 276 | + |
| 277 | + // Draw background |
| 278 | + ctx.fillStyle = 'white'; |
| 279 | + ctx.fillRect( |
| 280 | + textX - textWidth/2 - padding, |
| 281 | + textY - textHeight/2 - padding, |
| 282 | + textWidth + padding * 2, |
| 283 | + textHeight + padding * 2 |
| 284 | + ); |
| 285 | + |
| 286 | + // Draw text |
260 | 287 | ctx.globalAlpha = 1; |
261 | 288 | ctx.fillStyle = 'black'; |
262 | 289 | ctx.textAlign = 'center'; |
263 | 290 | ctx.textBaseline = 'middle'; |
264 | | - ctx.font = '2px Arial'; |
265 | | - ctx.fillText(link.label, 0, 0); |
266 | | - ctx.restore() |
| 291 | + ctx.fillText(link.label, textX, textY); |
| 292 | + |
| 293 | + ctx.rotate(-angle); // reset rotation |
267 | 294 | }} |
268 | 295 | onNodeClick={handleNodeClick} |
269 | 296 | onNodeDragEnd={(n, translate) => setPosition(prev => { |
|
0 commit comments