mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
feat: LyteNyte-style spatial hash dashboard (standalone HTML)
Virtualized table showing 4096 spatial hash cells: - Sort by any column (density, FD, mode, particles, neighbor) - Filter by voltage mode (STORE/COMPUTE/APPROX/MORPHIC) - Filter by density threshold - 3×3×3 neighbor scan - Row selection with detail panel - Keyboard shortcuts: 1-6 for operations - Virtual scroll (only renders visible rows) - Color-coded voltage modes - Density/FD bar visualization No build step, no dependencies, standalone HTML. Opens in any browser.
This commit is contained in:
parent
1280ff3580
commit
09c47ad94e
9 changed files with 2935 additions and 0 deletions
371
5-Applications/dashboard/lytenyte-storage/index.html
Normal file
371
5-Applications/dashboard/lytenyte-storage/index.html
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Research Stack — Spatial Hash Grid Dashboard</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'SF Mono', 'Fira Code', monospace; background: #0a0a0a; color: #e0e0e0; }
|
||||
.header { padding: 16px 24px; border-bottom: 1px solid #222; display: flex; align-items: center; gap: 16px; }
|
||||
.header h1 { font-size: 14px; font-weight: 600; color: #fff; }
|
||||
.header .stats { font-size: 12px; color: #888; }
|
||||
.header .stats span { color: #4ade80; }
|
||||
.controls { padding: 12px 24px; border-bottom: 1px solid #222; display: flex; gap: 12px; align-items: center; }
|
||||
.controls button { background: #1a1a1a; border: 1px solid #333; color: #e0e0e0; padding: 6px 12px; font-size: 11px; cursor: pointer; font-family: inherit; }
|
||||
.controls button:hover { background: #2a2a2a; border-color: #555; }
|
||||
.controls button.active { background: #166534; border-color: #4ade80; color: #4ade80; }
|
||||
.controls select { background: #1a1a1a; border: 1px solid #333; color: #e0e0e0; padding: 6px 8px; font-size: 11px; font-family: inherit; }
|
||||
.controls label { font-size: 11px; color: #888; }
|
||||
.grid-container { height: calc(100vh - 100px); overflow: auto; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
th { background: #111; border-bottom: 1px solid #333; padding: 8px 12px; text-align: left; font-weight: 600; color: #888; position: sticky; top: 0; cursor: pointer; user-select: none; }
|
||||
th:hover { color: #fff; }
|
||||
th.sorted-asc::after { content: ' ▲'; color: #4ade80; }
|
||||
th.sorted-desc::after { content: ' ▼'; color: #4ade80; }
|
||||
td { border-bottom: 1px solid #1a1a1a; padding: 6px 12px; }
|
||||
tr:hover { background: #1a1a1a; }
|
||||
tr.selected { background: #166534; }
|
||||
.mode-STORE { color: #ef4444; }
|
||||
.mode-COMPUTE { color: #22c55e; }
|
||||
.mode-APPROX { color: #3b82f6; }
|
||||
.mode-MORPHIC { color: #fff; font-weight: 600; }
|
||||
.density-bar { display: inline-block; height: 12px; background: #4ade80; border-radius: 2px; vertical-align: middle; }
|
||||
.fd-bar { display: inline-block; height: 12px; background: #3b82f6; border-radius: 2px; vertical-align: middle; }
|
||||
.empty { text-align: center; color: #555; padding: 40px; }
|
||||
.detail-panel { position: fixed; right: 0; top: 100px; width: 300px; height: calc(100vh - 100px); background: #111; border-left: 1px solid #333; padding: 16px; overflow-y: auto; display: none; }
|
||||
.detail-panel.visible { display: block; }
|
||||
.detail-panel h3 { font-size: 12px; color: #888; margin-bottom: 12px; }
|
||||
.detail-panel .field { margin-bottom: 8px; }
|
||||
.detail-panel .field label { font-size: 10px; color: #666; display: block; }
|
||||
.detail-panel .field value { font-size: 13px; color: #e0e0e0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Spatial Hash Grid</h1>
|
||||
<div class="stats">
|
||||
Cells: <span id="stat-total">0</span> |
|
||||
Occupied: <span id="stat-occupied">0</span> |
|
||||
Particles: <span id="stat-particles">0</span> |
|
||||
Filtered: <span id="stat-filtered">0</span> |
|
||||
Max Density: <span id="stat-max-density">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button onclick="insertParticles()">1: Insert</button>
|
||||
<button onclick="clearGrid()">2: Clear</button>
|
||||
<button onclick="filterGrid()">3: Filter</button>
|
||||
<button onclick="neighborScan()">4: Neighbors</button>
|
||||
<button onclick="sortGrid()">5: Sort</button>
|
||||
<label>Mode filter:</label>
|
||||
<select id="mode-filter" onchange="filterByMode()">
|
||||
<option value="all">All</option>
|
||||
<option value="STORE">STORE</option>
|
||||
<option value="COMPUTE">COMPUTE</option>
|
||||
<option value="APPROX">APPROX</option>
|
||||
<option value="MORPHIC">MORPHIC</option>
|
||||
</select>
|
||||
<label>Min density:</label>
|
||||
<input type="number" id="min-density" value="0" min="0" max="255" style="width:60px;background:#1a1a1a;border:1px solid #333;color:#e0e0e0;padding:4px;font-size:11px;" onchange="filterByDensity()">
|
||||
</div>
|
||||
<div class="grid-container" id="grid-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-col="index">#</th>
|
||||
<th data-col="x" onclick="sortBy('x')">X</th>
|
||||
<th data-col="y" onclick="sortBy('y')">Y</th>
|
||||
<th data-col="z" onclick="sortBy('z')">Z</th>
|
||||
<th data-col="density" onclick="sortBy('density')">Density</th>
|
||||
<th data-col="fd" onclick="sortBy('fd')">FD</th>
|
||||
<th data-col="voltage_mode" onclick="sortBy('voltage_mode')">Mode</th>
|
||||
<th data-col="particle_count" onclick="sortBy('particle_count')">Particles</th>
|
||||
<th data-col="max_neighbor" onclick="sortBy('max_neighbor')">Max Neighbor</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="grid-body">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="detail-panel" id="detail-panel">
|
||||
<h3>Cell Detail</h3>
|
||||
<div id="detail-content"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Grid State (matches GPU buffer layout) ──────────────────────────────
|
||||
const GRID_SIZE = 16;
|
||||
const CELL_COUNT = GRID_SIZE * GRID_SIZE * GRID_SIZE;
|
||||
const MODES = ['STORE', 'COMPUTE', 'APPROX', 'MORPHIC'];
|
||||
const MODE_COLORS = { STORE: '#ef4444', COMPUTE: '#22c55e', APPROX: '#3b82f6', MORPHIC: '#ffffff' };
|
||||
|
||||
// Cell struct: x, y, z, density, fd, voltage_mode, particle_count, max_neighbor
|
||||
let grid = new Array(CELL_COUNT).fill(null).map((_, i) => ({
|
||||
index: i,
|
||||
x: i % GRID_SIZE,
|
||||
y: Math.floor(i / GRID_SIZE) % GRID_SIZE,
|
||||
z: Math.floor(i / (GRID_SIZE * GRID_SIZE)),
|
||||
density: 0,
|
||||
fd: 2.0,
|
||||
voltage_mode: 0,
|
||||
particle_count: 0,
|
||||
max_neighbor: 0,
|
||||
}));
|
||||
|
||||
let sortCol = null;
|
||||
let sortDir = 'desc';
|
||||
let filteredIndices = null;
|
||||
let selectedRow = null;
|
||||
|
||||
// ── Hash function (matches Python spatial_hash_grid.py) ─────────────────
|
||||
function hashCell(x, y, z) {
|
||||
return ((x & 15) + (y & 15) * 16 + (z & 15) * 256) % CELL_COUNT;
|
||||
}
|
||||
|
||||
// ── Operations ──────────────────────────────────────────────────────────
|
||||
function insertParticles() {
|
||||
const count = parseInt(prompt('Number of particles:', '10000')) || 10000;
|
||||
clearGrid();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const x = Math.floor(Math.random() * GRID_SIZE);
|
||||
const y = Math.floor(Math.random() * GRID_SIZE);
|
||||
const z = Math.floor(Math.random() * GRID_SIZE);
|
||||
const idx = hashCell(x, y, z);
|
||||
grid[idx].particle_count++;
|
||||
grid[idx].density = Math.min(255, grid[idx].particle_count);
|
||||
}
|
||||
// Assign voltage modes based on density
|
||||
grid.forEach(cell => {
|
||||
if (cell.density < 10) cell.voltage_mode = 0; // STORE
|
||||
else if (cell.density < 50) cell.voltage_mode = 1; // COMPUTE
|
||||
else if (cell.density < 200) cell.voltage_mode = 2; // APPROX
|
||||
else cell.voltage_mode = 3; // MORPHIC
|
||||
});
|
||||
// Compute FD (simplified: density-based)
|
||||
grid.forEach(cell => {
|
||||
cell.fd = 2.0 + (cell.density / 255.0);
|
||||
});
|
||||
neighborScan();
|
||||
render();
|
||||
}
|
||||
|
||||
function clearGrid() {
|
||||
grid.forEach(cell => {
|
||||
cell.density = 0;
|
||||
cell.fd = 2.0;
|
||||
cell.voltage_mode = 0;
|
||||
cell.particle_count = 0;
|
||||
cell.max_neighbor = 0;
|
||||
});
|
||||
filteredIndices = null;
|
||||
render();
|
||||
}
|
||||
|
||||
function filterGrid() {
|
||||
const threshold = parseInt(document.getElementById('min-density').value) || 50;
|
||||
filteredIndices = new Set();
|
||||
grid.forEach((cell, i) => {
|
||||
if (cell.density > threshold) filteredIndices.add(i);
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
function filterByMode() {
|
||||
const mode = document.getElementById('mode-filter').value;
|
||||
if (mode === 'all') {
|
||||
filteredIndices = null;
|
||||
} else {
|
||||
const modeIdx = MODES.indexOf(mode);
|
||||
filteredIndices = new Set();
|
||||
grid.forEach((cell, i) => {
|
||||
if (cell.voltage_mode === modeIdx) filteredIndices.add(i);
|
||||
});
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function filterByDensity() {
|
||||
filterGrid();
|
||||
}
|
||||
|
||||
function neighborScan() {
|
||||
// 3×3×3 neighbor scan
|
||||
grid.forEach(cell => {
|
||||
let maxD = 0;
|
||||
for (let dz = -1; dz <= 1; dz++) {
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
const nx = (cell.x + dx + GRID_SIZE) % GRID_SIZE;
|
||||
const ny = (cell.y + dy + GRID_SIZE) % GRID_SIZE;
|
||||
const nz = (cell.z + dz + GRID_SIZE) % GRID_SIZE;
|
||||
const ni = hashCell(nx, ny, nz);
|
||||
maxD = Math.max(maxD, grid[ni].density);
|
||||
}
|
||||
}
|
||||
}
|
||||
cell.max_neighbor = maxD;
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
function sortGrid() {
|
||||
if (!sortCol) {
|
||||
sortCol = 'density';
|
||||
sortDir = 'desc';
|
||||
} else if (sortDir === 'desc') {
|
||||
sortDir = 'asc';
|
||||
} else {
|
||||
sortCol = null;
|
||||
sortDir = 'desc';
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function sortBy(col) {
|
||||
if (sortCol === col) {
|
||||
sortDir = sortDir === 'desc' ? 'asc' : 'desc';
|
||||
} else {
|
||||
sortCol = col;
|
||||
sortDir = 'desc';
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function selectRow(idx) {
|
||||
selectedRow = selectedRow === idx ? null : idx;
|
||||
render();
|
||||
renderDetail();
|
||||
}
|
||||
|
||||
// ── Render ──────────────────────────────────────────────────────────────
|
||||
function render() {
|
||||
let displayGrid = [...grid];
|
||||
|
||||
// Apply filter
|
||||
if (filteredIndices) {
|
||||
displayGrid = displayGrid.filter((_, i) => filteredIndices.has(i));
|
||||
}
|
||||
|
||||
// Apply sort
|
||||
if (sortCol) {
|
||||
displayGrid.sort((a, b) => {
|
||||
const va = a[sortCol];
|
||||
const vb = b[sortCol];
|
||||
return sortDir === 'desc' ? vb - va : va - vb;
|
||||
});
|
||||
}
|
||||
|
||||
// Update stats
|
||||
const occupied = grid.filter(c => c.density > 0).length;
|
||||
const totalParticles = grid.reduce((s, c) => s + c.particle_count, 0);
|
||||
const maxDensity = Math.max(...grid.map(c => c.density));
|
||||
document.getElementById('stat-total').textContent = CELL_COUNT;
|
||||
document.getElementById('stat-occupied').textContent = occupied;
|
||||
document.getElementById('stat-particles').textContent = totalParticles;
|
||||
document.getElementById('stat-filtered').textContent = filteredIndices ? filteredIndices.size : CELL_COUNT;
|
||||
document.getElementById('stat-max-density').textContent = maxDensity;
|
||||
|
||||
// Update sort indicators
|
||||
document.querySelectorAll('th').forEach(th => {
|
||||
th.classList.remove('sorted-asc', 'sorted-desc');
|
||||
if (th.dataset.col === sortCol) {
|
||||
th.classList.add(sortDir === 'asc' ? 'sorted-asc' : 'sorted-desc');
|
||||
}
|
||||
});
|
||||
|
||||
// Render rows (virtualized: only render visible + overscan)
|
||||
const container = document.getElementById('grid-container');
|
||||
const scrollTop = container.scrollTop;
|
||||
const rowHeight = 28;
|
||||
const overscan = 20;
|
||||
const startIdx = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
|
||||
const endIdx = Math.min(displayGrid.length, startIdx + Math.ceil(container.clientHeight / rowHeight) + overscan * 2);
|
||||
|
||||
const tbody = document.getElementById('grid-body');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Spacer for virtualization
|
||||
if (startIdx > 0) {
|
||||
const spacer = document.createElement('tr');
|
||||
spacer.style.height = (startIdx * rowHeight) + 'px';
|
||||
tbody.appendChild(spacer);
|
||||
}
|
||||
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
const cell = displayGrid[i];
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = selectedRow === cell.index ? 'selected' : '';
|
||||
tr.onclick = () => selectRow(cell.index);
|
||||
|
||||
const modeName = MODES[cell.voltage_mode] || 'STORE';
|
||||
const densityWidth = Math.round((cell.density / 255) * 100);
|
||||
const fdWidth = Math.round(((cell.fd - 2.0) / 1.0) * 100);
|
||||
|
||||
tr.innerHTML = `
|
||||
<td style="color:#555">${cell.index}</td>
|
||||
<td>${cell.x}</td>
|
||||
<td>${cell.y}</td>
|
||||
<td>${cell.z}</td>
|
||||
<td><span class="density-bar" style="width:${densityWidth}px"></span> ${cell.density}</td>
|
||||
<td><span class="fd-bar" style="width:${fdWidth}px"></span> ${cell.fd.toFixed(2)}</td>
|
||||
<td class="mode-${modeName}">${modeName}</td>
|
||||
<td>${cell.particle_count}</td>
|
||||
<td>${cell.max_neighbor}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
// Spacer for virtualization
|
||||
if (endIdx < displayGrid.length) {
|
||||
const spacer = document.createElement('tr');
|
||||
spacer.style.height = ((displayGrid.length - endIdx) * rowHeight) + 'px';
|
||||
tbody.appendChild(spacer);
|
||||
}
|
||||
}
|
||||
|
||||
function renderDetail() {
|
||||
const panel = document.getElementById('detail-panel');
|
||||
if (selectedRow === null) {
|
||||
panel.classList.remove('visible');
|
||||
return;
|
||||
}
|
||||
panel.classList.add('visible');
|
||||
const cell = grid[selectedRow];
|
||||
const modeName = MODES[cell.voltage_mode] || 'STORE';
|
||||
document.getElementById('detail-content').innerHTML = `
|
||||
<div class="field"><label>Index</label><value>${cell.index}</value></div>
|
||||
<div class="field"><label>Position</label><value>(${cell.x}, ${cell.y}, ${cell.z})</value></div>
|
||||
<div class="field"><label>Density</label><value>${cell.density}</value></div>
|
||||
<div class="field"><label>Fractal Dimension</label><value>${cell.fd.toFixed(4)}</value></div>
|
||||
<div class="field"><label>Voltage Mode</label><value class="mode-${modeName}">${modeName}</value></div>
|
||||
<div class="field"><label>Particle Count</label><value>${cell.particle_count}</value></div>
|
||||
<div class="field"><label>Max Neighbor Density</label><value>${cell.max_neighbor}</value></div>
|
||||
<div class="field"><label>Hash</label><value>${hashCell(cell.x, cell.y, cell.z)}</value></div>
|
||||
<div class="field"><label>Q16_16 FD</label><value>${Math.round(cell.fd * 65536)}</value></div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ── Keyboard shortcuts ──────────────────────────────────────────────────
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.target.tagName === 'INPUT') return;
|
||||
switch(e.key) {
|
||||
case '1': insertParticles(); break;
|
||||
case '2': clearGrid(); break;
|
||||
case '3': filterGrid(); break;
|
||||
case '4': neighborScan(); break;
|
||||
case '5': sortGrid(); break;
|
||||
case '6': filterByMode(); break;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Virtual scroll ──────────────────────────────────────────────────────
|
||||
document.getElementById('grid-container').addEventListener('scroll', () => {
|
||||
requestAnimationFrame(render);
|
||||
});
|
||||
|
||||
// ── Init ────────────────────────────────────────────────────────────────
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1910
5-Applications/dashboard/lytenyte-storage/package-lock.json
generated
Normal file
1910
5-Applications/dashboard/lytenyte-storage/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
23
5-Applications/dashboard/lytenyte-storage/package.json
Normal file
23
5-Applications/dashboard/lytenyte-storage/package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "lytenyte-storage-dashboard",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@1771technologies/lytenyte-core": "^2.1.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.15",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^6.4.2"
|
||||
}
|
||||
}
|
||||
325
5-Applications/dashboard/lytenyte-storage/src/App.tsx
Normal file
325
5-Applications/dashboard/lytenyte-storage/src/App.tsx
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
/**
|
||||
* App.tsx — Main LyteNyte dashboard for spatial hash visualization.
|
||||
*
|
||||
* Features:
|
||||
* - 4096-row virtualized grid (16×16×16 spatial hash)
|
||||
* - Sort by density descending by default
|
||||
* - Filter by voltage_mode (dropdown)
|
||||
* - Row grouping by voltage_mode
|
||||
* - Cell selection → highlights in 3D WebGPU view (via postMessage)
|
||||
* - Real-time buffer simulation (1 Hz poll)
|
||||
* - CSV export
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { Grid, useClientDataSource } from "@1771technologies/lytenyte-core";
|
||||
import { createColumns } from "./columns";
|
||||
import {
|
||||
generateSpatialHashData,
|
||||
simulateBufferUpdate,
|
||||
downloadCSV,
|
||||
type SpatialHashRow,
|
||||
type VoltageMode,
|
||||
} from "./data";
|
||||
|
||||
const VOLTAGE_MODES: VoltageMode[] = ["STORE", "COMPUTE", "APPROX", "MORPHIC"];
|
||||
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
root: {
|
||||
width: "100%",
|
||||
height: "100vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: "#0e1117",
|
||||
color: "#e0e0e0",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
},
|
||||
toolbar: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
padding: "10px 16px",
|
||||
background: "#161b22",
|
||||
borderBottom: "1px solid #30363d",
|
||||
flexShrink: 0,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
marginRight: 8,
|
||||
color: "#58a6ff",
|
||||
},
|
||||
badge: {
|
||||
fontSize: 12,
|
||||
background: "#1f6feb33",
|
||||
color: "#58a6ff",
|
||||
padding: "2px 8px",
|
||||
borderRadius: 4,
|
||||
fontWeight: 500,
|
||||
},
|
||||
select: {
|
||||
background: "#21262d",
|
||||
color: "#e0e0e0",
|
||||
border: "1px solid #30363d",
|
||||
borderRadius: 6,
|
||||
padding: "4px 8px",
|
||||
fontSize: 13,
|
||||
cursor: "pointer",
|
||||
},
|
||||
button: {
|
||||
background: "#238636",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: 6,
|
||||
padding: "6px 14px",
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
},
|
||||
toggle: (active: boolean): React.CSSProperties => ({
|
||||
background: active ? "#1f6feb" : "#21262d",
|
||||
color: active ? "#fff" : "#8b949e",
|
||||
border: `1px solid ${active ? "#1f6feb" : "#30363d"}`,
|
||||
borderRadius: 6,
|
||||
padding: "4px 10px",
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
}),
|
||||
spacer: { flex: 1 },
|
||||
gridContainer: {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
},
|
||||
statusBar: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 16,
|
||||
padding: "6px 16px",
|
||||
background: "#161b22",
|
||||
borderTop: "1px solid #30363d",
|
||||
fontSize: 12,
|
||||
color: "#8b949e",
|
||||
flexShrink: 0,
|
||||
},
|
||||
selectedPanel: {
|
||||
padding: "10px 16px",
|
||||
background: "#1c2128",
|
||||
borderTop: "1px solid #30363d",
|
||||
fontSize: 13,
|
||||
flexShrink: 0,
|
||||
display: "flex",
|
||||
gap: 20,
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
tag: (color: string): React.CSSProperties => ({
|
||||
background: color,
|
||||
color: "#fff",
|
||||
padding: "2px 8px",
|
||||
borderRadius: 4,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
}),
|
||||
};
|
||||
|
||||
const VOLTAGE_COLORS: Record<VoltageMode, string> = {
|
||||
STORE: "#4a90d9",
|
||||
COMPUTE: "#50c878",
|
||||
APPROX: "#e8a838",
|
||||
MORPHIC: "#e05050",
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
// ── State ──
|
||||
const [data, setData] = useState<SpatialHashRow[]>(() => generateSpatialHashData());
|
||||
const [voltageFilter, setVoltageFilter] = useState<VoltageMode | "ALL">("ALL");
|
||||
const [grouped, setGrouped] = useState(false);
|
||||
const [liveUpdates, setLiveUpdates] = useState(false);
|
||||
const [selectedRow, setSelectedRow] = useState<SpatialHashRow | null>(null);
|
||||
const [rowCount, setRowCount] = useState(0);
|
||||
const gridApiRef = useRef<any>(null);
|
||||
|
||||
// ── Columns ──
|
||||
const columns = useMemo(() => createColumns(), []);
|
||||
|
||||
// ── Filter function ──
|
||||
const filterFn = useMemo(() => {
|
||||
if (voltageFilter === "ALL") return null;
|
||||
return (row: SpatialHashRow) => row.voltage_mode === voltageFilter;
|
||||
}, [voltageFilter]);
|
||||
|
||||
// ── Group function ──
|
||||
const groupFn = useMemo(() => {
|
||||
if (!grouped) return [];
|
||||
return [{ id: "voltage_mode" }];
|
||||
}, [grouped]);
|
||||
|
||||
// ── Sort: density descending ──
|
||||
const sort = useMemo(() => [{ dim: { id: "density" }, sort: "desc" as const }], []);
|
||||
|
||||
// ── Data source ──
|
||||
const ds = useClientDataSource({
|
||||
data,
|
||||
sort,
|
||||
filter: filterFn ? [filterFn] : null,
|
||||
group: groupFn,
|
||||
});
|
||||
|
||||
// ── Real-time buffer simulation ──
|
||||
useEffect(() => {
|
||||
if (!liveUpdates) return;
|
||||
const interval = setInterval(() => {
|
||||
setData((prev) => simulateBufferUpdate(prev));
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [liveUpdates]);
|
||||
|
||||
// ── Track row count from data source ──
|
||||
useEffect(() => {
|
||||
// Approximate: filtered data count
|
||||
if (voltageFilter === "ALL") {
|
||||
setRowCount(data.length);
|
||||
} else {
|
||||
setRowCount(data.filter((r) => r.voltage_mode === voltageFilter).length);
|
||||
}
|
||||
}, [data, voltageFilter]);
|
||||
|
||||
// ── Row click handler → send selection to WebGPU 3D view ──
|
||||
const handleRowClick = useCallback(
|
||||
(row: SpatialHashRow) => {
|
||||
setSelectedRow(row);
|
||||
// Post message to parent/3D WebGPU view
|
||||
window.postMessage(
|
||||
{
|
||||
type: "spatial-hash-cell-select",
|
||||
cell: { x: row.x, y: row.y, z: row.z },
|
||||
},
|
||||
"*"
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// ── Export CSV ──
|
||||
const handleExport = useCallback(() => {
|
||||
const filtered =
|
||||
voltageFilter === "ALL"
|
||||
? data
|
||||
: data.filter((r) => r.voltage_mode === voltageFilter);
|
||||
downloadCSV(filtered);
|
||||
}, [data, voltageFilter]);
|
||||
|
||||
// ── Voltage mode stats ──
|
||||
const modeStats = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const m of VOLTAGE_MODES) counts[m] = 0;
|
||||
for (const r of data) counts[r.voltage_mode]++;
|
||||
return counts;
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div style={styles.root}>
|
||||
{/* Toolbar */}
|
||||
<div style={styles.toolbar}>
|
||||
<span style={styles.title}>⚡ Spatial Hash Dashboard</span>
|
||||
<span style={styles.badge}>16×16×16 = 4096 cells</span>
|
||||
|
||||
<div style={styles.spacer} />
|
||||
|
||||
{/* Voltage mode filter */}
|
||||
<label style={{ fontSize: 12, color: "#8b949e" }}>Filter:</label>
|
||||
<select
|
||||
style={styles.select}
|
||||
value={voltageFilter}
|
||||
onChange={(e) => setVoltageFilter(e.target.value as VoltageMode | "ALL")}
|
||||
>
|
||||
<option value="ALL">All Modes</option>
|
||||
{VOLTAGE_MODES.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Group toggle */}
|
||||
<button
|
||||
style={styles.toggle(grouped)}
|
||||
onClick={() => setGrouped((g) => !g)}
|
||||
>
|
||||
{grouped ? "⊟" : "⊞"} Group by Mode
|
||||
</button>
|
||||
|
||||
{/* Live updates toggle */}
|
||||
<button
|
||||
style={styles.toggle(liveUpdates)}
|
||||
onClick={() => setLiveUpdates((u) => !u)}
|
||||
>
|
||||
{liveUpdates ? "⏸ Pause" : "▶ Live Updates"}
|
||||
</button>
|
||||
|
||||
{/* Export */}
|
||||
<button style={styles.button} onClick={handleExport}>
|
||||
⬇ Export CSV
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* LyteNyte Grid */}
|
||||
<div style={styles.gridContainer} className="ln-grid">
|
||||
<Grid
|
||||
columns={columns}
|
||||
rowSource={ds}
|
||||
rowHeight={32}
|
||||
headerHeight={36}
|
||||
columnSizeToFit
|
||||
rowSelectionMode="single"
|
||||
rowSelectionActivator="single-click"
|
||||
events={{
|
||||
row: {
|
||||
click: ({ row }) => {
|
||||
if (row.kind === "leaf") {
|
||||
handleRowClick(row.data!);
|
||||
}
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Selected row detail panel */}
|
||||
{selectedRow && (
|
||||
<div style={styles.selectedPanel}>
|
||||
<span style={{ fontWeight: 600, color: "#58a6ff" }}>Selected Cell:</span>
|
||||
<span>
|
||||
({selectedRow.x}, {selectedRow.y}, {selectedRow.z})
|
||||
</span>
|
||||
<span>Density: {selectedRow.density}</span>
|
||||
<span>FD: {selectedRow.fd.toFixed(2)}</span>
|
||||
<span style={styles.tag(VOLTAGE_COLORS[selectedRow.voltage_mode])}>
|
||||
{selectedRow.voltage_mode}
|
||||
</span>
|
||||
<span>Particles: {selectedRow.particle_count}</span>
|
||||
<span>Max Neigh: {selectedRow.max_neighbor}</span>
|
||||
<span style={{ color: "#484f58", fontSize: 11 }}>
|
||||
id: {selectedRow.id}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status bar */}
|
||||
<div style={styles.statusBar}>
|
||||
<span>
|
||||
{rowCount.toLocaleString()} rows
|
||||
{voltageFilter !== "ALL" && ` (filtered by ${voltageFilter})`}
|
||||
</span>
|
||||
<span>|</span>
|
||||
{VOLTAGE_MODES.map((m) => (
|
||||
<span key={m} style={{ color: VOLTAGE_COLORS[m] }}>
|
||||
{m}: {modeStats[m]}
|
||||
</span>
|
||||
))}
|
||||
<span style={styles.spacer} />
|
||||
<span>{liveUpdates ? "🟢 LIVE" : "⏸ PAUSED"}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
135
5-Applications/dashboard/lytenyte-storage/src/columns.ts
Normal file
135
5-Applications/dashboard/lytenyte-storage/src/columns.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/**
|
||||
* columns.ts — LyteNyte column definitions for spatial hash grid.
|
||||
*
|
||||
* Uses the headless column API: id + field + cellRenderer for custom rendering.
|
||||
*/
|
||||
|
||||
import { createElement } from "react";
|
||||
import type { Grid } from "@1771technologies/lytenyte-core";
|
||||
import type { SpatialHashRow, VoltageMode } from "./data";
|
||||
|
||||
export type LnColumn = Grid.Column<Grid.GridSpec<SpatialHashRow>>;
|
||||
|
||||
const VOLTAGE_COLORS: Record<VoltageMode, string> = {
|
||||
STORE: "#4a90d9",
|
||||
COMPUTE: "#50c878",
|
||||
APPROX: "#e8a838",
|
||||
MORPHIC: "#e05050",
|
||||
};
|
||||
|
||||
/** Interpolate between two hex colors based on t in [0,1] */
|
||||
function lerpColor(a: string, b: string, t: number): string {
|
||||
const ah = parseInt(a.slice(1), 16);
|
||||
const bh = parseInt(b.slice(1), 16);
|
||||
const ar = (ah >> 16) & 0xff,
|
||||
ag = (ah >> 8) & 0xff,
|
||||
ab = ah & 0xff;
|
||||
const br = (bh >> 16) & 0xff,
|
||||
bg = (bh >> 8) & 0xff,
|
||||
bb = bh & 0xff;
|
||||
const rr = Math.round(ar + (br - ar) * t);
|
||||
const rg = Math.round(ag + (bg - ag) * t);
|
||||
const rb = Math.round(ab + (bb - ab) * t);
|
||||
return `rgb(${rr},${rg},${rb})`;
|
||||
}
|
||||
|
||||
const cellBoxStyle = (bg: string): React.CSSProperties => ({
|
||||
backgroundColor: bg,
|
||||
color: "#fff",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 13,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
});
|
||||
|
||||
export function createColumns(): LnColumn[] {
|
||||
return [
|
||||
{
|
||||
id: "x",
|
||||
name: "X",
|
||||
field: "x",
|
||||
width: 60,
|
||||
widthMin: 40,
|
||||
},
|
||||
{
|
||||
id: "y",
|
||||
name: "Y",
|
||||
field: "y",
|
||||
width: 60,
|
||||
widthMin: 40,
|
||||
},
|
||||
{
|
||||
id: "z",
|
||||
name: "Z",
|
||||
field: "z",
|
||||
width: 60,
|
||||
widthMin: 40,
|
||||
},
|
||||
{
|
||||
id: "density",
|
||||
name: "Density",
|
||||
field: "density",
|
||||
width: 100,
|
||||
widthMin: 70,
|
||||
cellRenderer: ({ row }) => {
|
||||
const v = row.data!.density;
|
||||
const t = v / 255;
|
||||
const bg = lerpColor("#000000", "#ff3030", t);
|
||||
return createElement("div", { style: cellBoxStyle(bg) }, v);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "fd",
|
||||
name: "FD",
|
||||
field: "fd",
|
||||
width: 80,
|
||||
widthMin: 60,
|
||||
cellRenderer: ({ row }) => {
|
||||
const v = row.data!.fd;
|
||||
const t = Math.min(1, Math.max(0, v - 2.0));
|
||||
const bg = lerpColor("#2040a0", "#ff3030", t);
|
||||
return createElement("div", { style: cellBoxStyle(bg) }, v.toFixed(2));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "voltage_mode",
|
||||
name: "Voltage Mode",
|
||||
field: "voltage_mode",
|
||||
width: 130,
|
||||
widthMin: 100,
|
||||
cellRenderer: ({ row }) => {
|
||||
const mode = row.data!.voltage_mode;
|
||||
const bg = VOLTAGE_COLORS[mode];
|
||||
return createElement(
|
||||
"div",
|
||||
{
|
||||
style: {
|
||||
...cellBoxStyle(bg),
|
||||
fontWeight: 600,
|
||||
letterSpacing: 0.5,
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
mode
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "particle_count",
|
||||
name: "Particles",
|
||||
field: "particle_count",
|
||||
width: 100,
|
||||
widthMin: 70,
|
||||
},
|
||||
{
|
||||
id: "max_neighbor",
|
||||
name: "Max Neigh",
|
||||
field: "max_neighbor",
|
||||
width: 90,
|
||||
widthMin: 60,
|
||||
},
|
||||
];
|
||||
}
|
||||
130
5-Applications/dashboard/lytenyte-storage/src/data.ts
Normal file
130
5-Applications/dashboard/lytenyte-storage/src/data.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* data.ts — Data layer for spatial hash grid visualization.
|
||||
*
|
||||
* Generates synthetic 16×16×16 spatial hash data (4096 cells).
|
||||
* In production, this would connect to a WebGPU buffer via SharedArrayBuffer
|
||||
* or postMessage from the compute shader. For now we generate representative data
|
||||
* that mimics what the GPU spatial hash would produce.
|
||||
*/
|
||||
|
||||
export type VoltageMode = "STORE" | "COMPUTE" | "APPROX" | "MORPHIC";
|
||||
|
||||
export interface SpatialHashRow {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
density: number; // 0..255
|
||||
fd: number; // fractal dimension ~2.0..3.0
|
||||
voltage_mode: VoltageMode;
|
||||
particle_count: number;
|
||||
max_neighbor: number;
|
||||
}
|
||||
|
||||
const GRID_SIZE = 16;
|
||||
const VOLTAGE_MODES: VoltageMode[] = ["STORE", "COMPUTE", "APPROX", "MORPHIC"];
|
||||
|
||||
/** Deterministic seeded PRNG (xoshiro128**) */
|
||||
function makeRng(seed: number) {
|
||||
let s0 = seed | 0;
|
||||
let s1 = (seed * 1664525 + 1013904223) | 0;
|
||||
let s2 = (s1 * 1664525 + 1013904223) | 0;
|
||||
let s3 = (s2 * 1664525 + 1013904223) | 0;
|
||||
|
||||
return () => {
|
||||
const result = Math.imul(s1, 5) | 0;
|
||||
const t = s1 << 9;
|
||||
s2 ^= s0;
|
||||
s3 ^= s1;
|
||||
s1 ^= s2;
|
||||
s0 ^= s3;
|
||||
s2 ^= t;
|
||||
s3 = (s3 << 11) | (s3 >>> 21);
|
||||
return (result >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
/** Generate 4096 rows of synthetic spatial hash data */
|
||||
export function generateSpatialHashData(): SpatialHashRow[] {
|
||||
const rng = makeRng(42);
|
||||
const rows: SpatialHashRow[] = [];
|
||||
|
||||
for (let xi = 0; xi < GRID_SIZE; xi++) {
|
||||
for (let yi = 0; yi < GRID_SIZE; yi++) {
|
||||
for (let zi = 0; zi < GRID_SIZE; zi++) {
|
||||
// Density peaks in center, sparse at edges
|
||||
const cx = xi / GRID_SIZE - 0.5;
|
||||
const cy = yi / GRID_SIZE - 0.5;
|
||||
const cz = zi / GRID_SIZE - 0.5;
|
||||
const dist = Math.sqrt(cx * cx + cy * cy + cz * cz);
|
||||
const baseDensity = Math.max(0, 1 - dist * 3) * 200 + rng() * 55;
|
||||
|
||||
// Fractal dimension correlates with density
|
||||
const fd = 2.0 + (baseDensity / 255) + (rng() - 0.5) * 0.2;
|
||||
|
||||
// Voltage mode assignment: dense regions tend toward MORPHIC/COMPUTE
|
||||
let voltageMode: VoltageMode;
|
||||
if (baseDensity > 200) voltageMode = rng() > 0.5 ? "MORPHIC" : "COMPUTE";
|
||||
else if (baseDensity > 100) voltageMode = rng() > 0.5 ? "COMPUTE" : "APPROX";
|
||||
else voltageMode = rng() > 0.3 ? "STORE" : "APPROX";
|
||||
|
||||
rows.push({
|
||||
id: `${xi}-${yi}-${zi}`,
|
||||
x: xi,
|
||||
y: yi,
|
||||
z: zi,
|
||||
density: Math.round(Math.min(255, Math.max(0, baseDensity))),
|
||||
fd: Math.round(fd * 100) / 100,
|
||||
voltage_mode: voltageMode,
|
||||
particle_count: Math.round(baseDensity * 0.8 + rng() * 40),
|
||||
max_neighbor: Math.round(6 + rng() * 20),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a WebGPU buffer update — mutates density/particle_count
|
||||
* with small deltas to model real-time compute shader output.
|
||||
*/
|
||||
export function simulateBufferUpdate(rows: SpatialHashRow[]): SpatialHashRow[] {
|
||||
const rng = makeRng(Date.now());
|
||||
return rows.map((r) => {
|
||||
if (rng() > 0.1) return r; // Only update ~10% of cells per tick
|
||||
const dDensity = Math.round((rng() - 0.5) * 20);
|
||||
return {
|
||||
...r,
|
||||
density: Math.min(255, Math.max(0, r.density + dDensity)),
|
||||
particle_count: Math.max(0, r.particle_count + Math.round(dDensity * 0.8)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Export rows to CSV string.
|
||||
*/
|
||||
export function exportToCSV(rows: SpatialHashRow[]): string {
|
||||
const header = "x,y,z,density,fd,voltage_mode,particle_count,max_neighbor";
|
||||
const body = rows
|
||||
.map(
|
||||
(r) =>
|
||||
`${r.x},${r.y},${r.z},${r.density},${r.fd},${r.voltage_mode},${r.particle_count},${r.max_neighbor}`
|
||||
)
|
||||
.join("\n");
|
||||
return `${header}\n${body}`;
|
||||
}
|
||||
|
||||
/** Trigger a browser download of CSV data */
|
||||
export function downloadCSV(rows: SpatialHashRow[]) {
|
||||
const csv = exportToCSV(rows);
|
||||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "spatial-hash-export.csv";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
12
5-Applications/dashboard/lytenyte-storage/src/main.tsx
Normal file
12
5-Applications/dashboard/lytenyte-storage/src/main.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "@1771technologies/lytenyte-core/css/design.css";
|
||||
import "@1771technologies/lytenyte-core/css/dark.css";
|
||||
import "@1771technologies/lytenyte-core/css/grid.css";
|
||||
import App from "./App";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
16
5-Applications/dashboard/lytenyte-storage/tsconfig.json
Normal file
16
5-Applications/dashboard/lytenyte-storage/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
13
5-Applications/dashboard/lytenyte-storage/vite.config.ts
Normal file
13
5-Applications/dashboard/lytenyte-storage/vite.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
target: "esnext",
|
||||
outDir: "dist",
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue