// Patch Three.js r128 Sprite raycast camera space vs world space bug (function() { const _worldScale = new THREE.Vector3(); const _mvPosition = new THREE.Vector3(); const _vA = new THREE.Vector3(); const _vB = new THREE.Vector3(); const _vC = new THREE.Vector3(); const _uvC = new THREE.Vector2(); const _intersectPoint = new THREE.Vector3(); const _modelViewMatrix = new THREE.Matrix4(); THREE.Sprite.prototype.raycast = function (raycaster, intersects) { if (!raycaster.camera) return; _worldScale.setFromMatrixScale(this.matrixWorld); _modelViewMatrix.multiplyMatrices(raycaster.camera.matrixWorldInverse, this.matrixWorld); _mvPosition.setFromMatrixPosition(_modelViewMatrix); if (raycaster.camera.isPerspectiveCamera && this.material.sizeAttenuation === false) { _worldScale.multiplyScalar(-_mvPosition.z); } const rotation = this.material.rotation; let sin = 0, cos = 1; if (rotation !== 0) { cos = Math.cos(rotation); sin = Math.sin(rotation); } const center = this.center; const transformVertex = (v, mvPosition, center, scale, sin, cos) => { v.x -= center.x; v.y -= center.y; let rx = v.x * scale.x; let ry = v.y * scale.y; if (sin !== 0) { const x = rx; const y = ry; rx = cos * x - sin * y; ry = sin * x + cos * y; } v.x = rx + mvPosition.x; v.y = ry + mvPosition.y; v.z = mvPosition.z; // Transform from Camera space to World space v.applyMatrix4(raycaster.camera.matrixWorld); }; transformVertex(_vA.set(-0.5, -0.5, 0), _mvPosition, center, _worldScale, sin, cos); transformVertex(_vB.set(0.5, -0.5, 0), _mvPosition, center, _worldScale, sin, cos); transformVertex(_vC.set(0.5, 0.5, 0), _mvPosition, center, _worldScale, sin, cos); let intersect = raycaster.ray.intersectTriangle(_vA, _vB, _vC, false, _intersectPoint); if (intersect === null) { transformVertex(_vB.set(-0.5, 0.5, 0), _mvPosition, center, _worldScale, sin, cos); intersect = raycaster.ray.intersectTriangle(_vA, _vC, _vB, false, _intersectPoint); if (intersect === null) { return; } } const distance = raycaster.ray.origin.distanceTo(_intersectPoint); if (distance < raycaster.near || distance > raycaster.far) return; intersects.push({ distance: distance, point: _intersectPoint.clone(), uv: _uvC.clone(), face: null, object: this }); }; })(); let currentMetric = 'cpu'; let clusterData = null; // Three.js Core Variables let scene, camera, renderer, controls; let podMeshes = []; let nodeMeshes = []; let raycaster = new THREE.Raycaster(); let mouse = new THREE.Vector2(); let hoveredPod = null; let maxCpuInCluster = 1; let maxMemInCluster = 1; let autoRotateEnabled = false; // Formatters const formatBytes = (bytes) => { if (!bytes || bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const formatCpu = (millicores) => { if (!millicores || millicores === 0) return '0 m'; return `${millicores} m`; }; // UI Controls Setup document.getElementById('btn-cpu').addEventListener('click', () => { document.getElementById('btn-cpu').classList.add('active'); document.getElementById('btn-mem').classList.remove('active'); currentMetric = 'cpu'; updatePodHeights(); }); document.getElementById('btn-mem').addEventListener('click', () => { document.getElementById('btn-mem').classList.add('active'); document.getElementById('btn-cpu').classList.remove('active'); currentMetric = 'memory'; updatePodHeights(); }); document.getElementById('btn-reset-cam').addEventListener('click', () => { resetCameraView(); }); document.getElementById('btn-auto-rotate').addEventListener('click', () => { autoRotateEnabled = !autoRotateEnabled; controls.autoRotate = autoRotateEnabled; const btn = document.getElementById('btn-auto-rotate'); const textSpan = document.getElementById('auto-rotate-text'); if (autoRotateEnabled) { btn.classList.add('active'); textSpan.innerText = 'Rotating...'; } else { btn.classList.remove('active'); textSpan.innerText = 'Auto Rotate'; } }); // Initialize 3D Engine function init3D() { const container = document.getElementById('chart-container'); const width = container.clientWidth; const height = container.clientHeight; // Scene scene = new THREE.Scene(); scene.background = new THREE.Color(0x090d16); scene.fog = new THREE.FogExp2(0x090d16, 0.0012); // Camera camera = new THREE.PerspectiveCamera(45, width / height, 1, 2000); camera.position.set(0, 140, 210); camera.lookAt(0, 0, 0); // Renderer renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(width, height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Clear previous elements if any container.innerHTML = ''; container.appendChild(renderer.domElement); // Lights const ambientLight = new THREE.AmbientLight(0xffffff, 0.65); scene.add(ambientLight); const dirLight = new THREE.DirectionalLight(0xffffff, 0.85); dirLight.position.set(120, 220, 120); dirLight.castShadow = true; dirLight.shadow.mapSize.width = 2048; dirLight.shadow.mapSize.height = 2048; scene.add(dirLight); const bluePointLight = new THREE.PointLight(0x3b82f6, 1.2, 600); bluePointLight.position.set(-150, 150, -150); scene.add(bluePointLight); const purplePointLight = new THREE.PointLight(0x8b5cf6, 1.0, 600); purplePointLight.position.set(150, 150, 150); scene.add(purplePointLight); // Grid Floor const gridHelper = new THREE.GridHelper(600, 60, 0x3b82f6, 0x1e293b); gridHelper.position.y = -0.5; scene.add(gridHelper); // Orbit Controls controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.maxPolarAngle = Math.PI / 2 - 0.02; // Don't go below ground controls.minDistance = 20; controls.maxDistance = 800; controls.autoRotate = autoRotateEnabled; controls.autoRotateSpeed = 1.0; // Events window.addEventListener('resize', onWindowResize); container.addEventListener('mousemove', onPointerMove); container.addEventListener('click', onPointerClick); // Start animation loop animate(); } function resetCameraView() { let startPos = camera.position.clone(); let targetPos = new THREE.Vector3(0, 140, 210); let startLookAt = controls.target.clone(); let targetLookAt = new THREE.Vector3(0, 0, 0); let duration = 600; let startTime = performance.now(); function step(now) { let progress = Math.min((now - startTime) / duration, 1); // Ease out cubic let ease = 1 - Math.pow(1 - progress, 3); camera.position.lerpVectors(startPos, targetPos, ease); controls.target.lerpVectors(startLookAt, targetLookAt, ease); controls.update(); if (progress < 1) { requestAnimationFrame(step); } } requestAnimationFrame(step); } function onWindowResize() { const container = document.getElementById('chart-container'); if (!container || !renderer || !camera) return; const width = container.clientWidth; const height = container.clientHeight; camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); } // Fetch Cluster Data async function fetchData() { try { const response = await fetch('/api/cluster-data'); if (!response.ok) throw new Error('Failed to fetch cluster data'); clusterData = await response.json(); document.getElementById('loading').classList.add('hidden'); if (!scene) { init3D(); } build3DCluster(clusterData); } catch (error) { console.error(error); document.getElementById('loading').innerHTML = `

