|
1 |
| - |
2 | 1 | import ForceGraph2D, { ForceGraphMethods, NodeObject } 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';
|
@@ -254,38 +253,77 @@ export default function GraphView({
|
254 | 253 |
|
255 | 254 | if (!start.x || !start.y || !end.x || !end.y) return
|
256 | 255 |
|
| 256 | + let textX, textY, angle; |
| 257 | + |
257 | 258 | if (start.id === end.id) {
|
258 | 259 | const radius = NODE_SIZE * link.curve * 6.2;
|
259 | 260 | const angleOffset = -Math.PI / 4; // 45 degrees offset for text alignment
|
260 |
| - const textX = start.x + radius * Math.cos(angleOffset); |
261 |
| - const textY = start.y + radius * Math.sin(angleOffset); |
262 |
| - |
263 |
| - ctx.save(); |
264 |
| - ctx.translate(textX, textY); |
265 |
| - ctx.rotate(-angleOffset); |
| 261 | + textX = start.x + radius * Math.cos(angleOffset); |
| 262 | + textY = start.y + radius * Math.sin(angleOffset); |
| 263 | + angle = -angleOffset; |
266 | 264 | } else {
|
267 |
| - const midX = (start.x + end.x) / 2 + (end.y - start.y) * (link.curve / 2); |
268 |
| - const midY = (start.y + end.y) / 2 + (start.x - end.x) * (link.curve / 2); |
| 265 | + const midX = (start.x + end.x) / 2; |
| 266 | + const midY = (start.y + end.y) / 2; |
| 267 | + const offset = link.curve / 2; |
269 | 268 |
|
270 |
| - let textAngle = Math.atan2(end.y - start.y, end.x - start.x) |
| 269 | + angle = Math.atan2(end.y - start.y, end.x - start.x); |
271 | 270 |
|
272 | 271 | // maintain label vertical orientation for legibility
|
273 |
| - if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle); |
274 |
| - if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle); |
| 272 | + if (angle > Math.PI / 2) angle = -(Math.PI - angle); |
| 273 | + if (angle < -Math.PI / 2) angle = -(-Math.PI - angle); |
| 274 | + |
| 275 | + // Calculate perpendicular offset |
| 276 | + const perpX = -Math.sin(angle) * offset; |
| 277 | + const perpY = Math.cos(angle) * offset; |
| 278 | + |
| 279 | + // Adjust position to compensate for rotation around origin |
| 280 | + const cos = Math.cos(angle); |
| 281 | + const sin = Math.sin(angle); |
| 282 | + textX = midX + perpX; |
| 283 | + textY = midY + perpY; |
| 284 | + const rotatedX = textX * cos + textY * sin; |
| 285 | + const rotatedY = -textX * sin + textY * cos; |
| 286 | + textX = rotatedX; |
| 287 | + textY = rotatedY; |
| 288 | + } |
275 | 289 |
|
276 |
| - ctx.save(); |
277 |
| - ctx.translate(midX, midY); |
278 |
| - ctx.rotate(textAngle); |
| 290 | + // Setup text properties to measure background size |
| 291 | + ctx.font = '2px Arial'; |
| 292 | + const padding = 0.5; |
| 293 | + // Get text width and height |
| 294 | + const label = graph.LabelsMap.get(link.label)! |
| 295 | + let { textWidth, textHeight } = label |
| 296 | + |
| 297 | + if (!textWidth || !textHeight) { |
| 298 | + const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } = ctx.measureText(link.label) |
| 299 | + textWidth = width |
| 300 | + textHeight = actualBoundingBoxAscent + actualBoundingBoxDescent |
| 301 | + graph.LabelsMap.set(link.label, { ...label, textWidth, textHeight }) |
279 | 302 | }
|
280 | 303 |
|
281 |
| - // add label |
| 304 | + // Save the current context state |
| 305 | + ctx.save(); |
| 306 | + |
| 307 | + // add label with background and rotation |
| 308 | + ctx.rotate(angle); |
| 309 | + |
| 310 | + // Draw background |
| 311 | + ctx.fillStyle = 'white'; |
| 312 | + ctx.fillRect( |
| 313 | + textX - textWidth / 2 - padding, |
| 314 | + textY - textHeight / 2 - padding, |
| 315 | + textWidth + padding * 2, |
| 316 | + textHeight + padding * 2 |
| 317 | + ); |
| 318 | + |
| 319 | + // Draw text |
282 | 320 | ctx.globalAlpha = 1;
|
283 | 321 | ctx.fillStyle = 'black';
|
284 | 322 | ctx.textAlign = 'center';
|
285 | 323 | ctx.textBaseline = 'middle';
|
286 |
| - ctx.font = '2px Arial'; |
287 |
| - ctx.fillText(link.label, 0, 0); |
288 |
| - ctx.restore() |
| 324 | + ctx.fillText(link.label, textX, textY); |
| 325 | + |
| 326 | + ctx.restore(); // reset rotation |
289 | 327 | }}
|
290 | 328 | onNodeClick={handleNodeClick}
|
291 | 329 | onNodeDragEnd={(n, translate) => setPosition(prev => {
|
|
0 commit comments