Spaces:
Sleeping
Sleeping
| import { | |
| useApplication, | |
| useExtend, | |
| useTick, | |
| } from '@pixi/react' | |
| import { | |
| Container, | |
| Graphics, | |
| Text, | |
| Ticker, | |
| type Graphics as PixiGraphics, | |
| } from 'pixi.js' | |
| import { | |
| useCallback, | |
| useEffect, | |
| useMemo, | |
| useRef, | |
| useState, | |
| type PointerEvent as ReactPointerEvent, | |
| } from 'react' | |
| import { PixiSurface } from './pixi/PixiSurface' | |
| import { | |
| buildTopologySceneModel, | |
| describeTarget, | |
| findHoverTarget, | |
| getFitViewport, | |
| worldToScreen, | |
| type HoverTarget, | |
| type SceneGpu, | |
| type SceneNode, | |
| type TargetDetails, | |
| type TopologySceneModel, | |
| type ViewportState, | |
| } from '../lib/topologyScene' | |
| import { matchesLinkedFocus, type LinkedFocus } from '../lib/linkedFocus' | |
| import { type WorkbenchViewModel } from '../lib/workbenchPresenter' | |
| import { | |
| TOPOLOGY_LOD_POLICY, | |
| getTopologyLodState, | |
| mix, | |
| screenStroke, | |
| screenWorld, | |
| type TopologyLodState, | |
| } from '../lib/topologyLod' | |
| type ClusterMapProps = { | |
| viewModel: WorkbenchViewModel | |
| debugEnabled: boolean | |
| snapshotMode: boolean | |
| linkedFocus: LinkedFocus | null | |
| } | |
| type DebugToggles = { | |
| bounds: boolean | |
| ids: boolean | |
| heat: boolean | |
| hitAreas: boolean | |
| stats: boolean | |
| } | |
| type ScenePointer = { | |
| x: number | |
| y: number | |
| } | |
| type DebugObjectMap = Record< | |
| string, | |
| { | |
| x: number | |
| y: number | |
| width: number | |
| height: number | |
| } | |
| > | |
| const MIN_SCALE = TOPOLOGY_LOD_POLICY.minScale | |
| const MAX_SCALE = TOPOLOGY_LOD_POLICY.maxScale | |
| const clamp = (value: number, min: number, max: number) => | |
| Math.min(Math.max(value, min), max) | |
| type ViewportConstraints = { | |
| minScale: number | |
| maxScale: number | |
| minX: number | |
| maxX: number | |
| minY: number | |
| maxY: number | |
| } | |
| const getViewportConstraints = ( | |
| model: TopologySceneModel, | |
| width: number, | |
| height: number, | |
| scale: number, | |
| ): ViewportConstraints => { | |
| const fitViewport = getFitViewport(model, width, height) | |
| const minScale = fitViewport.scale | |
| const maxScale = clamp(Math.max(minScale * 180, minScale + 0.001), minScale, MAX_SCALE) | |
| const safeScale = clamp(scale, minScale, maxScale) | |
| const scaledWidth = model.width * safeScale | |
| const scaledHeight = model.height * safeScale | |
| const centeredX = (width - scaledWidth) / 2 | |
| const centeredY = (height - scaledHeight) / 2 | |
| if (scaledWidth <= width) { | |
| return { | |
| minScale, | |
| maxScale, | |
| minX: centeredX, | |
| maxX: centeredX, | |
| minY: scaledHeight <= height ? centeredY : height - scaledHeight, | |
| maxY: scaledHeight <= height ? centeredY : 0, | |
| } | |
| } | |
| if (scaledHeight <= height) { | |
| return { | |
| minScale, | |
| maxScale, | |
| minX: width - scaledWidth, | |
| maxX: 0, | |
| minY: centeredY, | |
| maxY: centeredY, | |
| } | |
| } | |
| return { | |
| minScale, | |
| maxScale, | |
| minX: width - scaledWidth, | |
| maxX: 0, | |
| minY: height - scaledHeight, | |
| maxY: 0, | |
| } | |
| } | |
| const clampViewportToScene = ( | |
| nextViewport: ViewportState, | |
| model: TopologySceneModel, | |
| width: number, | |
| height: number, | |
| ): ViewportState => { | |
| if (width <= 0 || height <= 0) { | |
| return nextViewport | |
| } | |
| const constraints = getViewportConstraints(model, width, height, nextViewport.scale) | |
| const scale = clamp(nextViewport.scale, constraints.minScale, constraints.maxScale) | |
| const clamped = getViewportConstraints(model, width, height, scale) | |
| return { | |
| scale, | |
| x: clamp(nextViewport.x, clamped.minX, clamped.maxX), | |
| y: clamp(nextViewport.y, clamped.minY, clamped.maxY), | |
| } | |
| } | |
| const noopDraw = (graphics: PixiGraphics) => { | |
| graphics.clear() | |
| } | |
| const pulse = (timeMs: number, offset: number, depth: number) => | |
| 1 + Math.sin(timeMs / 1000 * 1.8 + offset) * depth | |
| const drawCornerFocus = ( | |
| graphics: PixiGraphics, | |
| bounds: { x: number; y: number; width: number; height: number }, | |
| scale: number, | |
| color: number, | |
| alpha: number, | |
| lengthPx: number, | |
| insetPx: number, | |
| strokePx: number, | |
| ) => { | |
| const length = screenStroke(scale, lengthPx, 0.3, 16) | |
| const inset = screenStroke(scale, insetPx, 0.12, 8) | |
| const stroke = screenStroke(scale, strokePx, 0.08, 2.4) | |
| const left = bounds.x - inset | |
| const top = bounds.y - inset | |
| const right = bounds.x + bounds.width + inset | |
| const bottom = bounds.y + bounds.height + inset | |
| graphics | |
| .moveTo(left, top + length) | |
| .lineTo(left, top) | |
| .lineTo(left + length, top) | |
| .stroke({ color, alpha, width: stroke, cap: 'square', join: 'miter' }) | |
| graphics | |
| .moveTo(right - length, top) | |
| .lineTo(right, top) | |
| .lineTo(right, top + length) | |
| .stroke({ color, alpha, width: stroke, cap: 'square', join: 'miter' }) | |
| graphics | |
| .moveTo(left, bottom - length) | |
| .lineTo(left, bottom) | |
| .lineTo(left + length, bottom) | |
| .stroke({ color, alpha, width: stroke, cap: 'square', join: 'miter' }) | |
| graphics | |
| .moveTo(right - length, bottom) | |
| .lineTo(right, bottom) | |
| .lineTo(right, bottom - length) | |
| .stroke({ color, alpha, width: stroke, cap: 'square', join: 'miter' }) | |
| } | |
| function createDebugObjectMap( | |
| model: TopologySceneModel, | |
| viewport: ViewportState, | |
| ): DebugObjectMap { | |
| const pods = Object.fromEntries( | |
| model.pods.map((pod) => [pod.id, worldToScreen(pod.hitBounds, viewport)]), | |
| ) | |
| const nodes = Object.fromEntries( | |
| model.nodes.map((node) => [node.id, worldToScreen(node.hitBounds, viewport)]), | |
| ) | |
| const gpus = Object.fromEntries( | |
| model.nodes | |
| .flatMap((node) => node.gpus) | |
| .map((gpu) => [gpu.id, worldToScreen(gpu.hitBounds, viewport)]), | |
| ) | |
| return { | |
| ...pods, | |
| ...nodes, | |
| ...gpus, | |
| } | |
| } | |
| const screenRadius = ( | |
| scale: number, | |
| pixels: number, | |
| minWorld = 0.06, | |
| maxWorld = 12, | |
| ) => screenWorld(scale, pixels, minWorld, maxWorld) | |
| const makeRect = (x: number, y: number, width: number, height: number) => ({ | |
| x, | |
| y, | |
| width, | |
| height, | |
| }) | |
| const insetRect = ( | |
| rect: { x: number; y: number; width: number; height: number }, | |
| insetX: number, | |
| insetY: number, | |
| ) => | |
| makeRect( | |
| rect.x + insetX, | |
| rect.y + insetY, | |
| Math.max(rect.width - insetX * 2, 0.0001), | |
| Math.max(rect.height - insetY * 2, 0.0001), | |
| ) | |
| const getWorldViewportBounds = ( | |
| viewport: ViewportState, | |
| width: number, | |
| height: number, | |
| paddingWorld: number, | |
| ) => | |
| makeRect( | |
| -viewport.x / viewport.scale - paddingWorld, | |
| -viewport.y / viewport.scale - paddingWorld, | |
| width / viewport.scale + paddingWorld * 2, | |
| height / viewport.scale + paddingWorld * 2, | |
| ) | |
| const rectsIntersect = ( | |
| left: { x: number; y: number; width: number; height: number }, | |
| right: { x: number; y: number; width: number; height: number }, | |
| ) => | |
| left.x <= right.x + right.width && | |
| left.x + left.width >= right.x && | |
| left.y <= right.y + right.height && | |
| left.y + left.height >= right.y | |
| const lineBounds = ( | |
| x1: number, | |
| y1: number, | |
| x2: number, | |
| y2: number, | |
| pad: number, | |
| ) => | |
| makeRect( | |
| Math.min(x1, x2) - pad, | |
| Math.min(y1, y2) - pad, | |
| Math.abs(x2 - x1) + pad * 2, | |
| Math.abs(y2 - y1) + pad * 2, | |
| ) | |
| function drawModule( | |
| graphics: PixiGraphics, | |
| gpu: SceneGpu, | |
| scale: number, | |
| linked: boolean, | |
| lod: TopologyLodState, | |
| emphasis: number, | |
| ) { | |
| const outer = gpu.lodFrame | |
| const projectedOuterWidth = outer.width * scale | |
| const projectedOuterHeight = outer.height * scale | |
| const activeLoad = gpu.active ? mix(0.42, 1, gpu.utilization) : 0 | |
| const shell = insetRect(outer, outer.width * 0.04, outer.height * 0.06) | |
| const carrier = insetRect(shell, shell.width * 0.05, shell.height * 0.08) | |
| const coldPlate = insetRect(carrier, carrier.width * 0.14, carrier.height * 0.18) | |
| const packageFrame = insetRect(coldPlate, coldPlate.width * 0.1, coldPlate.height * 0.13) | |
| const substrate = insetRect(packageFrame, packageFrame.width * 0.06, packageFrame.height * 0.1) | |
| const interposer = insetRect(substrate, substrate.width * 0.1, substrate.height * 0.14) | |
| const die = insetRect(interposer, interposer.width * 0.2, interposer.height * 0.2) | |
| const dieGrid = insetRect(die, die.width * 0.04, die.height * 0.05) | |
| const connectorStrip = makeRect( | |
| shell.x + shell.width * 0.24, | |
| shell.y + shell.height * 0.82, | |
| shell.width * 0.52, | |
| shell.height * 0.08, | |
| ) | |
| const boardStroke = linked ? 0xffefc0 : 0xcfdbe2 | |
| const overview = Math.max(lod.weights.overview - lod.weights.board * 0.18, 0) | |
| const board = Math.max(lod.weights.board - lod.weights.package * 0.42, 0) | |
| const packageAlpha = Math.max(lod.weights.package - lod.weights.silicon * 0.52, 0) | |
| const siliconAlpha = Math.max(lod.weights.silicon - lod.weights.micro * 0.4, 0) | |
| const microAlpha = lod.weights.micro | |
| const boardPresence = Math.max( | |
| lod.weights.board, | |
| lod.weights.package * 0.84, | |
| lod.weights.silicon * 0.66, | |
| ) | |
| const coldPlatePresence = Math.max(board * 0.7, packageAlpha * 0.88, siliconAlpha * 0.9, microAlpha * 0.8) | |
| const shellAlpha = mix(gpu.active ? 0.84 : 0.42, gpu.active ? 0.96 : 0.56, boardPresence) | |
| const frameAlpha = emphasis * (linked ? 0.92 : 0.56) | |
| const boardStrokeWidth = screenStroke(scale, linked ? 1.25 : 0.9, 0.08, 0.95) | |
| const detailStroke = screenStroke(scale, 0.6, 0.03, 0.5) | |
| const boardCorner = screenRadius(scale, 8, 0.18, 2.6) | |
| const innerCorner = screenRadius(scale, 5, 0.16, 2) | |
| const dieCorner = screenRadius(scale, 4, 0.14, 1.5) | |
| const renderCarrier = projectedOuterWidth >= 10 && projectedOuterHeight >= 8 | |
| const renderColdPlate = projectedOuterWidth >= 14 && projectedOuterHeight >= 10 | |
| const renderOverviewGlyph = overview > 0.02 && projectedOuterWidth >= 10 | |
| const renderConnectorStrip = (overview > 0.02 || board > 0.02) && projectedOuterWidth >= 15 | |
| const renderBoardTier = board > 0.03 && projectedOuterWidth >= 18 | |
| const renderPackageTier = packageAlpha > 0.04 && projectedOuterWidth >= 30 | |
| const renderSiliconTier = siliconAlpha > 0.05 && die.width * scale >= 26 | |
| const renderMicroTier = microAlpha > 0.06 && die.width * scale >= 72 | |
| const glowFrame = makeRect( | |
| shell.x - outer.width * 0.035, | |
| shell.y - outer.height * 0.05, | |
| shell.width + outer.width * 0.07, | |
| shell.height + outer.height * 0.1, | |
| ) | |
| if (activeLoad > 0.001) { | |
| graphics | |
| .roundRect( | |
| glowFrame.x, | |
| glowFrame.y, | |
| glowFrame.width, | |
| glowFrame.height, | |
| screenRadius(scale, 10, 0.22, 3), | |
| ) | |
| .fill({ | |
| color: 0x59e7d2, | |
| alpha: | |
| emphasis * | |
| mix( | |
| projectedOuterWidth < 18 ? 0.08 : 0.04, | |
| projectedOuterWidth < 18 ? 0.2 : 0.1, | |
| activeLoad, | |
| ), | |
| }) | |
| } | |
| graphics | |
| .roundRect(shell.x, shell.y, shell.width, shell.height, boardCorner) | |
| .fill({ color: gpu.active ? 0x0d1f29 : 0x0b1821, alpha: shellAlpha * emphasis }) | |
| .stroke({ color: boardStroke, alpha: frameAlpha, width: boardStrokeWidth }) | |
| if (projectedOuterWidth < 8 || projectedOuterHeight < 6) { | |
| if (activeLoad > 0.001) { | |
| const signalWidth = Math.min( | |
| shell.width * 0.54, | |
| screenWorld(scale, 5.6, 0.14, shell.width * 0.54), | |
| ) | |
| const signalHeight = Math.min( | |
| shell.height * 0.34, | |
| screenWorld(scale, 2.8, 0.1, shell.height * 0.34), | |
| ) | |
| const signalX = shell.x + (shell.width - signalWidth) / 2 | |
| const signalY = shell.y + (shell.height - signalHeight) / 2 | |
| graphics | |
| .roundRect( | |
| signalX, | |
| signalY, | |
| signalWidth, | |
| signalHeight, | |
| screenRadius(scale, 2.2, 0.05, 0.34), | |
| ) | |
| .fill({ | |
| color: 0x76f1df, | |
| alpha: emphasis * mix(0.68, 1, activeLoad), | |
| }) | |
| } | |
| return | |
| } | |
| if (projectedOuterWidth < 15 || projectedOuterHeight < 10) { | |
| const core = insetRect(shell, shell.width * 0.3, shell.height * 0.28) | |
| graphics | |
| .roundRect( | |
| core.x, | |
| core.y, | |
| core.width, | |
| core.height, | |
| screenRadius(scale, 1.8, 0.04, 0.4), | |
| ) | |
| .fill({ | |
| color: gpu.active ? 0x6ce9d7 : 0x193843, | |
| alpha: emphasis * (gpu.active ? mix(0.6, 0.95, activeLoad) : 0.36), | |
| }) | |
| return | |
| } | |
| if (renderCarrier) { | |
| graphics | |
| .roundRect(carrier.x, carrier.y, carrier.width, carrier.height, innerCorner) | |
| .fill({ | |
| color: gpu.active ? 0x112833 : 0x10202a, | |
| alpha: mix(0.56, 0.82, boardPresence) * emphasis, | |
| }) | |
| } | |
| if (renderColdPlate) { | |
| graphics | |
| .roundRect( | |
| coldPlate.x, | |
| coldPlate.y, | |
| coldPlate.width, | |
| coldPlate.height, | |
| screenRadius(scale, 4.5, 0.12, 1.8), | |
| ) | |
| .fill({ | |
| color: 0x163643, | |
| alpha: | |
| mix(0.02, 0.34, coldPlatePresence) * | |
| emphasis * | |
| Math.max(1 - microAlpha * 0.24, 0.76), | |
| }) | |
| } | |
| if (renderConnectorStrip) { | |
| const connectorAlpha = Math.max(overview * 0.8, board * 0.55) * emphasis * (gpu.active ? 0.84 : 0.36) | |
| const padCount = 6 | |
| const padWidth = connectorStrip.width * 0.11 | |
| const padGap = connectorStrip.width * 0.05 | |
| const totalWidth = padCount * padWidth + (padCount - 1) * padGap | |
| const padStart = connectorStrip.x + (connectorStrip.width - totalWidth) / 2 | |
| for (let index = 0; index < padCount; index += 1) { | |
| const padX = padStart + index * (padWidth + padGap) | |
| graphics | |
| .roundRect( | |
| padX, | |
| connectorStrip.y, | |
| padWidth, | |
| connectorStrip.height, | |
| screenRadius(scale, 2, 0.04, 0.6), | |
| ) | |
| .fill({ color: 0xd6ba72, alpha: connectorAlpha }) | |
| } | |
| } | |
| if (renderOverviewGlyph) { | |
| const moduleWindow = insetRect(carrier, carrier.width * 0.24, carrier.height * 0.26) | |
| const dieWindow = makeRect( | |
| moduleWindow.x + moduleWindow.width * 0.31, | |
| moduleWindow.y + moduleWindow.height * 0.26, | |
| moduleWindow.width * 0.38, | |
| moduleWindow.height * 0.48, | |
| ) | |
| graphics | |
| .roundRect( | |
| moduleWindow.x, | |
| moduleWindow.y, | |
| moduleWindow.width, | |
| moduleWindow.height, | |
| screenRadius(scale, 2.8, 0.06, 0.9), | |
| ) | |
| .fill({ | |
| color: gpu.active ? 0x235560 : 0x1a3d48, | |
| alpha: overview * emphasis * mix(gpu.active ? 0.5 : 0.42, gpu.active ? 0.82 : 0.42, activeLoad), | |
| }) | |
| for (const x of [ | |
| moduleWindow.x + moduleWindow.width * 0.14, | |
| moduleWindow.x + moduleWindow.width * 0.76, | |
| ]) { | |
| graphics | |
| .roundRect( | |
| x, | |
| moduleWindow.y + moduleWindow.height * 0.28, | |
| moduleWindow.width * 0.08, | |
| moduleWindow.height * 0.44, | |
| screenRadius(scale, 1.3, 0.03, 0.35), | |
| ) | |
| .fill({ | |
| color: gpu.active ? 0xdaf08e : 0xcddd73, | |
| alpha: overview * emphasis * mix(gpu.active ? 0.8 : 0.62, 1, activeLoad * 0.7), | |
| }) | |
| } | |
| graphics | |
| .roundRect( | |
| dieWindow.x, | |
| dieWindow.y, | |
| dieWindow.width, | |
| dieWindow.height, | |
| screenRadius(scale, 1.7, 0.03, 0.42), | |
| ) | |
| .fill({ | |
| color: gpu.active ? 0x0b1820 : 0x081219, | |
| alpha: overview * emphasis * mix(gpu.active ? 0.92 : 0.86, 1, activeLoad * 0.4), | |
| }) | |
| } | |
| if (renderBoardTier) { | |
| graphics | |
| .roundRect( | |
| coldPlate.x, | |
| coldPlate.y, | |
| coldPlate.width, | |
| coldPlate.height, | |
| screenRadius(scale, 4.5, 0.1, 1.2), | |
| ) | |
| .stroke({ | |
| color: 0x88b9c6, | |
| alpha: board * emphasis * 0.34, | |
| width: detailStroke, | |
| }) | |
| const mountRadius = screenWorld(scale, 2.6, 0.03, 0.26) | |
| const mountAlpha = board * emphasis * (gpu.active ? 0.32 : 0.14) | |
| for (const [x, y] of [ | |
| [carrier.x + carrier.width * 0.16, carrier.y + carrier.height * 0.2], | |
| [carrier.x + carrier.width * 0.84, carrier.y + carrier.height * 0.2], | |
| [carrier.x + carrier.width * 0.16, carrier.y + carrier.height * 0.74], | |
| [carrier.x + carrier.width * 0.84, carrier.y + carrier.height * 0.74], | |
| ]) { | |
| graphics.circle(x, y, mountRadius).fill({ color: 0x8ab7b7, alpha: mountAlpha }) | |
| } | |
| if (activeLoad > 0.001) { | |
| const liveZone = insetRect(coldPlate, coldPlate.width * 0.3, coldPlate.height * 0.28) | |
| graphics | |
| .roundRect( | |
| liveZone.x, | |
| liveZone.y, | |
| liveZone.width, | |
| liveZone.height, | |
| screenRadius(scale, 3, 0.06, 0.8), | |
| ) | |
| .fill({ | |
| color: 0x64e6d4, | |
| alpha: board * emphasis * mix(0.12, 0.28, activeLoad), | |
| }) | |
| } | |
| } | |
| if (renderPackageTier) { | |
| graphics | |
| .roundRect(packageFrame.x, packageFrame.y, packageFrame.width, packageFrame.height, innerCorner) | |
| .stroke({ color: 0xb7c7cd, alpha: packageAlpha * emphasis * 0.8, width: detailStroke }) | |
| graphics | |
| .roundRect(substrate.x, substrate.y, substrate.width, substrate.height, innerCorner) | |
| .fill({ color: 0x294546, alpha: packageAlpha * emphasis * 0.34 }) | |
| graphics | |
| .roundRect(interposer.x, interposer.y, interposer.width, interposer.height, innerCorner) | |
| .fill({ color: 0x2a5960, alpha: packageAlpha * emphasis * 0.3 }) | |
| .stroke({ color: 0x9deedb, alpha: packageAlpha * emphasis * 0.18, width: detailStroke }) | |
| const hbmWidth = interposer.width * 0.18 | |
| const hbmHeight = interposer.height * 0.16 | |
| for (let index = 0; index < 4; index += 1) { | |
| const hbmX = interposer.x + interposer.width * 0.04 + index * (hbmWidth + interposer.width * 0.03) | |
| for (const y of [interposer.y + interposer.height * 0.09, interposer.y + interposer.height * 0.75]) { | |
| graphics | |
| .roundRect( | |
| hbmX, | |
| y, | |
| hbmWidth, | |
| hbmHeight, | |
| screenRadius(scale, 2, 0.04, 0.45), | |
| ) | |
| .fill({ color: 0xcfd86f, alpha: packageAlpha * emphasis * 0.7 }) | |
| } | |
| } | |
| graphics | |
| .roundRect(die.x, die.y, die.width, die.height, dieCorner) | |
| .fill({ color: 0x09161d, alpha: packageAlpha * emphasis * 0.76 }) | |
| .stroke({ color: 0x8bdacd, alpha: packageAlpha * emphasis * 0.24, width: detailStroke }) | |
| } | |
| if (renderSiliconTier) { | |
| graphics | |
| .roundRect(die.x, die.y, die.width, die.height, dieCorner) | |
| .fill({ color: 0x0c1c22, alpha: siliconAlpha * emphasis * 0.58 }) | |
| const tileColumns = 7 | |
| const tileRows = 5 | |
| const tileWidth = dieGrid.width / tileColumns | |
| const tileHeight = dieGrid.height / tileRows | |
| for (let row = 0; row < tileRows; row += 1) { | |
| for (let column = 0; column < tileColumns; column += 1) { | |
| const tileX = dieGrid.x + column * tileWidth | |
| const tileY = dieGrid.y + row * tileHeight | |
| const tileFill = | |
| column === 0 | |
| ? 0xa2d8ec | |
| : row === 0 || row === tileRows - 1 | |
| ? 0x7fb7ca | |
| : 0xb8ece2 | |
| graphics | |
| .roundRect( | |
| tileX + tileWidth * 0.08, | |
| tileY + tileHeight * 0.12, | |
| tileWidth * 0.8, | |
| tileHeight * 0.72, | |
| screenRadius(scale, 1.2, 0.03, 0.26), | |
| ) | |
| .fill({ color: tileFill, alpha: siliconAlpha * emphasis * (column === 0 ? 0.22 : 0.14) }) | |
| } | |
| } | |
| for (const block of [ | |
| makeRect(die.x + die.width * 0.06, die.y + die.height * 0.18, die.width * 0.14, die.height * 0.64), | |
| makeRect(die.x + die.width * 0.78, die.y + die.height * 0.26, die.width * 0.1, die.height * 0.48), | |
| ]) { | |
| graphics | |
| .roundRect( | |
| block.x, | |
| block.y, | |
| block.width, | |
| block.height, | |
| screenRadius(scale, 1.2, 0.03, 0.3), | |
| ) | |
| .fill({ color: 0xaee6ff, alpha: siliconAlpha * emphasis * 0.14 }) | |
| } | |
| } | |
| if (renderMicroTier) { | |
| const cellColumns = 38 | |
| const cellRows = 24 | |
| const cellWidth = dieGrid.width / cellColumns | |
| const cellHeight = dieGrid.height / cellRows | |
| const cellAlpha = microAlpha * emphasis * 0.22 | |
| for (let row = 0; row < cellRows; row += 1) { | |
| for (let column = 0; column < cellColumns; column += 1) { | |
| const x = dieGrid.x + column * cellWidth | |
| const y = dieGrid.y + row * cellHeight | |
| const edgeZone = column < 4 || column > cellColumns - 5 || row < 2 || row > cellRows - 3 | |
| const seam = column % 6 === 0 || row % 5 === 0 | |
| const primaryColor = edgeZone | |
| ? 0x79afbd | |
| : seam | |
| ? 0x91d2dc | |
| : (row + column) % 5 === 0 | |
| ? 0xc7fff0 | |
| : (row + column) % 3 === 0 | |
| ? 0x94d9ef | |
| : 0xafe9dc | |
| graphics | |
| .roundRect( | |
| x + cellWidth * 0.12, | |
| y + cellHeight * 0.16, | |
| cellWidth * 0.72, | |
| cellHeight * 0.56, | |
| screenRadius(scale, 0.18, 0.002, 0.05), | |
| ) | |
| .fill({ color: primaryColor, alpha: cellAlpha * (seam ? 0.58 : 1) }) | |
| } | |
| } | |
| } | |
| } | |
| function drawNodeShell( | |
| graphics: PixiGraphics, | |
| node: SceneNode, | |
| scale: number, | |
| linked: boolean, | |
| heatEnabled: boolean, | |
| lod: TopologyLodState, | |
| emphasis: number, | |
| ) { | |
| const shellAlpha = mix(0.04, 0.14, lod.weights.board) * emphasis | |
| const trayOutlineAlpha = mix(0.08, 0.22, lod.weights.board) * emphasis | |
| const nodeRadius = screenRadius(scale, 18, 0.8, 10) | |
| graphics | |
| .roundRect(node.x, node.y, node.width, node.height, nodeRadius) | |
| .fill({ color: 0x09131b, alpha: 0.86 }) | |
| .stroke({ | |
| color: linked ? 0xffdc8a : 0x6fd9cd, | |
| alpha: linked ? 0.82 : trayOutlineAlpha, | |
| width: screenStroke(scale, linked ? 1.2 : 0.7, 0.08, 0.85), | |
| }) | |
| if (shellAlpha > 0.02) { | |
| graphics | |
| .roundRect( | |
| node.x + 2.5, | |
| node.y + 2.5, | |
| node.width - 5, | |
| node.height - 5, | |
| screenRadius(scale, 14, 0.6, 8), | |
| ) | |
| .fill({ color: 0x0b1720, alpha: shellAlpha }) | |
| } | |
| if (heatEnabled) { | |
| graphics | |
| .roundRect(node.x + 6, node.y + 6, node.width - 12, node.height - 12, 8) | |
| .fill({ | |
| color: 0xe58a43, | |
| alpha: node.interNodeLoad * 0.08 * emphasis, | |
| }) | |
| } | |
| } | |
| function drawCampusPods( | |
| graphics: PixiGraphics, | |
| model: TopologySceneModel, | |
| scale: number, | |
| lod: TopologyLodState, | |
| visiblePods: typeof model.pods, | |
| podEmphasis: (podId: string) => number, | |
| ) { | |
| const rackFabricAlpha = mix(0.02, 0.08, lod.weights.overview) | |
| for (let row = 0; row < model.podRows; row += 1) { | |
| const rowPods = model.pods.slice(row * model.podColumns, row * model.podColumns + model.podColumns) | |
| if (rowPods.length < 2) { | |
| continue | |
| } | |
| graphics | |
| .moveTo(rowPods[0].centerX, rowPods[0].centerY) | |
| .lineTo(rowPods[rowPods.length - 1].centerX, rowPods[rowPods.length - 1].centerY) | |
| .stroke({ | |
| color: 0xf1b067, | |
| alpha: rackFabricAlpha * Math.min(podEmphasis(rowPods[0].id), podEmphasis(rowPods[rowPods.length - 1].id)), | |
| width: screenStroke(scale, 2.4, 0.12, 2.2), | |
| }) | |
| } | |
| for (let column = 0; column < model.podColumns; column += 1) { | |
| const columnPods = model.pods.filter((_, index) => index % model.podColumns === column) | |
| if (columnPods.length < 2) { | |
| continue | |
| } | |
| graphics | |
| .moveTo(columnPods[0].centerX, columnPods[0].centerY) | |
| .lineTo(columnPods[columnPods.length - 1].centerX, columnPods[columnPods.length - 1].centerY) | |
| .stroke({ | |
| color: 0xf1b067, | |
| alpha: | |
| rackFabricAlpha * | |
| Math.min(podEmphasis(columnPods[0].id), podEmphasis(columnPods[columnPods.length - 1].id)), | |
| width: screenStroke(scale, 2.1, 0.12, 2), | |
| }) | |
| } | |
| const rackInnerAlpha = mix(0.02, 0.08, lod.weights.board) | |
| for (const pod of visiblePods) { | |
| const emphasis = podEmphasis(pod.id) | |
| graphics | |
| .roundRect(pod.x, pod.y, pod.width, pod.height, screenRadius(scale, 22, 1.2, 18)) | |
| .fill({ | |
| color: 0x08131c, | |
| alpha: mix(pod.active ? 0.76 : 0.66, pod.active ? 0.88 : 0.8, lod.weights.board) * emphasis, | |
| }) | |
| .stroke({ | |
| color: pod.active ? 0xe6dbb1 : 0x5ecfca, | |
| alpha: (pod.active ? 0.34 : 0.14) * emphasis, | |
| width: screenStroke(scale, pod.active ? 1.3 : 0.8, 0.08, 1), | |
| }) | |
| if (rackInnerAlpha > 0.02) { | |
| graphics | |
| .roundRect( | |
| pod.x + 8, | |
| pod.y + 8, | |
| pod.width - 16, | |
| pod.height - 16, | |
| screenRadius(scale, 18, 0.8, 14), | |
| ) | |
| .stroke({ | |
| color: 0x6fd9cd, | |
| alpha: rackInnerAlpha * emphasis, | |
| width: screenStroke(scale, 0.45, 0.04, 0.5), | |
| }) | |
| } | |
| } | |
| } | |
| function TopologyScene({ | |
| model, | |
| viewport, | |
| surfaceSize, | |
| hoveredTarget, | |
| pinnedTarget, | |
| linkedFocus, | |
| linkedGpuIds, | |
| linkedNodeIds, | |
| linkedPodIds, | |
| debugEnabled, | |
| snapshotMode, | |
| debugToggles, | |
| onFpsChange, | |
| }: { | |
| model: TopologySceneModel | |
| viewport: ViewportState | |
| surfaceSize: { width: number; height: number } | |
| hoveredTarget: HoverTarget | null | |
| pinnedTarget: HoverTarget | null | |
| linkedFocus: LinkedFocus | null | |
| linkedGpuIds: Set<string> | |
| linkedNodeIds: Set<string> | |
| linkedPodIds: Set<string> | |
| debugEnabled: boolean | |
| snapshotMode: boolean | |
| debugToggles: DebugToggles | |
| onFpsChange: (value: number) => void | |
| }) { | |
| useExtend({ Container, Graphics, Text }) | |
| const { app } = useApplication() | |
| const dynamicRef = useRef<PixiGraphics | null>(null) | |
| const hoverRef = useRef<HoverTarget | null>(hoveredTarget) | |
| const pinnedRef = useRef<HoverTarget | null>(pinnedTarget) | |
| const statsRef = useRef({ elapsed: 0, frames: 0 }) | |
| const allGpus = useMemo( | |
| () => model.nodes.flatMap((node) => node.gpus), | |
| [model.nodes], | |
| ) | |
| const gpuById = useMemo(() => new Map(allGpus.map((gpu) => [gpu.id, gpu])), [allGpus]) | |
| const nodeById = useMemo(() => new Map(model.nodes.map((node) => [node.id, node])), [model.nodes]) | |
| const podById = useMemo(() => new Map(model.pods.map((pod) => [pod.id, pod])), [model.pods]) | |
| const lodState = useMemo(() => getTopologyLodState(viewport.scale), [viewport.scale]) | |
| const worldViewportBounds = useMemo( | |
| () => | |
| getWorldViewportBounds( | |
| viewport, | |
| surfaceSize.width, | |
| surfaceSize.height, | |
| screenWorld(viewport.scale, 180, 12, 240), | |
| ), | |
| [surfaceSize.height, surfaceSize.width, viewport], | |
| ) | |
| const visiblePods = useMemo( | |
| () => model.pods.filter((pod) => rectsIntersect(pod.hitBounds, worldViewportBounds)), | |
| [model.pods, worldViewportBounds], | |
| ) | |
| const visibleNodes = useMemo( | |
| () => model.nodes.filter((node) => rectsIntersect(node.hitBounds, worldViewportBounds)), | |
| [model.nodes, worldViewportBounds], | |
| ) | |
| const visibleGpus = useMemo( | |
| () => visibleNodes.flatMap((node) => node.gpus), | |
| [visibleNodes], | |
| ) | |
| const visibleLinks = useMemo( | |
| () => ({ | |
| row: model.rowLinks.filter((link) => | |
| rectsIntersect(lineBounds(link.x1, link.y1, link.x2, link.y2, link.hitWidth), worldViewportBounds), | |
| ), | |
| column: model.columnLinks.filter((link) => | |
| rectsIntersect(lineBounds(link.x1, link.y1, link.x2, link.y2, link.hitWidth), worldViewportBounds), | |
| ), | |
| bus: model.busLinks.filter((link) => | |
| rectsIntersect(lineBounds(link.x1, link.y1, link.x2, link.y2, link.hitWidth), worldViewportBounds), | |
| ), | |
| }), | |
| [model.busLinks, model.columnLinks, model.rowLinks, worldViewportBounds], | |
| ) | |
| const visibleLinkCount = useMemo( | |
| () => visibleLinks.row.length + visibleLinks.column.length + visibleLinks.bus.length, | |
| [visibleLinks.bus.length, visibleLinks.column.length, visibleLinks.row.length], | |
| ) | |
| useEffect(() => { | |
| hoverRef.current = hoveredTarget | |
| }, [hoveredTarget]) | |
| useEffect(() => { | |
| pinnedRef.current = pinnedTarget | |
| }, [pinnedTarget]) | |
| useEffect(() => { | |
| if (debugEnabled || snapshotMode) { | |
| window.__PIXI_TOPOLOGY_APP__ = app | |
| return () => { | |
| delete window.__PIXI_TOPOLOGY_APP__ | |
| } | |
| } | |
| return undefined | |
| }, [app, debugEnabled, snapshotMode]) | |
| const getEmphasis = useCallback( | |
| (kind: 'pod' | 'node' | 'gpu', id: string) => { | |
| const focusTarget = pinnedRef.current ?? hoverRef.current | |
| const base = 1 | |
| if (!focusTarget || lodState.deepIsolation <= 0.001) { | |
| return base | |
| } | |
| const fadeTo = mix(1, 0.08, lodState.deepIsolation) | |
| if (kind === 'gpu') { | |
| if (focusTarget.kind === 'gpu') { | |
| const gpu = gpuById.get(id) | |
| const focusedGpu = gpuById.get(focusTarget.id) | |
| if (!gpu || !focusedGpu) { | |
| return fadeTo | |
| } | |
| if (gpu.id === focusedGpu.id) { | |
| return 1 | |
| } | |
| if (gpu.nodeId === focusedGpu.nodeId) { | |
| return mix(1, 0.34, lodState.deepIsolation) | |
| } | |
| if (gpu.domainIndex === focusedGpu.domainIndex) { | |
| return mix(1, 0.16, lodState.deepIsolation) | |
| } | |
| return fadeTo | |
| } | |
| if (focusTarget.kind === 'node') { | |
| const gpu = gpuById.get(id) | |
| const focusedNode = nodeById.get(focusTarget.id) | |
| if (!gpu || !focusedNode) { | |
| return fadeTo | |
| } | |
| if (gpu.nodeId === focusedNode.id) { | |
| return mix(1, 0.9, lodState.deepIsolation * 0.2) | |
| } | |
| if (gpu.domainIndex === focusedNode.domainIndex) { | |
| return mix(1, 0.18, lodState.deepIsolation) | |
| } | |
| return fadeTo | |
| } | |
| const gpu = gpuById.get(id) | |
| const focusedPod = podById.get(focusTarget.id) | |
| if (!gpu || !focusedPod) { | |
| return fadeTo | |
| } | |
| return gpu.domainIndex === focusedPod.index ? mix(1, 0.72, lodState.deepIsolation * 0.3) : fadeTo | |
| } | |
| if (kind === 'node') { | |
| const node = nodeById.get(id) | |
| if (!node) { | |
| return fadeTo | |
| } | |
| if (focusTarget.kind === 'gpu') { | |
| const gpu = gpuById.get(focusTarget.id) | |
| if (!gpu) { | |
| return fadeTo | |
| } | |
| if (node.id === gpu.nodeId) { | |
| return mix(1, 0.5, lodState.deepIsolation) | |
| } | |
| if (node.domainIndex === gpu.domainIndex) { | |
| return mix(1, 0.18, lodState.deepIsolation) | |
| } | |
| return fadeTo | |
| } | |
| if (focusTarget.kind === 'node') { | |
| const focusedNode = nodeById.get(focusTarget.id) | |
| if (!focusedNode) { | |
| return fadeTo | |
| } | |
| if (node.id === focusedNode.id) { | |
| return 1 | |
| } | |
| if (node.domainIndex === focusedNode.domainIndex) { | |
| return mix(1, 0.2, lodState.deepIsolation) | |
| } | |
| return fadeTo | |
| } | |
| const focusedPod = podById.get(focusTarget.id) | |
| if (!focusedPod) { | |
| return fadeTo | |
| } | |
| return node.domainIndex === focusedPod.index ? mix(1, 0.3, lodState.deepIsolation) : fadeTo | |
| } | |
| const pod = podById.get(id) | |
| if (!pod) { | |
| return fadeTo | |
| } | |
| if (focusTarget.kind === 'gpu') { | |
| const gpu = gpuById.get(focusTarget.id) | |
| return gpu && gpu.domainIndex === pod.index ? mix(1, 0.25, lodState.deepIsolation) : fadeTo | |
| } | |
| if (focusTarget.kind === 'node') { | |
| const node = nodeById.get(focusTarget.id) | |
| return node && node.domainIndex === pod.index ? mix(1, 0.32, lodState.deepIsolation) : fadeTo | |
| } | |
| return focusTarget.id === id ? 1 : fadeTo | |
| }, | |
| [gpuById, lodState.deepIsolation, nodeById, podById], | |
| ) | |
| const drawStatic = useCallback( | |
| (graphics: PixiGraphics) => { | |
| graphics.clear() | |
| drawCampusPods(graphics, model, viewport.scale, lodState, visiblePods, (podId) => | |
| getEmphasis('pod', podId), | |
| ) | |
| const localStructurePresence = Math.max( | |
| lodState.weights.board, | |
| lodState.weights.package * 0.9, | |
| lodState.weights.silicon * 0.7, | |
| lodState.weights.micro * 0.45, | |
| ) | |
| const connectorAlpha = 0.18 * localStructurePresence | |
| const linkPresence = mix(lodState.weights.overview * 0.35, 1, lodState.weights.board) | |
| const hubRadius = screenWorld(viewport.scale, 6, 0.1, 2.4) | |
| const drawStaticLink = (link: (typeof model.rowLinks)[number]) => { | |
| const isRackScope = link.scope === 'rack' | |
| if (!isRackScope && localStructurePresence < 0.08) { | |
| return | |
| } | |
| const rackFrom = | |
| isRackScope | |
| ? model.pods.find((pod) => pod.centerX === link.x1 && pod.centerY === link.y1) | |
| : null | |
| const rackTo = | |
| isRackScope | |
| ? model.pods.find((pod) => pod.centerX === link.x2 && pod.centerY === link.y2) | |
| : null | |
| const emphasis = | |
| isRackScope | |
| ? Math.min( | |
| rackFrom ? getEmphasis('pod', rackFrom.id) : 1, | |
| rackTo ? getEmphasis('pod', rackTo.id) : 1, | |
| ) | |
| : 1 | |
| graphics | |
| .moveTo(link.x1, link.y1) | |
| .lineTo(link.x2, link.y2) | |
| .stroke({ | |
| color: link.color, | |
| alpha: | |
| (isRackScope | |
| ? 0.08 + link.load * 0.24 | |
| : (0.04 + link.load * 0.12) * localStructurePresence) * | |
| linkPresence * | |
| emphasis, | |
| width: screenStroke( | |
| viewport.scale, | |
| isRackScope ? 1.6 + link.load * 2 : 0.75 + link.load * 0.9, | |
| 0.05, | |
| 2.2, | |
| ), | |
| }) | |
| } | |
| visibleLinks.row.forEach(drawStaticLink) | |
| visibleLinks.column.forEach(drawStaticLink) | |
| visibleLinks.bus.forEach((link) => { | |
| if (localStructurePresence < 0.12) { | |
| return | |
| } | |
| graphics | |
| .moveTo(link.x1, link.y1) | |
| .lineTo(link.x2, link.y2) | |
| .stroke({ | |
| color: link.color, | |
| alpha: (0.05 + link.load * 0.16) * linkPresence * localStructurePresence, | |
| width: screenStroke(viewport.scale, 0.55 + link.load * 0.55, 0.05, 1.1), | |
| }) | |
| }) | |
| for (const node of visibleNodes) { | |
| const nodeEmphasis = getEmphasis('node', node.id) | |
| if (localStructurePresence >= 0.08) { | |
| drawNodeShell( | |
| graphics, | |
| node, | |
| viewport.scale, | |
| linkedNodeIds.has(node.id), | |
| debugToggles.heat, | |
| lodState, | |
| nodeEmphasis, | |
| ) | |
| graphics.circle(node.hubX, node.hubY, hubRadius).fill({ | |
| color: linkedNodeIds.has(node.id) ? 0xffcf7a : 0x89f8ea, | |
| alpha: | |
| ((linkedNodeIds.has(node.id) ? 0.68 : 0.08 + node.interNodeLoad * 0.22) * | |
| nodeEmphasis * | |
| localStructurePresence), | |
| }) | |
| } | |
| for (const gpu of node.gpus) { | |
| const gpuEmphasis = getEmphasis('gpu', gpu.id) | |
| const gpuCenterX = gpu.x + gpu.width / 2 | |
| const connectorStartY = | |
| gpu.y + gpu.height / 2 <= node.hubY ? gpu.y + gpu.height : gpu.y | |
| const connectorEndY = | |
| gpu.y + gpu.height / 2 <= node.hubY ? node.hubY - 4 : node.hubY + 4 | |
| if (localStructurePresence >= 0.08) { | |
| graphics | |
| .moveTo(gpuCenterX, connectorStartY) | |
| .lineTo(gpuCenterX, connectorEndY) | |
| .stroke({ | |
| color: linkedGpuIds.has(gpu.id) ? 0xffd28a : 0x88efe0, | |
| alpha: | |
| (linkedGpuIds.has(gpu.id) | |
| ? 0.72 | |
| : connectorAlpha * (gpu.active ? 0.38 + gpu.linkLoad * 0.34 : 0.12)) * gpuEmphasis, | |
| width: screenStroke( | |
| viewport.scale, | |
| linkedGpuIds.has(gpu.id) ? 1 : gpu.active ? 0.55 + gpu.linkLoad * 0.4 : 0.28, | |
| 0.03, | |
| 0.8, | |
| ), | |
| }) | |
| } | |
| drawModule(graphics, gpu, viewport.scale, linkedGpuIds.has(gpu.id), lodState, gpuEmphasis) | |
| } | |
| } | |
| if (debugToggles.bounds) { | |
| for (const pod of visiblePods) { | |
| graphics.roundRect( | |
| pod.hitBounds.x, | |
| pod.hitBounds.y, | |
| pod.hitBounds.width, | |
| pod.hitBounds.height, | |
| screenRadius(viewport.scale, 24, 1.2, 16), | |
| ).stroke({ | |
| color: 0xfde6ab, | |
| alpha: 0.18, | |
| width: screenStroke(viewport.scale, 1, 0.06, 1), | |
| }) | |
| } | |
| for (const node of visibleNodes) { | |
| graphics.roundRect( | |
| node.hitBounds.x, | |
| node.hitBounds.y, | |
| node.hitBounds.width, | |
| node.hitBounds.height, | |
| screenRadius(viewport.scale, 12, 0.6, 8), | |
| ).stroke({ | |
| color: 0xfdf4cc, | |
| alpha: 0.34, | |
| width: screenStroke(viewport.scale, 1, 0.06, 1), | |
| }) | |
| for (const gpu of node.gpus) { | |
| graphics.roundRect( | |
| gpu.hitBounds.x, | |
| gpu.hitBounds.y, | |
| gpu.hitBounds.width, | |
| gpu.hitBounds.height, | |
| screenRadius(viewport.scale, 6, 0.4, 4), | |
| ).stroke({ | |
| color: 0x7adfff, | |
| alpha: 0.24, | |
| width: screenStroke(viewport.scale, 1, 0.06, 1), | |
| }) | |
| } | |
| } | |
| } | |
| if (debugToggles.hitAreas) { | |
| for (const link of [...visibleLinks.row, ...visibleLinks.column, ...visibleLinks.bus]) { | |
| graphics | |
| .moveTo(link.x1, link.y1) | |
| .lineTo(link.x2, link.y2) | |
| .stroke({ | |
| color: link.kind === 'column' ? 0x60aaf7 : 0xffd08a, | |
| alpha: 0.15, | |
| width: screenStroke(viewport.scale, link.hitWidth, 0.5, 16), | |
| }) | |
| } | |
| } | |
| }, | |
| [ | |
| debugToggles.bounds, | |
| debugToggles.heat, | |
| debugToggles.hitAreas, | |
| getEmphasis, | |
| linkedGpuIds, | |
| linkedNodeIds, | |
| lodState, | |
| model, | |
| viewport.scale, | |
| visibleLinks.bus, | |
| visibleLinks.column, | |
| visibleLinks.row, | |
| visibleNodes, | |
| visiblePods, | |
| ], | |
| ) | |
| const redrawDynamic = useCallback( | |
| (timeMs: number) => { | |
| const graphics = dynamicRef.current | |
| if (!graphics) { | |
| return | |
| } | |
| graphics.clear() | |
| const pulseTime = snapshotMode ? 0.42 : timeMs / 1000 | |
| const visibleTarget = pinnedRef.current ?? hoverRef.current | |
| const linkGlowAlpha = mix(0.08, 0.18, lodState.weights.board) | |
| const animateLinkGlow = | |
| lodState.weights.board > 0.14 && | |
| visibleLinkCount < 900 && | |
| viewport.scale >= 0.28 | |
| const drawGlowLink = (link: (typeof model.rowLinks)[number], index: number, color: number) => { | |
| const glow = pulse(timeMs, index * 0.19, snapshotMode ? 0 : 0.12) | |
| graphics | |
| .moveTo(link.x1, link.y1) | |
| .lineTo(link.x2, link.y2) | |
| .stroke({ | |
| color, | |
| alpha: linkGlowAlpha * (0.12 + link.load * 0.5) * glow, | |
| width: screenStroke(viewport.scale, 1.2 + link.load * 2.2, 0.08, 3.2), | |
| }) | |
| } | |
| if (animateLinkGlow) { | |
| visibleLinks.row.forEach((link, index) => { | |
| drawGlowLink(link, index, link.color) | |
| }) | |
| visibleLinks.column.forEach((link, index) => { | |
| drawGlowLink(link, index + visibleLinks.row.length, link.color) | |
| }) | |
| visibleLinks.bus.forEach((link, index) => { | |
| drawGlowLink( | |
| link, | |
| index + visibleLinks.row.length + visibleLinks.column.length, | |
| 0x9efef2, | |
| ) | |
| }) | |
| } | |
| if (linkedFocus) { | |
| const wave = 0.58 + Math.sin(pulseTime * 2.4) * 0.18 | |
| for (const pod of visiblePods) { | |
| if (!linkedPodIds.has(pod.id)) { | |
| continue | |
| } | |
| drawCornerFocus(graphics, pod.focusFrame, viewport.scale, 0xffd78e, wave, 18, 4, 2) | |
| } | |
| for (const node of visibleNodes) { | |
| if (!linkedNodeIds.has(node.id)) { | |
| continue | |
| } | |
| drawCornerFocus(graphics, node.focusFrame, viewport.scale, 0xffd78e, wave, 9, 2, 1.1) | |
| } | |
| for (const gpu of visibleGpus) { | |
| if (!linkedGpuIds.has(gpu.id)) { | |
| continue | |
| } | |
| drawCornerFocus(graphics, gpu.focusFrame, viewport.scale, 0xffefc3, wave + 0.12, 6, 1, 1) | |
| } | |
| } | |
| if (!visibleTarget) { | |
| return | |
| } | |
| if (visibleTarget.kind === 'pod') { | |
| const pod = podById.get(visibleTarget.id) | |
| if (!pod) { | |
| return | |
| } | |
| drawCornerFocus(graphics, pod.focusFrame, viewport.scale, 0xf9f5bc, 0.86, 22, 6, 2.3) | |
| return | |
| } | |
| if (visibleTarget.kind === 'node') { | |
| const node = nodeById.get(visibleTarget.id) | |
| if (!node) { | |
| return | |
| } | |
| drawCornerFocus(graphics, node.focusFrame, viewport.scale, 0xf9f5bc, 0.9, 10, 2, 1.7) | |
| return | |
| } | |
| if (visibleTarget.kind === 'gpu') { | |
| const gpu = gpuById.get(visibleTarget.id) | |
| if (!gpu) { | |
| return | |
| } | |
| drawCornerFocus(graphics, gpu.focusFrame, viewport.scale, 0xffffff, 0.96, 7, 1.5, 1.3) | |
| return | |
| } | |
| const link = [...model.rowLinks, ...model.columnLinks, ...model.busLinks].find( | |
| (item) => item.id === visibleTarget.id, | |
| ) | |
| if (!link) { | |
| return | |
| } | |
| graphics | |
| .moveTo(link.x1, link.y1) | |
| .lineTo(link.x2, link.y2) | |
| .stroke({ | |
| color: 0xfef4c8, | |
| alpha: 0.92, | |
| width: screenStroke(viewport.scale, 2.6 + link.load * 2.8, 0.14, 4.2), | |
| }) | |
| }, | |
| [ | |
| gpuById, | |
| linkedFocus, | |
| linkedGpuIds, | |
| linkedNodeIds, | |
| linkedPodIds, | |
| lodState.weights.board, | |
| model, | |
| nodeById, | |
| podById, | |
| snapshotMode, | |
| visibleGpus, | |
| visibleLinkCount, | |
| viewport.scale, | |
| visibleLinks.bus, | |
| visibleLinks.column, | |
| visibleLinks.row, | |
| visibleNodes, | |
| visiblePods, | |
| ], | |
| ) | |
| useEffect(() => { | |
| redrawDynamic(0) | |
| }, [redrawDynamic, hoveredTarget, pinnedTarget, linkedFocus]) | |
| useTick( | |
| useCallback( | |
| (ticker: Ticker) => { | |
| if (snapshotMode) { | |
| return | |
| } | |
| const shouldAnimate = | |
| linkedFocus != null || | |
| (lodState.weights.board > 0.14 && | |
| visibleLinkCount < 900 && | |
| viewport.scale >= 0.28) | |
| if (!shouldAnimate) { | |
| return | |
| } | |
| redrawDynamic(performance.now()) | |
| statsRef.current.elapsed += ticker.deltaMS | |
| statsRef.current.frames += 1 | |
| if (statsRef.current.elapsed >= 500) { | |
| const fps = (statsRef.current.frames * 1000) / statsRef.current.elapsed | |
| onFpsChange(fps) | |
| statsRef.current.elapsed = 0 | |
| statsRef.current.frames = 0 | |
| } | |
| }, | |
| [ | |
| linkedFocus, | |
| lodState.weights.board, | |
| onFpsChange, | |
| redrawDynamic, | |
| snapshotMode, | |
| viewport.scale, | |
| visibleLinkCount, | |
| ], | |
| ), | |
| ) | |
| const debugLabels = debugEnabled && debugToggles.ids | |
| return ( | |
| <pixiContainer x={viewport.x} y={viewport.y} scale={viewport.scale}> | |
| <pixiGraphics draw={drawStatic} /> | |
| <pixiGraphics ref={dynamicRef} draw={noopDraw} /> | |
| {debugLabels | |
| ? visiblePods.map((pod) => ( | |
| <pixiText | |
| key={`pod-label-${pod.id}`} | |
| x={pod.x + 30} | |
| y={pod.y + 24} | |
| text={pod.active ? 'ACTIVE RACK' : `R${pod.index + 1}`} | |
| style={{ | |
| fill: 0xdff7f0, | |
| fontSize: screenWorld(viewport.scale, 18, 3.5, 24) * lodState.textScale, | |
| fontFamily: 'IBM Plex Mono', | |
| letterSpacing: screenWorld(viewport.scale, 2, 0.2, 2), | |
| }} | |
| /> | |
| )) | |
| : null} | |
| {debugLabels | |
| ? visibleNodes.map((node) => ( | |
| <pixiText | |
| key={`node-label-${node.id}`} | |
| x={node.x + 10} | |
| y={node.y + 8} | |
| text={`N${node.index + 1}`} | |
| style={{ | |
| fill: 0xdff7f0, | |
| fontSize: screenWorld(viewport.scale, 8, 2, 10) * lodState.textScale, | |
| fontFamily: 'IBM Plex Mono', | |
| }} | |
| /> | |
| )) | |
| : null} | |
| </pixiContainer> | |
| ) | |
| } | |
| export function ClusterMap({ | |
| viewModel, | |
| debugEnabled, | |
| snapshotMode, | |
| linkedFocus, | |
| }: ClusterMapProps) { | |
| const model = useMemo(() => buildTopologySceneModel(viewModel), [viewModel]) | |
| const [viewport, setViewport] = useState<ViewportState>({ x: 0, y: 0, scale: 1 }) | |
| const [surfaceSize, setSurfaceSize] = useState({ width: 0, height: 0 }) | |
| const [sceneReady, setSceneReady] = useState(false) | |
| const [hoveredTarget, setHoveredTarget] = useState<HoverTarget | null>(null) | |
| const [pinnedTarget, setPinnedTarget] = useState<HoverTarget | null>(null) | |
| const [isDragging, setIsDragging] = useState(false) | |
| const [fps, setFps] = useState(0) | |
| const [debugToggles, setDebugToggles] = useState<DebugToggles>({ | |
| bounds: false, | |
| ids: false, | |
| heat: false, | |
| hitAreas: false, | |
| stats: true, | |
| }) | |
| const surfaceRef = useRef<HTMLDivElement | null>(null) | |
| const interactionLayerRef = useRef<HTMLDivElement | null>(null) | |
| const interactionRef = useRef({ | |
| dragging: false, | |
| moved: false, | |
| distance: 0, | |
| lastPointer: null as ScenePointer | null, | |
| pointers: new Map<number, ScenePointer>(), | |
| pinchDistance: 0, | |
| pinchMidpoint: null as ScenePointer | null, | |
| }) | |
| const linkedGpuIds = useMemo(() => { | |
| return new Set( | |
| model.nodes | |
| .flatMap((node) => node.gpus) | |
| .filter((gpu) => matchesLinkedFocus(gpu, linkedFocus)) | |
| .map((gpu) => gpu.id), | |
| ) | |
| }, [linkedFocus, model.nodes]) | |
| const linkedNodeIds = useMemo(() => { | |
| return new Set( | |
| model.nodes | |
| .filter((node) => node.gpus.some((gpu) => matchesLinkedFocus(gpu, linkedFocus))) | |
| .map((node) => node.id), | |
| ) | |
| }, [linkedFocus, model.nodes]) | |
| const linkedPodIds = useMemo(() => { | |
| if (!linkedFocus) { | |
| return new Set<string>() | |
| } | |
| return new Set( | |
| model.nodes | |
| .filter((node) => node.gpus.some((gpu) => matchesLinkedFocus(gpu, linkedFocus))) | |
| .map((node) => `pod-${node.domainIndex}`), | |
| ) | |
| }, [linkedFocus, model.nodes]) | |
| useEffect(() => { | |
| if (surfaceSize.width === 0 || surfaceSize.height === 0) { | |
| return | |
| } | |
| let settleFrame = 0 | |
| const frame = requestAnimationFrame(() => { | |
| setViewport(getFitViewport(model, surfaceSize.width, surfaceSize.height)) | |
| settleFrame = requestAnimationFrame(() => { | |
| setSceneReady(true) | |
| }) | |
| }) | |
| return () => { | |
| cancelAnimationFrame(frame) | |
| cancelAnimationFrame(settleFrame) | |
| } | |
| }, [model, surfaceSize.height, surfaceSize.width]) | |
| const focusedDetails = useMemo<TargetDetails | null>(() => { | |
| return describeTarget(model, viewModel, pinnedTarget ?? hoveredTarget) | |
| }, [hoveredTarget, model, pinnedTarget, viewModel]) | |
| const debugObjects = useMemo( | |
| () => createDebugObjectMap(model, viewport), | |
| [model, viewport], | |
| ) | |
| const detailLevel = useMemo(() => getTopologyLodState(viewport.scale).primaryBand, [viewport.scale]) | |
| const viewportConstraints = useMemo(() => { | |
| if (surfaceSize.width === 0 || surfaceSize.height === 0) { | |
| return null | |
| } | |
| return getViewportConstraints(model, surfaceSize.width, surfaceSize.height, viewport.scale) | |
| }, [model, surfaceSize.height, surfaceSize.width, viewport.scale]) | |
| useEffect(() => { | |
| if (!(debugEnabled || snapshotMode)) { | |
| delete window.__TOPOLOGY_DEBUG__ | |
| return | |
| } | |
| window.__TOPOLOGY_DEBUG__ = { | |
| ready: sceneReady, | |
| viewport, | |
| surfaceSize, | |
| objectCounts: model.objectCounts, | |
| objects: debugObjects, | |
| hoveredTarget, | |
| pinnedTarget, | |
| detailLevel, | |
| setViewport: (nextViewport: ViewportState) => { | |
| setViewport(clampViewportToScene(nextViewport, model, surfaceSize.width, surfaceSize.height)) | |
| }, | |
| } | |
| return () => { | |
| delete window.__TOPOLOGY_DEBUG__ | |
| } | |
| }, [ | |
| debugEnabled, | |
| debugObjects, | |
| hoveredTarget, | |
| model.objectCounts, | |
| pinnedTarget, | |
| sceneReady, | |
| snapshotMode, | |
| surfaceSize, | |
| detailLevel, | |
| model, | |
| viewport, | |
| ]) | |
| const scenePointerFromClient = useCallback((clientX: number, clientY: number) => { | |
| const bounds = interactionLayerRef.current?.getBoundingClientRect() | |
| if (!bounds) { | |
| return null | |
| } | |
| return { | |
| x: clientX - bounds.left, | |
| y: clientY - bounds.top, | |
| } | |
| }, []) | |
| const scenePointerFromEvent = useCallback( | |
| (event: Pick<ReactPointerEvent<HTMLDivElement>, 'clientX' | 'clientY'>) => | |
| scenePointerFromClient(event.clientX, event.clientY), | |
| [scenePointerFromClient], | |
| ) | |
| const toWorldPoint = useCallback( | |
| (pointer: ScenePointer) => ({ | |
| x: (pointer.x - viewport.x) / viewport.scale, | |
| y: (pointer.y - viewport.y) / viewport.scale, | |
| }), | |
| [viewport], | |
| ) | |
| const setViewportClamped = useCallback( | |
| (updater: ViewportState | ((current: ViewportState) => ViewportState)) => { | |
| setViewport((current) => { | |
| const nextViewport = | |
| typeof updater === 'function' | |
| ? (updater as (current: ViewportState) => ViewportState)(current) | |
| : updater | |
| return clampViewportToScene(nextViewport, model, surfaceSize.width, surfaceSize.height) | |
| }) | |
| }, | |
| [model, surfaceSize.height, surfaceSize.width], | |
| ) | |
| const applyZoomAtPointer = useCallback((screenPoint: ScenePointer, zoomFactor: number) => { | |
| setViewportClamped((current) => { | |
| const nextScale = clamp( | |
| current.scale * zoomFactor, | |
| viewportConstraints?.minScale ?? MIN_SCALE, | |
| viewportConstraints?.maxScale ?? MAX_SCALE, | |
| ) | |
| const worldX = (screenPoint.x - current.x) / current.scale | |
| const worldY = (screenPoint.y - current.y) / current.scale | |
| return { | |
| scale: nextScale, | |
| x: screenPoint.x - worldX * nextScale, | |
| y: screenPoint.y - worldY * nextScale, | |
| } | |
| }) | |
| }, [setViewportClamped, viewportConstraints?.maxScale, viewportConstraints?.minScale]) | |
| const updateHoverFromPointer = useCallback( | |
| (pointer: ScenePointer | null) => { | |
| if (!pointer) { | |
| setHoveredTarget((current) => (current === null ? current : null)) | |
| return | |
| } | |
| const worldPoint = toWorldPoint(pointer) | |
| const next = findHoverTarget(model, worldPoint.x, worldPoint.y) | |
| setHoveredTarget((current) => { | |
| if (current?.kind === next?.kind && current?.id === next?.id) { | |
| return current | |
| } | |
| return next | |
| }) | |
| }, | |
| [model, toWorldPoint], | |
| ) | |
| const resetViewport = useCallback(() => { | |
| if (surfaceSize.width === 0 || surfaceSize.height === 0) { | |
| return | |
| } | |
| setViewport(getFitViewport(model, surfaceSize.width, surfaceSize.height)) | |
| }, [model, surfaceSize.height, surfaceSize.width]) | |
| const handleSurfaceSizeChange = useCallback((width: number, height: number) => { | |
| setSurfaceSize((current) => { | |
| if (current.width === width && current.height === height) { | |
| return current | |
| } | |
| return { width, height } | |
| }) | |
| setSceneReady(false) | |
| }, []) | |
| useEffect(() => { | |
| const element = interactionLayerRef.current | |
| if (!element) { | |
| return | |
| } | |
| const handleWheel = (event: WheelEvent) => { | |
| if (event.target instanceof Element && event.target.closest('.scene-inspector, .scene-debug-panel')) { | |
| return | |
| } | |
| const pointer = scenePointerFromClient(event.clientX, event.clientY) | |
| if (!pointer) { | |
| return | |
| } | |
| event.preventDefault() | |
| event.stopPropagation() | |
| const delta = event.ctrlKey ? event.deltaY * 1.8 : event.deltaY | |
| const zoomFactor = Math.exp(-delta * 0.0015) | |
| applyZoomAtPointer(pointer, zoomFactor) | |
| } | |
| element.addEventListener('wheel', handleWheel, { passive: false }) | |
| return () => { | |
| element.removeEventListener('wheel', handleWheel) | |
| } | |
| }, [applyZoomAtPointer, scenePointerFromClient]) | |
| const togglePinnedTarget = useCallback( | |
| (pointer: ScenePointer) => { | |
| const worldPoint = toWorldPoint(pointer) | |
| const target = findHoverTarget(model, worldPoint.x, worldPoint.y) | |
| if (!target || target.kind === 'link') { | |
| setPinnedTarget(null) | |
| return | |
| } | |
| setPinnedTarget((current) => { | |
| if (current?.kind === target.kind && current.id === target.id) { | |
| return null | |
| } | |
| return target | |
| }) | |
| }, | |
| [model, toWorldPoint], | |
| ) | |
| const handlePointerDown = useCallback( | |
| (event: ReactPointerEvent<HTMLDivElement>) => { | |
| if (event.target !== event.currentTarget) { | |
| return | |
| } | |
| const pointer = scenePointerFromEvent(event) | |
| if (!pointer) { | |
| return | |
| } | |
| const interaction = interactionRef.current | |
| interaction.pointers.set(event.pointerId, pointer) | |
| interaction.lastPointer = pointer | |
| interaction.moved = false | |
| interaction.distance = 0 | |
| if (interaction.pointers.size === 1) { | |
| interaction.dragging = true | |
| setIsDragging(true) | |
| } else if (interaction.pointers.size === 2) { | |
| const [first, second] = Array.from(interaction.pointers.values()) | |
| const deltaX = second.x - first.x | |
| const deltaY = second.y - first.y | |
| interaction.dragging = false | |
| interaction.pinchDistance = Math.hypot(deltaX, deltaY) | |
| interaction.pinchMidpoint = { | |
| x: (first.x + second.x) / 2, | |
| y: (first.y + second.y) / 2, | |
| } | |
| setIsDragging(false) | |
| } | |
| event.currentTarget.setPointerCapture(event.pointerId) | |
| }, | |
| [scenePointerFromEvent], | |
| ) | |
| const handlePointerMove = useCallback( | |
| (event: ReactPointerEvent<HTMLDivElement>) => { | |
| const pointer = scenePointerFromEvent(event) | |
| if (!pointer) { | |
| return | |
| } | |
| const interaction = interactionRef.current | |
| if (interaction.pointers.has(event.pointerId)) { | |
| interaction.pointers.set(event.pointerId, pointer) | |
| } | |
| if (interaction.pointers.size === 2) { | |
| const [first, second] = Array.from(interaction.pointers.values()) | |
| const deltaX = second.x - first.x | |
| const deltaY = second.y - first.y | |
| const distance = Math.max(Math.hypot(deltaX, deltaY), 1) | |
| const midpoint = { | |
| x: (first.x + second.x) / 2, | |
| y: (first.y + second.y) / 2, | |
| } | |
| if (interaction.pinchDistance > 0 && interaction.pinchMidpoint) { | |
| const zoomFactor = distance / interaction.pinchDistance | |
| setViewportClamped((current) => { | |
| const nextScale = clamp( | |
| current.scale * zoomFactor, | |
| viewportConstraints?.minScale ?? MIN_SCALE, | |
| viewportConstraints?.maxScale ?? MAX_SCALE, | |
| ) | |
| const worldX = (midpoint.x - current.x) / current.scale | |
| const worldY = (midpoint.y - current.y) / current.scale | |
| return { | |
| scale: nextScale, | |
| x: | |
| midpoint.x - | |
| worldX * nextScale + | |
| (midpoint.x - interaction.pinchMidpoint!.x), | |
| y: | |
| midpoint.y - | |
| worldY * nextScale + | |
| (midpoint.y - interaction.pinchMidpoint!.y), | |
| } | |
| }) | |
| } | |
| interaction.pinchDistance = distance | |
| interaction.pinchMidpoint = midpoint | |
| interaction.moved = true | |
| return | |
| } | |
| if (interaction.dragging && interaction.lastPointer) { | |
| const deltaMoveX = pointer.x - interaction.lastPointer.x | |
| const deltaMoveY = pointer.y - interaction.lastPointer.y | |
| interaction.lastPointer = pointer | |
| interaction.distance += Math.abs(deltaMoveX) + Math.abs(deltaMoveY) | |
| if (interaction.distance > 2) { | |
| interaction.moved = true | |
| } | |
| setViewportClamped((current) => ({ | |
| ...current, | |
| x: current.x + deltaMoveX, | |
| y: current.y + deltaMoveY, | |
| })) | |
| return | |
| } | |
| if (event.target !== event.currentTarget) { | |
| return | |
| } | |
| updateHoverFromPointer(pointer) | |
| }, | |
| [ | |
| scenePointerFromEvent, | |
| setViewportClamped, | |
| updateHoverFromPointer, | |
| viewportConstraints?.maxScale, | |
| viewportConstraints?.minScale, | |
| ], | |
| ) | |
| const releasePointer = useCallback((pointerId: number) => { | |
| const interaction = interactionRef.current | |
| interaction.pointers.delete(pointerId) | |
| if (interaction.pointers.size < 2) { | |
| interaction.pinchDistance = 0 | |
| interaction.pinchMidpoint = null | |
| } | |
| if (interaction.pointers.size === 0) { | |
| interaction.dragging = false | |
| interaction.lastPointer = null | |
| setIsDragging(false) | |
| return | |
| } | |
| const remainingPointer = Array.from(interaction.pointers.values())[0] | |
| interaction.lastPointer = remainingPointer | |
| interaction.dragging = true | |
| }, []) | |
| const handlePointerUp = useCallback( | |
| (event: ReactPointerEvent<HTMLDivElement>) => { | |
| const pointer = scenePointerFromEvent(event) | |
| const interaction = interactionRef.current | |
| const wasClick = !interaction.moved && interaction.distance < 8 && interaction.pointers.size <= 1 | |
| if (event.currentTarget.hasPointerCapture(event.pointerId)) { | |
| event.currentTarget.releasePointerCapture(event.pointerId) | |
| } | |
| releasePointer(event.pointerId) | |
| if (pointer) { | |
| updateHoverFromPointer(pointer) | |
| } | |
| if (!pointer || !wasClick || event.target !== event.currentTarget) { | |
| return | |
| } | |
| togglePinnedTarget(pointer) | |
| }, | |
| [releasePointer, scenePointerFromEvent, togglePinnedTarget, updateHoverFromPointer], | |
| ) | |
| const handlePointerLeave = useCallback(() => { | |
| interactionRef.current.dragging = false | |
| interactionRef.current.lastPointer = null | |
| interactionRef.current.pointers.clear() | |
| interactionRef.current.pinchDistance = 0 | |
| interactionRef.current.pinchMidpoint = null | |
| setIsDragging(false) | |
| setHoveredTarget(null) | |
| }, []) | |
| const toggleDebugFlag = (key: keyof DebugToggles) => { | |
| setDebugToggles((current) => ({ | |
| ...current, | |
| [key]: !current[key], | |
| })) | |
| } | |
| const linkedSummary = linkedFocus ? linkedFocus.label : null | |
| return ( | |
| <div className="topology-scene-shell"> | |
| <div className="scene-toolbar"> | |
| <div className="scene-toolbar-actions"> | |
| <button | |
| type="button" | |
| className="scene-button" | |
| onClick={resetViewport} | |
| data-testid="camera-reset" | |
| > | |
| reset camera | |
| </button> | |
| </div> | |
| </div> | |
| <div | |
| ref={surfaceRef} | |
| className="pixi-surface-wrap topology-surface-wrap" | |
| > | |
| <PixiSurface | |
| className="pixi-surface" | |
| canvasClassName="pixi-canvas" | |
| testId="topology-scene" | |
| onSizeChange={handleSurfaceSizeChange} | |
| > | |
| {() => ( | |
| <TopologyScene | |
| model={model} | |
| viewport={viewport} | |
| surfaceSize={surfaceSize} | |
| hoveredTarget={hoveredTarget} | |
| pinnedTarget={pinnedTarget} | |
| linkedFocus={linkedFocus} | |
| linkedGpuIds={linkedGpuIds} | |
| linkedNodeIds={linkedNodeIds} | |
| linkedPodIds={linkedPodIds} | |
| debugEnabled={debugEnabled} | |
| snapshotMode={snapshotMode} | |
| debugToggles={debugToggles} | |
| onFpsChange={setFps} | |
| /> | |
| )} | |
| </PixiSurface> | |
| <div | |
| ref={interactionLayerRef} | |
| className={`topology-interaction-layer${isDragging ? ' is-dragging' : ''}`} | |
| data-testid="topology-interaction-layer" | |
| onPointerDown={handlePointerDown} | |
| onPointerMove={handlePointerMove} | |
| onPointerUp={handlePointerUp} | |
| onPointerCancel={handlePointerLeave} | |
| onPointerLeave={handlePointerLeave} | |
| onDoubleClick={(event) => { | |
| if (event.target !== event.currentTarget) { | |
| return | |
| } | |
| resetViewport() | |
| }} | |
| > | |
| <div className="scene-inspector" data-testid="topology-inspector"> | |
| <p className="mini-label"> | |
| {pinnedTarget ? 'Pinned target' : hoveredTarget ? 'Hover target' : 'Topology inspector'} | |
| </p> | |
| {focusedDetails ? ( | |
| <> | |
| <h3>{focusedDetails.heading}</h3> | |
| <p className="inspector-subheading">{focusedDetails.subheading}</p> | |
| {linkedSummary ? ( | |
| <p className="inspector-link-note">Transformer highlight: {linkedSummary}</p> | |
| ) : null} | |
| <dl className="inspector-grid"> | |
| {focusedDetails.metrics.map((metric) => ( | |
| <div key={`${focusedDetails.id}-${metric.label}`}> | |
| <dt>{metric.label}</dt> | |
| <dd>{metric.value}</dd> | |
| </div> | |
| ))} | |
| </dl> | |
| </> | |
| ) : ( | |
| <> | |
| <h3>Inspect the cluster</h3> | |
| <p className="inspector-subheading"> | |
| Hover a rack or GPU to inspect placement, memory headroom, and link load. | |
| Pan and zoom to move between fabric and package detail. | |
| </p> | |
| {linkedSummary ? ( | |
| <p className="inspector-link-note">Transformer highlight: {linkedSummary}</p> | |
| ) : null} | |
| </> | |
| )} | |
| </div> | |
| {(debugEnabled || snapshotMode) && ( | |
| <div className="scene-debug-panel" data-testid="topology-debug"> | |
| <p className="mini-label">Debug overlay</p> | |
| <div className="debug-toggle-grid"> | |
| <label> | |
| <input | |
| type="checkbox" | |
| checked={debugToggles.bounds} | |
| onChange={() => toggleDebugFlag('bounds')} | |
| /> | |
| Bounds | |
| </label> | |
| <label> | |
| <input | |
| type="checkbox" | |
| checked={debugToggles.ids} | |
| onChange={() => toggleDebugFlag('ids')} | |
| /> | |
| Node / GPU ids | |
| </label> | |
| <label> | |
| <input | |
| type="checkbox" | |
| checked={debugToggles.heat} | |
| onChange={() => toggleDebugFlag('heat')} | |
| /> | |
| Load heat | |
| </label> | |
| <label> | |
| <input | |
| type="checkbox" | |
| checked={debugToggles.hitAreas} | |
| onChange={() => toggleDebugFlag('hitAreas')} | |
| /> | |
| Link hit areas | |
| </label> | |
| <label> | |
| <input | |
| type="checkbox" | |
| checked={debugToggles.stats} | |
| onChange={() => toggleDebugFlag('stats')} | |
| /> | |
| FPS / counts | |
| </label> | |
| </div> | |
| {debugToggles.stats ? ( | |
| <div className="debug-stats"> | |
| <span>FPS {snapshotMode ? 'snapshot' : fps.toFixed(0)}</span> | |
| <span>Racks {model.objectCounts.pods}</span> | |
| <span>Nodes {model.objectCounts.nodes}</span> | |
| <span>GPUs {model.objectCounts.gpus}</span> | |
| <span>Detail {detailLevel}</span> | |
| <span>Zoom {viewport.scale.toFixed(2)}x</span> | |
| </div> | |
| ) : null} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |