| 2584 | } |
| 2585 | |
| 2586 | buildDigitSprite(text) { |
| 2587 | const size = 128; |
| 2588 | const canvas = document.createElement("canvas"); |
| 2589 | canvas.width = size; |
| 2590 | canvas.height = size; |
| 2591 | const ctx = canvas.getContext("2d"); |
| 2592 | if (!ctx) { |
| 2593 | throw new Error("Canvas 2D context unavailable for label rendering."); |
| 2594 | } |
| 2595 | ctx.clearRect(0, 0, size, size); |
| 2596 | ctx.font = `900 ${Math.floor(size * 0.62)}px "Inter", "Segoe UI", sans-serif`; |
| 2597 | ctx.textAlign = "center"; |
| 2598 | ctx.textBaseline = "middle"; |
| 2599 | ctx.fillStyle = "rgba(235, 245, 255, 0.95)"; |
| 2600 | ctx.strokeStyle = "rgba(12, 25, 44, 0.85)"; |
| 2601 | ctx.lineWidth = size * 0.08; |
| 2602 | ctx.shadowColor = "rgba(0, 0, 0, 0.55)"; |
| 2603 | ctx.shadowBlur = size * 0.12; |
| 2604 | ctx.strokeText(text, size / 2, size / 2); |
| 2605 | ctx.fillText(text, size / 2, size / 2); |
| 2606 | |
| 2607 | const texture = new THREE.CanvasTexture(canvas); |
| 2608 | texture.needsUpdate = true; |
| 2609 | texture.anisotropy = 2; |
| 2610 | texture.generateMipmaps = false; |
| 2611 | texture.minFilter = THREE.LinearFilter; |
| 2612 | texture.wrapS = THREE.ClampToEdgeWrapping; |
| 2613 | texture.wrapT = THREE.ClampToEdgeWrapping; |
| 2614 | |
| 2615 | const material = new THREE.SpriteMaterial({ |
| 2616 | map: texture, |
| 2617 | transparent: true, |
| 2618 | depthTest: false, |
| 2619 | depthWrite: false, |
| 2620 | }); |
| 2621 | const sprite = new THREE.Sprite(material); |
| 2622 | sprite.center.set(0, 0.5); |
| 2623 | return sprite; |
| 2624 | } |
| 2625 | |
| 2626 | buildConnections() { |
| 2627 | this.maxConnectionWeightMagnitude = 0; |