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:
Brandon Schneider 2026-05-30 01:52:20 -05:00
parent 1280ff3580
commit 09c47ad94e
9 changed files with 2935 additions and 0 deletions

View 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>

File diff suppressed because it is too large Load diff

View 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"
}
}

View 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>
);
}

View 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,
},
];
}

View 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);
}

View 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>
);

View 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"]
}

View 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,
},
});