Error loading cluster data: ${error.message}

`; } } // Helper to create 3D Canvas Text Sprite for Node Labels function createTextSprite(text, fontSize = 28, textColor = '#f8fafc', bgColor = 'rgba(15, 23, 42, 0.85)', isBold = false, isControlPlane = false) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // Allocate higher resolution canvas for crisp text canvas.width = isControlPlane ? 640 : 512; canvas.height = isControlPlane ? 160 : 128; const pad = 16; const w = canvas.width - pad * 2; const h = canvas.height - pad * 2; // Pill background ctx.fillStyle = bgColor; ctx.roundRect(pad, pad, w, h, isControlPlane ? 32 : 24); ctx.fill(); // Border ctx.strokeStyle = isControlPlane ? 'rgba(168, 85, 247, 0.95)' : 'rgba(59, 130, 246, 0.6)'; ctx.lineWidth = isControlPlane ? 8 : 6; ctx.stroke(); // Text - use 900 weight for control plane / master, 700 for regular const fontWeight = isControlPlane ? '900' : (isBold ? '800' : '700'); ctx.font = `${fontWeight} ${fontSize}px Inter, sans-serif`; ctx.fillStyle = textColor; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(text, canvas.width / 2, canvas.height / 2); const texture = new THREE.CanvasTexture(canvas); const spriteMaterial = new THREE.SpriteMaterial({ map: texture, transparent: true }); const sprite = new THREE.Sprite(spriteMaterial); // Differentiate scale: control plane sprite is larger if (isControlPlane) { sprite.scale.set(58, 14.5, 1); } else { sprite.scale.set(45, 11.25, 1); } // Add invisible Mesh plane for 100% reliable raycasting const hitGeo = new THREE.PlaneGeometry(1, 1); const hitMat = new THREE.MeshBasicMaterial({ visible: false, depthWrite: false }); const hitMesh = new THREE.Mesh(hitGeo, hitMat); hitMesh.name = 'labelHitMesh'; sprite.add(hitMesh); return sprite; } // Build 3D Topology from Data function build3DCluster(data) { // Clear existing node & pod meshes podMeshes.forEach(mesh => { scene.remove(mesh); mesh.geometry.dispose(); if (mesh.material) mesh.material.dispose(); }); podMeshes = []; nodeMeshes.forEach(obj => { scene.remove(obj); if (obj.geometry) obj.geometry.dispose(); if (obj.material) obj.material.dispose(); }); nodeMeshes = []; if (!data || !data.children || data.children.length === 0) return; // 1. Compute cluster max metrics for scaling height maxCpuInCluster = 1; maxMemInCluster = 1; data.children.forEach(node => { if (node.children) { node.children.forEach(pod => { if (pod.type === 'pod') { if (pod.cpu_raw > maxCpuInCluster) maxCpuInCluster = pod.cpu_raw; if (pod.memory_raw > maxMemInCluster) maxMemInCluster = pod.memory_raw; } }); } }); // 2. Layout on X-Z floor using D3 Treemap const BOUNDS_SIZE = 220; // 3D world dimension for ground layout const root = d3.hierarchy(data) .sum(d => { if (d.type === 'node') return 0; // Ensure minimum footprint for every pod block return Math.max(d.cpu_raw || 0, d.memory_raw ? d.memory_raw / 1000000 : 0, 15); }) .sort((a, b) => b.value - a.value); d3.treemap() .size([BOUNDS_SIZE, BOUNDS_SIZE]) .paddingTop(36) .paddingRight(10) .paddingInner(6) .paddingBottom(10) .paddingLeft(10)(root); // 3. Render Nodes and Pods const nodes = root.descendants().filter(d => d.depth === 1); const pods = root.leaves(); // Render Node Platforms nodes.forEach(n => { const nw = Math.max(10, n.x1 - n.x0); const nh = Math.max(10, n.y1 - n.y0); const centerX = (n.x0 + n.x1) / 2 - BOUNDS_SIZE / 2; const centerZ = (n.y0 + n.y1) / 2 - BOUNDS_SIZE / 2; const isControlPlane = n.data.isControlPlane || n.data.is_control_plane || (n.data.roles && (n.data.roles.includes('control-plane') || n.data.roles.includes('master'))) || n.data.name.includes('control-plane') || n.data.name.includes('master'); const platformGeo = new THREE.BoxGeometry(nw, 1.5, nh); const platformMat = new THREE.MeshStandardMaterial({ color: isControlPlane ? 0x1e1b4b : 0x1e293b, roughness: 0.5, metalness: 0.3, transparent: true, opacity: 0.95 }); const platformMesh = new THREE.Mesh(platformGeo, platformMat); platformMesh.position.set(centerX, 0.75, centerZ); platformMesh.receiveShadow = true; const nodeUserData = { type: 'node', name: n.data.name, isControlPlane: isControlPlane, roles: n.data.roles || (isControlPlane ? ['control-plane'] : ['worker']), internalIp: n.data.internalIp || n.data.internal_ip || 'N/A', osImage: n.data.osImage || n.data.os_image || 'N/A', nonTerminatedPods: n.data.nonTerminatedPods ?? n.data.non_terminated_pods ?? n.data.podCount ?? n.data.pod_count ?? (n.data.children ? n.data.children.filter(c => c.type === 'pod' && c.status !== 'Succeeded' && c.status !== 'Failed').length : 0) }; platformMesh.userData = nodeUserData; // Platform Border Frame const edges = new THREE.EdgesGeometry(platformGeo); const frameColor = isControlPlane ? 0xa855f7 : 0x3b82f6; const lineMat = new THREE.LineBasicMaterial({ color: frameColor, linewidth: isControlPlane ? 3 : 2 }); const wireframe = new THREE.LineSegments(edges, lineMat); platformMesh.add(wireframe); scene.add(platformMesh); nodeMeshes.push(platformMesh); // Node Label Sprite (Larger, bold font for control-plane/master nodes) const fontSize = isControlPlane ? 34 : 24; const textColor = isControlPlane ? '#f3e8ff' : '#f8fafc'; const bgColor = isControlPlane ? 'rgba(46, 16, 101, 0.92)' : 'rgba(15, 23, 42, 0.85)'; const sprite = createTextSprite(n.data.name, fontSize, textColor, bgColor, true, isControlPlane); sprite.position.set(centerX, isControlPlane ? 16 : 14, centerZ - nh / 2 - 2); sprite.userData = nodeUserData; const labelHit = sprite.getObjectByName('labelHitMesh'); if (labelHit) { labelHit.userData = nodeUserData; } scene.add(sprite); nodeMeshes.push(sprite); }); // Render Pod Towers pods.forEach(p => { const pw = Math.max(2, (p.x1 - p.x0) * 0.88); const pd = Math.max(2, (p.y1 - p.y0) * 0.88); const centerX = (p.x0 + p.x1) / 2 - BOUNDS_SIZE / 2; const centerZ = (p.y0 + p.y1) / 2 - BOUNDS_SIZE / 2; // Calculate height const targetHeight = getTargetHeight(p.data); // Box geometry: 1 unit height base, scaled in animation loop const podGeo = new THREE.BoxGeometry(pw, 1, pd); // Color & Material based on Pod Status let color = 0x10b981; // Running emerald let emissive = 0x059669; let opacity = 0.9; let transparent = false; if (p.data.type === 'unused') { color = 0x64748b; emissive = 0x334155; opacity = 0.25; transparent = true; } else if (p.data.status === 'Pending') { color = 0xf59e0b; emissive = 0xd97706; } else if (p.data.status === 'Succeeded') { color = 0x94a3b8; emissive = 0x475569; } else if (p.data.status !== 'Running') { color = 0xef4444; emissive = 0xdc2626; } const podMat = new THREE.MeshStandardMaterial({ color: color, emissive: emissive, emissiveIntensity: p.data.type === 'unused' ? 0.05 : 0.25, roughness: 0.3, metalness: 0.4, transparent: transparent, opacity: opacity }); const podMesh = new THREE.Mesh(podGeo, podMat); podMesh.castShadow = true; podMesh.receiveShadow = true; // Initial positions & properties podMesh.position.set(centerX, 1.5 + 0.1 / 2, centerZ); podMesh.scale.set(1, 0.1, 1); podMesh.userData = { ...p.data, nodeName: p.parent ? p.parent.data.name : 'Unknown Node' }; podMesh.targetHeight = targetHeight; podMesh.currentHeight = 0.1; podMesh.baseWidth = pw; podMesh.baseDepth = pd; podMesh.baseCenterX = centerX; podMesh.baseCenterZ = centerZ; podMesh.originalColor = color; podMesh.originalEmissive = emissive; podMesh.originalEmissiveIntensity = p.data.type === 'unused' ? 0.05 : 0.25; scene.add(podMesh); podMeshes.push(podMesh); }); } // Calculate target 3D tower height based on current metric function getTargetHeight(data) { if (data.type === 'unused') return 0.5; // low plate for available capacity const MAX_HEIGHT = 50; // Maximum tower height units const MIN_HEIGHT = 1.5; // Base minimum height for visibility if (currentMetric === 'cpu') { const val = data.cpu_raw || 0; return MIN_HEIGHT + (val / (maxCpuInCluster || 1)) * MAX_HEIGHT; } else { const val = data.memory_raw || 0; return MIN_HEIGHT + (val / (maxMemInCluster || 1)) * MAX_HEIGHT; } } // Update heights smoothly when toggling metric function updatePodHeights() { podMeshes.forEach(mesh => { mesh.targetHeight = getTargetHeight(mesh.userData); }); } // Main Render & Animation Loop function animate() { requestAnimationFrame(animate); // Smoothly interpolate pod tower heights (lerp) podMeshes.forEach(mesh => { if (Math.abs(mesh.currentHeight - mesh.targetHeight) > 0.01) { mesh.currentHeight += (mesh.targetHeight - mesh.currentHeight) * 0.1; mesh.scale.y = mesh.currentHeight; // Keep tower base resting on the node platform at y = 1.5 mesh.position.y = 1.5 + mesh.currentHeight / 2; } }); controls.update(); renderer.render(scene, camera); } // Mouse Pointer Raycasting & Hover Tooltip function onPointerMove(event) { const container = document.getElementById('chart-container'); const rect = container.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / container.clientWidth) * 2 - 1; mouse.y = -((event.clientY - rect.top) / container.clientHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects([...podMeshes, ...nodeMeshes], true); const tooltip = document.getElementById('tooltip'); if (intersects.length > 0) { const hitMesh = intersects[0].object; const data = (hitMesh.userData && hitMesh.userData.type) ? hitMesh.userData : (hitMesh.parent && hitMesh.parent.userData ? hitMesh.parent.userData : {}); if (data && data.type) { container.style.cursor = 'pointer'; if (hoveredPod !== hitMesh) { // Reset previous hovered pod if (hoveredPod && hoveredPod.userData && hoveredPod.userData.type !== 'node' && hoveredPod.material && hoveredPod.material.emissive) { hoveredPod.material.emissive.setHex(hoveredPod.originalEmissive); hoveredPod.material.emissiveIntensity = hoveredPod.originalEmissiveIntensity; } hoveredPod = hitMesh; // Highlight hovered pod tower if (hoveredPod.userData && hoveredPod.userData.type !== 'node' && hoveredPod.material && hoveredPod.material.emissive) { hoveredPod.material.emissive.setHex(0x60a5fa); hoveredPod.material.emissiveIntensity = 0.6; } } // Display Tooltip tooltip.classList.remove('hidden'); // Boundary handling const tWidth = 260; let left = event.pageX + 15; let top = event.pageY + 15; if (left + tWidth > window.innerWidth) { left = event.pageX - tWidth - 15; } tooltip.style.left = left + 'px'; tooltip.style.top = top + 'px'; if (data.type === 'node') { const isCP = data.isControlPlane || (data.roles && (data.roles.includes('control-plane') || data.roles.includes('master'))); const badgeLabel = data.roles && data.roles.length ? data.roles.join(', ') : (isCP ? 'control-plane' : 'Node'); const podCount = data.nonTerminatedPods ?? data.non_terminated_pods ?? data.podCount ?? data.pod_count ?? 0; tooltip.innerHTML = `
${data.name} ${badgeLabel}
Internal IP: ${data.internalIp || 'N/A'}
OS Image: ${data.osImage || 'N/A'}
Non-terminated Pods: ${podCount}
`; } else if (data.type === 'unused') { const valCpu = formatCpu(data.cpu_raw); const valMem = formatBytes(data.memory_raw); tooltip.innerHTML = `
Available Capacity
Node: ${data.nodeName}
Unused CPU: ${valCpu}
Unused Memory: ${valMem}
`; } else { const valCpu = formatCpu(data.cpu_raw); const valMem = formatBytes(data.memory_raw); const statusColor = data.status === 'Running' ? '#10b981' : (data.status === 'Pending' ? '#f59e0b' : (data.status === 'Succeeded' ? '#94a3b8' : '#ef4444')); tooltip.innerHTML = `
${data.name}
Namespace: ${data.namespace}
Node: ${data.nodeName}
Status: ${data.status}
CPU Usage: ${valCpu}
Memory Usage: ${valMem}
`; } return; } } container.style.cursor = 'grab'; if (hoveredPod) { if (hoveredPod.userData && hoveredPod.userData.type !== 'node' && hoveredPod.material && hoveredPod.material.emissive) { hoveredPod.material.emissive.setHex(hoveredPod.originalEmissive); hoveredPod.material.emissiveIntensity = hoveredPod.originalEmissiveIntensity; } hoveredPod = null; } tooltip.classList.add('hidden'); } // Click to focus camera on selected Pod function onPointerClick(event) { if (!hoveredPod || hoveredPod.userData.type === 'node') return; let targetPos = hoveredPod.position.clone(); let startLookAt = controls.target.clone(); let duration = 500; let startTime = performance.now(); function step(now) { let progress = Math.min((now - startTime) / duration, 1); let ease = 1 - Math.pow(1 - progress, 3); controls.target.lerpVectors(startLookAt, targetPos, ease); controls.update(); if (progress < 1) { requestAnimationFrame(step); } } requestAnimationFrame(step); } // Initial fetch and auto-polling every 30s fetchData(); setInterval(fetchData, 30000);